From df34ded1536aeffc1aea744465ba04d14371be69 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 11:35:03 +0300 Subject: [PATCH 001/105] 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 2f5ad03431e2a398c091997fbe1f254043ea22d8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 29 Jun 2025 10:57:18 +0300 Subject: [PATCH 002/105] created empty charts widget. --- .../total_energy_consumption_chart_box.dart | 12 ++- .../analytics_chart_empty_state_widget.dart | 80 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index 4d88471d..52a0b0ba 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -41,7 +42,16 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), - TotalEnergyConsumptionChart(chartData: state.chartData), + Visibility( + visible: state.chartData.isNotEmpty, + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == TotalEnergyConsumptionStatus.loading, + isError: state.status == TotalEnergyConsumptionStatus.failure, + isInitial: state.status == TotalEnergyConsumptionStatus.initial, + errorMessage: state.errorMessage, + ), + child: TotalEnergyConsumptionChart(chartData: state.chartData), + ), ], ), ), diff --git a/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart new file mode 100644 index 00000000..086cf959 --- /dev/null +++ b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AnalyticsChartEmptyStateWidget extends StatelessWidget { + const AnalyticsChartEmptyStateWidget({ + this.isLoading = false, + this.isError = false, + this.isInitial = false, + this.errorMessage, + this.noDataMessage = 'No data to display', + this.initialMessage = 'Please select a space to see data', + super.key, + }); + + final bool isLoading; + final bool isError; + final bool isInitial; + final String? errorMessage; + final String noDataMessage; + final String initialMessage; + + @override + Widget build(BuildContext context) { + return Expanded( + child: isLoading + ? const Center( + child: AppLoadingIndicator(), + ) + : isError + ? _buildState( + context, + icon: Icons.error_outline, + message: errorMessage ?? 'Something went wrong', + color: ColorsManager.red, + ) + : isInitial + ? _buildState( + context, + icon: Icons.filter_list, + message: initialMessage, + ) + : _buildState( + context, + icon: Icons.bar_chart, + message: noDataMessage, + ), + ); + } + + Widget _buildState( + BuildContext context, { + required IconData icon, + required String message, + Color? color, + }) { + final disabledColor = context.theme.disabledColor; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 48, + color: color ?? disabledColor, + ), + const SizedBox(height: 16), + SelectableText( + message, + style: context.textTheme.bodyMedium?.copyWith( + color: color ?? disabledColor, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} From 23cfee14901c7b176b4a20ba0a76fbc65c35d702 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 11:12:28 +0300 Subject: [PATCH 003/105] fix curtain name in curtain if then containers dialogs --- .../routines/widgets/routine_dialogs/curtain_dialog.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart index bdf8660d..64295e2a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -58,7 +58,9 @@ class CurtainHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('AC Functions'), + DialogHeader(dialogType == 'THEN' + ? 'Curtain Functions' + : 'Curtain Conditions'), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, From 388391eec4b900326aac70776f22b175c0bd19cd Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 11:29:43 +0300 Subject: [PATCH 004/105] stop stacking snackbars --- .../all_devices/widgets/device_managment_body.dart | 2 ++ 1 file changed, 2 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 c865a5dc..4f86c8e3 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 @@ -111,6 +111,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { onPressed: isControlButtonEnabled ? () { if (isAnyDeviceOffline) { + ScaffoldMessenger.of(context) + .clearSnackBars(); ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( From 19cdd371f8bb212696585e6467bb2d28ed8839be Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 12:43:23 +0300 Subject: [PATCH 005/105] fix edit problem and funtion name in dialog was olways keep close now it is really take the real value --- .../schedule_device/schedule_widgets/schedule_table.dart | 8 ++++---- .../water_heater/helper/add_schedule_dialog_helper.dart | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index b23e48df..21f404ff 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 @@ -195,10 +195,10 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - schedule.category == 'CUR_2' - ? Center( - child: Text(schedule.function.value == true ? 'open' : 'close')) - : Center(child: Text(schedule.function.value ? 'On' : 'Off')), + if (schedule.category == 'CUR_2') + Center(child: Text(schedule.function.value)) + else + Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 389eac3f..f55b32ab 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 @@ -19,13 +19,19 @@ class ScheduleDialogHelper { bool isEdit = false, String? code, }) { + bool temp; + if (schedule?.category == 'CUR_2') { + temp = schedule!.function.value == 'open' ? true : false; + } else { + temp = schedule!.function.value; + } final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) : TimeOfDay.now(); final initialDays = schedule != null ? _convertDaysStringToBooleans(schedule.days) : List.filled(7, false); - bool? functionOn = schedule?.function.value ?? true; + bool? functionOn = temp; TimeOfDay selectedTime = initialTime; List selectedDays = List.of(initialDays); From 01f55c14de682d1cccc1e061b93fa4bcaae7f3b7 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 29 Jun 2025 13:03:33 +0300 Subject: [PATCH 006/105] Add update events for device and subspace names implement copyWith methods in models --- .../device_managment_bloc.dart | 98 +++++++++++++++++++ .../device_managment_event.dart | 18 ++++ .../models/device_subspace.model.dart | 16 +++ .../all_devices/models/devices_model.dart | 68 +++++++++++++ .../widgets/device_managment_body.dart | 3 +- .../device_management_content.dart | 5 + .../device_setting/device_settings_panel.dart | 11 ++- 7 files changed, 216 insertions(+), 3 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 98b0c195..ecb2b207 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 @@ -21,6 +21,7 @@ class DeviceManagementBloc String currentProductName = ''; String? currentCommunity; String? currentUnitName; + String subSpaceName = ''; DeviceManagementBloc() : super(DeviceManagementInitial()) { on(_onFetchDevices); @@ -31,6 +32,8 @@ class DeviceManagementBloc on(_onResetFilters); on(_onResetSelectedDevices); on(_onUpdateSelection); + on(_onUpdateDeviceName); + on(_onUpdateSubSpaceName); } Future _onFetchDevices( @@ -343,5 +346,100 @@ class DeviceManagementBloc } } + void _onUpdateDeviceName( + UpdateDeviceName event, Emitter emit) { + _devices = _devices.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith(name: event.newName); + } + return device; + }).toList(); + + if (state is DeviceManagementLoaded) { + final loaded = state as DeviceManagementLoaded; + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: loaded.selectedIndex, + onlineCount: loaded.onlineCount, + offlineCount: loaded.offlineCount, + lowBatteryCount: loaded.lowBatteryCount, + selectedDevice: loaded.selectedDevice, + isControlButtonEnabled: loaded.isControlButtonEnabled, + )); + } else if (state is DeviceManagementFiltered) { + final filtered = state as DeviceManagementFiltered; + emit(DeviceManagementFiltered( + filteredDevices: _filteredDevices, + selectedIndex: filtered.selectedIndex, + onlineCount: filtered.onlineCount, + offlineCount: filtered.offlineCount, + lowBatteryCount: filtered.lowBatteryCount, + selectedDevice: filtered.selectedDevice, + isControlButtonEnabled: filtered.isControlButtonEnabled, + )); + } + } + + void _onUpdateSubSpaceName( + UpdateSubSpaceName event, Emitter emit) { + _devices = _devices.map((device) { + print('before update: ${device.subspace?.subspaceName}'); + if (device.uuid == event.deviceId) { + print('Updating subspace name for device: ${device.uuid}'); + print('New subspace name: ${event.newSubSpaceName}'); + final updatedSubspace = device.subspace?.copyWith( + subspaceName: event.newSubSpaceName, + ); + final s = device.copyWith(subspace: updatedSubspace); + + return s; + } + subSpaceName = device.subspace!.subspaceName; + + return device; + }).toList(); + print('After update:'); + for (final device in _devices) { + if (device.uuid == event.deviceId) { + print( + 'Device: ${device.uuid}, subspace: ${device.subspace?.uuid}, subspaceName: ${device.subspace?.subspaceName}', + ); + } + } + print('Subspace name updated to: $subSpaceName'); + if (state is DeviceManagementLoaded) { + final loaded = state as DeviceManagementLoaded; + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: loaded.selectedIndex, + onlineCount: loaded.onlineCount, + offlineCount: loaded.offlineCount, + lowBatteryCount: loaded.lowBatteryCount, + selectedDevice: loaded.selectedDevice, + isControlButtonEnabled: loaded.isControlButtonEnabled, + )); + } else if (state is DeviceManagementFiltered) { + // final filtered = state as DeviceManagementFiltered; + // emit(DeviceManagementFiltered( + // filteredDevices: _filteredDevices, + // selectedIndex: filtered.selectedIndex, + // onlineCount: filtered.onlineCount, + // offlineCount: filtered.offlineCount, + // lowBatteryCount: filtered.lowBatteryCount, + // selectedDevice: filtered.selectedDevice, + // isControlButtonEnabled: filtered.isControlButtonEnabled, + // )); + } + } + + void changeSubspaceName( + String deviceId, String newSubSpaceName, String subspaceId) { + add(UpdateSubSpaceName( + deviceId: deviceId, + newSubSpaceName: newSubSpaceName, + subspaceId: subspaceId, + )); + } + List get selectedDevices => _selectedDevices; } diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart index 5292de0e..e3b3acac 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart @@ -70,3 +70,21 @@ class UpdateSelection extends DeviceManagementEvent { const UpdateSelection(this.selectedRows); } + +class UpdateDeviceName extends DeviceManagementEvent { + final String deviceId; + final String newName; + + const UpdateDeviceName({required this.deviceId, required this.newName}); +} + +class UpdateSubSpaceName extends DeviceManagementEvent { + final String deviceId; + final String newSubSpaceName; + final String subspaceId; + + const UpdateSubSpaceName( + {required this.deviceId, + required this.newSubSpaceName, + required this.subspaceId}); +} diff --git a/lib/pages/device_managment/all_devices/models/device_subspace.model.dart b/lib/pages/device_managment/all_devices/models/device_subspace.model.dart index dc2386de..5d5f44bf 100644 --- a/lib/pages/device_managment/all_devices/models/device_subspace.model.dart +++ b/lib/pages/device_managment/all_devices/models/device_subspace.model.dart @@ -44,4 +44,20 @@ class DeviceSubspace { static List> listToJson(List subspaces) { return subspaces.map((subspace) => subspace.toJson()).toList(); } + + DeviceSubspace copyWith({ + String? uuid, + DateTime? createdAt, + DateTime? updatedAt, + String? subspaceName, + bool? disabled, + }) { + return DeviceSubspace( + uuid: uuid ?? this.uuid, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + subspaceName: subspaceName ?? this.subspaceName, + disabled: disabled ?? this.disabled, + ); + } } diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index e491214d..21fd1193 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -588,4 +588,72 @@ SOS "NCPS": DeviceType.NCPS, "PC": DeviceType.PC, }; + + AllDevicesModel copyWith({ + DevicesModelRoom? room, + DeviceSubspace? subspace, + DevicesModelUnit? unit, + DeviceCommunityModel? community, + String? productUuid, + String? productType, + String? permissionType, + int? activeTime, + String? category, + String? categoryName, + int? createTime, + String? gatewayId, + String? icon, + String? ip, + String? lat, + String? localKey, + String? lon, + String? model, + String? name, + String? nodeId, + bool? online, + String? ownerId, + bool? sub, + String? timeZone, + int? updateTime, + String? uuid, + int? batteryLevel, + String? productName, + List? spaces, + List? deviceTags, + DeviceSubSpace? deviceSubSpace, + }) { + return AllDevicesModel( + room: room ?? this.room, + subspace: subspace ?? this.subspace, + unit: unit ?? this.unit, + community: community ?? this.community, + productUuid: productUuid ?? this.productUuid, + productType: productType ?? this.productType, + permissionType: permissionType ?? this.permissionType, + activeTime: activeTime ?? this.activeTime, + category: category ?? this.category, + categoryName: categoryName ?? this.categoryName, + createTime: createTime ?? this.createTime, + gatewayId: gatewayId ?? this.gatewayId, + icon: icon ?? this.icon, + ip: ip ?? this.ip, + lat: lat ?? this.lat, + localKey: localKey ?? this.localKey, + lon: lon ?? this.lon, + model: model ?? this.model, + name: name ?? this.name, + nodeId: nodeId ?? this.nodeId, + online: online ?? this.online, + ownerId: ownerId ?? this.ownerId, + sub: sub ?? this.sub, + timeZone: timeZone ?? this.timeZone, + updateTime: updateTime ?? this.updateTime, + uuid: uuid ?? this.uuid, + batteryLevel: batteryLevel ?? this.batteryLevel, + productName: productName ?? this.productName, + spaces: spaces ?? this.spaces, + deviceTags: deviceTags ?? this.deviceTags, + deviceSubSpace: deviceSubSpace ?? this.deviceSubSpace, + ); + } } diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index f4baad0c..75dda30b 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 @@ -31,7 +31,6 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { int lowBatteryCount = 0; bool isControlButtonEnabled = false; List selectedDevices = []; - if (state is DeviceManagementLoaded) { devicesToShow = state.devices; selectedIndex = state.selectedIndex; @@ -223,7 +222,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .map((device) => device.uuid!) .toList(), isEmpty: devicesToShow.isEmpty, - onSettingsPressed: (rowIndex) { + onSettingsPressed: (rowIndex) async { final device = devicesToShow[rowIndex]; showDeviceSettingsSidebar(context, device); }, diff --git a/lib/pages/device_managment/device_setting/device_management_content.dart b/lib/pages/device_managment/device_setting/device_management_content.dart index a087e5bb..c2cdb9a3 100644 --- a/lib/pages/device_managment/device_setting/device_management_content.dart +++ b/lib/pages/device_managment/device_setting/device_management_content.dart @@ -87,6 +87,11 @@ class DeviceManagementContent extends StatelessWidget { ), ); }); + + context.read().add(UpdateSubSpaceName( + subspaceId: selectedSubSpace.id!, + deviceId: device.uuid!, + newSubSpaceName: selectedSubSpace.name ?? '')); } }, child: infoRow( diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart index 48458b3b..0436b971 100644 --- a/lib/pages/device_managment/device_setting/device_settings_panel.dart +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -1,6 +1,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/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart'; @@ -134,8 +135,16 @@ class DeviceSettingsPanel extends StatelessWidget { onFieldSubmitted: (value) { _bloc.add(const ChangeNameEvent( value: false)); + context + .read< + DeviceManagementBloc>() + .add(UpdateDeviceName( + deviceId: device.uuid!, + newName: _bloc + .nameController + .text)); }, - decoration: InputDecoration( + decoration:const InputDecoration( isDense: true, contentPadding: EdgeInsets.zero, border: InputBorder.none, From 814cbf787f1cd3ebeac588b5621d3c189be9cbdb Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 13:58:57 +0300 Subject: [PATCH 007/105] edit the UI as wanted in ticket (note: in figma is not updated yet to the requested ticket) --- .../view/visitor_password_dialog.dart | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index d1fb172a..f2c85169 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -602,8 +602,9 @@ class VisitorPasswordDialog extends StatelessWidget { } else { return AlertDialog( alignment: Alignment.center, + backgroundColor: Colors.white, content: SizedBox( - height: size.height * 0.25, + height: size.height * 0.13, child: Column( children: [ Column( @@ -617,13 +618,16 @@ class VisitorPasswordDialog extends StatelessWidget { width: 35, ), ), + const SizedBox( + height: 20, + ), Text( 'Set Password', style: Theme.of(context) .textTheme .headlineLarge! .copyWith( - fontSize: 30, + fontSize: 24, fontWeight: FontWeight.w400, color: Colors.black, ), @@ -631,15 +635,6 @@ class VisitorPasswordDialog extends StatelessWidget { ], ), const SizedBox(width: 15), - Text( - 'This action will update all of the selected\n door locks passwords in the property.\n\nAre you sure you want to continue?', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.grayColor, - fontWeight: FontWeight.w400, - fontSize: 18, - ), - ), ], ), ), @@ -668,6 +663,7 @@ class VisitorPasswordDialog extends StatelessWidget { decoration: containerDecoration, width: size.width * 0.1, child: DefaultButton( + backgroundColor: Color(0xff023DFE), borderRadius: 8, onPressed: () { Navigator.pop(context); @@ -715,7 +711,7 @@ class VisitorPasswordDialog extends StatelessWidget { } }, child: Text( - 'Ok', + 'Confirm', style: Theme.of(context).textTheme.bodySmall!.copyWith( fontWeight: FontWeight.w400, color: ColorsManager.whiteColors, From e0cfe541dded683cda71eff0b481866f412f4233 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 29 Jun 2025 14:13:25 +0300 Subject: [PATCH 008/105] name changes in table when changed. --- .../device_managment_bloc.dart | 28 +++++++++---------- .../widgets/device_managment_body.dart | 1 + .../device_setting/device_settings_panel.dart | 18 ++++++++---- 3 files changed, 26 insertions(+), 21 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 ecb2b207..4c3e5b39 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 @@ -348,17 +348,27 @@ class DeviceManagementBloc void _onUpdateDeviceName( UpdateDeviceName event, Emitter emit) { - _devices = _devices.map((device) { + final devices = _devices.map((device) { if (device.uuid == event.deviceId) { return device.copyWith(name: event.newName); } return device; }).toList(); + final filteredDevices = _filteredDevices.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith(name: event.newName); + } + return device; + }).toList(); + + _devices = devices; + _filteredDevices = filteredDevices; + if (state is DeviceManagementLoaded) { final loaded = state as DeviceManagementLoaded; emit(DeviceManagementLoaded( - devices: _devices, + devices: devices, selectedIndex: loaded.selectedIndex, onlineCount: loaded.onlineCount, offlineCount: loaded.offlineCount, @@ -369,7 +379,7 @@ class DeviceManagementBloc } else if (state is DeviceManagementFiltered) { final filtered = state as DeviceManagementFiltered; emit(DeviceManagementFiltered( - filteredDevices: _filteredDevices, + filteredDevices: filteredDevices, selectedIndex: filtered.selectedIndex, onlineCount: filtered.onlineCount, offlineCount: filtered.offlineCount, @@ -383,10 +393,7 @@ class DeviceManagementBloc void _onUpdateSubSpaceName( UpdateSubSpaceName event, Emitter emit) { _devices = _devices.map((device) { - print('before update: ${device.subspace?.subspaceName}'); if (device.uuid == event.deviceId) { - print('Updating subspace name for device: ${device.uuid}'); - print('New subspace name: ${event.newSubSpaceName}'); final updatedSubspace = device.subspace?.copyWith( subspaceName: event.newSubSpaceName, ); @@ -398,15 +405,6 @@ class DeviceManagementBloc return device; }).toList(); - print('After update:'); - for (final device in _devices) { - if (device.uuid == event.deviceId) { - print( - 'Device: ${device.uuid}, subspace: ${device.subspace?.uuid}, subspaceName: ${device.subspace?.subspaceName}', - ); - } - } - print('Subspace name updated to: $subSpaceName'); if (state is DeviceManagementLoaded) { final loaded = state as DeviceManagementLoaded; emit(DeviceManagementLoaded( 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 75dda30b..8d108671 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 @@ -254,6 +254,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { child: DeviceSettingsPanel( device: device, onClose: () => Navigator.of(context).pop(), + deviceManagementBloc: context.read(), ), ), ), diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart index 0436b971..96e48f11 100644 --- a/lib/pages/device_managment/device_setting/device_settings_panel.dart +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -3,12 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -18,7 +18,13 @@ import 'package:syncrow_web/web_layout/default_container.dart'; class DeviceSettingsPanel extends StatelessWidget { final VoidCallback? onClose; final AllDevicesModel device; - const DeviceSettingsPanel({super.key, this.onClose, required this.device}); + final DeviceManagementBloc deviceManagementBloc; + const DeviceSettingsPanel({ + super.key, + this.onClose, + required this.device, + required this.deviceManagementBloc, + }); @override Widget build(BuildContext context) { @@ -72,10 +78,10 @@ class DeviceSettingsPanel extends StatelessWidget { 'Device Settings', style: context.theme.textTheme.titleLarge! .copyWith( - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w700, color: ColorsManager.vividBlue .withOpacity(0.7), - fontSize: 24), + fontSize: 24), ), ], ), @@ -166,7 +172,7 @@ class DeviceSettingsPanel extends StatelessWidget { onTap: () { _bloc.add( const ChangeNameEvent( - value: true)); + value: true)); }, child: SvgPicture.asset( Assets From 8916efcebb43ee235ddbda857035692960d2eb4e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 29 Jun 2025 15:39:30 +0300 Subject: [PATCH 009/105] fixed aqi filter bugs. --- .../air_quality_distribution_bloc.dart | 6 +++--- .../blocs/range_of_aqi/range_of_aqi_bloc.dart | 2 +- .../widgets/aqi_distribution_chart_title.dart | 1 + .../widgets/aqi_type_dropdown.dart | 20 +++++++++---------- .../widgets/range_of_aqi_chart_title.dart | 6 +++--- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart index 40d51d2b..455dff23 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -46,11 +46,11 @@ class AirQualityDistributionBloc } } - Future _onClearAirQualityDistribution( + void _onClearAirQualityDistribution( ClearAirQualityDistribution event, Emitter emit, - ) async { - emit(const AirQualityDistributionState()); + ) { + emit(AirQualityDistributionState(selectedAqiType: state.selectedAqiType)); } void _onUpdateAqiTypeEvent( diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart index 88c3715e..326a87a2 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -75,6 +75,6 @@ class RangeOfAqiBloc extends Bloc { ClearRangeOfAqiEvent event, Emitter emit, ) { - emit(const RangeOfAqiState()); + emit(RangeOfAqiState(selectedAqiType: state.selectedAqiType)); } } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index f7be6ee3..7b6b113a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -34,6 +34,7 @@ class AqiDistributionChartTitle extends StatelessWidget { alignment: AlignmentDirectional.centerEnd, fit: BoxFit.scaleDown, child: AqiTypeDropdown( + selectedAqiType: context.watch().state.selectedAqiType, onChanged: (value) { if (value != null) { final bloc = context.read(); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 6640c717..8233fe5a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -18,19 +18,20 @@ enum AqiType { } class AqiTypeDropdown extends StatefulWidget { - const AqiTypeDropdown({super.key, required this.onChanged}); + const AqiTypeDropdown({ + required this.onChanged, + this.selectedAqiType, + super.key, + }); final ValueChanged onChanged; + final AqiType? selectedAqiType; @override State createState() => _AqiTypeDropdownState(); } class _AqiTypeDropdownState extends State { - AqiType? _selectedItem = AqiType.aqi; - - void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); - @override Widget build(BuildContext context) { return Container( @@ -41,8 +42,8 @@ class _AqiTypeDropdownState extends State { width: 1, ), ), - child: DropdownButton( - value: _selectedItem, + child: DropdownButton( + value: widget.selectedAqiType, isDense: true, borderRadius: BorderRadius.circular(16), dropdownColor: ColorsManager.whiteColors, @@ -59,10 +60,7 @@ class _AqiTypeDropdownState extends State { items: AqiType.values .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) .toList(), - onChanged: (value) { - _updateSelectedItem(value); - widget.onChanged(value); - }, + onChanged: widget.onChanged, ), ); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 1b0da288..421fbb13 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -63,15 +63,15 @@ class RangeOfAqiChartTitle extends StatelessWidget { fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AqiTypeDropdown( + selectedAqiType: context.watch().state.selectedAqiType, onChanged: (value) { final spaceTreeState = context.read().state; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; - - if (spaceUuid == null) return; - if (value != null) { context.read().add(UpdateAqiTypeEvent(value)); } + + if (spaceUuid == null) return; }, ), ), From 354d61dfa24dbf62d627ee92c8e9444ee0aeca8d Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 15:50:37 +0300 Subject: [PATCH 010/105] UI Enhancement --- .../widgets/accurate_calibrating_dialog.dart | 4 +- .../widgets/accurate_calibration_dialog.dart | 4 +- .../widgets/accurate_dialog_widget.dart | 131 ++++++++++-------- .../widgets/calibrate_completed_dialog.dart | 96 +++++++------ .../widgets/normal_text_body_for_dialog.dart | 86 +++++++++--- .../widgets/number_input_textfield.dart | 2 +- .../widgets/pref_revers_card_widget.dart | 2 +- .../widgets/prefrences_dialog.dart | 6 +- .../widgets/quick_calibrating_dialog.dart | 102 ++++++++------ .../widgets/quick_calibration_dialog.dart | 4 +- lib/utils/color_manager.dart | 4 +- lib/utils/constants/assets.dart | 4 +- 12 files changed, 266 insertions(+), 179 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart index 54107420..64044b94 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart @@ -23,8 +23,8 @@ class AccurteCalibratingDialog extends StatelessWidget { body: const NormalTextBodyForDialog( title: '', step1: - '1. Click Close Button to make the Curtain run to Full Close and Position.', - step2: '2. click Next to complete the Calibration.', + 'Click Close Button to make the Curtain run to Full Close and Position.', + step2: 'click Next to complete the Calibration.', ), leftOnTap: () => Navigator.of(parentContext).pop(), rightOnTap: () { diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart index a9d1b010..997e70cf 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart @@ -20,8 +20,8 @@ class AccurateCalibrationDialog extends StatelessWidget { title: 'Accurate Calibration', body: const NormalTextBodyForDialog( title: 'Prepare Calibration:', - step1: '1. Run The Curtain to the Fully Open Position,and pause.', - step2: '2. click Next to Start accurate calibration.', + step1: 'Run The Curtain to the Fully Open Position,and pause.', + step2: 'click Next to Start accurate calibration.', ), leftOnTap: () => Navigator.of(parentContext).pop(), rightOnTap: () { diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart index 5be376ae..433608ac 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -17,78 +17,87 @@ class AccurateDialogWidget extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: 300, - width: 400, + height: 250, + width: 500, child: Column( children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Text( - title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: ColorsManager.blueColor, - ), + Expanded( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.dialogBlueTitle, + ), + ), + ), + const Divider( + indent: 60, + endIndent: 60, + ), + ], ), ), - const SizedBox(height: 5), - const Divider( - indent: 10, - endIndent: 10, - ), - Padding( - padding: const EdgeInsets.all(10), + Expanded( child: body, ), - const SizedBox(height: 20), - const Spacer(), - const Divider(), - Row( - children: [ - Expanded( - child: InkWell( - onTap: leftOnTap, - child: Container( - height: 60, - alignment: Alignment.center, - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: ColorsManager.grayBorder, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Divider(), + Row( + children: [ + Expanded( + child: InkWell( + onTap: leftOnTap, + child: Container( + height: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.grayBorder), + ), ), ), ), - child: const Text( - 'Cancel', - style: TextStyle(color: ColorsManager.grayBorder), - ), - ), - ), - ), - Expanded( - child: InkWell( - onTap: rightOnTap, - child: Container( - height: 60, - alignment: Alignment.center, - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: ColorsManager.grayBorder, + Expanded( + child: InkWell( + onTap: rightOnTap, + child: Container( + height: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Next', + style: TextStyle( + color: ColorsManager.blueColor, + ), + ), ), ), - ), - child: const Text( - 'Next', - style: TextStyle( - color: ColorsManager.blueColor, - ), - ), - ), - ), - ) - ], + ) + ], + ) + ], + ), ) ], ), diff --git a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart index 9b2b5ea9..bd0cbb37 100644 --- a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class CalibrateCompletedDialog extends StatelessWidget { final BuildContext parentContext; @@ -21,52 +23,62 @@ class CalibrateCompletedDialog extends StatelessWidget { width: 400, child: Column( children: [ - const Padding( - padding: EdgeInsets.all(10), - child: Text( - 'Calibration Completed', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: ColorsManager.blueColor, - ), + Expanded( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + 'Calibration Completed', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.dialogBlueTitle, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + ], ), ), - const SizedBox(height: 5), - const Divider( - indent: 10, - endIndent: 10, + Expanded( + child: SvgPicture.asset(Assets.completedDoneIcon), ), - const Icon( - Icons.check_circle, - size: 100, - color: ColorsManager.blueColor, - ), - const Spacer(), - const Divider( - indent: 10, - endIndent: 10, - ), - InkWell( - onTap: () { - parentContext.read().add( - FetchCurtainModuleStatusEvent( - deviceId: deviceId, - ), - ); - Navigator.of(parentContext).pop(); - Navigator.of(parentContext).pop(); - }, - child: Container( - height: 40, - width: double.infinity, - alignment: Alignment.center, - child: const Text( - 'Close', - style: TextStyle( - color: ColorsManager.grayBorder, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Divider( + indent: 10, + endIndent: 10, ), - ), + InkWell( + onTap: () { + parentContext.read().add( + FetchCurtainModuleStatusEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + Navigator.of(parentContext).pop(); + }, + child: Container( + height: 40, + width: double.infinity, + alignment: Alignment.center, + child: const Text( + 'Close', + style: TextStyle( + color: ColorsManager.grayBorder, + ), + ), + ), + ) + ], ), ) ], diff --git a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart index 8818cb7b..c322fe9d 100644 --- a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart @@ -15,28 +15,72 @@ class NormalTextBodyForDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - color: ColorsManager.grayColor, + return Padding( + padding: EdgeInsetsGeometry.only(left: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title.isEmpty) + const SizedBox() + else + Expanded( + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 17, + ), + ), + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 10, + ), + const Text('1. ', + style: TextStyle( + color: ColorsManager.grayColor, + fontSize: 17, + )), + SizedBox( + width: 450, + child: Text( + step1, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 17, + ), + ), + ), + ], + ), ), - ), - Text( - step1, - style: const TextStyle( - color: ColorsManager.grayColor, - ), - ), - Text( - step2, - style: const TextStyle( - color: ColorsManager.grayColor, - ), - ) - ], + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 10, + ), + const Text('2. ', + style: TextStyle( + color: ColorsManager.grayColor, + fontSize: 17, + )), + Text( + step2, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 17, + ), + ), + ], + ), + ) + ], + ), ); } } diff --git a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart index ea95f838..428f6531 100644 --- a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart +++ b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart @@ -19,7 +19,7 @@ class NumberInputField extends StatelessWidget { contentPadding: EdgeInsets.zero, ), style: const TextStyle( - fontSize: 20, + fontSize: 15, color: ColorsManager.blackColor, ), ); diff --git a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart index 81912e80..85c45d27 100644 --- a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart @@ -18,7 +18,7 @@ class PrefReversCardWidget extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultContainer( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart index 1e4f932c..35844c05 100644 --- a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -23,12 +23,12 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { Widget build(_) { return AlertDialog( backgroundColor: ColorsManager.CircleImageBackground, - contentPadding: const EdgeInsets.all(30), - title: const Center( + contentPadding: const EdgeInsets.all(20), + title: Center( child: Text( 'Preferences', style: TextStyle( - color: ColorsManager.blueColor, + color: ColorsManager.dialogBlueTitle, fontSize: 24, fontWeight: FontWeight.bold, ), diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart index 0b86c96e..8514d432 100644 --- a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart @@ -69,49 +69,71 @@ class _QuickCalibratingDialogState extends State { body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '1. please Enter the Travel Time:', - style: TextStyle(color: ColorsManager.grayBorder), - ), - const SizedBox(height: 10), - Container( - width: 150, - height: 40, - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: NumberInputField(controller: _controller), - ), - const Expanded( - child: Text( - 'seconds', - style: TextStyle( - fontSize: 15, - color: ColorsManager.blueColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - if (_errorText != null) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _errorText!, - style: const TextStyle( - color: ColorsManager.red, - fontSize: 14, + const Expanded( + child: Align( + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.only(right: 75), + child: Text( + '1.please Enter the Travel Time:', + style: TextStyle(color: ColorsManager.lightGrayColor), ), ), ), + ), + Expanded( + child: Align( + alignment: Alignment.center, + child: Container( + width: 110, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: NumberInputField(controller: _controller), + ), + Expanded( + child: Text( + 'seconds', + style: TextStyle( + fontSize: 12, + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ), + ), + if (_errorText != null) + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _errorText!, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 14, + ), + ), + ), + ), + const Expanded( + child: Align( + alignment: Alignment.center, + child: Text( + '2.click Next to Complete the calibration', + style: TextStyle(color: ColorsManager.lightGrayColor), + ), + ), + ) ], ), leftOnTap: () => Navigator.of(widget.parentContext).pop(), diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart index 803d904f..6c776293 100644 --- a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart @@ -23,8 +23,8 @@ class QuickCalibrationDialog extends StatelessWidget { body: const NormalTextBodyForDialog( title: 'Prepare Calibration:', step1: - '1. Confirm that the curtain is in the fully closed and suspended state.', - step2: '2. click Next to Start calibration.', + 'Confirm that the curtain is in the fully closed and suspended state.', + step2: 'click Next to Start calibration.', ), leftOnTap: () => Navigator.of(parentContext).pop(), rightOnTap: () { diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 50170ed9..40fca1fa 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -83,7 +83,5 @@ abstract class ColorsManager { static const Color maxPurpleDot = Color(0xFF5F00BD); static const Color minBlue = Color(0xFF93AAFD); static const Color minBlueDot = Color(0xFF023DFE); - static const Color grey25 = Color(0xFFF9F9F9); - - + static const Color grey25 = Color(0xFFF9F9F9); } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 8979c446..821df6e3 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -394,6 +394,7 @@ class Assets { static const String emptyBox = 'assets/icons/empty_box.png'; static const String completeProcessIcon = 'assets/icons/compleate_process_icon.svg'; + static const String completedDoneIcon = 'assets/images/completed_done.svg'; static const String currentProcessIcon = 'assets/icons/current_process_icon.svg'; static const String uncomplete_ProcessIcon = @@ -505,5 +506,6 @@ class Assets { static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg'; static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; - static const String autocadOccupancyImage = 'assets/images/autocad_occupancy_image.png'; + static const String autocadOccupancyImage = + 'assets/images/autocad_occupancy_image.png'; } From d8bb234537434b261ce5565b3f5b3f2811aeec5a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 29 Jun 2025 16:00:15 +0300 Subject: [PATCH 011/105] SP-1771 --- .../device_managment_bloc.dart | 157 +++++++++++------- .../widgets/device_managment_body.dart | 5 +- .../device_management_content.dart | 5 +- .../device_setting/device_settings_panel.dart | 10 +- 4 files changed, 108 insertions(+), 69 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 b7f04a58..da039a8e 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -16,7 +16,7 @@ class DeviceManagementBloc int _onlineCount = 0; int _offlineCount = 0; int _lowBatteryCount = 0; - List _selectedDevices = []; + final List _selectedDevices = []; List _filteredDevices = []; String currentProductName = ''; String? currentCommunity; @@ -40,15 +40,15 @@ class DeviceManagementBloc FetchDevices event, Emitter emit) async { emit(DeviceManagementLoading()); try { - List devices = []; + var devices = []; _devices.clear(); - var spaceBloc = event.context.read(); + final spaceBloc = event.context.read(); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (spaceBloc.state.selectedCommunities.isEmpty) { devices = await DevicesManagementApi().fetchDevices(projectUuid); } else { - for (var community in spaceBloc.state.selectedCommunities) { - List spacesList = + for (final community in spaceBloc.state.selectedCommunities) { + final spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; devices.addAll(await DevicesManagementApi() .fetchDevices(projectUuid, spacesId: spacesList)); @@ -73,7 +73,7 @@ class DeviceManagementBloc } } - void _onFilterDevices( + Future _onFilterDevices( FilterDevices event, Emitter emit) async { if (_devices.isNotEmpty) { _filteredDevices = List.from(_devices.where((device) { @@ -155,8 +155,7 @@ class DeviceManagementBloc add(FilterDevices(_getFilterFromIndex(_selectedIndex))); } - void _onSelectDevice( - SelectDevice event, Emitter emit) { + void _onSelectDevice(SelectDevice event, Emitter emit) { final selectedUuid = event.selectedDevice.uuid; if (_selectedDevices.any((device) => device.uuid == selectedUuid)) { @@ -165,9 +164,9 @@ class DeviceManagementBloc _selectedDevices.add(event.selectedDevice); } - List clonedSelectedDevices = List.from(_selectedDevices); + final clonedSelectedDevices = List.from(_selectedDevices); - bool isControlButtonEnabled = + final isControlButtonEnabled = _checkIfControlButtonEnabled(clonedSelectedDevices); if (state is DeviceManagementLoaded) { @@ -197,8 +196,8 @@ class DeviceManagementBloc void _onUpdateSelection( UpdateSelection event, Emitter emit) { - List selectedDevices = []; - List devicesToSelectFrom = []; + final selectedDevices = []; + var devicesToSelectFrom = []; if (state is DeviceManagementLoaded) { devicesToSelectFrom = (state as DeviceManagementLoaded).devices; @@ -206,7 +205,7 @@ class DeviceManagementBloc devicesToSelectFrom = (state as DeviceManagementFiltered).filteredDevices; } - for (int i = 0; i < event.selectedRows.length; i++) { + for (var i = 0; i < event.selectedRows.length; i++) { if (event.selectedRows[i]) { selectedDevices.add(devicesToSelectFrom[i]); } @@ -252,8 +251,7 @@ class DeviceManagementBloc _onlineCount = _devices.where((device) => device.online == true).length; _offlineCount = _devices.where((device) => device.online == false).length; _lowBatteryCount = _devices - .where((device) => - device.batteryLevel != null && device.batteryLevel! < 20) + .where((device) => device.batteryLevel != null && device.batteryLevel! < 20) .length; } @@ -270,8 +268,7 @@ class DeviceManagementBloc } } - void _onSearchDevices( - SearchDevices event, Emitter emit) { + void _onSearchDevices(SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) && (event.deviceNameOrProductName == null || @@ -300,7 +297,7 @@ class DeviceManagementBloc currentCommunity = event.community; currentUnitName = event.unitName; - List devicesToSearch = _devices; + final devicesToSearch = _devices; if (devicesToSearch.isNotEmpty) { final searchText = event.deviceNameOrProductName?.toLowerCase() ?? ''; @@ -347,14 +344,84 @@ class DeviceManagementBloc UpdateDeviceName event, Emitter emit) { final devices = _devices.map((device) { if (device.uuid == event.deviceId) { - return device.copyWith(name: event.newName); + final modifiedDevice = device.copyWith(name: event.newName); + _selectedDevices.removeWhere((device) => device.uuid == event.deviceId); + _selectedDevices.add(modifiedDevice); + return modifiedDevice; } return device; }).toList(); final filteredDevices = _filteredDevices.map((device) { if (device.uuid == event.deviceId) { - return device.copyWith(name: event.newName); + final modifiedDevice = device.copyWith(name: event.newName); + _selectedDevices.removeWhere((device) => device.uuid == event.deviceId); + _selectedDevices.add(modifiedDevice); + return modifiedDevice; + } + return device; + }).toList(); + + _devices = devices; + _filteredDevices = filteredDevices; + + + + if (state is DeviceManagementLoaded) { + final loaded = state as DeviceManagementLoaded; + final selectedDevices01 = _selectedDevices.map((device) { + if (device.uuid == event.deviceId) { + final modifiedDevice = device.copyWith(name: event.newName); + return modifiedDevice; + } + return device; + }).toList(); + emit(DeviceManagementLoaded( + devices: devices, + selectedIndex: loaded.selectedIndex, + onlineCount: loaded.onlineCount, + offlineCount: loaded.offlineCount, + lowBatteryCount: loaded.lowBatteryCount, + selectedDevice: selectedDevices01, + isControlButtonEnabled: loaded.isControlButtonEnabled, + )); + } else if (state is DeviceManagementFiltered) { + final filtered = state as DeviceManagementFiltered; + final selectedDevices01 = filtered.selectedDevice?.map((device) { + if (device.uuid == event.deviceId) { + final modifiedDevice = device.copyWith(name: event.newName); + return modifiedDevice; + } + return device; + }).toList(); + emit(DeviceManagementFiltered( + filteredDevices: filteredDevices, + selectedIndex: filtered.selectedIndex, + onlineCount: filtered.onlineCount, + offlineCount: filtered.offlineCount, + lowBatteryCount: filtered.lowBatteryCount, + selectedDevice: selectedDevices01, + isControlButtonEnabled: filtered.isControlButtonEnabled, + )); + } + } + + void _onUpdateSubSpaceName( + UpdateSubSpaceName event, Emitter emit) { + final devices = _devices.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith( + subspace: + device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); + } + return device; + }).toList(); + + final filteredDevices = _filteredDevices.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith( + subspace: + device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); } return device; }).toList(); @@ -364,53 +431,21 @@ class DeviceManagementBloc if (state is DeviceManagementLoaded) { final loaded = state as DeviceManagementLoaded; - emit(DeviceManagementLoaded( - devices: devices, - selectedIndex: loaded.selectedIndex, - onlineCount: loaded.onlineCount, - offlineCount: loaded.offlineCount, - lowBatteryCount: loaded.lowBatteryCount, - selectedDevice: loaded.selectedDevice, - isControlButtonEnabled: loaded.isControlButtonEnabled, - )); - } else if (state is DeviceManagementFiltered) { - final filtered = state as DeviceManagementFiltered; - emit(DeviceManagementFiltered( - filteredDevices: filteredDevices, - selectedIndex: filtered.selectedIndex, - onlineCount: filtered.onlineCount, - offlineCount: filtered.offlineCount, - lowBatteryCount: filtered.lowBatteryCount, - selectedDevice: filtered.selectedDevice, - isControlButtonEnabled: filtered.isControlButtonEnabled, - )); - } - } - - void _onUpdateSubSpaceName( - UpdateSubSpaceName event, Emitter emit) { - _devices = _devices.map((device) { - if (device.uuid == event.deviceId) { - final updatedSubspace = device.subspace?.copyWith( - subspaceName: event.newSubSpaceName, - ); - final s = device.copyWith(subspace: updatedSubspace); - - return s; - } - subSpaceName = device.subspace!.subspaceName; - - return device; - }).toList(); - if (state is DeviceManagementLoaded) { - final loaded = state as DeviceManagementLoaded; + final selectedDevices = loaded.selectedDevice?.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith( + subspace: + device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); + } + return device; + }).toList(); emit(DeviceManagementLoaded( devices: _devices, selectedIndex: loaded.selectedIndex, onlineCount: loaded.onlineCount, offlineCount: loaded.offlineCount, lowBatteryCount: loaded.lowBatteryCount, - selectedDevice: loaded.selectedDevice, + selectedDevice: selectedDevices, isControlButtonEnabled: loaded.isControlButtonEnabled, )); } else if (state is DeviceManagementFiltered) { 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 b4eb60e6..7ca33d1d 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -23,6 +23,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { @override Widget build(BuildContext context) { return BlocBuilder( + buildWhen: (previous, current) => previous != current, builder: (context, state) { List devicesToShow = []; int selectedIndex = 0; @@ -191,7 +192,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Product Name', 'Device ID', 'Space Name', - 'location', + 'Location', 'Battery Level', 'Installation Date and Time', 'Status', @@ -265,7 +266,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { barrierDismissible: true, barrierLabel: "Device Settings", transitionDuration: const Duration(milliseconds: 300), - pageBuilder: (context, anim1, anim2) { + pageBuilder: (_, anim1, anim2) { return Align( alignment: Alignment.centerRight, child: Material( diff --git a/lib/pages/device_managment/device_setting/device_management_content.dart b/lib/pages/device_managment/device_setting/device_management_content.dart index c2cdb9a3..c73899fc 100644 --- a/lib/pages/device_managment/device_setting/device_management_content.dart +++ b/lib/pages/device_managment/device_setting/device_management_content.dart @@ -19,11 +19,14 @@ class DeviceManagementContent extends StatelessWidget { required this.device, required this.subSpaces, required this.deviceInfo, + required this.deviceManagementBloc, }); final AllDevicesModel device; final List subSpaces; final DeviceInfoModel deviceInfo; + final DeviceManagementBloc deviceManagementBloc; + @override Widget build(BuildContext context) { @@ -88,7 +91,7 @@ class DeviceManagementContent extends StatelessWidget { ); }); - context.read().add(UpdateSubSpaceName( + deviceManagementBloc.add(UpdateSubSpaceName( subspaceId: selectedSubSpace.id!, deviceId: device.uuid!, newSubSpaceName: selectedSubSpace.name ?? '')); diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart index 96e48f11..0856b5d0 100644 --- a/lib/pages/device_managment/device_setting/device_settings_panel.dart +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; @@ -141,14 +142,12 @@ class DeviceSettingsPanel extends StatelessWidget { onFieldSubmitted: (value) { _bloc.add(const ChangeNameEvent( value: false)); - context - .read< - DeviceManagementBloc>() - .add(UpdateDeviceName( + deviceManagementBloc + ..add(UpdateDeviceName( deviceId: device.uuid!, newName: _bloc .nameController - .text)); + .text))..add(ResetSelectedDevices()); }, decoration:const InputDecoration( isDense: true, @@ -205,6 +204,7 @@ class DeviceSettingsPanel extends StatelessWidget { device: device, subSpaces: subSpaces.cast(), deviceInfo: deviceInfo, + deviceManagementBloc: deviceManagementBloc, ), const SizedBox(height: 32), RemoveDeviceWidget(bloc: _bloc), From e87dffd76bd3e9c98bd473b40a910ecfdf49064b Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 08:28:19 +0300 Subject: [PATCH 012/105] when it is CUR module there is no countdown and other selector --- .../schedule_device/schedule_widgets/schedual_view.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index c511b8bd..52a5c56f 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 @@ -52,9 +52,12 @@ class BuildScheduleView extends StatelessWidget { children: [ const ScheduleHeader(), const SizedBox(height: 20), - ScheduleModeSelector( - currentMode: state.scheduleMode, - ), + if (category == 'CUR_2') + const SizedBox() + else + ScheduleModeSelector( + currentMode: state.scheduleMode, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) ScheduleManagementUI( From d4625a8f0484b7dafc9605793785d24bbe42f811 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 08:45:18 +0300 Subject: [PATCH 013/105] fix edit to accept string of cur module --- .../schedule_widgets/schedule_table.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 21f404ff..213afd61 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 @@ -212,12 +212,21 @@ class _ScheduleTableView extends StatelessWidget { isEdit: true, ).then((updatedSchedule) { if (updatedSchedule != null) { + bool temp; + if (schedule.category == 'CUR_2') { + updatedSchedule.function.value == 'open' + ? temp = true + : temp = false; + } else { + temp = updatedSchedule.function.value; + } context.read().add( ScheduleEditEvent( scheduleId: schedule.scheduleId, category: schedule.category, time: updatedSchedule.time, - functionOn: updatedSchedule.function.value, + functionOn: temp, + // updatedSchedule.function.value, selectedDays: updatedSchedule.days), ); } From 0cfd58d82083c1ef3c017bd11260228f71109a53 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 08:56:42 +0300 Subject: [PATCH 014/105] fix to send fit data to integrate with API (was true and false)now cur module send close and open with control key --- .../all_devices/models/device_status.dart | 4 ++-- .../schedule_device/bloc/schedule_bloc.dart | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/all_devices/models/device_status.dart b/lib/pages/device_managment/all_devices/models/device_status.dart index b78f2a30..b3d582f1 100644 --- a/lib/pages/device_managment/all_devices/models/device_status.dart +++ b/lib/pages/device_managment/all_devices/models/device_status.dart @@ -35,8 +35,8 @@ class DeviceStatus { } class Status { - final String code; - final dynamic value; + String code; + dynamic value; Status({ required this.code, diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 0ec55e39..f84f95e7 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -286,11 +286,20 @@ class ScheduleBloc extends Bloc { try { if (state is ScheduleLoaded) { final dateTime = DateTime.parse(event.time); + Status status = Status(code: '', value: ''); + if (event.category == 'CUR_2') { + status.code = 'control'; + status.value = event.functionOn == true ? 'open' : 'close'; + } else { + status.code = event.category; + status.value = event.functionOn; + } final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, time: getTimeStampWithoutSeconds(dateTime).toString(), - function: Status(code: event.category, value: event.functionOn), + function: status, + // Status(code: event.category, value: event.functionOn), days: event.selectedDays, ); final success = await DevicesManagementApi().editScheduleRecord( From 32938404dd65d20e8d3f8e3ce2b7d9baf020a889 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 09:28:09 +0300 Subject: [PATCH 015/105] PR requested changes --- .../all_devices/models/device_status.dart | 14 ++++++++++++-- .../schedule_device/bloc/schedule_bloc.dart | 10 +++++----- .../schedule_widgets/schedule_table.dart | 1 - 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/pages/device_managment/all_devices/models/device_status.dart b/lib/pages/device_managment/all_devices/models/device_status.dart index b3d582f1..f4efe36b 100644 --- a/lib/pages/device_managment/all_devices/models/device_status.dart +++ b/lib/pages/device_managment/all_devices/models/device_status.dart @@ -35,8 +35,8 @@ class DeviceStatus { } class Status { - String code; - dynamic value; + final String code; + final dynamic value; Status({ required this.code, @@ -57,6 +57,16 @@ class Status { }; } + Status copyWith({ + String? code, + dynamic value, + }) { + return Status( + code: code ?? this.code, + value: value ?? this.value, + ); + } + factory Status.fromJson(String source) => Status.fromMap(json.decode(source)); String toJson() => json.encode(toMap()); 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 f84f95e7..0db1445f 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -288,18 +288,18 @@ class ScheduleBloc extends Bloc { final dateTime = DateTime.parse(event.time); Status status = Status(code: '', value: ''); if (event.category == 'CUR_2') { - status.code = 'control'; - status.value = event.functionOn == true ? 'open' : 'close'; + status = status.copyWith( + code: 'control', + value: event.functionOn == true ? 'open' : 'close'); } else { - status.code = event.category; - status.value = event.functionOn; + status = + status.copyWith(code: event.category, value: event.functionOn); } final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, time: getTimeStampWithoutSeconds(dateTime).toString(), function: status, - // Status(code: event.category, value: event.functionOn), days: event.selectedDays, ); final success = await DevicesManagementApi().editScheduleRecord( 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 213afd61..84d8e1f5 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 @@ -226,7 +226,6 @@ class _ScheduleTableView extends StatelessWidget { category: schedule.category, time: updatedSchedule.time, functionOn: temp, - // updatedSchedule.function.value, selectedDays: updatedSchedule.days), ); } From 4d51321675605f50691287e198145d0f379ddd29 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 10:05:15 +0300 Subject: [PATCH 016/105] add the new devices to mapIconToProduct func --- .../spaces_management/all_spaces/model/product_model.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pages/spaces_management/all_spaces/model/product_model.dart b/lib/pages/spaces_management/all_spaces/model/product_model.dart index 8f905032..a7b23d0f 100644 --- a/lib/pages/spaces_management/all_spaces/model/product_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/product_model.dart @@ -58,11 +58,14 @@ class ProductModel { '3G': Assets.Gang3SwitchIcon, '3GT': Assets.threeTouchSwitch, 'CUR': Assets.curtain, + 'CUR_2': Assets.curtain, 'GD': Assets.garageDoor, 'GW': Assets.SmartGatewayIcon, 'DL': Assets.DoorLockIcon, 'WL': Assets.waterLeakSensor, 'WH': Assets.waterHeater, + 'WM': Assets.waterLeakSensor, + 'SOS': Assets.sos, 'AC': Assets.ac, 'CPS': Assets.presenceSensor, 'PC': Assets.powerClamp, From b90f25f7b03ff0e51039195462a85537e43c47ac Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 10:39:23 +0300 Subject: [PATCH 017/105] fix all UI notes make white dialogs with fix calibrating textfield for seconds && fix icon of completed action dialog --- assets/images/completed_done.svg | 4 ++++ .../widgets/accurate_calibrating_dialog.dart | 2 ++ .../widgets/accurate_calibration_dialog.dart | 2 ++ .../widgets/accurate_dialog_widget.dart | 5 ++++- .../widgets/normal_text_body_for_dialog.dart | 10 +++++----- .../widgets/quick_calibrating_dialog.dart | 15 ++++++++++----- .../widgets/quick_calibration_dialog.dart | 2 ++ 7 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 assets/images/completed_done.svg diff --git a/assets/images/completed_done.svg b/assets/images/completed_done.svg new file mode 100644 index 00000000..759f0cba --- /dev/null +++ b/assets/images/completed_done.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart index 64044b94..0d3a1a92 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class AccurteCalibratingDialog extends StatelessWidget { final String deviceId; @@ -17,6 +18,7 @@ class AccurteCalibratingDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Calibrating', diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart index 997e70cf..7124639d 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class AccurateCalibrationDialog extends StatelessWidget { final String deviceId; @@ -15,6 +16,7 @@ class AccurateCalibrationDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Accurate Calibration', diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart index 433608ac..d13ebca0 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -22,6 +22,7 @@ class AccurateDialogWidget extends StatelessWidget { child: Column( children: [ Expanded( + flex: 3, child: Column( children: [ Padding( @@ -43,13 +44,15 @@ class AccurateDialogWidget extends StatelessWidget { ), ), Expanded( + flex: 5, child: body, ), Expanded( + flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - const Divider(), + const Expanded(child: Divider()), Row( children: [ Expanded( diff --git a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart index c322fe9d..fa293ec6 100644 --- a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart @@ -28,7 +28,7 @@ class NormalTextBodyForDialog extends StatelessWidget { title, style: const TextStyle( color: ColorsManager.grayColor, - fontSize: 17, + fontSize: 15, ), ), ), @@ -42,7 +42,7 @@ class NormalTextBodyForDialog extends StatelessWidget { const Text('1. ', style: TextStyle( color: ColorsManager.grayColor, - fontSize: 17, + fontSize: 15, )), SizedBox( width: 450, @@ -50,7 +50,7 @@ class NormalTextBodyForDialog extends StatelessWidget { step1, style: const TextStyle( color: ColorsManager.grayColor, - fontSize: 17, + fontSize: 15, ), ), ), @@ -67,13 +67,13 @@ class NormalTextBodyForDialog extends StatelessWidget { const Text('2. ', style: TextStyle( color: ColorsManager.grayColor, - fontSize: 17, + fontSize: 15, )), Text( step2, style: const TextStyle( color: ColorsManager.grayColor, - fontSize: 17, + fontSize: 15, ), ), ], diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart index 8514d432..6fc9adf2 100644 --- a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart @@ -63,6 +63,7 @@ class _QuickCalibratingDialogState extends State { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Calibrating', @@ -71,7 +72,7 @@ class _QuickCalibratingDialogState extends State { children: [ const Expanded( child: Align( - alignment: Alignment.center, + alignment: Alignment.topCenter, child: Padding( padding: EdgeInsets.only(right: 75), child: Text( @@ -85,17 +86,21 @@ class _QuickCalibratingDialogState extends State { child: Align( alignment: Alignment.center, child: Container( - width: 110, + width: 130, padding: const EdgeInsets.all(5), decoration: BoxDecoration( - color: ColorsManager.whiteColors, + color: ColorsManager.neutralGray.withValues( + alpha: 0.5, + ), borderRadius: BorderRadius.circular(12), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: NumberInputField(controller: _controller), + child: Padding( + padding: const EdgeInsetsGeometry.only(left: 5), + child: NumberInputField(controller: _controller)), ), Expanded( child: Text( @@ -127,7 +132,7 @@ class _QuickCalibratingDialogState extends State { ), const Expanded( child: Align( - alignment: Alignment.center, + alignment: Alignment.bottomCenter, child: Text( '2.click Next to Complete the calibration', style: TextStyle(color: ColorsManager.lightGrayColor), diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart index 6c776293..06b386c8 100644 --- a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class QuickCalibrationDialog extends StatelessWidget { final int timControl; @@ -17,6 +18,7 @@ class QuickCalibrationDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Quick Calibration', From 8c3861e83ca6edc37927d758aca27163f3a9b617 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 10:44:16 +0300 Subject: [PATCH 018/105] fix the button border Raduis when hovering --- .../widgets/accurate_dialog_widget.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart index d13ebca0..0d6ea90c 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -57,6 +57,9 @@ class AccurateDialogWidget extends StatelessWidget { children: [ Expanded( child: InkWell( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(26), + ), onTap: leftOnTap, child: Container( height: 40, @@ -67,6 +70,9 @@ class AccurateDialogWidget extends StatelessWidget { color: ColorsManager.grayBorder, ), ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(26), + ), ), child: const Text( 'Cancel', @@ -77,6 +83,9 @@ class AccurateDialogWidget extends StatelessWidget { ), Expanded( child: InkWell( + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(26), + ), onTap: rightOnTap, child: Container( height: 40, @@ -87,6 +96,9 @@ class AccurateDialogWidget extends StatelessWidget { color: ColorsManager.grayBorder, ), ), + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(26), + ), ), child: const Text( 'Next', From 44c88fb1c442270246d33ea4346b2f2a234c1a8f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 10:54:33 +0300 Subject: [PATCH 019/105] added empty charts icons. --- assets/icons/empty_barred_chart.svg | 8 ++ .../icons/empty_energy_management_chart.svg | 5 + .../empty_energy_management_per_device.svg | 7 ++ assets/icons/empty_heatmap.svg | 99 +++++++++++++++++++ assets/icons/empty_range_of_aqi.svg | 7 ++ lib/utils/constants/assets.dart | 7 ++ 6 files changed, 133 insertions(+) create mode 100644 assets/icons/empty_barred_chart.svg create mode 100644 assets/icons/empty_energy_management_chart.svg create mode 100644 assets/icons/empty_energy_management_per_device.svg create mode 100644 assets/icons/empty_heatmap.svg create mode 100644 assets/icons/empty_range_of_aqi.svg diff --git a/assets/icons/empty_barred_chart.svg b/assets/icons/empty_barred_chart.svg new file mode 100644 index 00000000..723d5e14 --- /dev/null +++ b/assets/icons/empty_barred_chart.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/empty_energy_management_chart.svg b/assets/icons/empty_energy_management_chart.svg new file mode 100644 index 00000000..042b9990 --- /dev/null +++ b/assets/icons/empty_energy_management_chart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/empty_energy_management_per_device.svg b/assets/icons/empty_energy_management_per_device.svg new file mode 100644 index 00000000..0408cd3a --- /dev/null +++ b/assets/icons/empty_energy_management_per_device.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/empty_heatmap.svg b/assets/icons/empty_heatmap.svg new file mode 100644 index 00000000..bbb2cfed --- /dev/null +++ b/assets/icons/empty_heatmap.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/empty_range_of_aqi.svg b/assets/icons/empty_range_of_aqi.svg new file mode 100644 index 00000000..aa51a41c --- /dev/null +++ b/assets/icons/empty_range_of_aqi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 6a0ef799..226766b3 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -502,4 +502,11 @@ class Assets { static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; static const String autocadOccupancyImage = 'assets/images/autocad_occupancy_image.png'; + static const String emptyBarredChart = 'assets/icons/empty_barred_chart.svg'; + static const String emptyEnergyManagementChart = + 'assets/icons/empty_energy_management_chart.svg'; + static const String emptyEnergyManagementPerDevice = + 'assets/icons/empty_energy_management_per_device.svg'; + static const String emptyHeatmap = 'assets/icons/empty_heatmap.svg'; + static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; } From cdc76c2c8eb564140a317b16b9d3606ad8fd3e23 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 11:04:22 +0300 Subject: [PATCH 020/105] handle more cases when decoding analytics devices. --- lib/pages/analytics/models/analytics_device.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 869de23f..6571eae4 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -39,8 +39,12 @@ class AnalyticsDevice { ? ProductDevice.fromJson(json['productDevice'] as Map) : null, spaceUuid: json['spaceUuid'] as String?, - latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null, - longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null, + latitude: json['lat'] != null && json['lat'] != '' + ? double.tryParse(json['lat']?.toString() ?? '0.0') + : null, + longitude: json['lon'] != null && json['lon'] != '' + ? double.tryParse(json['lon']?.toString() ?? '0.0') + : null, ); } } From db05331e9a4e348a0e354e1102dfd5fb9129a59b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 10:54:53 +0300 Subject: [PATCH 021/105] update `AnalyticsChartEmptyStateWidget` to use new icons. --- .../total_energy_consumption_chart_box.dart | 5 +- .../analytics_chart_empty_state_widget.dart | 60 ++++++++++--------- 2 files changed, 36 insertions(+), 29 deletions(-) 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 52a0b0ba..a7992223 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/to import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class TotalEnergyConsumptionChartBox extends StatelessWidget { @@ -43,12 +44,14 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { const Divider(), const SizedBox(height: 20), Visibility( - visible: state.chartData.isNotEmpty, + visible: state.chartData.isNotEmpty && + state.chartData.every((e) => e.value != 0), replacement: AnalyticsChartEmptyStateWidget( isLoading: state.status == TotalEnergyConsumptionStatus.loading, isError: state.status == TotalEnergyConsumptionStatus.failure, isInitial: state.status == TotalEnergyConsumptionStatus.initial, errorMessage: state.errorMessage, + iconPath: Assets.emptyEnergyManagementChart, ), child: TotalEnergyConsumptionChart(chartData: state.chartData), ), diff --git a/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart index 086cf959..786fe020 100644 --- a/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart +++ b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AnalyticsChartEmptyStateWidget extends StatelessWidget { const AnalyticsChartEmptyStateWidget({ + required this.iconPath, this.isLoading = false, this.isError = false, this.isInitial = false, @@ -20,59 +22,61 @@ class AnalyticsChartEmptyStateWidget extends StatelessWidget { final String? errorMessage; final String noDataMessage; final String initialMessage; + final String iconPath; @override Widget build(BuildContext context) { return Expanded( - child: isLoading - ? const Center( - child: AppLoadingIndicator(), - ) - : isError - ? _buildState( - context, - icon: Icons.error_outline, - message: errorMessage ?? 'Something went wrong', - color: ColorsManager.red, - ) - : isInitial - ? _buildState( - context, - icon: Icons.filter_list, - message: initialMessage, - ) - : _buildState( - context, - icon: Icons.bar_chart, - message: noDataMessage, - ), + child: () { + if (isLoading) { + return const AppLoadingIndicator(); + } else if (isError) { + return _buildState( + context, + message: errorMessage ?? 'Something went wrong', + color: ColorsManager.red, + ); + } else if (isInitial) { + return _buildState(context, message: initialMessage); + } else { + return _buildState(context, message: noDataMessage); + } + }(), ); } Widget _buildState( BuildContext context, { - required IconData icon, required String message, Color? color, }) { final disabledColor = context.theme.disabledColor; return Center( child: Column( + spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - icon, - size: 48, - color: color ?? disabledColor, - ), const SizedBox(height: 16), + Expanded( + child: SvgPicture.asset( + iconPath, + fit: BoxFit.contain, + colorFilter: ColorFilter.mode( + color ?? disabledColor, + BlendMode.srcIn, + ), + ), + ), SelectableText( message, style: context.textTheme.bodyMedium?.copyWith( color: color ?? disabledColor, + fontSize: 16, + fontWeight: FontWeight.w700, ), textAlign: TextAlign.center, ), + const SizedBox(height: 16), ], ), ); From 9ebf474a600ff577927079bd658b57a7a18ba055 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 12:52:22 +0300 Subject: [PATCH 022/105] analytics-empty-state. --- .../widgets/aqi_distribution_chart_box.dart | 18 +++++- .../widgets/range_of_aqi_chart_box.dart | 20 +++++-- ...ergy_consumption_per_device_chart_box.dart | 22 +++++++- .../widgets/occupancy_chart_box.dart | 21 ++++++- .../widgets/occupancy_heat_map_box.dart | 33 ++++++++--- .../analytics_chart_empty_state_widget.dart | 56 +++++++------------ 6 files changed, 116 insertions(+), 54 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 25cfd19d..41448f4e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class AqiDistributionChartBox extends StatelessWidget { @@ -32,8 +34,20 @@ class AqiDistributionChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded( - child: AqiDistributionChart(chartData: state.chartData), + Visibility( + visible: state.chartData.isNotEmpty, + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == AirQualityDistributionStatus.loading, + isError: state.status == AirQualityDistributionStatus.failure, + isInitial: state.status == AirQualityDistributionStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyBarredChart, + ), + child: Expanded( + child: AqiDistributionChart( + chartData: state.chartData, + ), + ), ), ], ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index cb189dce..5ec5158f 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class RangeOfAqiChartBox extends StatelessWidget { @@ -32,10 +34,20 @@ class RangeOfAqiChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded( - child: RangeOfAqiChart( - chartData: state.filteredRangeOfAqi, - selectedAqiType: state.selectedAqiType, + Visibility( + visible: state.filteredRangeOfAqi.isNotEmpty, + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == RangeOfAqiStatus.loading, + isError: state.status == RangeOfAqiStatus.failure, + isInitial: state.status == RangeOfAqiStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyRangeOfAqi, + ), + child: Expanded( + child: RangeOfAqiChart( + chartData: state.filteredRangeOfAqi, + selectedAqiType: state.selectedAqiType, + ), ), ), ], diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index 06b6c529..48c9af94 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -5,8 +5,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { @@ -54,8 +56,24 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { const SizedBox(height: 20), const Divider(height: 0), const SizedBox(height: 20), - Expanded( - child: EnergyConsumptionPerDeviceChart(chartData: state.chartData), + Visibility( + visible: state.chartData.isNotEmpty && + state.chartData + .every((e) => e.energy.every((e) => e.value != 0)), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: + state.status == EnergyConsumptionPerDeviceStatus.loading, + isError: state.status == EnergyConsumptionPerDeviceStatus.failure, + isInitial: + state.status == EnergyConsumptionPerDeviceStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyEnergyManagementPerDevice, + ), + child: Expanded( + child: EnergyConsumptionPerDeviceChart( + chartData: state.chartData, + ), + ), ), ], ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart index 08f7223f..30d96ac5 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart @@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class OccupancyChartBox extends StatelessWidget { @@ -67,7 +69,24 @@ class OccupancyChartBox extends StatelessWidget { const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), - Expanded(child: OccupancyChart(chartData: state.chartData)), + Visibility( + visible: state.chartData.isNotEmpty && + state.chartData.every( + (e) => e.occupancy.isNotEmpty, + ), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == OccupancyStatus.loading, + isError: state.status == OccupancyStatus.failure, + isInitial: state.status == OccupancyStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyBarredChart, + ), + child: Expanded( + child: OccupancyChart( + chartData: state.chartData, + ), + ), + ), ], ), ); diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart index a5f56aa4..9c8e3a1b 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class OccupancyHeatMapBox extends StatelessWidget { @@ -68,16 +70,29 @@ class OccupancyHeatMapBox extends StatelessWidget { const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), - Expanded( - child: OccupancyHeatMap( - selectedDate: - context.watch().state.yearlyDate, - heatMapData: state.heatMapData.asMap().map( - (_, value) => MapEntry( - value.eventDate, - value.countTotalPresenceDetected, + Visibility( + visible: state.heatMapData.isNotEmpty && + state.heatMapData.every( + (e) => e.countTotalPresenceDetected != 0, + ), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == OccupancyHeatMapStatus.loading, + isError: state.status == OccupancyHeatMapStatus.failure, + isInitial: state.status == OccupancyHeatMapStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyHeatmap, + ), + child: Expanded( + child: OccupancyHeatMap( + selectedDate: + context.watch().state.yearlyDate, + heatMapData: state.heatMapData.asMap().map( + (_, value) => MapEntry( + value.eventDate, + value.countTotalPresenceDetected, + ), ), - ), + ), ), ), ], diff --git a/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart index 786fe020..f65e1de0 100644 --- a/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart +++ b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart @@ -27,53 +27,37 @@ class AnalyticsChartEmptyStateWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( - child: () { - if (isLoading) { - return const AppLoadingIndicator(); - } else if (isError) { - return _buildState( - context, - message: errorMessage ?? 'Something went wrong', - color: ColorsManager.red, - ); - } else if (isInitial) { - return _buildState(context, message: initialMessage); - } else { - return _buildState(context, message: noDataMessage); - } - }(), + child: _buildWidgetBasedOnState(context), ); } - Widget _buildState( - BuildContext context, { - required String message, - Color? color, - }) { - final disabledColor = context.theme.disabledColor; + Widget _buildWidgetBasedOnState(BuildContext context) { + final widgetsMap = { + isLoading: const AppLoadingIndicator(), + isInitial: _buildState(context, initialMessage), + isError: _buildState(context, errorMessage ?? 'Something went wrong'), + }; + + return widgetsMap[true] ?? _buildState(context, noDataMessage); + } + + Widget _buildState(BuildContext context, String message) { return Center( child: Column( spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 16), - Expanded( - child: SvgPicture.asset( - iconPath, - fit: BoxFit.contain, - colorFilter: ColorFilter.mode( - color ?? disabledColor, - BlendMode.srcIn, - ), - ), - ), + Expanded(child: SvgPicture.asset(iconPath, fit: BoxFit.contain)), SelectableText( message, - style: context.textTheme.bodyMedium?.copyWith( - color: color ?? disabledColor, - fontSize: 16, - fontWeight: FontWeight.w700, - ), + style: isError + ? context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.red, + fontSize: 16, + fontWeight: FontWeight.w700, + ) + : null, textAlign: TextAlign.center, ), const SizedBox(height: 16), From 62d5bbce7e0cc9362e0c2e0fb8e13a17b41adbe2 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 13:22:04 +0300 Subject: [PATCH 023/105] add isValid to basic step (1) and insure that user can go to another step using next button without filling the form --- .../add_user_dialog/view/add_user_dialog.dart | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart index 44ba81ff..0fea43d9 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart @@ -34,7 +34,8 @@ class _AddNewUserDialogState extends State { return Dialog( child: Container( decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), width: 900, child: Column( children: [ @@ -63,7 +64,8 @@ class _AddNewUserDialogState extends State { children: [ _buildStep1Indicator(1, "Basics", _blocRole), _buildStep2Indicator(2, "Spaces", _blocRole), - _buildStep3Indicator(3, "Role & Permissions", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), ], ), ), @@ -105,18 +107,32 @@ class _AddNewUserDialogState extends State { ), InkWell( onTap: () { + final isBasicsStep = currentStep == 1; + + if (isBasicsStep) { + // Validate the form first + final isValid = _blocRole.formKey.currentState + ?.validate() ?? + false; + + if (!isValid) + return; // Stop if form is not valid + } _blocRole.add(const CheckEmailEvent()); setState(() { if (currentStep < 3) { currentStep++; if (currentStep == 2) { - _blocRole.add(const CheckStepStatus(isEditUser: false)); + _blocRole.add(const CheckStepStatus( + isEditUser: false)); } else if (currentStep == 3) { - _blocRole.add(const CheckSpacesStepStatus()); + _blocRole + .add(const CheckSpacesStepStatus()); } } else { - _blocRole.add(SendInviteUsers(context: context)); + _blocRole + .add(SendInviteUsers(context: context)); } }); }, @@ -124,8 +140,11 @@ class _AddNewUserDialogState extends State { currentStep < 3 ? "Next" : "Save", style: TextStyle( color: (_blocRole.isCompleteSpaces == false || - _blocRole.isCompleteBasics == false || - _blocRole.isCompleteRolePermissions == false) && + _blocRole.isCompleteBasics == + false || + _blocRole + .isCompleteRolePermissions == + false) && currentStep == 3 ? ColorsManager.grayColor : ColorsManager.secondaryColor), @@ -196,8 +215,12 @@ class _AddNewUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -260,8 +283,12 @@ class _AddNewUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -318,8 +345,12 @@ class _AddNewUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], From e5e88385e9d0e230921cd35b5ad7b695c0abd92b Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 13:31:38 +0300 Subject: [PATCH 024/105] change autoValidae mode to userInteraction and give some time to check validate when typing on keyboard with debouncer --- .../add_user_dialog/view/add_user_dialog.dart | 2 +- .../add_user_dialog/view/basics_view.dart | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart index 0fea43d9..501cd02e 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart @@ -162,7 +162,7 @@ class _AddNewUserDialogState extends State { Widget _getFormContent() { switch (currentStep) { case 1: - return const BasicsView( + return BasicsView( userId: '', ); case 2: diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart index fa04c051..14022cab 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart @@ -1,9 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl_phone_field/countries.dart'; import 'package:intl_phone_field/country_picker_dialog.dart'; import 'package:intl_phone_field/intl_phone_field.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -11,7 +14,9 @@ import 'package:syncrow_web/utils/style.dart'; class BasicsView extends StatelessWidget { final String? userId; - const BasicsView({super.key, this.userId = ''}); + Timer? _debounce; + + BasicsView({super.key, this.userId = ''}); @override Widget build(BuildContext context) { return BlocBuilder(builder: (context, state) { @@ -21,6 +26,7 @@ class BasicsView extends StatelessWidget { } return Form( key: _blocRole.formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, child: ListView( shrinkWrap: true, children: [ @@ -208,6 +214,14 @@ class BasicsView extends StatelessWidget { fontSize: 12, color: ColorsManager.textGray), ), + + onChanged: (value) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + _blocRole.add(const CheckEmailEvent()); + }); + }, + validator: (value) { if (value == null || value.isEmpty) { return 'Enter Email Address'; From 15ee79688dc9e9d0a1de7004cddb99988210f0f4 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Mon, 30 Jun 2025 13:36:52 +0300 Subject: [PATCH 025/105] reComite --- .../users_page/add_user_dialog/bloc/users_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart index 54187152..72c4501c 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart @@ -455,7 +455,7 @@ class UsersBloc extends Bloc { Future checkEmail( CheckEmailEvent event, Emitter emit) async { emit(UsersLoadingState()); - String? res = await UserPermissionApi().checkEmail( + String? res = await UserPermissionApi().checkEmail( emailController.text, ); checkEmailValid = res!; From bd9a74b3804e8ed1c747915388c0c336b4d94cb2 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 13:58:10 +0300 Subject: [PATCH 026/105] fix touch gangs realtime. --- .../bloc/one_gang_glass_switch_bloc.dart | 54 ++- .../bloc/three_gang_glass_switch_bloc.dart | 54 ++- .../bloc/two_gang_glass_switch_bloc.dart | 322 +++++++++--------- 3 files changed, 209 insertions(+), 221 deletions(-) diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart index c1e976ab..bb6f8e29 100644 --- a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart @@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc emit(OneGangGlassSwitchLoading()); try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId, emit); + _listenToChanges(event.deviceId); deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { @@ -48,42 +48,28 @@ class OneGangGlassSwitchBloc } } - void _listenToChanges( - String deviceId, - Emitter emit, - ) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { + if (event.snapshot.value == null) return; - stream.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + final usersMap = event.snapshot.value! as Map; final statusList = []; - if (data['status'] != null) { - for (var element in data['status']) { - statusList.add( - Status( - code: element['code'].toString(), - value: element['value'].toString(), - ), - ); - } - } - if (statusList.isNotEmpty) { - final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList); - if (newStatus != deviceStatus) { - deviceStatus = newStatus; - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - } - } + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + deviceStatus = + OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList); + + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - emit(OneGangGlassSwitchError('Failed to listen to changes: $e')); - } + } catch (_) {} } void _onStatusUpdated( @@ -174,4 +160,10 @@ class OneGangGlassSwitchBloc deviceStatus = deviceStatus.copyWith(switch1: value); } } + + @override + Future close() { + _deviceStatusSubscription?.cancel(); + return super.close(); + } } diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart index 766c3163..4a122345 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart @@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc emit(ThreeGangGlassSwitchLoading()); try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId, emit); + _listenToChanges(event.deviceId); deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); @@ -50,42 +50,28 @@ class ThreeGangGlassSwitchBloc } } - void _listenToChanges( - String deviceId, - Emitter emit, - ) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { + if (event.snapshot.value == null) return; - stream.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + final usersMap = event.snapshot.value! as Map; final statusList = []; - if (data['status'] != null) { - for (var element in data['status']) { - statusList.add( - Status( - code: element['code'].toString(), - value: element['value'].toString(), - ), - ); - } - } - if (statusList.isNotEmpty) { - final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList); - if (newStatus != deviceStatus) { - deviceStatus = newStatus; - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - } - } + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + deviceStatus = + ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList); + + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e')); - } + } catch (_) {} } void _onStatusUpdated( @@ -184,4 +170,10 @@ class ThreeGangGlassSwitchBloc break; } } + + @override + Future close() { + _deviceStatusSubscription?.cancel(); + return super.close(); + } } diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart index 8f82c198..08b40362 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart @@ -1,173 +1,177 @@ -import 'dart:async'; -import 'dart:developer'; + import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:firebase_database/firebase_database.dart'; -import 'package:flutter/foundation.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; -import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; -import 'package:syncrow_web/services/batch_control_devices_service.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; -import 'package:syncrow_web/services/devices_mang_api.dart'; + import 'package:bloc/bloc.dart'; + import 'package:equatable/equatable.dart'; + import 'package:firebase_database/firebase_database.dart'; + import 'package:flutter/foundation.dart'; + import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; + import 'package:syncrow_web/services/batch_control_devices_service.dart'; + import 'package:syncrow_web/services/control_device_service.dart'; + import 'package:syncrow_web/services/devices_mang_api.dart'; -part 'two_gang_glass_switch_event.dart'; -part 'two_gang_glass_switch_state.dart'; + part 'two_gang_glass_switch_event.dart'; + part 'two_gang_glass_switch_state.dart'; -class TwoGangGlassSwitchBloc - extends Bloc { - final String deviceId; - final ControlDeviceService controlDeviceService; - final BatchControlDevicesService batchControlDevicesService; + class TwoGangGlassSwitchBloc + extends Bloc { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - late TwoGangGlassStatusModel deviceStatus; + late TwoGangGlassStatusModel deviceStatus; - TwoGangGlassSwitchBloc({ - required this.deviceId, - required this.controlDeviceService, - required this.batchControlDevicesService, - }) : super(TwoGangGlassSwitchInitial()) { - on(_onFetchDeviceStatus); - on(_onControl); - on(_onBatchControl); - on(_onFetchBatchStatus); - on(_onFactoryReset); - on(_onStatusUpdated); - } - - Future _onFetchDeviceStatus( - TwoGangGlassSwitchFetchDeviceEvent event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } catch (e) { - emit(TwoGangGlassSwitchError(e.toString())); + TwoGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangGlassSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFactoryReset); + on(_onStatusUpdated); } - } - void _listenToChanges(String deviceId) { - try { - final ref = FirebaseDatabase.instance.ref( - 'device-status/$deviceId', - ); - - ref.onValue.listen((event) { - final eventsMap = event.snapshot.value as Map; - - List statusList = []; - eventsMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList); - add(StatusUpdated(deviceStatus)); - }); - } catch (_) { - log( - 'Error listening to changes', - name: 'TwoGangGlassSwitchBloc._listenToChanges', - ); - } - } - - Future _onControl( - TwoGangGlassSwitchControl event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - _updateLocalValue(event.code, event.value); - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - - try { - await controlDeviceService.controlDevice( - deviceUuid: event.deviceId, - status: Status(code: event.code, value: event.value), - ); - } catch (e) { - _updateLocalValue(event.code, !event.value); - emit(TwoGangGlassSwitchError(e.toString())); - } - } - - Future _onBatchControl( - TwoGangGlassSwitchBatchControl event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - _updateLocalValue(event.code, event.value); - emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - - try { - await batchControlDevicesService.batchControlDevices( - uuids: event.deviceIds, - code: event.code, - value: event.value, - ); - } catch (e) { - _updateLocalValue(event.code, !event.value); - emit(TwoGangGlassSwitchError(e.toString())); - } - } - - Future _onFetchBatchStatus( - TwoGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - try { - final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = TwoGangGlassStatusModel.fromJson( - event.deviceIds.first, - status.status, - ); - emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - } catch (e) { - emit(TwoGangGlassSwitchError(e.toString())); - } - } - - Future _onFactoryReset( - TwoGangGlassFactoryReset event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - try { - final response = await DevicesManagementApi().factoryReset( - event.factoryReset, - event.deviceId, - ); - if (!response) { - emit(TwoGangGlassSwitchError('Failed to reset device')); - } else { - add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); + Future _onFetchDeviceStatus( + TwoGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); + _listenToChanges(event.deviceId); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); } - } catch (e) { - emit(TwoGangGlassSwitchError(e.toString())); } - } - void _onStatusUpdated( - StatusUpdated event, - Emitter emit, - ) { - deviceStatus = event.deviceStatus; - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } + StreamSubscription? _deviceStatusSubscription; - void _updateLocalValue(String code, bool value) { - switch (code) { - case 'switch_1': - deviceStatus = deviceStatus.copyWith(switch1: value); - break; - case 'switch_2': - deviceStatus = deviceStatus.copyWith(switch2: value); - break; + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { + if (event.snapshot.value == null) return; + + final usersMap = event.snapshot.value! as Map; + + final statusList = []; + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + deviceStatus = + TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList); + + add(StatusUpdated(deviceStatus)); + }); + } catch (_) {} } + + Future _onControl( + TwoGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + _updateLocalValue(event.code, event.value); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onBatchControl( + TwoGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + _updateLocalValue(event.code, event.value); + emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); + + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onFetchBatchStatus( + TwoGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = TwoGangGlassStatusModel.fromJson( + event.deviceIds.first, + status.status, + ); + emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onFactoryReset( + TwoGangGlassFactoryReset event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(TwoGangGlassSwitchError('Failed to reset device')); + } else { + add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); + } + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + } + } + + @override + Future close() { + _deviceStatusSubscription?.cancel(); + return super.close(); } } From 859416854823f6fcb29a30c20c99ab0ef2846a16 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 14:22:54 +0300 Subject: [PATCH 027/105] hardcoded device location to dubai for demo purposes. --- .../device_location_details_service_decorator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart index f38f607d..0a49a797 100644 --- a/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart +++ b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart @@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService { 'reverse', queryParameters: { 'format': 'json', - 'lat': param.latitude, - 'lon': param.longitude, + 'lat': 25.1880567, + 'lon': 55.266608, }, ); From 289922071a3828f04a5321c17bf47e00deb5d9d1 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 30 Jun 2025 15:05:59 +0300 Subject: [PATCH 028/105] Add countdown functionality and device type support across device management views --- .../all_devices/models/device_status.dart | 9 + .../view/curtain_module_items.dart | 5 +- .../one_gang_glass_switch_control_view.dart | 2 + .../view/wall_light_device_control.dart | 2 + .../schedule_device/bloc/schedule_bloc.dart | 160 ++++++++++-------- .../schedule_device/bloc/schedule_event.dart | 23 ++- .../schedule_device/bloc/schedule_state.dart | 4 +- .../schedule_widgets/count_down_button.dart | 11 +- .../count_down_inching_view.dart | 32 ++-- .../inching_mode_buttons.dart | 4 +- .../schedule_widgets/schedual_view.dart | 53 +++--- .../schedule_managment_ui.dart | 10 +- .../schedule_mode_selector.dart | 5 +- .../schedule_widgets/schedule_table.dart | 58 ++++--- .../three_gang_glass_switch_control_view.dart | 6 + .../view/living_room_device_control.dart | 6 + .../two_gang_glass_switch_control_view.dart | 4 + .../view/wall_light_batch_control.dart | 9 +- .../view/wall_light_device_control.dart | 4 + .../helper/add_schedule_dialog_helper.dart | 62 +++---- .../view/water_heater_device_control.dart | 2 + 21 files changed, 292 insertions(+), 179 deletions(-) diff --git a/lib/pages/device_managment/all_devices/models/device_status.dart b/lib/pages/device_managment/all_devices/models/device_status.dart index b78f2a30..1f23e3f9 100644 --- a/lib/pages/device_managment/all_devices/models/device_status.dart +++ b/lib/pages/device_managment/all_devices/models/device_status.dart @@ -60,4 +60,13 @@ class Status { factory Status.fromJson(String source) => Status.fromMap(json.decode(source)); String toJson() => json.encode(toMap()); + Status copyWith({ + String? code, + dynamic value, + }) { + return Status( + code: code ?? this.code, + value: value ?? this.value, + ); + } } diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 82c812ce..6e595565 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -62,9 +62,10 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, - category: 'CUR_2', + category: 'timer', code: 'control', - + countdownCode: 'timer', + deviceType: 'CUR_2', ), )); }, diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 1ad5d43b..6b180f8d 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -90,6 +90,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceId, + countdownCode: 'countdown_1', + deviceType: '1GT', ), )); }, diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index 2f6008d2..a9d7c47b 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -80,6 +80,8 @@ class WallLightDeviceControl extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceId, + countdownCode: 'countdown_1', + deviceType: '1G', ), )); }, diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 0ec55e39..65773590 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc { final success = await RemoteControlDeviceService().controlDevice( deviceUuid: deviceId, status: Status( - code: 'countdown_1', + code: event.countdownCode, value: 0, ), ); @@ -80,15 +80,18 @@ class ScheduleBloc extends Bloc { ) { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownSeconds: currentState.countdownSeconds, + selectedTime: currentState.selectedTime, + deviceId: deviceId, scheduleMode: event.scheduleMode, - countdownRemaining: Duration.zero, - countdownHours: 0, - countdownMinutes: 0, - inchingHours: 0, - inchingMinutes: 0, - isCountdownActive: false, + countdownHours: currentState.countdownHours, + countdownMinutes: currentState.countdownMinutes, + inchingHours: currentState.inchingHours, + inchingMinutes: currentState.inchingMinutes, isInchingActive: false, + isCountdownActive: currentState.countdownRemaining > Duration.zero, )); } } @@ -221,7 +224,6 @@ class ScheduleBloc extends Bloc { deviceId, event.category, ); - if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; emit(currentState.copyWith( @@ -285,12 +287,22 @@ class ScheduleBloc extends Bloc { ) async { try { if (state is ScheduleLoaded) { + Status status = Status(code: '', value: ''); + if (event.deviceType == 'CUR_2') { + status = status.copyWith( + code: 'control', + value: event.functionOn == true ? 'open' : 'close'); + } else { + status = + status.copyWith(code: event.category, value: event.functionOn); + } + final dateTime = DateTime.parse(event.time); final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, time: getTimeStampWithoutSeconds(dateTime).toString(), - function: Status(code: event.category, value: event.functionOn), + function: status, days: event.selectedDays, ); final success = await DevicesManagementApi().editScheduleRecord( @@ -396,7 +408,7 @@ class ScheduleBloc extends Bloc { final totalSeconds = Duration(hours: event.hours, minutes: event.minutes).inSeconds; final code = event.mode == ScheduleModes.countdown - ? 'countdown_1' + ? event.countDownCode : 'switch_inching'; final currentState = state as ScheduleLoaded; final duration = Duration(seconds: totalSeconds); @@ -423,7 +435,7 @@ class ScheduleBloc extends Bloc { ); if (success) { - if (code == 'countdown_1') { + if (code == event.countDownCode) { final countdownDuration = Duration(seconds: totalSeconds); emit( @@ -437,7 +449,7 @@ class ScheduleBloc extends Bloc { ); if (countdownDuration.inSeconds > 0) { - _startCountdownTimer(emit, countdownDuration); + _startCountdownTimer(emit, countdownDuration, event.countDownCode); } else { _countdownTimer?.cancel(); emit( @@ -467,9 +479,7 @@ class ScheduleBloc extends Bloc { } void _startCountdownTimer( - Emitter emit, - Duration duration, - ) { + Emitter emit, Duration duration, String countdownCode) { _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_currentCountdown != null && _currentCountdown! > Duration.zero) { @@ -479,6 +489,7 @@ class ScheduleBloc extends Bloc { } else { timer.cancel(); add(StopScheduleEvent( + countdownCode: countdownCode, mode: _currentCountdown == null ? ScheduleModes.countdown : ScheduleModes.inching, @@ -515,70 +526,75 @@ class ScheduleBloc extends Bloc { try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - print(status.status); + int totalSeconds = 0; + final countdownItem = status.status.firstWhere( + (item) => item.code == event.countdownCode, + orElse: () => Status(code: '', value: 0), + ); + totalSeconds = (countdownItem.value as int?) ?? 0; + final countdownHours = totalSeconds ~/ 3600; + final countdownMinutes = (totalSeconds % 3600) ~/ 60; + final countdownSeconds = totalSeconds % 60; + final deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + final isCountdownActive = totalSeconds > 0; + final isInchingActive = !isCountdownActive && + (deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0); - final scheduleMode = - deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0 - ? ScheduleModes.countdown - : deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0 - ? ScheduleModes.inching - : ScheduleModes.schedule; - final isCountdown = scheduleMode == ScheduleModes.countdown; - final isInching = scheduleMode == ScheduleModes.inching; + final newState = state is ScheduleLoaded + ? (state as ScheduleLoaded).copyWith( + scheduleMode: ScheduleModes.schedule, + countdownHours: countdownHours, + countdownMinutes: countdownMinutes, + countdownSeconds: countdownSeconds, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: isCountdownActive + ? Duration(seconds: totalSeconds) + : Duration.zero, + ) + : ScheduleLoaded( + scheduleMode: ScheduleModes.schedule, + schedules: const [], + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + deviceId: event.deviceId, + countdownHours: countdownHours, + countdownMinutes: countdownMinutes, + countdownSeconds: countdownSeconds, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: isCountdownActive + ? Duration(seconds: totalSeconds) + : Duration.zero, + ); + emit(newState); - Duration? countdownRemaining; - var isCountdownActive = false; - var isInchingActive = false; + if (isCountdownActive) { + _countdownTimer?.cancel(); + _currentCountdown = Duration(seconds: totalSeconds); + countdownRemaining = _currentCountdown!; - if (isCountdown) { - countdownRemaining = Duration( - hours: deviceStatus.countdownHours, - minutes: deviceStatus.countdownMinutes, - ); - isCountdownActive = countdownRemaining > Duration.zero; - } else if (isInching) { - isInchingActive = Duration( - hours: deviceStatus.inchingHours, - minutes: deviceStatus.inchingMinutes, - ) > - Duration.zero; - } - if (state is ScheduleLoaded) { - final currentState = state as ScheduleLoaded; - emit(currentState.copyWith( - scheduleMode: scheduleMode, - countdownHours: deviceStatus.countdownHours, - countdownMinutes: deviceStatus.countdownMinutes, - inchingHours: deviceStatus.inchingHours, - inchingMinutes: deviceStatus.inchingMinutes, - isCountdownActive: isCountdownActive, - isInchingActive: isInchingActive, - countdownRemaining: countdownRemaining ?? Duration.zero, - )); + if (totalSeconds > 0) { + _startCountdownTimer( + emit, Duration(seconds: totalSeconds), event.countdownCode); + } else { + add(StopScheduleEvent( + countdownCode: event.countdownCode, + mode: ScheduleModes.countdown, + deviceId: event.deviceId, + )); + } } else { - emit(ScheduleLoaded( - schedules: const [], - selectedTime: null, - selectedDays: List.filled(7, false), - functionOn: false, - isEditing: false, - deviceId: deviceId, - scheduleMode: scheduleMode, - countdownHours: deviceStatus.countdownHours, - countdownMinutes: deviceStatus.countdownMinutes, - inchingHours: deviceStatus.inchingHours, - inchingMinutes: deviceStatus.inchingMinutes, - isCountdownActive: isCountdownActive, - isInchingActive: isInchingActive, - countdownRemaining: countdownRemaining ?? Duration.zero, - )); + _countdownTimer?.cancel(); } - - // if (isCountdownActive && countdownRemaining != null) { - // _startCountdownTimer(emit, countdownRemaining); - // } } catch (e) { emit(ScheduleError('Failed to fetch device status: $e')); } diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index a28b8757..6c79c8b6 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -91,6 +91,7 @@ class ScheduleEditEvent extends ScheduleEvent { final String time; final List selectedDays; final bool functionOn; + final String deviceType; const ScheduleEditEvent({ required this.scheduleId, @@ -98,6 +99,7 @@ class ScheduleEditEvent extends ScheduleEvent { required this.time, required this.selectedDays, required this.functionOn, + required this.deviceType, }); @override @@ -107,6 +109,7 @@ class ScheduleEditEvent extends ScheduleEvent { time, selectedDays, functionOn, + deviceType, ]; } @@ -138,11 +141,13 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent { class UpdateScheduleModeEvent extends ScheduleEvent { final ScheduleModes scheduleMode; + final String countdownCode; - const UpdateScheduleModeEvent({required this.scheduleMode}); + const UpdateScheduleModeEvent( + {required this.scheduleMode, required this.countdownCode}); @override - List get props => [scheduleMode]; + List get props => [scheduleMode, countdownCode!]; } class UpdateCountdownTimeEvent extends ScheduleEvent { @@ -177,28 +182,32 @@ class StartScheduleEvent extends ScheduleEvent { final ScheduleModes mode; final int hours; final int minutes; + final String countDownCode; const StartScheduleEvent({ required this.mode, required this.hours, required this.minutes, + required this.countDownCode, }); @override - List get props => [mode, hours, minutes]; + List get props => [mode, hours, minutes, countDownCode]; } class StopScheduleEvent extends ScheduleEvent { final ScheduleModes mode; final String deviceId; + final String countdownCode; const StopScheduleEvent({ required this.mode, required this.deviceId, + required this.countdownCode, }); @override - List get props => [mode, deviceId]; + List get props => [mode, deviceId, countdownCode]; } class ScheduleDecrementCountdownEvent extends ScheduleEvent { @@ -210,11 +219,13 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent { class ScheduleFetchStatusEvent extends ScheduleEvent { final String deviceId; + final String countdownCode; - const ScheduleFetchStatusEvent(this.deviceId); + const ScheduleFetchStatusEvent( + {required this.deviceId, required this.countdownCode}); @override - List get props => [deviceId]; + List get props => [deviceId, countdownCode]; } class DeleteScheduleEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart index 63551c3a..66547788 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState { final int inchingSeconds; final bool isInchingActive; final ScheduleModes scheduleMode; - final Duration? countdownRemaining; + final Duration countdownRemaining; final int? countdownSeconds; const ScheduleLoaded({ @@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState { this.inchingMinutes = 0, this.isInchingActive = false, this.scheduleMode = ScheduleModes.countdown, - this.countdownRemaining, + this.countdownRemaining = Duration.zero, }); ScheduleLoaded copyWith({ diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart index 4919018c..b28a6a23 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart @@ -11,6 +11,7 @@ class CountdownModeButtons extends StatelessWidget { final String deviceId; final int hours; final int minutes; + final String countDownCode; const CountdownModeButtons({ super.key, @@ -18,6 +19,7 @@ class CountdownModeButtons extends StatelessWidget { required this.deviceId, required this.hours, required this.minutes, + required this.countDownCode, }); @override @@ -43,6 +45,7 @@ class CountdownModeButtons extends StatelessWidget { StopScheduleEvent( mode: ScheduleModes.countdown, deviceId: deviceId, + countdownCode: countDownCode, ), ); }, @@ -54,10 +57,10 @@ class CountdownModeButtons extends StatelessWidget { onPressed: () { context.read().add( StartScheduleEvent( - mode: ScheduleModes.countdown, - hours: hours, - minutes: minutes, - ), + mode: ScheduleModes.countdown, + hours: hours, + minutes: minutes, + countDownCode: countDownCode), ); }, backgroundColor: ColorsManager.primaryColor, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart index 418bab6c..e64b7cf7 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -75,23 +75,33 @@ class _CountdownInchingViewState extends State { final isCountDown = state.scheduleMode == ScheduleModes.countdown; final isActive = isCountDown ? state.isCountdownActive : state.isInchingActive; - final displayHours = isActive && state.countdownRemaining != null - ? state.countdownRemaining!.inHours - : (isCountDown ? state.countdownHours : state.inchingHours); - final displayMinutes = isActive && state.countdownRemaining != null - ? state.countdownRemaining!.inMinutes.remainder(60) - : (isCountDown ? state.countdownMinutes : state.inchingMinutes); - final displaySeconds = isActive && state.countdownRemaining != null - ? state.countdownRemaining!.inSeconds.remainder(60) - : (isCountDown ? state.countdownSeconds : state.inchingSeconds); - _updateControllers(displayHours, displayMinutes, displaySeconds!); + final displayHours = + isActive && state.countdownRemaining != Duration.zero + ? state.countdownRemaining.inHours + : (isCountDown ? state.countdownHours : state.inchingHours); - if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) { + final displayMinutes = + isActive && state.countdownRemaining != Duration.zero + ? state.countdownRemaining.inMinutes.remainder(60) + : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + + final displaySeconds = + isActive && state.countdownRemaining != Duration.zero + ? state.countdownRemaining.inSeconds.remainder(60) + : (isCountDown ? (state.countdownSeconds ?? 0) : 0); + + _updateControllers(displayHours, displayMinutes, displaySeconds); + + if (isActive && + displayHours == 0 && + displayMinutes == 0 && + displaySeconds == 0) { context.read().add( StopScheduleEvent( mode: ScheduleModes.countdown, deviceId: widget.deviceId, + countdownCode: '', ), ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart index e75c5d46..e8dc5e79 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart @@ -43,7 +43,9 @@ class InchingModeButtons extends StatelessWidget { onPressed: () { context.read().add( StopScheduleEvent( - deviceId: deviceId, mode: ScheduleModes.inching), + deviceId: deviceId, + mode: ScheduleModes.inching, + countdownCode: ''), ); }, backgroundColor: Colors.red, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index c511b8bd..b654698d 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -18,11 +18,15 @@ class BuildScheduleView extends StatelessWidget { super.key, required this.deviceUuid, required this.category, + required this.countdownCode, this.code, + required this.deviceType, }); final String deviceUuid; final String category; final String? code; + final String? countdownCode; + final String deviceType; @override Widget build(BuildContext context) { @@ -31,7 +35,8 @@ class BuildScheduleView extends StatelessWidget { deviceId: deviceUuid, ) ..add(ScheduleGetEvent(category: category)) - ..add(ScheduleFetchStatusEvent(deviceUuid)), + ..add(ScheduleFetchStatusEvent( + deviceId: deviceUuid, countdownCode: countdownCode ?? '')), child: Dialog( backgroundColor: Colors.white, insetPadding: const EdgeInsets.all(20), @@ -52,28 +57,32 @@ class BuildScheduleView extends StatelessWidget { children: [ const ScheduleHeader(), const SizedBox(height: 20), - ScheduleModeSelector( - currentMode: state.scheduleMode, - ), + if (deviceType == 'CUR_2') + const SizedBox() + else + ScheduleModeSelector( + countdownCode: countdownCode ?? '', + currentMode: state.scheduleMode, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) ScheduleManagementUI( + deviceType: deviceType, category: category, deviceUuid: deviceUuid, onAddSchedule: () async { final entry = await ScheduleDialogHelper - .showAddScheduleDialog( - context, - schedule: ScheduleEntry( - category: category, - time: '', - function: Status( - code: code.toString(), value: null), - days: [], - ), - isEdit: false, - code: code, - ); + .showAddScheduleDialog(context, + schedule: ScheduleEntry( + category: category, + time: '', + function: Status( + code: code.toString(), value: null), + days: [], + ), + isEdit: false, + code: code, + deviceType: deviceType); if (entry != null) { context.read().add( ScheduleAddEvent( @@ -87,14 +96,16 @@ class BuildScheduleView extends StatelessWidget { } }, ), - if (state.scheduleMode == ScheduleModes.countdown || - state.scheduleMode == ScheduleModes.inching) - CountdownInchingView( - deviceId: deviceUuid, - ), + if (deviceType != 'CUR_2') + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + CountdownInchingView( + deviceId: deviceUuid, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.countdown) CountdownModeButtons( + countDownCode: countdownCode ?? '', isActive: state.isCountdownActive, deviceId: deviceUuid, hours: state.countdownHours, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index 8f871ce4..1a89c1ee 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -5,14 +5,16 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleManagementUI extends StatelessWidget { - final String deviceUuid; + final String deviceUuid; final VoidCallback onAddSchedule; final String category; + final String deviceType; const ScheduleManagementUI({ super.key, required this.deviceUuid, required this.onAddSchedule, + required this.deviceType, this.category = 'switch_1', }); @@ -44,7 +46,11 @@ class ScheduleManagementUI extends StatelessWidget { ), ), const SizedBox(height: 20), - ScheduleTableWidget(deviceUuid: deviceUuid, category: category), + ScheduleTableWidget( + deviceUuid: deviceUuid, + category: category, + deviceType: deviceType, + ), ], ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart index 25bf7f2c..200d8c66 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -7,10 +7,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleModeSelector extends StatelessWidget { final ScheduleModes currentMode; + final String countdownCode; const ScheduleModeSelector({ super.key, required this.currentMode, + required this.countdownCode, }); @override @@ -71,7 +73,8 @@ class ScheduleModeSelector extends StatelessWidget { onChanged: (ScheduleModes? value) { if (value != null) { context.read().add( - UpdateScheduleModeEvent(scheduleMode: value), + UpdateScheduleModeEvent( + scheduleMode: value, countdownCode: countdownCode), ); if (value == ScheduleModes.schedule) { context.read().add( diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index b23e48df..886a6f73 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -12,11 +12,13 @@ import 'package:syncrow_web/utils/format_date_time.dart'; class ScheduleTableWidget extends StatelessWidget { final String deviceUuid; final String category; + final String deviceType; const ScheduleTableWidget({ super.key, required this.deviceUuid, this.category = 'switch_1', + required this.deviceType, }); @override @@ -25,13 +27,14 @@ class ScheduleTableWidget extends StatelessWidget { create: (_) => ScheduleBloc( deviceId: deviceUuid, )..add(ScheduleGetEvent(category: category)), - child: _ScheduleTableView(), + child: _ScheduleTableView(deviceType), ); } } class _ScheduleTableView extends StatelessWidget { - const _ScheduleTableView(); + final String deviceType; + const _ScheduleTableView(this.deviceType); @override Widget build(BuildContext context) { @@ -81,7 +84,7 @@ class _ScheduleTableView extends StatelessWidget { bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), - child: _buildTableBody(state.schedules, context)); + child: _buildTableBody(state.schedules, context, deviceType)); } if (state is ScheduleError) { return Center(child: Text(state.error)); @@ -123,7 +126,8 @@ class _ScheduleTableView extends StatelessWidget { ); } - Widget _buildTableBody(List schedules, BuildContext context) { + Widget _buildTableBody( + List schedules, BuildContext context, String deviceType) { return SizedBox( height: 200, child: SingleChildScrollView( @@ -132,7 +136,8 @@ class _ScheduleTableView extends StatelessWidget { defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ for (int i = 0; i < schedules.length; i++) - _buildScheduleRow(schedules[i], i, context), + _buildScheduleRow(schedules[i], i, context, + deviceType: deviceType), ], ), ), @@ -155,25 +160,19 @@ class _ScheduleTableView extends StatelessWidget { } TableRow _buildScheduleRow( - ScheduleModel schedule, int index, BuildContext context) { + ScheduleModel schedule, int index, BuildContext context, + {required String deviceType}) { return TableRow( children: [ Center( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - bool temp; - if (schedule.category == 'CUR_2') { - temp = schedule.function.value == 'open' ? true : false; - } else { - temp = schedule.function.value as bool; - } context.read().add( ScheduleUpdateEntryEvent( category: schedule.category, scheduleId: schedule.scheduleId, - functionOn: temp, - // schedule.function.value, + functionOn: schedule.function.value, enable: !schedule.enable, ), ); @@ -195,10 +194,11 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - schedule.category == 'CUR_2' - ? Center( - child: Text(schedule.function.value == true ? 'open' : 'close')) - : Center(child: Text(schedule.function.value ? 'On' : 'Off')), + if (deviceType == 'CUR_2') + Center( + child: Text(schedule.function.value == true ? 'open' : 'close')) + else + Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, @@ -206,18 +206,28 @@ class _ScheduleTableView extends StatelessWidget { TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - ScheduleDialogHelper.showAddScheduleDialog( - context, - schedule: ScheduleEntry.fromScheduleModel(schedule), - isEdit: true, - ).then((updatedSchedule) { + ScheduleDialogHelper.showAddScheduleDialog(context, + schedule: ScheduleEntry.fromScheduleModel(schedule), + isEdit: true, + deviceType: deviceType) + .then((updatedSchedule) { if (updatedSchedule != null) { + bool temp; + if (deviceType == 'CUR_2') { + updatedSchedule.function.value == 'open' + ? temp = true + : temp = false; + } else { + temp = updatedSchedule.function.value; + } + print('deviceType $deviceType'); context.read().add( ScheduleEditEvent( + deviceType: deviceType, scheduleId: schedule.scheduleId, category: schedule.category, time: updatedSchedule.time, - functionOn: updatedSchedule.function.value, + functionOn: temp, selectedDays: updatedSchedule.days), ); } diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 72435b74..8878a159 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -111,6 +111,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceId, + countdownCode: 'countdown_1', + deviceType: '3GT', ), )); }, @@ -127,6 +129,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_2', deviceUuid: deviceId, + countdownCode: 'countdown_2', + deviceType: '3GT', ), )); }, @@ -143,6 +147,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_3', deviceUuid: deviceId, + countdownCode: 'countdown_3', + deviceType: '3GT', ), )); }, diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 66784bd5..79b843a3 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -102,6 +102,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_1', + countdownCode: 'countdown_1', + deviceType: '3G', ), )); }, @@ -118,6 +120,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_2', + countdownCode: 'countdown_2', + deviceType: '3G', ), )); }, @@ -134,6 +138,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_3', + countdownCode: 'countdown_3', + deviceType: '3G', ), )); }, diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 34b30dd3..91a8497b 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -102,6 +102,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( + deviceType: '1GT', + countdownCode: 'countdown_1', deviceUuid: deviceId, category: 'switch_1', ), @@ -118,6 +120,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( + deviceType: '1GT', + countdownCode: 'countdown_2', deviceUuid: deviceId, category: 'switch_2', ), diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index 849412f2..10909e8f 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -97,6 +97,8 @@ class TwoGangBatchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceIds.first, + countdownCode: 'countdown_1', + deviceType: '2G', ), )); }, @@ -114,6 +116,8 @@ class TwoGangBatchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_2', deviceUuid: deviceIds.first, + countdownCode: 'countdown_2', + deviceType: '2G', ), )); }, @@ -121,10 +125,7 @@ class TwoGangBatchControlView extends StatelessWidget subtitle: 'Scheduling', iconPath: Assets.scheduling, ), - // FirmwareUpdateWidget( - // deviceId: deviceIds.first, - // version: 12, - // ), + FactoryResetWidget(callFactoryReset: () { context.read().add( TwoGangFactoryReset( diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index ac3fe579..0ff7d964 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -103,6 +103,8 @@ class TwoGangDeviceControlView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_1', + countdownCode: 'countdown_1', + deviceType: '2G', ), )); }, @@ -125,6 +127,8 @@ class TwoGangDeviceControlView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_2', + countdownCode: 'countdown_2', + deviceType: '2G', ), )); }, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 389eac3f..e483e7c1 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -18,14 +18,21 @@ class ScheduleDialogHelper { ScheduleEntry? schedule, bool isEdit = false, String? code, + required String deviceType, }) { + bool temp; + if (deviceType == 'CUR_2') { + temp = schedule!.function.value == 'open' ? true : false; + } else { + temp = schedule!.function.value; + } final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) : TimeOfDay.now(); final initialDays = schedule != null ? _convertDaysStringToBooleans(schedule.days) : List.filled(7, false); - bool? functionOn = schedule?.function.value ?? true; + bool? functionOn = temp; TimeOfDay selectedTime = initialTime; List selectedDays = List.of(initialDays); @@ -97,8 +104,7 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(schedule!.category, ctx, functionOn!, - (v) { + _buildFunctionSwitch(deviceType, ctx, functionOn!, (v) { setState(() => functionOn = v); }), ], @@ -114,32 +120,30 @@ class ScheduleDialogHelper { ), ), SizedBox( - width: 100, - child: ElevatedButton( - onPressed: () { - dynamic temp; - if (schedule?.category == 'CUR_2') { - temp = functionOn! ? 'open' : 'close'; - } else { - temp = functionOn; - } - print(temp); - final entry = ScheduleEntry( - category: schedule?.category ?? 'switch_1', - time: _formatTimeOfDayToISO(selectedTime), - function: Status( - code: code ?? 'switch_1', - value: temp, - // functionOn, - ), - days: _convertSelectedDaysToStrings(selectedDays), - scheduleId: schedule?.scheduleId, - ); - Navigator.pop(ctx, entry); - }, - child: const Text('Save'), - ), - ), + width: 100, + child: ElevatedButton( + onPressed: () { + dynamic temp; + if (deviceType == 'CUR_2') { + temp = functionOn! ? 'open' : 'close'; + } else { + temp = functionOn; + } + final entry = ScheduleEntry( + category: schedule?.category ?? 'switch_1', + time: _formatTimeOfDayToISO(selectedTime), + function: Status( + code: code ?? 'switch_1', + value: temp, + + ), + days: _convertSelectedDaysToStrings(selectedDays), + scheduleId: schedule.scheduleId, + ); + Navigator.pop(ctx, entry); + }, + child: const Text('Save'), + )), ], ); }, diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 16eff86a..a0e39bfa 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -84,6 +84,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget child: BuildScheduleView( deviceUuid: device.uuid ?? '', category: 'switch_1', + countdownCode: 'countdown_1', + deviceType: 'WH', ), )); }, From e2d6f5eea83af1deed21cdcb2ee5adfd37f21581 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 30 Jun 2025 15:11:17 +0300 Subject: [PATCH 029/105] Update device type from '1GT' to '2GT' in TwoGangGlassSwitchControlView --- .../view/two_gang_glass_switch_control_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 91a8497b..90328896 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -102,7 +102,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - deviceType: '1GT', + deviceType: '2GT', countdownCode: 'countdown_1', deviceUuid: deviceId, category: 'switch_1', @@ -120,7 +120,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - deviceType: '1GT', + deviceType: '2GT', countdownCode: 'countdown_2', deviceUuid: deviceId, category: 'switch_2', From 5486f0832de017243bb4e677642f4b6d3a89f1b4 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 30 Jun 2025 15:22:02 +0300 Subject: [PATCH 030/105] Remove unused copyWith method from Status class and simplify status assignment in ScheduleBloc --- .../all_devices/models/device_status.dart | 10 ---------- .../schedule_device/bloc/schedule_bloc.dart | 9 --------- 2 files changed, 19 deletions(-) diff --git a/lib/pages/device_managment/all_devices/models/device_status.dart b/lib/pages/device_managment/all_devices/models/device_status.dart index b0af600e..1f23e3f9 100644 --- a/lib/pages/device_managment/all_devices/models/device_status.dart +++ b/lib/pages/device_managment/all_devices/models/device_status.dart @@ -57,16 +57,6 @@ class Status { }; } - Status copyWith({ - String? code, - dynamic value, - }) { - return Status( - code: code ?? this.code, - value: value ?? this.value, - ); - } - factory Status.fromJson(String source) => Status.fromMap(json.decode(source)); String toJson() => json.encode(toMap()); 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 61896a74..65773590 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -298,15 +298,6 @@ class ScheduleBloc extends Bloc { } final dateTime = DateTime.parse(event.time); - Status status = Status(code: '', value: ''); - if (event.category == 'CUR_2') { - status = status.copyWith( - code: 'control', - value: event.functionOn == true ? 'open' : 'close'); - } else { - status = - status.copyWith(code: event.category, value: event.functionOn); - } final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, From 8cb6c13cd562abf5494bf6b05419038f2119ec46 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 30 Jun 2025 15:53:23 +0300 Subject: [PATCH 031/105] Changed timer codes in curtain module to match what the API expects. --- .../curtain_module/view/curtain_module_items.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 6e595565..2bd6f9cd 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -62,9 +62,9 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, - category: 'timer', + category: 'Timer', code: 'control', - countdownCode: 'timer', + countdownCode: 'Timer', deviceType: 'CUR_2', ), )); From e6fe9f35b04f09f3bc04034936da5c2f4cac50c7 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Tue, 1 Jul 2025 08:41:38 +0300 Subject: [PATCH 032/105] problem fixed should reset filters when select space or community --- .../all_devices/widgets/device_managment_body.dart | 1 + 1 file changed, 1 insertion(+) 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 7ca33d1d..5b709540 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -68,6 +68,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { children: [ Expanded(child: SpaceTreeView( onSelect: () { + context.read().add(ResetFilters()); context.read().add(FetchDevices(context)); }, )), From c07bae5cbc89d23671ae6eba7e150b7cd25612fa Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 1 Jul 2025 09:32:00 +0300 Subject: [PATCH 033/105] align bar charts to start. --- .../modules/air_quality/widgets/aqi_distribution_chart.dart | 1 + .../analytics/modules/occupancy/widgets/occupancy_chart.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 4fdd8a2a..20365325 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -20,6 +20,7 @@ class AqiDistributionChart extends StatelessWidget { return BarChart( BarChartData( maxY: 100.1, + alignment: BarChartAlignment.start, gridData: EnergyManagementChartsHelper.gridData( horizontalInterval: 20, ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 1205a66e..23cc527d 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -18,6 +18,7 @@ class OccupancyChart extends StatelessWidget { return BarChart( BarChartData( maxY: 100.001, + alignment: BarChartAlignment.start, gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, horizontalInterval: 20, From 037895844a37c3fd7ab0fc0cc846f960982e350e Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 1 Jul 2025 09:44:59 +0300 Subject: [PATCH 034/105] Add EnergyConsumptionPage to SmartPowerDeviceControl for enhanced energy data visualization --- .../view/smart_power_device_control.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart index 11d1cc8f..03202ba0 100644 --- a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -277,6 +277,32 @@ class SmartPowerDeviceControl extends StatelessWidget totalConsumption: 10000, date: blocProvider.formattedDate, ), + EnergyConsumptionPage( + formattedDate: + '${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}', + onTap: () { + blocProvider.add(SelectDateEvent(context: context)); + }, + widget: blocProvider.dateSwitcher(), + chartData: blocProvider.energyDataList.isNotEmpty + ? blocProvider.energyDataList + : [ + EnergyData('12:00 AM', 4.0), + EnergyData('01:00 AM', 6.5), + EnergyData('02:00 AM', 3.8), + EnergyData('03:00 AM', 3.2), + EnergyData('04:00 AM', 6.0), + EnergyData('05:00 AM', 3.4), + EnergyData('06:00 AM', 5.2), + EnergyData('07:00 AM', 3.5), + EnergyData('08:00 AM', 6.8), + EnergyData('09:00 AM', 5.6), + EnergyData('10:00 AM', 3.9), + EnergyData('11:00 AM', 4.0), + ], + totalConsumption: 10000, + date: blocProvider.formattedDate, + ), ], ), ), From 9c9b7d99dcbb9221ece7c0506190caf130606aef Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 1 Jul 2025 09:55:38 +0300 Subject: [PATCH 035/105] enhance sizing of energy management view. --- .../views/analytics_energy_management_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart index 09fb6155..a5af9e10 100644 --- a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart +++ b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart @@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: MediaQuery.sizeOf(context).height * 1, + height: MediaQuery.sizeOf(context).height * 1.05, child: const Column( children: [ Expanded( From 9f86b8d63869ddb81927659e21b20e6c1d84f7e4 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 1 Jul 2025 11:14:02 +0300 Subject: [PATCH 036/105] remove countdownRemaining from ScheduleLoaded state --- .../device_managment/schedule_device/bloc/schedule_bloc.dart | 1 - 1 file changed, 1 deletion(-) 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 65773590..62bef920 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -232,7 +232,6 @@ class ScheduleBloc extends Bloc { selectedDays: List.filled(7, false), functionOn: false, isEditing: false, - countdownRemaining: Duration.zero, )); } else { emit(ScheduleLoaded( From f33b3e8bd2692aa11e6da43efad76aefcc2c9f4c Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Tue, 1 Jul 2025 11:19:35 +0300 Subject: [PATCH 037/105] now if user change end time into value before start time it prevent it with dialog information showing the error and if the init start date is null fill it with the needed value --- .../bloc/visitor_password_bloc.dart | 190 +++++++++++------- 1 file changed, 115 insertions(+), 75 deletions(-) diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 6dc20cfd..c5ee3259 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -21,7 +21,6 @@ import 'package:syncrow_web/utils/snack_bar.dart'; class VisitorPasswordBloc extends Bloc { - VisitorPasswordBloc() : super(VisitorPasswordInitial()) { on(selectUsageFrequency); on(_onFetchDevice); @@ -87,6 +86,9 @@ class VisitorPasswordBloc SelectTimeVisitorPassword event, Emitter emit, ) async { + // Ensure expirationTimeTimeStamp has a value + effectiveTimeTimeStamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000; + final DateTime? picked = await showDatePicker( context: event.context, initialDate: DateTime.now(), @@ -94,86 +96,124 @@ class VisitorPasswordBloc lastDate: DateTime.now().add(const Duration(days: 5095)), ); - if (picked != null) { - final TimeOfDay? timePicked = await showTimePicker( - context: event.context, - initialTime: TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: ThemeData.light().copyWith( - colorScheme: const ColorScheme.light( - primary: ColorsManager.primaryColor, - onSurface: Colors.black, - ), - buttonTheme: const ButtonThemeData( - colorScheme: ColorScheme.light( - primary: Colors.green, - ), - ), + if (picked == null) return; + + final TimeOfDay? timePicked = await showTimePicker( + context: event.context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + onSurface: Colors.black, ), - child: child!, - ); - }, - ); - - if (timePicked != null) { - final selectedDateTime = DateTime( - picked.year, - picked.month, - picked.day, - timePicked.hour, - timePicked.minute, + ), + child: child!, ); + }, + ); - final selectedTimestamp = - selectedDateTime.millisecondsSinceEpoch ~/ 1000; + if (timePicked == null) return; - if (event.isStart) { - if (expirationTimeTimeStamp != null && - selectedTimestamp > expirationTimeTimeStamp!) { - CustomSnackBar.displaySnackBar( + final selectedDateTime = DateTime( + picked.year, + picked.month, + picked.day, + timePicked.hour, + timePicked.minute, + ); + final selectedTimestamp = selectedDateTime.millisecondsSinceEpoch ~/ 1000; + final currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + if (event.isStart) { + // START TIME VALIDATION + if (expirationTimeTimeStamp != null && + selectedTimestamp > expirationTimeTimeStamp!) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text( 'Effective Time cannot be later than Expiration Time.', - ); - return; - } - if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { - if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { - await showDialog( - context: event.context, - builder: (context) => AlertDialog( - title: const Text('Effective Time cannot be earlier than current time.'), - actionsAlignment: MainAxisAlignment.center, - content: - FilledButton( - onPressed: () { - Navigator.of(event.context).pop(); - add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false)); - }, - child: const Text('OK'), - ), - - ), - ); - } - return; - } - effectiveTimeTimeStamp = selectedTimestamp; - startTimeAccess = selectedDateTime.toString().split('.').first; - } else { - if (effectiveTimeTimeStamp != null && - selectedTimestamp < effectiveTimeTimeStamp!) { - CustomSnackBar.displaySnackBar( - 'Expiration Time cannot be earlier than Effective Time.', - ); - return; - } - expirationTimeTimeStamp = selectedTimestamp; - endTimeAccess = selectedDateTime.toString().split('.').first; - } - emit(ChangeTimeState()); - emit(VisitorPasswordInitial()); + ), + actionsAlignment: MainAxisAlignment.center, + content: FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword( + context: event.context, + isStart: true, + isRepeat: false, + )); + }, + child: const Text('OK'), + ), + ), + ); + return; } + + if (selectedTimestamp < currentTimestamp) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text( + 'Effective Time cannot be earlier than current time.', + ), + actionsAlignment: MainAxisAlignment.center, + content: FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword( + context: event.context, + isStart: true, + isRepeat: false, + )); + }, + child: const Text('OK'), + ), + ), + ); + return; + } + + // Save effective time + effectiveTimeTimeStamp = selectedTimestamp; + startTimeAccess = selectedDateTime.toString().split('.').first; + } else { + // END TIME VALIDATION + if (effectiveTimeTimeStamp != null && + selectedTimestamp < effectiveTimeTimeStamp!) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text( + 'Expiration Time cannot be earlier than Effective Time.', + ), + actionsAlignment: MainAxisAlignment.center, + content: FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword( + context: event.context, + isStart: false, + isRepeat: false, + )); + }, + child: const Text('OK'), + ), + ), + ); + return; + } + + // Save expiration time + expirationTimeTimeStamp = selectedTimestamp; + endTimeAccess = selectedDateTime.toString().split('.').first; } + + emit(ChangeTimeState()); + emit(VisitorPasswordInitial()); } bool toggleRepeat( @@ -213,7 +253,7 @@ class VisitorPasswordBloc FetchDevice event, Emitter emit) async { try { emit(DeviceLoaded()); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; data = await AccessMangApi().fetchDoorLockDeviceList(projectUuid); emit(TableLoaded(data)); From 818bdee7456111dbba0aba6bcbff5195c22c8b8a Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Tue, 1 Jul 2025 15:04:50 +0300 Subject: [PATCH 038/105] change calibration completed dialog color --- .../curtain_module/widgets/calibrate_completed_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart index bd0cbb37..77957d76 100644 --- a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart @@ -17,6 +17,7 @@ class CalibrateCompletedDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: SizedBox( height: 250, From 0847cb8a41b1ade3d5e921b6fdef50ee4eafb9e0 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 2 Jul 2025 08:19:56 +0300 Subject: [PATCH 039/105] fix UI --- .../create_new_routines/space_dropdown.dart | 205 +++++++++--------- .../main_routine_view/routine_view_card.dart | 16 +- 2 files changed, 113 insertions(+), 108 deletions(-) diff --git a/lib/pages/routines/create_new_routines/space_dropdown.dart b/lib/pages/routines/create_new_routines/space_dropdown.dart index 0d2dc075..0605b7fc 100644 --- a/lib/pages/routines/create_new_routines/space_dropdown.dart +++ b/lib/pages/routines/create_new_routines/space_dropdown.dart @@ -32,113 +32,114 @@ class SpaceDropdown extends StatelessWidget { color: ColorsManager.blackColor, ), ), - SizedBox( - child: Container( + DropdownButton2( + underline: const SizedBox(), + buttonStyleData: ButtonStyleData( + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(12)), + ), + value: selectedValue, + items: spaces.map((space) { + return DropdownMenuItem( + value: space.uuid, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + ' ${space.name}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + color: selectedValue == space.uuid + ? ColorsManager.dialogBlueTitle + : ColorsManager.blackColor, + ), + ), + Text( + ' ${space.lastThreeParents}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 12, + color: selectedValue == space.uuid + ? ColorsManager.dialogBlueTitle + : ColorsManager.blackColor, + ), + ), + ], + ), + ); + }).toList(), + onChanged: onChanged, + style: TextStyle( + color: Colors.black, + fontSize: 13, + ), + hint: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + hintMessage, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.textGray, + ), + ), + ), + customButton: Container( height: 40, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.textGray, width: 1.0), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 8, + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedValue != null + ? spaces + .firstWhere((e) => e.uuid == selectedValue) + .name + : hintMessage, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 13, + color: selectedValue != null + ? Colors.black + : ColorsManager.textGray, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + height: 45, + child: const Icon( + Icons.keyboard_arrow_down, + color: ColorsManager.textGray, + ), + ), + ), + ], + ), + ), + dropdownStyleData: DropdownStyleData( + maxHeight: MediaQuery.of(context).size.height * 0.4, decoration: BoxDecoration( color: ColorsManager.whiteColors, borderRadius: BorderRadius.circular(10), ), - child: DropdownButton2( - underline: const SizedBox(), - value: selectedValue, - items: spaces.map((space) { - return DropdownMenuItem( - value: space.uuid, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - ' ${space.name}', - style: - Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - ' ${space.lastThreeParents}', - style: - Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 12, - ), - ), - ], - ), - ); - }).toList(), - onChanged: onChanged, - style: TextStyle(color: Colors.black), - hint: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - hintMessage, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.textGray, - ), - ), - ), - customButton: Container( - height: 45, - decoration: BoxDecoration( - border: - Border.all(color: ColorsManager.textGray, width: 1.0), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 8, - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - selectedValue != null - ? spaces - .firstWhere((e) => e.uuid == selectedValue) - .name - : hintMessage, - style: - Theme.of(context).textTheme.bodySmall!.copyWith( - color: selectedValue != null - ? Colors.black - : ColorsManager.textGray, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: const BorderRadius.only( - topRight: Radius.circular(10), - bottomRight: Radius.circular(10), - ), - ), - height: 45, - child: const Icon( - Icons.keyboard_arrow_down, - color: ColorsManager.textGray, - ), - ), - ), - ], - ), - ), - dropdownStyleData: DropdownStyleData( - maxHeight: MediaQuery.of(context).size.height * 0.4, - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(10), - ), - ), - menuItemStyleData: const MenuItemStyleData( - height: 60, - ), - ), + ), + menuItemStyleData: const MenuItemStyleData( + height: 60, ), ), ], diff --git a/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart b/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart index 4fc4bd0f..df21c93e 100644 --- a/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart +++ b/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart @@ -121,7 +121,8 @@ class _RoutineViewCardState extends State { child: SizedBox( width: 16, height: 16, - child: CircularProgressIndicator(strokeWidth: 2), + child: + CircularProgressIndicator(strokeWidth: 2), ), ), ) @@ -159,8 +160,9 @@ class _RoutineViewCardState extends State { height: iconSize, width: iconSize, fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - Image.asset( + errorBuilder: + (context, error, stackTrace) => + Image.asset( Assets.logo, height: iconSize, width: iconSize, @@ -203,7 +205,8 @@ class _RoutineViewCardState extends State { maxLines: 1, style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, - fontSize: widget.isSmallScreenSize(context) ? 10 : 12, + fontSize: + widget.isSmallScreenSize(context) ? 10 : 12, ), ), if (widget.spaceName != '') @@ -222,8 +225,9 @@ class _RoutineViewCardState extends State { maxLines: 1, style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, - fontSize: - widget.isSmallScreenSize(context) ? 10 : 12, + fontSize: widget.isSmallScreenSize(context) + ? 10 + : 12, ), ), ], From b06e4bd2ba7500fac6202908c6efe4025807b7c3 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 2 Jul 2025 08:27:09 +0300 Subject: [PATCH 040/105] hot fix thermostat string --- .../ac/view/ac_device_batch_control.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart index aad0669b..61168f55 100644 --- a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -15,7 +15,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class AcDeviceBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const AcDeviceBatchControlView({super.key, required this.devicesIds}); final List devicesIds; @@ -51,7 +52,7 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo deviceId: devicesIds.first, code: 'switch', value: state.status.acSwitch, - label: 'ThermoState', + label: 'Thermostat', icon: Assets.ac, onChange: (value) { context.read().add(AcBatchControlEvent( @@ -100,8 +101,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo ), Text( 'h', - style: - context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor), + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blackColor), ), Text( '30', @@ -148,7 +149,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo callFactoryReset: () { context.read().add(AcFactoryResetEvent( deviceId: state.status.uuid, - factoryResetModel: FactoryResetModel(devicesUuid: devicesIds), + factoryResetModel: + FactoryResetModel(devicesUuid: devicesIds), )); }, ), From f7e4d6ff0717facb49447c06cedadea90647f156 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 09:33:45 +0300 Subject: [PATCH 041/105] added default dialog background color to be white. --- lib/utils/theme/theme.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/utils/theme/theme.dart b/lib/utils/theme/theme.dart index 5ac61afa..5c036762 100644 --- a/lib/utils/theme/theme.dart +++ b/lib/utils/theme/theme.dart @@ -52,4 +52,7 @@ final myTheme = ThemeData( borderRadius: BorderRadius.circular(4), ), ), + dialogTheme: const DialogThemeData( + backgroundColor: ColorsManager.whiteColors, + ), ); From 7750290be481c2fb7a1a5bc29b55066235740ec0 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 2 Jul 2025 10:14:54 +0300 Subject: [PATCH 042/105] Fix device status display in Control modal to reflect actual status --- lib/pages/common/custom_table.dart | 365 ++++++++++++++++------------- 1 file changed, 206 insertions(+), 159 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index fb8237b7..93f8998e 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -50,6 +50,9 @@ class _DynamicTableState extends State { bool _selectAll = false; final ScrollController _verticalScrollController = ScrollController(); final ScrollController _horizontalScrollController = ScrollController(); + static const double _fixedRowHeight = 60; + static const double _checkboxColumnWidth = 50; + static const double _settingsColumnWidth = 100; @override void initState() { @@ -67,7 +70,6 @@ class _DynamicTableState extends State { bool _compareListOfLists( List> oldList, List> newList) { - // Check if the old and new lists are the same if (oldList.length != newList.length) return false; for (int i = 0; i < oldList.length; i++) { @@ -104,73 +106,130 @@ class _DynamicTableState extends State { context.read().add(UpdateSelection(_selectedRows)); } + double get _totalTableWidth { + final hasSettings = widget.headers.contains('Settings'); + final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) + + (hasSettings ? _settingsColumnWidth : 0); + final regularCount = widget.headers.length - (hasSettings ? 1 : 0); + final regularWidth = (widget.size.width - base) / regularCount; + return base + regularCount * regularWidth; + } + @override Widget build(BuildContext context) { return Container( + width: widget.size.width, + height: widget.size.height, decoration: widget.cellDecoration, - child: Scrollbar( - controller: _verticalScrollController, - thumbVisibility: true, - trackVisibility: true, + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), child: Scrollbar( - //fixed the horizontal scrollbar issue controller: _horizontalScrollController, thumbVisibility: true, trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, + notificationPredicate: (notif) => + notif.metrics.axis == Axis.horizontal, child: SingleChildScrollView( - controller: _verticalScrollController, - child: SingleChildScrollView( - controller: _horizontalScrollController, - scrollDirection: Axis.horizontal, - child: SizedBox( - width: widget.size.width, - child: Column( - children: [ - Container( - decoration: widget.headerDecoration ?? - const BoxDecoration( - color: ColorsManager.boxColor, + controller: _horizontalScrollController, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: _totalTableWidth, + child: Column( + children: [ + Container( + height: _fixedRowHeight, + decoration: widget.headerDecoration ?? + const BoxDecoration(color: ColorsManager.boxColor), + child: Row( + children: [ + if (widget.withCheckBox) + _buildSelectAllCheckbox(_checkboxColumnWidth), + for (var i = 0; i < widget.headers.length; i++) + _buildTableHeaderCell( + widget.headers[i], + widget.headers[i] == 'Settings' + ? _settingsColumnWidth + : (_totalTableWidth - + (widget.withCheckBox + ? _checkboxColumnWidth + : 0) - + (widget.headers.contains('Settings') + ? _settingsColumnWidth + : 0)) / + (widget.headers.length - + (widget.headers.contains('Settings') + ? 1 + : 0)), ), - child: Row( - children: [ - if (widget.withCheckBox) _buildSelectAllCheckbox(), - ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell( - widget.headers[index], index); - }) - //...widget.headers.map((header) => _buildTableHeaderCell(header)), - ], - ), + ], ), - SizedBox( - width: widget.size.width, - child: widget.isEmpty - ? _buildEmptyState() - : Column( - children: - List.generate(widget.data.length, (rowIndex) { + ), + + Expanded( + child: widget.isEmpty + ? _buildEmptyState() + : Scrollbar( + controller: _verticalScrollController, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => + notif.metrics.axis == Axis.vertical, + child: ListView.builder( + controller: _verticalScrollController, + itemCount: widget.data.length, + itemBuilder: (_, rowIndex) { final row = widget.data[rowIndex]; - return Row( - children: [ - if (widget.withCheckBox) - _buildRowCheckbox( - rowIndex, widget.size.height * 0.08), - ...row.asMap().entries.map((entry) { - return _buildTableCell( - entry.value.toString(), - widget.size.height * 0.08, - rowIndex: rowIndex, - columnIndex: entry.key, - ); - }).toList(), - ], + return SizedBox( + height: _fixedRowHeight, + child: Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox( + rowIndex, + _checkboxColumnWidth, + ), + for (var colIndex = 0; + colIndex < row.length; + colIndex++) + widget.headers[colIndex] == 'Settings' + ? buildSettingsIcon( + width: _settingsColumnWidth, + onTap: () => widget + .onSettingsPressed + ?.call(rowIndex), + ) + : _buildTableCell( + row[colIndex].toString(), + width: widget.headers[ + colIndex] == + 'Settings' + ? _settingsColumnWidth + : (_totalTableWidth - + (widget.withCheckBox + ? _checkboxColumnWidth + : 0) - + (widget.headers + .contains( + 'Settings') + ? _settingsColumnWidth + : 0)) / + (widget.headers.length - + (widget.headers + .contains( + 'Settings') + ? 1 + : 0)), + rowIndex: rowIndex, + columnIndex: colIndex, + ), + ], + ), ); - }), + }, ), - ), - ], - ), + ), + ), + ], ), ), ), @@ -210,9 +269,10 @@ class _DynamicTableState extends State { ], ), ); - Widget _buildSelectAllCheckbox() { + + Widget _buildSelectAllCheckbox(double width) { return Container( - width: 50, + width: width, decoration: const BoxDecoration( border: Border.symmetric( vertical: BorderSide(color: ColorsManager.boxDivider), @@ -227,11 +287,11 @@ class _DynamicTableState extends State { ); } - Widget _buildRowCheckbox(int index, double size) { + Widget _buildRowCheckbox(int index, double width) { return Container( - width: 50, + width: width, padding: const EdgeInsets.all(8.0), - height: size, + height: _fixedRowHeight, decoration: const BoxDecoration( border: Border( bottom: BorderSide( @@ -253,50 +313,47 @@ class _DynamicTableState extends State { ); } - Widget _buildTableHeaderCell(String title, int index) { - return Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border.symmetric( - vertical: BorderSide(color: ColorsManager.boxDivider), - ), + Widget _buildTableHeaderCell(String title, double width) { + return Container( + width: width, + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: ColorsManager.boxDivider), ), - constraints: const BoxConstraints.expand(height: 40), - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: index == widget.headers.length - 1 ? 12 : 8.0, - vertical: 4), - child: Text( - title, - style: context.textTheme.titleSmall!.copyWith( - color: ColorsManager.grayColor, - fontSize: 12, - fontWeight: FontWeight.w400, - ), - maxLines: 2, + ), + constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight), + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: Text( + title, + style: context.textTheme.titleSmall!.copyWith( + color: ColorsManager.grayColor, + fontSize: 12, + fontWeight: FontWeight.w400, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), ); } - Widget _buildTableCell(String content, double size, - {required int rowIndex, required int columnIndex}) { + Widget _buildTableCell(String content, + {required double width, + required int rowIndex, + required int columnIndex}) { bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; if (isBatteryLevel) { batteryLevel = double.tryParse(content.replaceAll('%', '').trim()); } + bool isSettingsColumn = widget.headers[columnIndex] == 'Settings'; if (isSettingsColumn) { return buildSettingsIcon( - width: 120, - height: 60, - iconSize: 40, - onTap: () => widget.onSettingsPressed?.call(rowIndex), - ); + width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex)); } Color? statusColor; @@ -320,92 +377,82 @@ class _DynamicTableState extends State { statusColor = Colors.black; } - return Expanded( - child: Container( - height: size, - padding: const EdgeInsets.all(5.0), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ColorsManager.boxDivider, - width: 1.0, - ), + return Container( + width: width, + height: _fixedRowHeight, + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, ), - color: Colors.white, ), - alignment: Alignment.centerLeft, - child: Text( - content, - style: TextStyle( - color: (batteryLevel != null && batteryLevel < 20) - ? ColorsManager.red - : (batteryLevel != null && batteryLevel > 20) - ? ColorsManager.green - : statusColor, - fontSize: 13, - fontWeight: FontWeight.w400), - maxLines: 2, + color: Colors.white, + ), + alignment: Alignment.centerLeft, + child: Text( + content, + style: TextStyle( + color: (batteryLevel != null && batteryLevel < 20) + ? ColorsManager.red + : (batteryLevel != null && batteryLevel > 20) + ? ColorsManager.green + : statusColor, + fontSize: 13, + fontWeight: FontWeight.w400, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ); } - Widget buildSettingsIcon( - {double width = 120, - double height = 60, - double iconSize = 40, - VoidCallback? onTap}) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10), - margin: const EdgeInsets.only(right: 15), - decoration: const BoxDecoration( - color: ColorsManager.whiteColors, - border: Border( - bottom: BorderSide( - color: ColorsManager.boxDivider, - width: 1.0, - ), - ), + Widget buildSettingsIcon({required double width, VoidCallback? onTap}) { + return Container( + width: width, + height: _fixedRowHeight, + padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10), + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, ), - width: width, - child: Padding( - padding: const EdgeInsets.only( - right: 16.0, - left: 17.0, - ), - child: Container( - width: 50, - decoration: BoxDecoration( - color: const Color(0xFFF7F8FA), - borderRadius: BorderRadius.circular(height / 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.17), - blurRadius: 14, - offset: const Offset(0, 4), - ), - ], + ), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Container( + width: 50, + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.17), + blurRadius: 14, + offset: const Offset(0, 4), ), - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: SvgPicture.asset( - Assets.settings, - width: 40, - height: 22, - color: ColorsManager.primaryColor, - ), - ), + ], + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Center( + child: SvgPicture.asset( + Assets.settings, + width: 40, + height: 20, + color: ColorsManager.primaryColor, ), ), ), ), ), - ], + ), ); } } From 62a6f9c993e2b64d45d017c041ef8b3c6ff1062a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:03 +0300 Subject: [PATCH 043/105] Add ButtonContentWidget for customizable button UI in space details. --- .../widgets/button_content_widget.dart | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart new file mode 100644 index 00000000..4c95634e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ButtonContentWidget extends StatelessWidget { + final String label; + final String? svgAssets; + final bool disabled; + + const ButtonContentWidget({ + required this.label, + this.svgAssets, + this.disabled = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Opacity( + opacity: disabled ? 0.5 : 1.0, + child: Container( + width: screenWidth * 0.25, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + border: Border.all( + color: ColorsManager.neutralGray, + width: 3.0, + ), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Row( + children: [ + if (svgAssets != null) + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + svgAssets!, + width: screenWidth * 0.015, + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle( + color: ColorsManager.blackColor, + fontSize: 16, + ), + ), + ), + ], + ), + ), + ); + } +} From 87c2e3261dce3f3dcdd3bea074c8ba0163a2a486 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:13 +0300 Subject: [PATCH 044/105] Add SpaceDetailsActionButtons widget for improved action handling in space details. --- .../widgets/space_details_action_buttons.dart | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart new file mode 100644 index 00000000..b26a6590 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceDetailsActionButtons extends StatelessWidget { + const SpaceDetailsActionButtons({ + super.key, + required this.onSave, + required this.onCancel, + }); + + final VoidCallback onCancel; + final VoidCallback onSave; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 10, + children: [ + Expanded(child: _buildCancelButton(context)), + Expanded(child: _buildSaveButton()), + ], + ); + } + + Widget _buildCancelButton(BuildContext context) { + return CancelButton( + onPressed: onCancel, + label: 'Cancel', + ); + } + + Widget _buildSaveButton() { + return Expanded( + child: DefaultButton( + onPressed: onSave, + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } +} From 50ff17a0c17b346d090736573af46d1347707e1c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:26 +0300 Subject: [PATCH 045/105] Add SpaceIconSelectionDialog widget for selecting space icons in a dialog. --- .../widgets/space_icon_selection_dialog.dart | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart new file mode 100644 index 00000000..428048b3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceIconSelectionDialog extends StatelessWidget { + const SpaceIconSelectionDialog({super.key}); + + static const List _icons = [ + Assets.location, + Assets.villa, + Assets.gym, + Assets.sauna, + Assets.bbq, + Assets.building, + Assets.desk, + Assets.door, + Assets.parking, + Assets.pool, + Assets.stair, + Assets.steamRoom, + Assets.street, + Assets.unit, + ]; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: SelectableText( + 'Space Icon', + style: context.textTheme.headlineMedium, + ), + backgroundColor: ColorsManager.whiteColors, + content: Container( + width: context.screenWidth * 0.45, + height: context.screenHeight * 0.275, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(12), + ), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 8, + mainAxisSpacing: 16, + ), + itemCount: _icons.length, + itemBuilder: (context, index) => IconButton( + onPressed: Navigator.of(context).pop, + icon: SvgPicture.asset( + _icons[index], + width: context.screenWidth * 0.03, + height: context.screenHeight * 0.08, + ), + ), + ), + ), + ); + } +} From bdeec7d325ea07ddbc621c497ac235fb0c082cd2 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:36 +0300 Subject: [PATCH 046/105] Add SpaceIconPicker widget for selecting and displaying space icons with a dialog option. --- .../widgets/space_icon_picker.dart | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart new file mode 100644 index 00000000..00552b88 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceIconPicker extends StatelessWidget { + const SpaceIconPicker({ + required this.iconPath, + super.key, + }); + + final String iconPath; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Stack( + alignment: Alignment.center, + children: [ + Container( + width: context.screenWidth * 0.1, + height: context.screenWidth * 0.1, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + ), + SvgPicture.asset( + iconPath, + width: context.screenWidth * 0.04, + height: context.screenWidth * 0.04, + ), + Positioned( + top: 20, + right: 20, + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (context) => const SpaceIconSelectionDialog(), + ), + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ), + ), + ], + ), + ], + ); + } +} From fdd0526c78c17d955edcab3a1e2d4393c67c3219 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 14:17:27 +0300 Subject: [PATCH 047/105] added copyWith to `SpaceDetailsModel` and its property models. --- .../domain/models/space_details_model.dart | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 891e7eb2..8e8bbb90 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -41,6 +41,22 @@ class SpaceDetailsModel extends Equatable { }; } + SpaceDetailsModel copyWith({ + String? uuid, + String? spaceName, + String? icon, + List? productAllocations, + List? subspaces, + }) { + return SpaceDetailsModel( + uuid: uuid ?? this.uuid, + spaceName: spaceName ?? this.spaceName, + icon: icon ?? this.icon, + productAllocations: productAllocations ?? this.productAllocations, + subspaces: subspaces ?? this.subspaces, + ); + } + @override List get props => [uuid, spaceName, icon, productAllocations, subspaces]; } @@ -70,6 +86,18 @@ class ProductAllocation extends Equatable { }; } + ProductAllocation copyWith({ + Product? product, + Tag? tag, + String? location, + }) { + return ProductAllocation( + product: product ?? this.product, + tag: tag ?? this.tag, + location: location ?? this.location, + ); + } + @override List get props => [product, tag]; } @@ -103,6 +131,18 @@ class Subspace extends Equatable { }; } + Subspace copyWith({ + String? uuid, + String? name, + List? productAllocations, + }) { + return Subspace( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + productAllocations: productAllocations ?? this.productAllocations, + ); + } + @override List get props => [uuid, name, productAllocations]; } From 3601b02bc3bb7404310f43698d4f4bd3f46f68d5 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:02:55 +0300 Subject: [PATCH 048/105] Add SpaceDetailsModelBloc and events for managing space details state --- .../views/space_management_page.dart | 2 + .../space_details_model_bloc.dart | 43 ++++++++++++++++++ .../space_details_model_event.dart | 44 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart create mode 100644 lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.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 957be65a..c44236df 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_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'; @@ -18,6 +19,7 @@ class SpaceManagementPage extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ + BlocProvider(create: (context) => SpaceDetailsModelBloc()), BlocProvider( create: (context) => CommunitiesBloc( communitiesService: DebouncedCommunitiesService( diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart new file mode 100644 index 00000000..57e794e8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -0,0 +1,43 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; + +part 'space_details_model_event.dart'; + +class SpaceDetailsModelBloc extends Bloc { + SpaceDetailsModelBloc() : super(SpaceDetailsModel.empty()) { + on(_onUpdateSpaceDetailsIcon); + on(_onUpdateSpaceDetailsName); + on(_onUpdateSpaceDetailsSubspaces); + on( + _onUpdateSpaceDetailsProductAllocations); + } + + void _onUpdateSpaceDetailsIcon( + UpdateSpaceDetailsIcon event, + Emitter emit, + ) { + emit(state.copyWith(icon: event.icon)); + } + + void _onUpdateSpaceDetailsName( + UpdateSpaceDetailsName event, + Emitter emit, + ) { + emit(state.copyWith(spaceName: event.name)); + } + + void _onUpdateSpaceDetailsSubspaces( + UpdateSpaceDetailsSubspaces event, + Emitter emit, + ) { + emit(state.copyWith(subspaces: event.subspaces)); + } + + void _onUpdateSpaceDetailsProductAllocations( + UpdateSpaceDetailsProductAllocations event, + Emitter emit, + ) { + emit(state.copyWith(productAllocations: event.productAllocations)); + } +} diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart new file mode 100644 index 00000000..d3e04bb9 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart @@ -0,0 +1,44 @@ +part of 'space_details_model_bloc.dart'; + +sealed class SpaceDetailsModelEvent extends Equatable { + const SpaceDetailsModelEvent(); + + @override + List get props => []; +} + +final class UpdateSpaceDetailsIcon extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsIcon(this.icon); + + final String icon; + + @override + List get props => [icon]; +} + +final class UpdateSpaceDetailsName extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsName(this.name); + + final String name; + + @override + List get props => [name]; +} + +final class UpdateSpaceDetailsSubspaces extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsSubspaces(this.subspaces); + + final List subspaces; + + @override + List get props => [subspaces]; +} + +final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsProductAllocations(this.productAllocations); + + final List productAllocations; + + @override + List get props => [productAllocations]; +} From fa6ee9a0afbf112dc571fc87939d910fdc3f308c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:00 +0300 Subject: [PATCH 049/105] Add factory method `empty` to `SpaceDetailsModel` for creating default instances --- .../space_details/domain/models/space_details_model.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 8e8bbb90..8b9ff666 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class SpaceDetailsModel extends Equatable { final String uuid; @@ -17,6 +18,13 @@ class SpaceDetailsModel extends Equatable { required this.subspaces, }); + factory SpaceDetailsModel.empty() => const SpaceDetailsModel( + uuid: '', + spaceName: '', + icon: Assets.villa, + productAllocations: [], + subspaces: [], + ); factory SpaceDetailsModel.fromJson(Map json) { return SpaceDetailsModel( uuid: json['uuid'] as String, From 68b6c9b18c4db1e88ed3bbbe93dc81b5d9cd3775 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:07 +0300 Subject: [PATCH 050/105] Refactor SpaceDetailsBloc to move SpaceDetailsService declaration for improved clarity --- .../space_details/presentation/bloc/space_details_bloc.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart index 59c1a06d..6eb96632 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart @@ -9,12 +9,12 @@ part 'space_details_event.dart'; part 'space_details_state.dart'; class SpaceDetailsBloc extends Bloc { - final SpaceDetailsService _spaceDetailsService; - SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) { on(_onLoadSpaceDetails); } + final SpaceDetailsService _spaceDetailsService; + Future _onLoadSpaceDetails( LoadSpaceDetails event, Emitter emit, From 63353af38bd5885f8a98bb2596e243a95770b964 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:23 +0300 Subject: [PATCH 051/105] Add SpaceDetails dialog and related widgets for creating and editing spaces, including SpaceDetailsDevicesBox and SpaceSubSpacesBox for managing devices and subspaces. --- .../helpers/space_details_dialog_helper.dart | 28 +++- .../widgets/space_details_action_buttons.dart | 14 +- .../widgets/space_details_devices_box.dart | 90 +++++++++++ .../widgets/space_details_dialog.dart | 72 ++++++++- .../widgets/space_name_text_field.dart | 84 ++++++++++ .../widgets/space_sub_spaces_box.dart | 67 ++++++++ .../widgets/space_sub_spaces_dialog.dart | 143 ++++++++++++++++++ .../presentation/widgets/subspace_chip.dart | 55 +++++++ .../widgets/subspace_name_display_widget.dart | 73 +++++++++ 9 files changed, 614 insertions(+), 12 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index e871f4d0..5d6ffee7 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,11 +1,37 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.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(), + builder: (_) => SpaceDetailsDialog( + title: const Text('Create Space'), + space: SpaceDetailsModel.empty(), + onSave: (space) {}, + ), + ); + } + + static void showEdit( + BuildContext context, { + required SpaceModel spaceModel, + }) { + showDialog( + context: context, + builder: (_) => SpaceDetailsDialog( + title: const Text('Edit Space'), + space: SpaceDetailsModel( + uuid: spaceModel.uuid, + spaceName: spaceModel.spaceName, + icon: spaceModel.icon, + productAllocations: const [], + subspaces: const [], + ), + onSave: (space) {}, + ), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index b26a6590..502ae4bb 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -33,14 +33,12 @@ class SpaceDetailsActionButtons extends StatelessWidget { } Widget _buildSaveButton() { - return Expanded( - child: DefaultButton( - onPressed: onSave, - borderRadius: 10, - backgroundColor: ColorsManager.secondaryColor, - foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), - ), + return DefaultButton( + onPressed: onSave, + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart new file mode 100644 index 00000000..4873dc09 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceDetailsDevicesBox extends StatelessWidget { + const SpaceDetailsDevicesBox({super.key, required this.space}); + + final SpaceDetailsModel space; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (space.productAllocations.isNotEmpty || + space.subspaces + .any((subspace) => subspace.productAllocations.isNotEmpty)) + SizedBox( + width: context.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + // ...TagHelper.groupTags([ + // ...?tags, + // ...?subspaces?.expand((subspace) => subspace.tags ?? []) + // ]).entries.map( + // (entry) => Chip( + // avatar: SizedBox( + // width: 24, + // height: 24, + // child: SvgPicture.asset( + // entry.key.icon ?? 'assets/icons/gateway.svg', + // fit: BoxFit.contain, + // ), + // ), + // label: Text( + // 'x${entry.value}', // Show count + // style: Theme.of(context) + // .textTheme + // .bodySmall + // ?.copyWith(color: ColorsManager.spaceColor), + // ), + // backgroundColor: ColorsManager.whiteColors, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // side: const BorderSide( + // color: ColorsManager.spaceColor, + // ), + // ), + // ), + // ), + + EditChip( + onTap: () {}, + ), + ], + ), + ), + ) + else + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Add Devices', + // disabled: isTagsAndSubspaceModelDisabled, + ), + ) + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 7213c99e..43b52ef1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,12 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' + show SpaceDetailsModel; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDialog extends StatelessWidget { - const SpaceDetailsDialog({super.key}); + const SpaceDetailsDialog({ + required this.title, + required this.space, + required this.onSave, + super.key, + }); + + final Widget title; + final SpaceDetailsModel space; + final void Function(SpaceDetailsModel space) onSave; @override Widget build(BuildContext context) { - return const Dialog( - child: Text('Create Space'), + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: SpaceIconPicker( + iconPath: space.icon, + ), + ), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text(context.watch().state.toString()), + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], + ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart new file mode 100644 index 00000000..d5bd1016 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceNameTextField extends StatefulWidget { + const SpaceNameTextField({ + required this.initialValue, + required this.isNameFieldExist, + super.key, + }); + + final String? initialValue; + final bool Function(String value) isNameFieldExist; + + @override + State createState() => _SpaceNameTextFieldState(); +} + +class _SpaceNameTextFieldState extends State { + late final TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(text: widget.initialValue); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + + String? _validateName(String? value) { + if (value == null || value.isEmpty) { + return '*Space name should not be empty.'; + } + if (widget.isNameFieldExist(value)) { + return '*Name already exists'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + controller: _controller, + onChanged: (value) => context.read().add( + UpdateSpaceDetailsName(value), + ), + validator: _validateName, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.lightGrayColor, + ), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + ), + ), + errorStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart new file mode 100644 index 00000000..071d6fec --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceSubSpacesBox extends StatelessWidget { + const SpaceSubSpacesBox({super.key, required this.subspaces}); + + final List subspaces; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (subspaces.isEmpty) + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () => showDialog( + context: context, + builder: (_) => SpaceSubSpacesDialog(subspaces: subspaces), + ), + child: const ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Create Sub Spaces', + // disabled: widget.isTagsAndSubspaceModelDisabled, + disabled: false, + ), + ) + else + SizedBox( + width: context.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map( + (e) => SubspaceNameDisplayWidget(subSpace: e), + ), + EditChip( + onTap: () {}, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart new file mode 100644 index 00000000..1c435d8b --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceSubSpacesDialog extends StatelessWidget { + const SpaceSubSpacesDialog({ + required this.subspaces, + super.key, + }); + + final List subspaces; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create Sub Spaces'), + content: TextFieldSubSpaceDialogWidget( + subSpaces: subspaces, + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: Navigator.of(context).pop, + ) + ], + ); + } +} + +class TextFieldSubSpaceDialogWidget extends StatefulWidget { + const TextFieldSubSpaceDialogWidget({ + super.key, + required this.subSpaces, + }); + + final List subSpaces; + + @override + State createState() => _TextFieldSubSpaceDialogWidgetState(); +} + +class _TextFieldSubSpaceDialogWidgetState extends State { + + late final TextEditingController _subspaceNameController; + + @override + void initState() { + super.initState(); + _subspaceNameController = TextEditingController(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.name.toLowerCase(); + + final duplicateIndices = widget.subSpaces + .asMap() + .entries + .where((e) => e.value.name.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + onDeleted: () => context.read().add( + UpdateSpaceDetailsSubspaces( + widget.subSpaces.where((e) => e.uuid != subSpace.uuid).toList(), + ), + ), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + context.read().add( + UpdateSpaceDetailsSubspaces( + [ + ...widget.subSpaces, + Subspace( + name: trimmedValue, + uuid: '', + productAllocations: const [], + ), + ], + ), + ); + _subspaceNameController.clear(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart new file mode 100644 index 00000000..a80ddd15 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceChip extends StatelessWidget { + const SubspaceChip({ + required this.subSpace, + required this.isDuplicate, + required this.onDeleted, + super.key, + }); + + final Subspace subSpace; + final bool isDuplicate; + final void Function() onDeleted; + + @override + Widget build(BuildContext context) { + return Chip( + label: Text( + subSpace.name, + style: context.textTheme.bodySmall?.copyWith( + color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), + ), + onDeleted: onDeleted, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart new file mode 100644 index 00000000..3d7ab59b --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceNameDisplayWidget extends StatefulWidget { + const SubspaceNameDisplayWidget({super.key, required this.subSpace}); + + final Subspace subSpace; + + @override + State createState() => + _SubspaceNameDisplayWidgetState(); +} + +class _SubspaceNameDisplayWidgetState extends State { + late final TextEditingController _controller; + bool isEditing = false; + @override + void initState() { + _controller = TextEditingController(text: widget.subSpace.name); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.spaceColor, + ); + return InkWell( + onTap: () => setState(() => isEditing = true), + child: Visibility( + visible: isEditing, + replacement: Text( + widget.subSpace.name, + style: textStyle, + ), + child: TextField( + controller: _controller, + style: textStyle, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsetsDirectional.symmetric( + horizontal: 8, + ), + ), + onSubmitted: (value) { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid + ? e.copyWith(name: value) + : e, + ) + .toList(), + ), + ); + }, + ), + ), + ); + } +} From 9dfb3ed369dfdb11a71b5fb43f930209d7ef4f2e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:20:52 +0300 Subject: [PATCH 052/105] Refactor SpaceDetailsDialog and SpaceIconPicker to integrate Bloc for state management, enhancing icon selection and dialog functionality. --- .../views/space_management_page.dart | 2 - .../widgets/space_details_dialog.dart | 99 ++++++++++--------- .../widgets/space_icon_picker.dart | 22 ++++- .../widgets/space_icon_selection_dialog.dart | 31 ++++-- .../widgets/space_sub_spaces_box.dart | 6 +- .../space_details_model_bloc.dart | 4 +- 6 files changed, 99 insertions(+), 65 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 c44236df..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 @@ -7,7 +7,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_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'; @@ -19,7 +18,6 @@ class SpaceManagementPage extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => SpaceDetailsModelBloc()), BlocProvider( create: (context) => CommunitiesBloc( communitiesService: DebouncedCommunitiesService( diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 43b52ef1..fd3093c5 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' show SpaceDetailsModel; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; @@ -6,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/pres import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -23,56 +25,59 @@ class SpaceDetailsDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 1, - child: SpaceIconPicker( - iconPath: space.icon, - ), + return BlocProvider( + create: (context) => SpaceDetailsModelBloc(initialState: space), + child: Builder(builder: (context) { + final space = context.watch().state; + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: SpaceIconPicker(iconPath: space.icon) + ), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], ), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text(context.watch().state.toString()), - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, - ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), - ], - ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, ), ], - ), - ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], + ); + }), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart index 00552b88..fd6f79e6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -39,10 +41,22 @@ class SpaceIconPicker extends StatelessWidget { top: 20, right: 20, child: InkWell( - onTap: () => showDialog( - context: context, - builder: (context) => const SpaceIconSelectionDialog(), - ), + onTap: () { + showDialog( + context: context, + builder: (context) => SpaceIconSelectionDialog( + selectedIcon: iconPath, + ), + ).then((value) { + if (value != null) { + if (context.mounted) { + context.read().add( + UpdateSpaceDetailsIcon(value), + ); + } + } + }); + }, child: Container( width: 24, height: 24, diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart index 428048b3..5fe5b463 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart @@ -5,7 +5,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceIconSelectionDialog extends StatelessWidget { - const SpaceIconSelectionDialog({super.key}); + const SpaceIconSelectionDialog({super.key, required this.selectedIcon}); + final String selectedIcon; static const List _icons = [ Assets.location, @@ -47,14 +48,26 @@ class SpaceIconSelectionDialog extends StatelessWidget { mainAxisSpacing: 16, ), itemCount: _icons.length, - itemBuilder: (context, index) => IconButton( - onPressed: Navigator.of(context).pop, - icon: SvgPicture.asset( - _icons[index], - width: context.screenWidth * 0.03, - height: context.screenHeight * 0.08, - ), - ), + itemBuilder: (context, index) { + final isSelected = selectedIcon == _icons[index]; + return Container( + padding: const EdgeInsetsDirectional.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isSelected + ? Border.all(color: ColorsManager.vividBlue, width: 2) + : null, + ), + child: IconButton( + onPressed: () => Navigator.of(context).pop(_icons[index]), + icon: SvgPicture.asset( + _icons[index], + width: context.screenWidth * 0.03, + height: context.screenHeight * 0.08, + ), + ), + ); + }, ), ), ); diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 071d6fec..30677e1e 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -30,7 +30,6 @@ class SpaceSubSpacesBox extends StatelessWidget { child: const ButtonContentWidget( svgAssets: Assets.addIcon, label: 'Create Sub Spaces', - // disabled: widget.isTagsAndSubspaceModelDisabled, disabled: false, ), ) @@ -55,7 +54,10 @@ class SpaceSubSpacesBox extends StatelessWidget { (e) => SubspaceNameDisplayWidget(subSpace: e), ), EditChip( - onTap: () {}, + onTap: () => showDialog( + context: context, + builder: (_) => const SpaceSubSpacesDialog(subspaces: []), + ), ), ], ), diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart index 57e794e8..21a72557 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -5,7 +5,9 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma part 'space_details_model_event.dart'; class SpaceDetailsModelBloc extends Bloc { - SpaceDetailsModelBloc() : super(SpaceDetailsModel.empty()) { + SpaceDetailsModelBloc({ + required SpaceDetailsModel initialState, + }) : super(initialState) { on(_onUpdateSpaceDetailsIcon); on(_onUpdateSpaceDetailsName); on(_onUpdateSpaceDetailsSubspaces); From e448eabda64cc1bec339f0a2068f7624caed6cee Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:41:13 +0300 Subject: [PATCH 053/105] Refactor SpaceSubSpacesDialog to use SubSpacesInput for managing subspaces, enhancing state management and UI structure. Update SpaceDetailsActionButtons to handle optional save callback. --- .../widgets/space_details_action_buttons.dart | 2 +- .../widgets/space_sub_spaces_dialog.dart | 179 ++++++------------ .../widgets/sub_spaces_input.dart | 103 ++++++++++ 3 files changed, 164 insertions(+), 120 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index 502ae4bb..3de9c192 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -11,7 +11,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { }); final VoidCallback onCancel; - final VoidCallback onSave; + final VoidCallback? onSave; @override Widget build(BuildContext context) { diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 1c435d8b..4230f7f4 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart'; +import 'package:uuid/uuid.dart'; -class SpaceSubSpacesDialog extends StatelessWidget { +class SpaceSubSpacesDialog extends StatefulWidget { const SpaceSubSpacesDialog({ required this.subspaces, super.key, @@ -15,129 +12,73 @@ class SpaceSubSpacesDialog extends StatelessWidget { final List subspaces; + @override + State createState() => _SpaceSubSpacesDialogState(); +} + +class _SpaceSubSpacesDialogState extends State { + late List _subspaces; + + bool get _hasDuplicateNames => + _subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length != + _subspaces.length; + + @override + void initState() { + super.initState(); + _subspaces = List.from(widget.subspaces); + } + + void _handleSubspaceAdded(String name) { + setState(() { + _subspaces = [ + ..._subspaces, + Subspace( + name: name, + uuid: const Uuid().v4(), + productAllocations: const [], + ), + ]; + }); + } + + void _handleSubspaceDeleted(String uuid) => setState( + () => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(), + ); + + void _handleSave() => Navigator.of(context).pop(_subspaces); + @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Create Sub Spaces'), - content: TextFieldSubSpaceDialogWidget( - subSpaces: subspaces, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SubSpacesInput( + subSpaces: _subspaces, + onSubspaceAdded: _handleSubspaceAdded, + onSubspaceDeleted: _handleSubspaceDeleted, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + child: Visibility( + key: ValueKey(_hasDuplicateNames), + visible: _hasDuplicateNames, + child: const Text( + 'Error: Duplicate subspace names are not allowed.', + style: TextStyle(color: Colors.red), + ), + ), + ), + ], ), actions: [ SpaceDetailsActionButtons( - onSave: () {}, + onSave: _hasDuplicateNames ? null : _handleSave, onCancel: Navigator.of(context).pop, ) ], ); } } - -class TextFieldSubSpaceDialogWidget extends StatefulWidget { - const TextFieldSubSpaceDialogWidget({ - super.key, - required this.subSpaces, - }); - - final List subSpaces; - - @override - State createState() => _TextFieldSubSpaceDialogWidgetState(); -} - -class _TextFieldSubSpaceDialogWidgetState extends State { - - late final TextEditingController _subspaceNameController; - - @override - void initState() { - super.initState(); - _subspaceNameController = TextEditingController(); - } - - @override - void dispose() { - _subspaceNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - width: context.screenWidth * 0.35, - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(10), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ...widget.subSpaces.asMap().entries.map( - (entry) { - final index = entry.key; - final subSpace = entry.value; - - final lowerName = subSpace.name.toLowerCase(); - - final duplicateIndices = widget.subSpaces - .asMap() - .entries - .where((e) => e.value.name.toLowerCase() == lowerName) - .map((e) => e.key) - .toList(); - final isDuplicate = duplicateIndices.length > 1 && - duplicateIndices.indexOf(index) != 0; - return SubspaceChip( - subSpace: subSpace, - isDuplicate: isDuplicate, - onDeleted: () => context.read().add( - UpdateSpaceDetailsSubspaces( - widget.subSpaces.where((e) => e.uuid != subSpace.uuid).toList(), - ), - ), - ); - }, - ), - SizedBox( - width: 200, - child: TextField( - controller: _subspaceNameController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, - hintStyle: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.lightGrayColor, - ), - ), - onSubmitted: (value) { - final trimmedValue = value.trim(); - if (trimmedValue.isNotEmpty) { - context.read().add( - UpdateSpaceDetailsSubspaces( - [ - ...widget.subSpaces, - Subspace( - name: trimmedValue, - uuid: '', - productAllocations: const [], - ), - ], - ), - ); - _subspaceNameController.clear(); - } - }, - style: context.textTheme.bodyMedium, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart new file mode 100644 index 00000000..1e1edb89 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpacesInput extends StatefulWidget { + const SubSpacesInput({ + super.key, + required this.subSpaces, + required this.onSubspaceAdded, + required this.onSubspaceDeleted, + }); + + final List subSpaces; + final void Function(String name) onSubspaceAdded; + final void Function(String uuid) onSubspaceDeleted; + + @override + State createState() => _SubSpacesInputState(); +} + +class _SubSpacesInputState extends State { + late final TextEditingController _subspaceNameController; + + @override + void initState() { + super.initState(); + _subspaceNameController = TextEditingController(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.name.toLowerCase(); + + final duplicateIndices = widget.subSpaces + .asMap() + .entries + .where((e) => e.value.name.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + widget.onSubspaceAdded(trimmedValue); + _subspaceNameController.clear(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} From 779c0fe9167430ee39e53f18d293b4ce5a969367 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:42:19 +0300 Subject: [PATCH 054/105] Refactor SpaceDetailsDialog to improve code readability and structure by simplifying widget hierarchy and enhancing the use of Bloc for state management. --- .../widgets/space_details_dialog.dart | 97 +++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index fd3093c5..ee6c7e7c 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -27,57 +27,56 @@ class SpaceDetailsDialog extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SpaceDetailsModelBloc(initialState: space), - child: Builder(builder: (context) { - final space = context.watch().state; - return AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 1, - child: SpaceIconPicker(iconPath: space.icon) - ), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, - ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), - ], + child: Builder( + builder: (context) { + final space = context.watch().state; + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: SpaceIconPicker(iconPath: space.icon)), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), ), - ), - ], + ], + ), ), - ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], - ); - }), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], + ); + }, + ), ); } } From 72af55ef98723f2d4f306f7003f8230dd6eaff52 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 2 Jul 2025 15:50:46 +0300 Subject: [PATCH 055/105] Add booking page and related routes, icons, and button widget --- assets/icons/group_icon.svg | 15 + assets/icons/home_icon.svg | 4 + .../booking_system/view/booking_page.dart | 52 +++ .../view/widgets/icon_text_button.dart | 73 ++++ .../view/access_management.dart | 338 ++++-------------- .../view/access_overview_content.dart | 289 +++++++++++++++ lib/utils/constants/assets.dart | 119 ++++-- 7 files changed, 574 insertions(+), 316 deletions(-) create mode 100644 assets/icons/group_icon.svg create mode 100644 assets/icons/home_icon.svg create mode 100644 lib/pages/access_management/booking_system/view/booking_page.dart create mode 100644 lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart create mode 100644 lib/pages/access_management/view/access_overview_content.dart diff --git a/assets/icons/group_icon.svg b/assets/icons/group_icon.svg new file mode 100644 index 00000000..efca14dd --- /dev/null +++ b/assets/icons/group_icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/home_icon.svg b/assets/icons/home_icon.svg new file mode 100644 index 00000000..35080c4e --- /dev/null +++ b/assets/icons/home_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/view/booking_page.dart new file mode 100644 index 00000000..6fdb53bd --- /dev/null +++ b/lib/pages/access_management/booking_system/view/booking_page.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BookingPage extends StatelessWidget { + const BookingPage({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + child: Row( + children: [ + Expanded( + child: Container( + color: Colors.blueGrey[100], + child: const Center( + child: Text( + 'Side bar', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + )), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: SizedBox( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () {}), + SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}) + ], + ) + ], + ), + ), + )) + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart b/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart new file mode 100644 index 00000000..afccafdb --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SvgTextButton extends StatelessWidget { + final String svgAsset; + final String label; + final VoidCallback onPressed; + final Color backgroundColor; + final Color svgColor; + final Color labelColor; + final double borderRadius; + final List boxShadow; + final double svgSize; + + const SvgTextButton({ + super.key, + required this.svgAsset, + required this.label, + required this.onPressed, + this.backgroundColor = ColorsManager.circleRolesBackground, + this.svgColor = const Color(0xFF496EFF), + this.labelColor = Colors.black87, + this.borderRadius = 10.0, + this.boxShadow = const [ + BoxShadow( + color: ColorsManager.textGray, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + this.svgSize = 24.0, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onPressed, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius), + boxShadow: boxShadow, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + svgAsset, + width: svgSize, + height: svgSize, + color: svgColor, + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + color: labelColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index d7c7a9dd..e035d252 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -2,302 +2,86 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; -import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; -import 'package:syncrow_web/pages/common/custom_table.dart'; -import 'package:syncrow_web/pages/common/date_time_widget.dart'; -import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; -import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; -import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; -// import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/app_enum.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; -class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout { +class AccessManagementPage extends StatefulWidget { const AccessManagementPage({super.key}); @override - Widget build(BuildContext context) { - final isLargeScreen = isLargeScreenSize(context); - final isSmallScreen = isSmallScreenSize(context); - final isHalfMediumScreen = isHafMediumScreenSize(context); - final padding = - isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15); + State createState() => _AccessManagementPageState(); +} - return WebScaffold( +class _AccessManagementPageState extends State + with HelperResponsiveLayout { + final PageController _pageController = PageController(initialPage: 0); + int _currentPageIndex = 0; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => AccessBloc()..add(FetchTableData()), + child: WebScaffold( enableMenuSidebar: false, appBarTitle: Text( 'Access Management', style: ResponsiveTextTheme.of(context).deviceManagementTitle, ), - rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocProvider( - create: (BuildContext context) => - AccessBloc()..add(FetchTableData()), - child: BlocConsumer( - listener: (context, state) {}, - builder: (context, state) { - final accessBloc = BlocProvider.of(context); - final filteredData = accessBloc.filteredData; - return state is AccessLoaded - ? const Center(child: CircularProgressIndicator()) - : Container( - padding: padding, - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FilterWidget( - size: MediaQuery.of(context).size, - tabs: accessBloc.tabs, - selectedIndex: accessBloc.selectedIndex, - onTabChanged: (index) { - accessBloc.add(TabChangedEvent(index)); - }, - ), - const SizedBox(height: 20), - if (isSmallScreen || isHalfMediumScreen) - _buildSmallSearchFilters(context, accessBloc) - else - _buildNormalSearchWidgets(context, accessBloc), - const SizedBox(height: 20), - _buildVisitorAdminPasswords(context, accessBloc), - const SizedBox(height: 20), - Expanded( - child: DynamicTable( - tableName: 'AccessManagement', - uuidIndex: 1, - withSelectAll: true, - isEmpty: filteredData.isEmpty, - withCheckBox: false, - size: MediaQuery.of(context).size, - cellDecoration: containerDecoration, - headers: const [ - 'Name', - 'Access Type', - 'Access Start', - 'Access End', - 'Accessible Device', - 'Authorizer', - 'Authorization Date & Time', - 'Access Status' - ], - data: filteredData.map((item) { - return [ - item.passwordName, - item.passwordType.value, - accessBloc - .timestampToDate(item.effectiveTime), - accessBloc - .timestampToDate(item.invalidTime), - item.deviceName.toString(), - item.authorizerEmail.toString(), - accessBloc - .timestampToDate(item.invalidTime), - item.passwordStatus.value, - ]; - }).toList(), - )), - ], - ), - ); - }))); - } - - Wrap _buildVisitorAdminPasswords( - BuildContext context, AccessBloc accessBloc) { - return Wrap( - spacing: 10, - runSpacing: 10, - children: [ - Container( - width: 205, - height: 42, - decoration: containerDecoration, - child: DefaultButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return const VisitorPasswordDialog(); - }, - ).then((v) { - if (v != null) { - accessBloc.add(FetchTableData()); - } - }); - }, - borderRadius: 8, + centerBody: Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () => _switchPage(0), child: Text( - 'Create Visitor Password ', - style: context.textTheme.titleSmall! - .copyWith(color: Colors.white, fontSize: 12), - )), + 'Access Overview', + style: context.textTheme.titleMedium?.copyWith( + color: _currentPageIndex == 0 ? Colors.white : Colors.grey, + fontWeight: _currentPageIndex == 0 + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ), + TextButton( + onPressed: () => _switchPage(1), + child: Text( + 'Booking System', + style: context.textTheme.titleMedium?.copyWith( + color: _currentPageIndex == 1 ? Colors.white : Colors.grey, + fontWeight: _currentPageIndex == 1 + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ), + ], ), - // Container( - // width: 133, - // height: 42, - // decoration: containerDecoration, - // child: DefaultButton( - // borderRadius: 8, - // backgroundColor: ColorsManager.whiteColors, - // child: Text( - // 'Admin Password', - // style: context.textTheme.titleSmall! - // .copyWith(color: Colors.black, fontSize: 12), - // )), - // ), - ], + rightBody: const NavigateHomeGridView(), + scaffoldBody: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + AccessOverviewContent(), + BookingPage(), + ], + ), + ), ); } - Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) { - // TimeOfDay _selectedTime = TimeOfDay.now(); - - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - textBaseline: TextBaseline.ideographic, - children: [ - SizedBox( - width: 250, - child: CustomWebTextField( - controller: accessBloc.passwordName, - height: 43, - isRequired: false, - textFieldName: 'Name', - description: '', - onSubmitted: (value) { - accessBloc.add(FilterDataEvent( - emailAuthorizer: - accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - ), - ), - const SizedBox(width: 15), - SizedBox( - width: 250, - child: CustomWebTextField( - controller: accessBloc.emailAuthorizer, - height: 43, - isRequired: false, - textFieldName: 'Authorizer', - description: '', - onSubmitted: (value) { - accessBloc.add(FilterDataEvent( - emailAuthorizer: - accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - ), - ), - const SizedBox(width: 15), - SizedBox( - child: DateTimeWebWidget( - icon: Assets.calendarIcon, - isRequired: false, - title: 'Access Time', - size: MediaQuery.of(context).size, - endTime: () { - accessBloc.add(SelectTime(context: context, isStart: false)); - }, - startTime: () { - accessBloc.add(SelectTime(context: context, isStart: true)); - }, - firstString: BlocProvider.of(context).startTime, - secondString: BlocProvider.of(context).endTime, - ), - ), - const SizedBox(width: 15), - SearchResetButtons( - onSearch: () { - accessBloc.add(FilterDataEvent( - emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - onReset: () { - accessBloc.add(ResetSearch()); - }, - ), - ], - ); - } - - Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) { - return Wrap( - spacing: 20, - runSpacing: 10, - children: [ - SizedBox( - width: 300, - child: CustomWebTextField( - controller: accessBloc.passwordName, - isRequired: true, - height: 40, - textFieldName: 'Name', - description: '', - onSubmitted: (value) { - accessBloc.add(FilterDataEvent( - emailAuthorizer: - accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }), - ), - DateTimeWebWidget( - icon: Assets.calendarIcon, - isRequired: false, - title: 'Access Time', - size: MediaQuery.of(context).size, - endTime: () { - accessBloc.add(SelectTime(context: context, isStart: false)); - }, - startTime: () { - accessBloc.add(SelectTime(context: context, isStart: true)); - }, - firstString: BlocProvider.of(context).startTime, - secondString: BlocProvider.of(context).endTime, - ), - SearchResetButtons( - onSearch: () { - accessBloc.add(FilterDataEvent( - emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - onReset: () { - accessBloc.add(ResetSearch()); - }, - ), - ], - ); + void _switchPage(int index) { + setState(() => _currentPageIndex = index); + _pageController.jumpToPage(index); } } diff --git a/lib/pages/access_management/view/access_overview_content.dart b/lib/pages/access_management/view/access_overview_content.dart new file mode 100644 index 00000000..b6b8748a --- /dev/null +++ b/lib/pages/access_management/view/access_overview_content.dart @@ -0,0 +1,289 @@ +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; +import 'package:syncrow_web/pages/common/custom_table.dart'; +import 'package:syncrow_web/pages/common/date_time_widget.dart'; +import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AccessOverviewContent extends StatelessWidget + with HelperResponsiveLayout { + const AccessOverviewContent({super.key}); + + @override + Widget build(BuildContext context) { + final isLargeScreen = isLargeScreenSize(context); + final isSmallScreen = isSmallScreenSize(context); + final isHalfMediumScreen = isHafMediumScreenSize(context); + final padding = + isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15); + + return BlocProvider( + create: (BuildContext context) => AccessBloc()..add(FetchTableData()), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + final accessBloc = BlocProvider.of(context); + final filteredData = accessBloc.filteredData; + return state is AccessLoaded + ? const Center(child: CircularProgressIndicator()) + : Container( + padding: padding, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FilterWidget( + size: MediaQuery.of(context).size, + tabs: accessBloc.tabs, + selectedIndex: accessBloc.selectedIndex, + onTabChanged: (index) { + accessBloc.add(TabChangedEvent(index)); + }, + ), + const SizedBox(height: 20), + if (isSmallScreen || isHalfMediumScreen) + _buildSmallSearchFilters(context, accessBloc) + else + _buildNormalSearchWidgets(context, accessBloc), + const SizedBox(height: 20), + _buildVisitorAdminPasswords(context, accessBloc), + const SizedBox(height: 20), + Expanded( + child: DynamicTable( + tableName: 'AccessManagement', + uuidIndex: 1, + withSelectAll: true, + isEmpty: filteredData.isEmpty, + withCheckBox: false, + size: MediaQuery.of(context).size, + cellDecoration: containerDecoration, + headers: const [ + 'Name', + 'Access Type', + 'Access Start', + 'Access End', + 'Accessible Device', + 'Authorizer', + 'Authorization Date & Time', + 'Access Status' + ], + data: filteredData.map((item) { + return [ + item.passwordName, + item.passwordType.value, + accessBloc.timestampToDate(item.effectiveTime), + accessBloc.timestampToDate(item.invalidTime), + item.deviceName.toString(), + item.authorizerEmail.toString(), + accessBloc.timestampToDate(item.invalidTime), + item.passwordStatus.value, + ]; + }).toList(), + )), + ], + ), + ); + })); + } + + Wrap _buildVisitorAdminPasswords( + BuildContext context, AccessBloc accessBloc) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: [ + Container( + width: 205, + height: 42, + decoration: containerDecoration, + child: DefaultButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const VisitorPasswordDialog(); + }, + ).then((v) { + if (v != null) { + accessBloc.add(FetchTableData()); + } + }); + }, + borderRadius: 8, + child: Text( + 'Create Visitor Password ', + style: context.textTheme.titleSmall! + .copyWith(color: Colors.white, fontSize: 12), + )), + ), + // Container( + // width: 133, + // height: 42, + // decoration: containerDecoration, + // child: DefaultButton( + // borderRadius: 8, + // backgroundColor: ColorsManager.whiteColors, + // child: Text( + // 'Admin Password', + // style: context.textTheme.titleSmall! + // .copyWith(color: Colors.black, fontSize: 12), + // )), + // ), + ], + ); + } + + Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) { + // TimeOfDay _selectedTime = TimeOfDay.now(); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.ideographic, + children: [ + SizedBox( + width: 250, + child: CustomWebTextField( + controller: accessBloc.passwordName, + height: 43, + isRequired: false, + textFieldName: 'Name', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + ), + ), + const SizedBox(width: 15), + SizedBox( + width: 250, + child: CustomWebTextField( + controller: accessBloc.emailAuthorizer, + height: 43, + isRequired: false, + textFieldName: 'Authorizer', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + ), + ), + const SizedBox(width: 15), + SizedBox( + child: DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: MediaQuery.of(context).size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString: BlocProvider.of(context).startTime, + secondString: BlocProvider.of(context).endTime, + ), + ), + const SizedBox(width: 15), + SearchResetButtons( + onSearch: () { + accessBloc.add(FilterDataEvent( + emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + onReset: () { + accessBloc.add(ResetSearch()); + }, + ), + ], + ); + } + + Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) { + return Wrap( + spacing: 20, + runSpacing: 10, + children: [ + SizedBox( + width: 300, + child: CustomWebTextField( + controller: accessBloc.passwordName, + isRequired: true, + height: 40, + textFieldName: 'Name', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }), + ), + DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: MediaQuery.of(context).size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString: BlocProvider.of(context).startTime, + secondString: BlocProvider.of(context).endTime, + ), + SearchResetButtons( + onSearch: () { + accessBloc.add(FilterDataEvent( + emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + onReset: () { + accessBloc.add(ResetSearch()); + }, + ), + ], + ); + } +} diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 87f6e73f..f92975f3 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -14,7 +14,8 @@ class Assets { static const String rightLine = 'assets/images/right_line.png'; static const String google = 'assets/images/google.svg'; static const String facebook = 'assets/images/facebook.svg'; - static const String invisiblePassword = 'assets/images/Password_invisible.svg'; + static const String invisiblePassword = + 'assets/images/Password_invisible.svg'; static const String visiblePassword = 'assets/images/password_visible.svg'; static const String accessIcon = 'assets/images/access_icon.svg'; static const String spaseManagementIcon = @@ -33,7 +34,8 @@ class Assets { static const String emptyTable = 'assets/images/empty_table.svg'; // General assets - static const String motionlessDetection = 'assets/icons/motionless_detection.svg'; + static const String motionlessDetection = + 'assets/icons/motionless_detection.svg'; static const String acHeating = 'assets/icons/ac_heating.svg'; static const String acPowerOff = 'assets/icons/ac_power_off.svg'; static const String acFanMiddle = 'assets/icons/ac_fan_middle.svg'; @@ -70,19 +72,22 @@ class Assets { 'assets/icons/automation_functions/temp_password_unlock.svg'; static const String doorlockNormalOpen = 'assets/icons/automation_functions/doorlock_normal_open.svg'; - static const String doorbell = 'assets/icons/automation_functions/doorbell.svg'; + static const String doorbell = + 'assets/icons/automation_functions/doorbell.svg'; static const String remoteUnlockViaApp = 'assets/icons/automation_functions/remote_unlock_via_app.svg'; static const String doubleLock = 'assets/icons/automation_functions/double_lock.svg'; static const String selfTestResult = 'assets/icons/automation_functions/self_test_result.svg'; - static const String lockAlarm = 'assets/icons/automation_functions/lock_alarm.svg'; + static const String lockAlarm = + 'assets/icons/automation_functions/lock_alarm.svg'; static const String presenceState = 'assets/icons/automation_functions/presence_state.svg'; static const String currentTemp = 'assets/icons/automation_functions/current_temp.svg'; - static const String presence = 'assets/icons/automation_functions/presence.svg'; + static const String presence = + 'assets/icons/automation_functions/presence.svg'; static const String residualElectricity = 'assets/icons/automation_functions/residual_electricity.svg'; static const String hijackAlarm = @@ -99,12 +104,15 @@ class Assets { // Presence Sensor Assets static const String sensorMotionIcon = 'assets/icons/sensor_motion_ic.svg'; - static const String sensorPresenceIcon = 'assets/icons/sensor_presence_ic.svg'; + static const String sensorPresenceIcon = + 'assets/icons/sensor_presence_ic.svg'; static const String sensorVacantIcon = 'assets/icons/sensor_vacant_ic.svg'; static const String illuminanceRecordIcon = 'assets/icons/illuminance_record_ic.svg'; - static const String presenceRecordIcon = 'assets/icons/presence_record_ic.svg'; - static const String helpDescriptionIcon = 'assets/icons/help_description_ic.svg'; + static const String presenceRecordIcon = + 'assets/icons/presence_record_ic.svg'; + static const String helpDescriptionIcon = + 'assets/icons/help_description_ic.svg'; static const String lightPulp = 'assets/icons/light_pulb.svg'; static const String acDevice = 'assets/icons/ac_device.svg'; @@ -158,10 +166,12 @@ class Assets { static const String unit = 'assets/icons/unit_icon.svg'; static const String villa = 'assets/icons/villa_icon.svg'; static const String iconEdit = 'assets/icons/icon_edit_icon.svg'; - static const String textFieldSearch = 'assets/icons/textfield_search_icon.svg'; + static const String textFieldSearch = + 'assets/icons/textfield_search_icon.svg'; static const String roundedAddIcon = 'assets/icons/rounded_add_icon.svg'; static const String addIcon = 'assets/icons/add_icon.svg'; - static const String smartThermostatIcon = 'assets/icons/smart_thermostat_icon.svg'; + static const String smartThermostatIcon = + 'assets/icons/smart_thermostat_icon.svg'; static const String smartLightIcon = 'assets/icons/smart_light_icon.svg'; static const String presenceSensor = 'assets/icons/presence_sensor.svg'; static const String Gang3SwitchIcon = 'assets/icons/3_Gang_switch_icon.svg'; @@ -209,7 +219,8 @@ class Assets { //assets/icons/water_leak_normal.svg static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; //assets/icons/water_leak_detected.svg - static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; + static const String waterLeakDetected = + 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -280,13 +291,16 @@ class Assets { 'assets/icons/functions_icons/sensitivity.svg'; static const String assetsSensitivityOperationIcon = 'assets/icons/functions_icons/sesitivity_operation_icon.svg'; - static const String assetsAcPower = 'assets/icons/functions_icons/ac_power.svg'; + static const String assetsAcPower = + 'assets/icons/functions_icons/ac_power.svg'; static const String assetsAcPowerOFF = 'assets/icons/functions_icons/ac_power_off.svg'; static const String assetsChildLock = 'assets/icons/functions_icons/child_lock.svg'; - static const String assetsFreezing = 'assets/icons/functions_icons/freezing.svg'; - static const String assetsFanSpeed = 'assets/icons/functions_icons/fan_speed.svg'; + static const String assetsFreezing = + 'assets/icons/functions_icons/freezing.svg'; + static const String assetsFanSpeed = + 'assets/icons/functions_icons/fan_speed.svg'; static const String assetsAcCooling = 'assets/icons/functions_icons/ac_cooling.svg'; static const String assetsAcHeating = @@ -295,7 +309,8 @@ class Assets { 'assets/icons/functions_icons/celsius_degrees.svg'; static const String assetsTempreture = 'assets/icons/functions_icons/tempreture.svg'; - static const String assetsAcFanLow = 'assets/icons/functions_icons/ac_fan_low.svg'; + static const String assetsAcFanLow = + 'assets/icons/functions_icons/ac_fan_low.svg'; static const String assetsAcFanMiddle = 'assets/icons/functions_icons/ac_fan_middle.svg'; static const String assetsAcFanHigh = @@ -314,7 +329,8 @@ class Assets { 'assets/icons/functions_icons/far_detection.svg'; static const String assetsFarDetectionFunction = 'assets/icons/functions_icons/far_detection_function.svg'; - static const String assetsIndicator = 'assets/icons/functions_icons/indicator.svg'; + static const String assetsIndicator = + 'assets/icons/functions_icons/indicator.svg'; static const String assetsMotionDetection = 'assets/icons/functions_icons/motion_detection.svg'; static const String assetsMotionlessDetection = @@ -327,7 +343,8 @@ class Assets { 'assets/icons/functions_icons/master_state.svg'; static const String assetsSwitchAlarmSound = 'assets/icons/functions_icons/switch_alarm_sound.svg'; - static const String assetsResetOff = 'assets/icons/functions_icons/reset_off.svg'; + static const String assetsResetOff = + 'assets/icons/functions_icons/reset_off.svg'; // Assets for automation_functions static const String assetsCardUnlock = @@ -371,13 +388,15 @@ class Assets { static const String activeUser = 'assets/icons/active_user.svg'; static const String deActiveUser = 'assets/icons/deactive_user.svg'; static const String invitedIcon = 'assets/icons/invited_icon.svg'; - static const String rectangleCheckBox = 'assets/icons/rectangle_check_box.png'; + static const String rectangleCheckBox = + 'assets/icons/rectangle_check_box.png'; static const String CheckBoxChecked = 'assets/icons/box_checked.png'; static const String emptyBox = 'assets/icons/empty_box.png'; static const String completeProcessIcon = 'assets/icons/compleate_process_icon.svg'; static const String completedDoneIcon = 'assets/images/completed_done.svg'; - static const String currentProcessIcon = 'assets/icons/current_process_icon.svg'; + static const String currentProcessIcon = + 'assets/icons/current_process_icon.svg'; static const String uncomplete_ProcessIcon = 'assets/icons/uncompleate_process_icon.svg'; static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg'; @@ -398,9 +417,11 @@ class Assets { static const String successIcon = 'assets/icons/success_icon.svg'; static const String spaceLocationIcon = 'assets/icons/spaseLocationIcon.svg'; static const String scenesPlayIcon = 'assets/icons/scenesPlayIcon.png'; - static const String scenesPlayIconCheck = 'assets/icons/scenesPlayIconCheck.png'; + static const String scenesPlayIconCheck = + 'assets/icons/scenesPlayIconCheck.png'; static const String presenceStateIcon = 'assets/icons/presence_state.svg'; - static const String currentDistanceIcon = 'assets/icons/current_distance_icon.svg'; + static const String currentDistanceIcon = + 'assets/icons/current_distance_icon.svg'; static const String farDetectionIcon = 'assets/icons/far_detection_icon.svg'; static const String motionDetectionSensitivityIcon = @@ -423,29 +444,44 @@ class Assets { static const String cpsMode4 = 'assets/icons/cps_mode4.svg'; static const String closeToMotion = 'assets/icons/close_to_motion.svg'; static const String farAwayMotion = 'assets/icons/far_away_motion.svg'; - static const String communicationFault = 'assets/icons/communication_fault.svg'; + static const String communicationFault = + 'assets/icons/communication_fault.svg'; static const String radarFault = 'assets/icons/radar_fault.svg'; - static const String selfTestingSuccess = 'assets/icons/self_testing_success.svg'; - static const String selfTestingFailure = 'assets/icons/self_testing_failure.svg'; - static const String selfTestingTimeout = 'assets/icons/self_testing_timeout.svg'; + static const String selfTestingSuccess = + 'assets/icons/self_testing_success.svg'; + static const String selfTestingFailure = + 'assets/icons/self_testing_failure.svg'; + static const String selfTestingTimeout = + 'assets/icons/self_testing_timeout.svg'; static const String movingSpeed = 'assets/icons/moving_speed.svg'; static const String boundary = 'assets/icons/boundary.svg'; static const String motionMeter = 'assets/icons/motion_meter.svg'; - static const String spatialStaticValue = 'assets/icons/spatial_static_value.svg'; - static const String spatialMotionValue = 'assets/icons/spatial_motion_value.svg'; + static const String spatialStaticValue = + 'assets/icons/spatial_static_value.svg'; + static const String spatialMotionValue = + 'assets/icons/spatial_motion_value.svg'; static const String presenceJudgementThrshold = 'assets/icons/presence_judgement_threshold.svg'; static const String spaceType = 'assets/icons/space_type.svg'; static const String sportsPara = 'assets/icons/sports_para.svg'; - static const String sensitivityFeature1 = 'assets/icons/sensitivity_feature_1.svg'; - static const String sensitivityFeature2 = 'assets/icons/sensitivity_feature_2.svg'; - static const String sensitivityFeature3 = 'assets/icons/sensitivity_feature_3.svg'; - static const String sensitivityFeature4 = 'assets/icons/sensitivity_feature_4.svg'; - static const String sensitivityFeature5 = 'assets/icons/sensitivity_feature_5.svg'; - static const String sensitivityFeature6 = 'assets/icons/sensitivity_feature_6.svg'; - static const String sensitivityFeature7 = 'assets/icons/sensitivity_feature_7.svg'; - static const String sensitivityFeature8 = 'assets/icons/sensitivity_feature_8.svg'; - static const String sensitivityFeature9 = 'assets/icons/sensitivity_feature_9.svg'; + static const String sensitivityFeature1 = + 'assets/icons/sensitivity_feature_1.svg'; + static const String sensitivityFeature2 = + 'assets/icons/sensitivity_feature_2.svg'; + static const String sensitivityFeature3 = + 'assets/icons/sensitivity_feature_3.svg'; + static const String sensitivityFeature4 = + 'assets/icons/sensitivity_feature_4.svg'; + static const String sensitivityFeature5 = + 'assets/icons/sensitivity_feature_5.svg'; + static const String sensitivityFeature6 = + 'assets/icons/sensitivity_feature_6.svg'; + static const String sensitivityFeature7 = + 'assets/icons/sensitivity_feature_7.svg'; + static const String sensitivityFeature8 = + 'assets/icons/sensitivity_feature_8.svg'; + static const String sensitivityFeature9 = + 'assets/icons/sensitivity_feature_9.svg'; static const String deviceTagIcon = 'assets/icons/device_tag_ic.svg'; static const String targetConfirmTimeIcon = 'assets/icons/target_confirm_time_icon.svg'; @@ -453,10 +489,13 @@ class Assets { static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg'; static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg'; static const String blankCalendar = 'assets/icons/blank_calendar.svg'; - static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg'; - static const String energyConsumedIcon = 'assets/icons/energy_consumed_icon.svg'; + static const String refreshStatusIcon = + 'assets/icons/refresh_status_icon.svg'; + static const String energyConsumedIcon = + 'assets/icons/energy_consumed_icon.svg'; - static const String closeSettingsIcon = 'assets/icons/close_settings_icon.svg'; + static const String closeSettingsIcon = + 'assets/icons/close_settings_icon.svg'; static const String editNameIconSettings = 'assets/icons/edit_name_icon_settings.svg'; @@ -476,4 +515,6 @@ class Assets { 'assets/icons/empty_energy_management_per_device.svg'; static const String emptyHeatmap = 'assets/icons/empty_heatmap.svg'; static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; + static const String homeIcon = 'assets/icons/home_icon.svg'; + static const String groupIcon = 'assets/icons/group_icon.svg'; } From 009b7c0316805f7c1a54362bb74f00d3281215b5 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:53:54 +0300 Subject: [PATCH 056/105] Refactor SpaceDetails feature to replace LoadSpacesParam with LoadSpaceDetailsParam, enhancing clarity in parameter handling. Introduce ClearSpaceDetails event in SpaceDetailsBloc for better state management. Update SpaceDetailsDialog and SpaceDetailsForm to utilize new parameter and improve dialog functionality. --- .../remote_space_details_service.dart | 4 +- .../params/load_space_details_param.dart | 7 + .../domain/params/load_spaces_param.dart | 3 - .../services/space_details_service.dart | 4 +- .../presentation/bloc/space_details_bloc.dart | 10 +- .../bloc/space_details_event.dart | 11 +- .../bloc/space_details_state.dart | 6 +- .../widgets/space_details_dialog.dart | 134 ++++++++++-------- .../widgets/space_details_form.dart | 76 ++++++++++ 9 files changed, 183 insertions(+), 72 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart delete mode 100644 lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart index 2e999361..17514e85 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -15,7 +15,7 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { static const _defaultErrorMessage = 'Failed to load space details'; @override - Future getSpaceDetails(LoadSpacesParam param) async { + Future getSpaceDetails(LoadSpaceDetailsParam param) async { try { final response = await _httpService.get( path: 'endpoint', diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart new file mode 100644 index 00000000..e5efbb83 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -0,0 +1,7 @@ +class LoadSpaceDetailsParam { + const LoadSpaceDetailsParam({ + this.spaceUuid, + }); + + final String? spaceUuid; +} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart deleted file mode 100644 index 5324ed98..00000000 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart +++ /dev/null @@ -1,3 +0,0 @@ -class LoadSpacesParam { - const LoadSpacesParam(); -} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart index b032560b..16b09ff1 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart @@ -1,6 +1,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; abstract class SpaceDetailsService { - Future getSpaceDetails(LoadSpacesParam param); + Future getSpaceDetails(LoadSpaceDetailsParam param); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart index 6eb96632..d397daf9 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -11,6 +11,7 @@ part 'space_details_state.dart'; class SpaceDetailsBloc extends Bloc { SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) { on(_onLoadSpaceDetails); + on(_onClearSpaceDetails); } final SpaceDetailsService _spaceDetailsService; @@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc { emit(SpaceDetailsFailure(e.toString())); } } + + void _onClearSpaceDetails( + ClearSpaceDetails event, + Emitter emit, + ) { + emit(SpaceDetailsInitial()); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart index fe559e26..9dd40fba 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart @@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable { List get props => []; } -class LoadSpaceDetails extends SpaceDetailsEvent { +final class LoadSpaceDetails extends SpaceDetailsEvent { const LoadSpaceDetails(this.param); - final LoadSpacesParam param; + final LoadSpaceDetailsParam param; @override List get props => [param]; } + +final class ClearSpaceDetails extends SpaceDetailsEvent { + const ClearSpaceDetails(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart index c7378f89..53c4cf77 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart @@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState { } final class SpaceDetailsFailure extends SpaceDetailsState { - final String message; + final String errorMessage; - const SpaceDetailsFailure(this.message); + const SpaceDetailsFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index ee6c7e7c..98b5027a 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' - show SpaceDetailsModel; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class SpaceDetailsDialog extends StatelessWidget { +class SpaceDetailsDialog extends StatefulWidget { const SpaceDetailsDialog({ required this.title, required this.space, @@ -24,58 +20,78 @@ class SpaceDetailsDialog extends StatelessWidget { final void Function(SpaceDetailsModel space) onSave; @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SpaceDetailsModelBloc(initialState: space), - child: Builder( - builder: (context) { - final space = context.watch().state; - return AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: SpaceIconPicker(iconPath: space.icon)), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, - ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), - ], - ), - ), - ], - ), + State createState() => _SpaceDetailsDialogState(); +} + +class _SpaceDetailsDialogState extends State { + @override + void initState() { + final isCreateMode = widget.space.uuid.isEmpty; + + if (!isCreateMode) { + context.read().add( + LoadSpaceDetails( + LoadSpaceDetailsParam(spaceUuid: widget.space.uuid), ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], ); - }, + } + super.initState(); + } + + @override + void deactivate() { + context.read().add(const ClearSpaceDetails()); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + final isCreateMode = widget.space.uuid.isEmpty; + if (isCreateMode) { + return SpaceDetailsForm( + title: widget.title, + space: widget.space, + onSave: widget.onSave, + ); + } + + return BlocBuilder( + builder: (context, state) => switch (state) { + SpaceDetailsInitial() => _buildLoadingDialog(), + SpaceDetailsLoading() => _buildLoadingDialog(), + SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm( + title: widget.title, + space: spaceDetails, + onSave: widget.onSave, + ), + SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog( + errorMessage, + ), + }, + ); + } + + Widget _buildLoadingDialog() { + return AlertDialog( + title: widget.title, + backgroundColor: ColorsManager.whiteColors, + content: const Center(child: CircularProgressIndicator()), + ); + } + + Widget _buildErrorDialog(String errorMessage) { + return AlertDialog( + title: widget.title, + backgroundColor: ColorsManager.whiteColors, + content: Center( + child: Text( + errorMessage, + style: context.textTheme.bodyLarge?.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), ), ); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart new file mode 100644 index 00000000..aa08d7fe --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceDetailsForm extends StatelessWidget { + const SpaceDetailsForm({ + required this.title, + required this.space, + required this.onSave, + super.key, + }); + + final Widget title; + final SpaceDetailsModel space; + final void Function(SpaceDetailsModel space) onSave; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SpaceDetailsModelBloc(initialState: space), + child: AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: SpaceIconPicker(iconPath: space.icon)), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], + ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], + ), + ); + } +} From 9990b1805e0c4c283113748c4e65f214748e6035 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 16:27:03 +0300 Subject: [PATCH 057/105] Fix typo in HomeBloc: change 'Devices Management' to 'Device Management' for consistency in naming. --- lib/pages/home/bloc/home_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index c1bcba6a..aa642e25 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -105,7 +105,7 @@ class HomeBloc extends Bloc { color: const Color(0xFF0026A2), ), HomeItemModel( - title: 'Devices Management', + title: 'Device Management', icon: Assets.devicesIcon, active: true, onPress: (context) { From c43cf9347f40e6a848b5fa1c982909a0a974cc07 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 16:29:02 +0300 Subject: [PATCH 058/105] Remove unnecessary deactivate method from SpaceDetailsDialog to streamline state management and improve code clarity. --- .../presentation/widgets/space_details_dialog.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 98b5027a..09355950 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -38,12 +38,6 @@ class _SpaceDetailsDialogState extends State { super.initState(); } - @override - void deactivate() { - context.read().add(const ClearSpaceDetails()); - super.deactivate(); - } - @override Widget build(BuildContext context) { final isCreateMode = widget.space.uuid.isEmpty; From 71cf4b9feb7c588feeafb8ee75d919142a4cc9dd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 16:30:23 +0300 Subject: [PATCH 059/105] Update LoadSpaceDetailsParam to require spaceUuid and refactor SpaceDetailsDialog to enhance clarity in parameter handling. --- .../domain/params/load_space_details_param.dart | 4 ++-- .../presentation/widgets/space_details_dialog.dart | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart index e5efbb83..c4c6c565 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -1,7 +1,7 @@ class LoadSpaceDetailsParam { const LoadSpaceDetailsParam({ - this.spaceUuid, + required this.spaceUuid, }); - final String? spaceUuid; + final String spaceUuid; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 09355950..69b54b4f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -29,11 +29,8 @@ class _SpaceDetailsDialogState extends State { final isCreateMode = widget.space.uuid.isEmpty; if (!isCreateMode) { - context.read().add( - LoadSpaceDetails( - LoadSpaceDetailsParam(spaceUuid: widget.space.uuid), - ), - ); + final param = LoadSpaceDetailsParam(spaceUuid: widget.space.uuid); + context.read().add(LoadSpaceDetails(param)); } super.initState(); } From c221c8499f69336c22d5ccae07fdd77a9f0a3f0d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 17:05:56 +0300 Subject: [PATCH 060/105] Add factory method `empty` to `SpaceModel` for default instance creation. Refactor SpaceDetailsDialog and related widgets to utilize SpaceModel, enhancing parameter handling and state management in space creation and editing flows. --- .../domain/models/space_model.dart | 10 +++ .../helpers/space_details_dialog_helper.dart | 13 +--- .../widgets/space_details_dialog.dart | 13 ++-- .../widgets/space_details_form.dart | 78 +++++++++---------- .../widgets/space_sub_spaces_box.dart | 27 +++++-- .../widgets/space_sub_spaces_dialog.dart | 7 +- .../widgets/subspace_name_display_widget.dart | 73 ++++++++++------- 7 files changed, 130 insertions(+), 91 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 36943adb..ddcc6a86 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -19,6 +19,16 @@ class SpaceModel extends Equatable { required this.parent, }); + factory SpaceModel.empty() => const SpaceModel( + uuid: '', + createdAt: null, + updatedAt: null, + spaceName: '', + icon: '', + children: [], + parent: null, + ); + factory SpaceModel.fromJson(Map json) { return SpaceModel( uuid: json['uuid'] as String? ?? '', diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 5d6ffee7..723a5bc1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; abstract final class SpaceDetailsDialogHelper { @@ -9,8 +8,8 @@ abstract final class SpaceDetailsDialogHelper { context: context, builder: (_) => SpaceDetailsDialog( title: const Text('Create Space'), - space: SpaceDetailsModel.empty(), - onSave: (space) {}, + spaceModel: SpaceModel.empty(), + onSave: (space) => print(space), ), ); } @@ -23,13 +22,7 @@ abstract final class SpaceDetailsDialogHelper { context: context, builder: (_) => SpaceDetailsDialog( title: const Text('Edit Space'), - space: SpaceDetailsModel( - uuid: spaceModel.uuid, - spaceName: spaceModel.spaceName, - icon: spaceModel.icon, - productAllocations: const [], - subspaces: const [], - ), + spaceModel: spaceModel, onSave: (space) {}, ), ); diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 69b54b4f..85e9f009 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -10,13 +11,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDialog extends StatefulWidget { const SpaceDetailsDialog({ required this.title, - required this.space, + required this.spaceModel, required this.onSave, super.key, }); final Widget title; - final SpaceDetailsModel space; + final SpaceModel spaceModel; final void Function(SpaceDetailsModel space) onSave; @override @@ -26,10 +27,10 @@ class SpaceDetailsDialog extends StatefulWidget { class _SpaceDetailsDialogState extends State { @override void initState() { - final isCreateMode = widget.space.uuid.isEmpty; + final isCreateMode = widget.spaceModel.uuid.isEmpty; if (!isCreateMode) { - final param = LoadSpaceDetailsParam(spaceUuid: widget.space.uuid); + final param = LoadSpaceDetailsParam(spaceUuid: widget.spaceModel.uuid); context.read().add(LoadSpaceDetails(param)); } super.initState(); @@ -37,11 +38,11 @@ class _SpaceDetailsDialogState extends State { @override Widget build(BuildContext context) { - final isCreateMode = widget.space.uuid.isEmpty; + final isCreateMode = widget.spaceModel.uuid.isEmpty; if (isCreateMode) { return SpaceDetailsForm( title: widget.title, - space: widget.space, + space: SpaceDetailsModel.empty(), onSave: widget.onSave, ); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index aa08d7fe..24bae9a3 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -26,51 +26,51 @@ class SpaceDetailsForm extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SpaceDetailsModelBloc(initialState: space), - child: AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: SpaceIconPicker(iconPath: space.icon)), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, + child: BlocBuilder( + buildWhen: (previous, current) => previous != current, + builder: (context, state) { + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.3, + child: Row( + spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, + Expanded(child: SpaceIconPicker(iconPath: state.icon)), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: state.spaceName, + isNameFieldExist: (value) => state.subspaces.any( + (subspace) => subspace.name == value, + ), + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: state.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: state), + ], + ), ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), ], ), ), - ], - ), - ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], - ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(state), + onCancel: Navigator.of(context).pop, + ), + ], + ); + }), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 30677e1e..5c3cef25 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -23,10 +25,7 @@ class SpaceSubSpacesBox extends StatelessWidget { padding: EdgeInsets.zero, overlayColor: ColorsManager.transparentColor, ), - onPressed: () => showDialog( - context: context, - builder: (_) => SpaceSubSpacesDialog(subspaces: subspaces), - ), + onPressed: () => _showSubSpacesDialog(context), child: const ButtonContentWidget( svgAssets: Assets.addIcon, label: 'Create Sub Spaces', @@ -54,10 +53,7 @@ class SpaceSubSpacesBox extends StatelessWidget { (e) => SubspaceNameDisplayWidget(subSpace: e), ), EditChip( - onTap: () => showDialog( - context: context, - builder: (_) => const SpaceSubSpacesDialog(subspaces: []), - ), + onTap: () => _showSubSpacesDialog(context), ), ], ), @@ -66,4 +62,19 @@ class SpaceSubSpacesBox extends StatelessWidget { ], ); } + + void _showSubSpacesDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => SpaceSubSpacesDialog( + subspaces: subspaces, + onSave: (subspaces) { + context.read().add( + UpdateSpaceDetailsSubspaces(subspaces), + ); + }, + ), + ); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 4230f7f4..4c537a8a 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -7,10 +7,12 @@ import 'package:uuid/uuid.dart'; class SpaceSubSpacesDialog extends StatefulWidget { const SpaceSubSpacesDialog({ required this.subspaces, + required this.onSave, super.key, }); final List subspaces; + final void Function(List subspaces) onSave; @override State createState() => _SpaceSubSpacesDialogState(); @@ -46,7 +48,10 @@ class _SpaceSubSpacesDialogState extends State { () => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(), ); - void _handleSave() => Navigator.of(context).pop(_subspaces); + void _handleSave() { + widget.onSave(_subspaces); + Navigator.of(context).pop(); + } @override Widget build(BuildContext context) { diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index 3d7ab59b..9169efda 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -17,16 +17,19 @@ class SubspaceNameDisplayWidget extends StatefulWidget { class _SubspaceNameDisplayWidgetState extends State { late final TextEditingController _controller; + late final FocusNode _focusNode; bool isEditing = false; @override void initState() { _controller = TextEditingController(text: widget.subSpace.name); + _focusNode = FocusNode(); super.initState(); } @override void dispose() { _controller.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -36,38 +39,54 @@ class _SubspaceNameDisplayWidgetState extends State { color: ColorsManager.spaceColor, ); return InkWell( - onTap: () => setState(() => isEditing = true), - child: Visibility( - visible: isEditing, - replacement: Text( - widget.subSpace.name, - style: textStyle, + onTap: () { + setState(() => isEditing = true); + _focusNode.requestFocus(); + }, + child: Chip( + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), ), - child: TextField( - controller: _controller, - style: textStyle, - decoration: const InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsetsDirectional.symmetric( - horizontal: 8, + label: Visibility( + visible: isEditing, + replacement: Text( + widget.subSpace.name, + style: textStyle, + ), + child: SizedBox( + width: context.screenWidth * 0.065, + height: context.screenHeight * 0.025, + child: TextField( + focusNode: _focusNode, + controller: _controller, + style: textStyle, + decoration: const InputDecoration.collapsed(hintText: ''), + onTapOutside: (_) => _onFinishEditing(), + onSubmitted: (value) { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid + ? e.copyWith(name: value) + : e, + ) + .toList(), + ), + ); + _onFinishEditing(); + }, ), ), - onSubmitted: (value) { - final bloc = context.read(); - bloc.add( - UpdateSpaceDetailsSubspaces( - bloc.state.subspaces - .map( - (e) => e.uuid == widget.subSpace.uuid - ? e.copyWith(name: value) - : e, - ) - .toList(), - ), - ); - }, ), ), ); } + + void _onFinishEditing() { + setState(() => isEditing = false); + _focusNode.unfocus(); + } } From d47dc349bcd8cfafd7e3b6735c472cb071a42c8d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 12:04:03 +0300 Subject: [PATCH 061/105] Enhance SubspaceNameDisplayWidget to handle duplicate names during editing. Introduced logic to check for existing subspace names and provide user feedback. Refactored state management for editing and submission processes, improving overall user experience and code clarity. --- .../widgets/subspace_name_display_widget.dart | 108 +++++++++++++----- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index 9169efda..bbc9549d 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -18,7 +18,9 @@ class SubspaceNameDisplayWidget extends StatefulWidget { class _SubspaceNameDisplayWidgetState extends State { late final TextEditingController _controller; late final FocusNode _focusNode; - bool isEditing = false; + bool _isEditing = false; + bool _hasDuplicateName = false; + @override void initState() { _controller = TextEditingController(text: widget.subSpace.name); @@ -33,6 +35,41 @@ class _SubspaceNameDisplayWidgetState extends State { super.dispose(); } + bool _checkForDuplicateName(String name) { + final bloc = context.read(); + return bloc.state.subspaces + .where((s) => s.uuid != widget.subSpace.uuid) + .any((s) => s.name.toLowerCase() == name.toLowerCase()); + } + + void _handleNameChange(String value) { + setState(() { + _hasDuplicateName = _checkForDuplicateName(value); + }); + } + + void _tryToFinishEditing() { + if (!_hasDuplicateName) { + _onFinishEditing(); + } + } + + void _tryToSubmit(String value) { + if (_hasDuplicateName) return; + + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e, + ) + .toList(), + ), + ); + _onFinishEditing(); + } + @override Widget build(BuildContext context) { final textStyle = context.textTheme.bodySmall?.copyWith( @@ -40,7 +77,7 @@ class _SubspaceNameDisplayWidgetState extends State { ); return InkWell( onTap: () { - setState(() => isEditing = true); + setState(() => _isEditing = true); _focusNode.requestFocus(); }, child: Chip( @@ -49,36 +86,48 @@ class _SubspaceNameDisplayWidgetState extends State { borderRadius: BorderRadius.circular(10), ), label: Visibility( - visible: isEditing, + visible: _isEditing, replacement: Text( widget.subSpace.name, style: textStyle, ), - child: SizedBox( - width: context.screenWidth * 0.065, - height: context.screenHeight * 0.025, - child: TextField( - focusNode: _focusNode, - controller: _controller, - style: textStyle, - decoration: const InputDecoration.collapsed(hintText: ''), - onTapOutside: (_) => _onFinishEditing(), - onSubmitted: (value) { - final bloc = context.read(); - bloc.add( - UpdateSpaceDetailsSubspaces( - bloc.state.subspaces - .map( - (e) => e.uuid == widget.subSpace.uuid - ? e.copyWith(name: value) - : e, - ) - .toList(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: context.screenWidth * 0.065, + height: context.screenHeight * 0.025, + child: TextField( + focusNode: _focusNode, + controller: _controller, + style: textStyle?.copyWith( + color: _hasDuplicateName ? Colors.red : null, ), - ); - _onFinishEditing(); - }, - ), + decoration: const InputDecoration.collapsed( + hintText: '', + ), + onChanged: _handleNameChange, + onTapOutside: (_) => _tryToFinishEditing(), + onSubmitted: _tryToSubmit, + ), + ), + if (_hasDuplicateName) + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: Visibility( + key: ValueKey(_hasDuplicateName), + visible: _hasDuplicateName, + child: Text( + 'Name already exists', + style: textStyle?.copyWith( + color: Colors.red, + fontSize: 8, + ), + ), + ), + ), + ], ), ), ), @@ -86,7 +135,10 @@ class _SubspaceNameDisplayWidgetState extends State { } void _onFinishEditing() { - setState(() => isEditing = false); + setState(() { + _isEditing = false; + _hasDuplicateName = false; + }); _focusNode.unfocus(); } } From 318e1d9af7571658fd4a0e0cfa894557186b571d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 13:09:43 +0300 Subject: [PATCH 062/105] Implement Space Management Header and Action Buttons; integrate SpaceDetailsBloc for improved space management functionality. Add CommunityStructureHeader, CommunityStructureHeaderActionButtons, and CommunityStructureHeaderButton widgets to enhance UI and user interactions. Update SpaceManagementCommunityStructure to include the new header and refactor space details service for better endpoint handling. --- .../views/space_management_page.dart | 7 ++ .../widgets/community_structure_header.dart | 116 ++++++++++++++++++ ...unity_structure_header_action_buttons.dart | 46 +++++++ .../community_structure_header_button.dart | 61 +++++++++ .../space_management_community_structure.dart | 15 ++- .../remote_space_details_service.dart | 17 ++- .../params/load_space_details_param.dart | 2 + .../helpers/space_details_dialog_helper.dart | 32 +++-- .../widgets/space_details_dialog.dart | 14 ++- 9 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.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 957be65a..05768035 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -26,6 +28,11 @@ class SpaceManagementPage extends StatelessWidget { )..add(const LoadCommunities(LoadCommunitiesParam())), ), BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), ], child: WebScaffold( appBarTitle: Text( diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart new file mode 100644 index 00000000..b457c413 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeader extends StatelessWidget { + const CommunityStructureHeader({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenWidth = MediaQuery.of(context).size.width; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.shadowBlackColor, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildCommunityInfo(context, theme, screenWidth), + ), + const SizedBox(width: 16), + ], + ), + ], + ), + ); + } + + void _showCreateCommunityDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + title: const Text('Edit Community'), + onCreateCommunity: (community) { + // TODO(FarisArmoush): Implement + }, + ), + ); + } + + Widget _buildCommunityInfo( + BuildContext context, ThemeData theme, double screenWidth) { + final selectedCommunity = + context.watch().state.selectedCommunity; + final selectedSpace = + context.watch().state.selectedSpace; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Community Structure', + style: theme.textTheme.headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + if (selectedCommunity != null) + Row( + children: [ + Expanded( + child: Row( + children: [ + Flexible( + child: SelectableText( + selectedCommunity.name, + style: theme.textTheme.bodyLarge + ?.copyWith(color: ColorsManager.blackColor), + maxLines: 1, + ), + ), + const SizedBox(width: 2), + GestureDetector( + onTap: () => _showCreateCommunityDialog(context), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + CommunityStructureHeaderActionButtons( + onDelete: (space) {}, + onDuplicate: (space) {}, + onEdit: (space) { + SpaceDetailsDialogHelper.showEdit( + context, + spaceModel: selectedSpace!, + ); + }, + selectedSpace: selectedSpace, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart new file mode 100644 index 00000000..a965c866 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeaderActionButtons extends StatelessWidget { + const CommunityStructureHeaderActionButtons({ + super.key, + required this.onDelete, + required this.selectedSpace, + required this.onDuplicate, + required this.onEdit, + }); + + final void Function(SpaceModel space) onDelete; + final void Function(SpaceModel space) onDuplicate; + final void Function(SpaceModel space) onEdit; + final SpaceModel? selectedSpace; + + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.end, + spacing: 10, + children: [ + if (selectedSpace != null) ...[ + CommunityStructureHeaderButton( + label: 'Edit', + svgAsset: Assets.editSpace, + onPressed: () => onEdit(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Duplicate', + svgAsset: Assets.duplicate, + onPressed: () => onDuplicate(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Delete', + svgAsset: Assets.spaceDelete, + onPressed: () => onDelete(selectedSpace!), + ), + ], + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart new file mode 100644 index 00000000..4c0285e3 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityStructureHeaderButton extends StatelessWidget { + const CommunityStructureHeaderButton({ + super.key, + required this.label, + required this.onPressed, + this.svgAsset, + }); + + final String label; + final VoidCallback onPressed; + final String? svgAsset; + + @override + Widget build(BuildContext context) { + const double buttonHeight = 40; + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 130, + minHeight: buttonHeight, + ), + child: DefaultButton( + onPressed: onPressed, + borderWidth: 2, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: ColorsManager.blackColor, + borderRadius: 12.0, + padding: 2.0, + height: buttonHeight, + elevation: 0, + borderColor: ColorsManager.lightGrayColor, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (svgAsset != null) + SvgPicture.asset( + svgAsset!, + width: 20, + height: 20, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + label, + style: context.textTheme.bodySmall + ?.copyWith(color: ColorsManager.blackColor, fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 99d0668a..e1f1fc00 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; @@ -18,9 +19,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget { replacement: const Row( children: [spacer, Expanded(child: CreateSpaceButton()), spacer], ), - child: CommunityStructureCanvas( - community: selectedCommunity, - selectedSpace: selectedSpace, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CommunityStructureHeader(), + Expanded( + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), + ), + ], ), ); } diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart index 17514e85..8b40cbfb 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart @@ -1,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/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; @@ -18,9 +19,12 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { Future getSpaceDetails(LoadSpaceDetailsParam param) async { try { final response = await _httpService.get( - path: 'endpoint', + path: await _makeEndpoint(param), expectedResponseModel: (data) { - return SpaceDetailsModel.fromJson(data as Map); + final response = data as Map; + return SpaceDetailsModel.fromJson( + response['data'] as Map, + ); }, ); return response; @@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { throw APIException(formattedErrorMessage); } } + + Future _makeEndpoint(LoadSpaceDetailsParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}'; + } } diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart index c4c6c565..7242e62e 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -1,7 +1,9 @@ class LoadSpaceDetailsParam { const LoadSpaceDetailsParam({ required this.spaceUuid, + required this.communityUuid, }); final String spaceUuid; + final String communityUuid; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 723a5bc1..c8835716 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,15 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; abstract final class SpaceDetailsDialogHelper { static void showCreate(BuildContext context) { showDialog( context: context, - builder: (_) => SpaceDetailsDialog( - title: const Text('Create Space'), - spaceModel: SpaceModel.empty(), - onSave: (space) => print(space), + builder: (_) => BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + child: SpaceDetailsDialog( + context: context, + title: const Text('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: print, + ), ), ); } @@ -20,10 +30,16 @@ abstract final class SpaceDetailsDialogHelper { }) { showDialog( context: context, - builder: (_) => SpaceDetailsDialog( - title: const Text('Edit Space'), - spaceModel: spaceModel, - onSave: (space) {}, + builder: (_) => BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + child: SpaceDetailsDialog( + context: context, + title: const Text('Edit Space'), + spaceModel: spaceModel, + onSave: (space) {}, + ), ), ); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 85e9f009..abbd9aae 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -13,12 +14,14 @@ class SpaceDetailsDialog extends StatefulWidget { required this.title, required this.spaceModel, required this.onSave, + required this.context, super.key, }); final Widget title; final SpaceModel spaceModel; final void Function(SpaceDetailsModel space) onSave; + final BuildContext context; @override State createState() => _SpaceDetailsDialogState(); @@ -30,8 +33,15 @@ class _SpaceDetailsDialogState extends State { final isCreateMode = widget.spaceModel.uuid.isEmpty; if (!isCreateMode) { - final param = LoadSpaceDetailsParam(spaceUuid: widget.spaceModel.uuid); - context.read().add(LoadSpaceDetails(param)); + final param = LoadSpaceDetailsParam( + spaceUuid: widget.spaceModel.uuid, + communityUuid: widget.context + .read() + .state + .selectedCommunity! + .uuid, + ); + widget.context.read().add(LoadSpaceDetails(param)); } super.initState(); } From fc797c26462691d1bff82a162a7079c906a02fd3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 13:19:34 +0300 Subject: [PATCH 063/105] Refactor SpaceDetailsModel and ProductAllocation: Update JSON parsing for clarity and remove unused location field. Change subspace name mapping for consistency with API response. --- .../domain/models/space_details_model.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 8b9ff666..737d5060 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -31,8 +31,8 @@ class SpaceDetailsModel extends Equatable { spaceName: json['spaceName'] as String, icon: json['icon'] as String, productAllocations: (json['productAllocations'] as List) - .map((e) => ProductAllocation.fromJson(e as Map)) - .toList(), + .map((e) => ProductAllocation.fromJson(e as Map)) + .toList(), subspaces: (json['subspaces'] as List) .map((e) => Subspace.fromJson(e as Map)) .toList(), @@ -72,12 +72,10 @@ class SpaceDetailsModel extends Equatable { class ProductAllocation extends Equatable { final Product product; final Tag tag; - final String? location; const ProductAllocation({ required this.product, required this.tag, - this.location, }); factory ProductAllocation.fromJson(Map json) { @@ -97,12 +95,10 @@ class ProductAllocation extends Equatable { ProductAllocation copyWith({ Product? product, Tag? tag, - String? location, }) { return ProductAllocation( product: product ?? this.product, tag: tag ?? this.tag, - location: location ?? this.location, ); } @@ -124,7 +120,7 @@ class Subspace extends Equatable { factory Subspace.fromJson(Map json) { return Subspace( uuid: json['uuid'] as String, - name: json['name'] as String, + name: json['subspaceName'] as String, productAllocations: (json['productAllocations'] as List) .map((e) => ProductAllocation.fromJson(e as Map)) .toList(), From 9451ec0cc4da3322e3c24fd182a39592b555018b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 13:19:42 +0300 Subject: [PATCH 064/105] Update SpaceDetailsDialog to utilize SelectableText for error messages and ensure proper Bloc context usage. This enhances user experience by allowing text selection for easier copying of error information. --- .../presentation/widgets/space_details_dialog.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index abbd9aae..2461b3c6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -58,6 +58,7 @@ class _SpaceDetailsDialogState extends State { } return BlocBuilder( + bloc: widget.context.read(), builder: (context, state) => switch (state) { SpaceDetailsInitial() => _buildLoadingDialog(), SpaceDetailsLoading() => _buildLoadingDialog(), @@ -86,7 +87,7 @@ class _SpaceDetailsDialogState extends State { title: widget.title, backgroundColor: ColorsManager.whiteColors, content: Center( - child: Text( + child: SelectableText( errorMessage, style: context.textTheme.bodyLarge?.copyWith( color: ColorsManager.red, From 227df6fe3d74c455b34aecd6cd3d29db695e1095 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:23:00 +0300 Subject: [PATCH 065/105] Refactor SpaceDetailsWidgets: Simplify layout and improve responsiveness in SpaceDetailsDevicesBox and SpaceSubSpacesBox. Update SpaceDetailsForm to enhance dialog width for better user experience. This refactor enhances maintainability and aligns with Clean Architecture principles. --- .../widgets/space_details_devices_box.dart | 140 +++++++++--------- .../widgets/space_details_form.dart | 1 + .../widgets/space_sub_spaces_box.dart | 82 +++++----- 3 files changed, 107 insertions(+), 116 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 4873dc09..0208ccba 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_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'; class SpaceDetailsDevicesBox extends StatelessWidget { const SpaceDetailsDevicesBox({super.key, required this.space}); @@ -13,78 +12,75 @@ class SpaceDetailsDevicesBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - if (space.productAllocations.isNotEmpty || - space.subspaces - .any((subspace) => subspace.productAllocations.isNotEmpty)) - SizedBox( - width: context.screenWidth * 0.25, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width - ), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - // Combine tags from spaceModel and subspaces - // ...TagHelper.groupTags([ - // ...?tags, - // ...?subspaces?.expand((subspace) => subspace.tags ?? []) - // ]).entries.map( - // (entry) => Chip( - // avatar: SizedBox( - // width: 24, - // height: 24, - // child: SvgPicture.asset( - // entry.key.icon ?? 'assets/icons/gateway.svg', - // fit: BoxFit.contain, - // ), - // ), - // label: Text( - // 'x${entry.value}', // Show count - // style: Theme.of(context) - // .textTheme - // .bodySmall - // ?.copyWith(color: ColorsManager.spaceColor), - // ), - // backgroundColor: ColorsManager.whiteColors, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(16), - // side: const BorderSide( - // color: ColorsManager.spaceColor, - // ), - // ), - // ), - // ), + if (space.productAllocations.isNotEmpty || + space.subspaces.any((subspace) => subspace.productAllocations.isNotEmpty)) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + // ...TagHelper.groupTags([ + // ...?tags, + // ...?subspaces?.expand((subspace) => subspace.tags ?? []) + // ]).entries.map( + // (entry) => Chip( + // avatar: SizedBox( + // width: 24, + // height: 24, + // child: SvgPicture.asset( + // entry.key.icon ?? 'assets/icons/gateway.svg', + // fit: BoxFit.contain, + // ), + // ), + // label: Text( + // 'x${entry.value}', // Show count + // style: Theme.of(context) + // .textTheme + // .bodySmall + // ?.copyWith(color: ColorsManager.spaceColor), + // ), + // backgroundColor: ColorsManager.whiteColors, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // side: const BorderSide( + // color: ColorsManager.spaceColor, + // ), + // ), + // ), + // ), - EditChip( - onTap: () {}, - ), - ], - ), + EditChip( + onTap: () {}, ), - ) - else - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - ), - child: const ButtonContentWidget( - svgAssets: Assets.addIcon, - label: 'Add Devices', - // disabled: isTagsAndSubspaceModelDisabled, - ), - ) - ], - ); + ], + ), + ); + } else { + return TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const SizedBox( + width: double.infinity, + child: ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Add Devices', + // disabled: isTagsAndSubspaceModelDisabled, + ), + ), + ); + } } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index 24bae9a3..f9a5ad64 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -34,6 +34,7 @@ class SpaceDetailsForm extends StatelessWidget { backgroundColor: ColorsManager.whiteColors, content: SizedBox( height: context.screenHeight * 0.3, + width: context.screenWidth * 0.5, child: Row( spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 5c3cef25..68bf68bd 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -8,7 +8,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/pres import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceSubSpacesBox extends StatelessWidget { const SpaceSubSpacesBox({super.key, required this.subspaces}); @@ -17,50 +16,45 @@ class SpaceSubSpacesBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - if (subspaces.isEmpty) - TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - overlayColor: ColorsManager.transparentColor, - ), - onPressed: () => _showSubSpacesDialog(context), - child: const ButtonContentWidget( - svgAssets: Assets.addIcon, - label: 'Create Sub Spaces', - disabled: false, - ), - ) - else - SizedBox( - width: context.screenWidth * 0.25, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: ColorsManager.textFieldGreyColor, - width: 3.0, - ), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - ...subspaces.map( - (e) => SubspaceNameDisplayWidget(subSpace: e), - ), - EditChip( - onTap: () => _showSubSpacesDialog(context), - ), - ], - ), - ), + if (subspaces.isEmpty) { + return TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () => _showSubSpacesDialog(context), + child: const SizedBox( + width: double.infinity, + child: ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Create Sub Spaces', ), - ], - ); + ), + ); + } else { + return Container( + padding: const EdgeInsets.all(8.0), + width: double.infinity, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)), + EditChip( + onTap: () => _showSubSpacesDialog(context), + ), + ], + ), + ); + } } void _showSubSpacesDialog(BuildContext context) { From 58e99f95b20c1cb711700220fc643f8bee38a89b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:25:04 +0300 Subject: [PATCH 066/105] removed comments. --- .../presentation/widgets/space_details_devices_box.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 0208ccba..6def57d9 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -22,7 +22,7 @@ class SpaceDetailsDevicesBox extends StatelessWidget { borderRadius: BorderRadius.circular(15), border: Border.all( color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width + width: 3.0, ), ), child: Wrap( @@ -44,7 +44,7 @@ class SpaceDetailsDevicesBox extends StatelessWidget { // ), // ), // label: Text( - // 'x${entry.value}', // Show count + // 'x${entry.value}', // style: Theme.of(context) // .textTheme // .bodySmall From c07ddb0ccdd967826c45f0ca8d64a030d75ee86f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:27:06 +0300 Subject: [PATCH 067/105] Refactor SpaceDetailsDevicesBox: Improve readability by extracting variables for product allocations and subspaces. This change enhances code clarity and maintainability in line with Clean Architecture principles. --- .../widgets/space_details_devices_box.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 6def57d9..bfd68b9e 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -6,14 +6,20 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class SpaceDetailsDevicesBox extends StatelessWidget { - const SpaceDetailsDevicesBox({super.key, required this.space}); + const SpaceDetailsDevicesBox({ + required this.space, + super.key, + }); final SpaceDetailsModel space; @override Widget build(BuildContext context) { - if (space.productAllocations.isNotEmpty || - space.subspaces.any((subspace) => subspace.productAllocations.isNotEmpty)) { + final productAllocations = space.productAllocations; + final subspaces = space.subspaces; + final isAnySubspaceHasProductAllocations = + subspaces.any((subspace) => subspace.productAllocations.isNotEmpty); + if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) { return Container( width: double.infinity, padding: const EdgeInsets.all(8.0), From 1fccd51440c020803e2cbcd1021dbce1bd5dbde3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:33:34 +0300 Subject: [PATCH 068/105] Refactor SubspaceNameDisplayWidget: Update Chip border radius for improved aesthetics and add delete functionality to remove subspaces. This enhances user interaction and aligns with maintainability principles. --- .../widgets/space_details_devices_box.dart | 2 +- .../widgets/subspace_name_display_widget.dart | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index bfd68b9e..5b21bd61 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -22,7 +22,7 @@ class SpaceDetailsDevicesBox extends StatelessWidget { if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) { return Container( width: double.infinity, - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: ColorsManager.textFieldGreyColor, borderRadius: BorderRadius.circular(15), diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index bbc9549d..bf13ffd3 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -83,7 +83,34 @@ class _SubspaceNameDisplayWidgetState extends State { child: Chip( backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: ColorsManager.transparentColor), + ), + onDeleted: () { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .where((s) => s.uuid != widget.subSpace.uuid) + .toList(), + ), + ); + }, + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), ), label: Visibility( visible: _isEditing, From b857736e10abe1c1eeb40ba10f7fe0f5cc38924a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:37:06 +0300 Subject: [PATCH 069/105] Refactor SpaceNameTextField: Update text styling and border handling to utilize context-based theming. This improves consistency with the app's theme and enhances maintainability by centralizing border styling logic. --- .../widgets/space_name_text_field.dart | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart index d5bd1016..0c62490f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart @@ -56,7 +56,7 @@ class _SpaceNameTextFieldState extends State { UpdateSpaceDetailsName(value), ), validator: _validateName, - style: Theme.of(context).textTheme.bodyMedium, + style: context.textTheme.bodyMedium, decoration: InputDecoration( hintText: 'Please enter the name', hintStyle: context.textTheme.bodyMedium!.copyWith( @@ -64,21 +64,22 @@ class _SpaceNameTextFieldState extends State { ), filled: true, fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(width: 1.5), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - ), - ), + enabledBorder: _buildBorder(context, ColorsManager.vividBlue), + focusedBorder: _buildBorder(context, ColorsManager.primaryColor), + errorBorder: _buildBorder(context, context.theme.colorScheme.error), + focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error), errorStyle: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.red, + color: context.theme.colorScheme.error, ), ), ), ); } + + OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor), + ); + } } From 757a96ed9f6d5e3044cbe7bc5d539f86bd48abc3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 16:15:31 +0300 Subject: [PATCH 070/105] Refactor SpaceDetailsActionButtons and SpaceIconPicker: Adjust layout properties for improved responsiveness and user experience. Set mainAxisSize to min in SpaceDetailsActionButtons and simplify the layout in SpaceIconPicker for better alignment and interaction. This enhances maintainability and adheres to Clean Architecture principles. --- .../widgets/space_details_action_buttons.dart | 1 + .../widgets/space_icon_picker.dart | 98 +++++++++---------- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index 3de9c192..8518227f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -16,6 +16,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { return Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, spacing: 10, children: [ diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart index fd6f79e6..68bfdefc 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -17,64 +17,60 @@ class SpaceIconPicker extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 50), - Stack( - alignment: Alignment.center, - children: [ - Container( - width: context.screenWidth * 0.1, - height: context.screenWidth * 0.1, - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - shape: BoxShape.circle, - ), + return Center( + child: Stack( + + alignment: Alignment.center, + children: [ + Container( + width: context.screenWidth * 0.175, + height: context.screenHeight * 0.175, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, ), - SvgPicture.asset( + padding: const EdgeInsets.all(24), + child: SvgPicture.asset( iconPath, - width: context.screenWidth * 0.04, - height: context.screenWidth * 0.04, + width: context.screenWidth * 0.08, + height: context.screenHeight * 0.08, ), - Positioned( - top: 20, - right: 20, - child: InkWell( - onTap: () { - showDialog( - context: context, - builder: (context) => SpaceIconSelectionDialog( - selectedIcon: iconPath, - ), - ).then((value) { - if (value != null) { - if (context.mounted) { - context.read().add( - UpdateSpaceDetailsIcon(value), - ); - } + ), + Positioned.directional( + top: 12, + start: context.screenHeight * 0.06, + textDirection: Directionality.of(context), + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) => SpaceIconSelectionDialog( + selectedIcon: iconPath, + ), + ).then((value) { + if (value != null) { + if (context.mounted) { + context.read().add( + UpdateSpaceDetailsIcon(value), + ); } - }); - }, - child: Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: SvgPicture.asset( - Assets.iconEdit, - width: 16, - height: 16, - ), + } + }); + }, + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, ), ), ), - ], - ), - ], + ), + ], + ), ); } } From 95cded4bf5e0143ca4e28e4e3897250051a560b0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 09:04:13 +0300 Subject: [PATCH 071/105] Enhance SubSpacesInput: Introduce FocusNode for improved text field focus management. This change allows the input field to regain focus after adding a subspace, enhancing user experience and maintaining clean state management practices. --- .../presentation/widgets/sub_spaces_input.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart index 1e1edb89..854b79bc 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -22,16 +22,18 @@ class SubSpacesInput extends StatefulWidget { class _SubSpacesInputState extends State { late final TextEditingController _subspaceNameController; - + late final FocusNode _focusNode; @override void initState() { super.initState(); _subspaceNameController = TextEditingController(); + _focusNode = FocusNode(); } @override void dispose() { _subspaceNameController.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -78,6 +80,7 @@ class _SubSpacesInputState extends State { SizedBox( width: 200, child: TextField( + focusNode: _focusNode, controller: _subspaceNameController, decoration: InputDecoration( border: InputBorder.none, @@ -91,6 +94,7 @@ class _SubSpacesInputState extends State { if (trimmedValue.isNotEmpty) { widget.onSubspaceAdded(trimmedValue); _subspaceNameController.clear(); + _focusNode.requestFocus(); } }, style: context.textTheme.bodyMedium, From 15b36fd0527ebd0f0afdeb0123135e8a0f0286db Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 09:33:27 +0300 Subject: [PATCH 072/105] Enhance SpaceDetailsDialog: Adjust loading indicator layout for improved responsiveness. The CircularProgressIndicator is now wrapped in a SizedBox to ensure proper sizing based on screen dimensions, enhancing user experience and maintainability. --- .../presentation/widgets/space_details_dialog.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 2461b3c6..ae772036 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -78,7 +78,11 @@ class _SpaceDetailsDialogState extends State { return AlertDialog( title: widget.title, backgroundColor: ColorsManager.whiteColors, - content: const Center(child: CircularProgressIndicator()), + content: SizedBox( + height: context.screenHeight * 0.3, + width: context.screenWidth * 0.5, + child: const Center(child: CircularProgressIndicator()), + ), ); } From 6dcc851d971742fcc5fd82efe95ea0aa3cf33740 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 10:46:20 +0300 Subject: [PATCH 073/105] Made `CommunityDialog` reusable for edit and create features. --- ...ce_management_community_dialog_helper.dart | 27 +++---- .../shared/widgets/community_dialog.dart} | 78 ++++++++----------- .../widgets/community_structure_header.dart | 21 ++--- .../domain/models/community_model.dart | 20 +++++ .../presentation/create_community_dialog.dart | 44 ++++++----- .../remote_update_community_service.dart | 3 +- .../domain/params/update_community_param.dart | 10 --- .../services/update_community_service.dart | 3 +- .../bloc/update_community_bloc.dart | 3 +- .../bloc/update_community_event.dart | 6 +- .../presentation/edit_community_dialog.dart | 57 ++++++++++++++ 11 files changed, 161 insertions(+), 111 deletions(-) rename lib/pages/space_management_v2/{modules/create_community/presentation/create_community_dialog_widget.dart => main_module/shared/widgets/community_dialog.dart} (55%) delete mode 100644 lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart create mode 100644 lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.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 index 5322c3ea..791ed10e 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -1,24 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart'; abstract final class SpaceManagementCommunityDialogHelper { - static void showCreateDialog(BuildContext context) { + static void showCreateDialog(BuildContext context) => showDialog( + context: context, + builder: (_) => const CreateCommunityDialog(), + ); + + static void showEditDialog( + BuildContext context, + CommunityModel community, + ) { showDialog( context: context, - builder: (_) => CreateCommunityDialog( - title: const SelectableText('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), + builder: (_) => EditCommunityDialog(community: community), ); } } diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart similarity index 55% rename from lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart rename to lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart index ab9f7b9a..f420abbd 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart +++ b/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart @@ -1,28 +1,28 @@ 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 { +class CommunityDialog extends StatefulWidget { final String? initialName; final Widget title; + final void Function(String name) onSubmit; + final String? errorMessage; - const CreateCommunityDialogWidget({ - super.key, + const CommunityDialog({ required this.title, + required this.onSubmit, this.initialName, + this.errorMessage, + super.key, }); @override - State createState() => - _CreateCommunityDialogWidgetState(); + State createState() => _CommunityDialogState(); } -class _CreateCommunityDialogWidgetState extends State { +class _CommunityDialogState extends State { late final TextEditingController _nameController; @override @@ -63,35 +63,31 @@ class _CreateCommunityDialogWidgetState extends State( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle( - style: Theme.of(context).textTheme.headlineMedium!, - child: widget.title, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField( + nameController: _nameController, + ), + if (widget.errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: 18), + child: SelectableText( + '* ${widget.errorMessage}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), ), - 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), - ], - ); - }, + ), + const SizedBox(height: 24), + _buildActionButtons(context), + ], ), ), ), @@ -132,13 +128,7 @@ class _CreateCommunityDialogWidgetState extends State().add( - CreateCommunity( - CreateCommunityParam( - name: _nameController.text.trim(), - ), - ), - ); + widget.onSubmit.call(_nameController.text.trim()); } } } diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart index b457c413..4f71075b 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -44,18 +44,6 @@ class CommunityStructureHeader extends StatelessWidget { ); } - void _showCreateCommunityDialog(BuildContext context) { - showDialog( - context: context, - builder: (context) => CreateCommunityDialog( - title: const Text('Edit Community'), - onCreateCommunity: (community) { - // TODO(FarisArmoush): Implement - }, - ), - ); - } - Widget _buildCommunityInfo( BuildContext context, ThemeData theme, double screenWidth) { final selectedCommunity = @@ -86,7 +74,12 @@ class CommunityStructureHeader extends StatelessWidget { ), const SizedBox(width: 2), GestureDetector( - onTap: () => _showCreateCommunityDialog(context), + onTap: () { + SpaceManagementCommunityDialogHelper.showEditDialog( + context, + selectedCommunity, + ); + }, child: SvgPicture.asset( Assets.iconEdit, width: 16, diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index 37f131b3..c2489bf6 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -39,6 +39,26 @@ class CommunityModel extends Equatable { .toList(); } + CommunityModel copyWith({ + String? uuid, + String? name, + DateTime? createdAt, + DateTime? updatedAt, + String? description, + String? externalId, + List? spaces, + }) { + return CommunityModel( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + description: description ?? this.description, + externalId: externalId ?? this.externalId, + spaces: spaces ?? this.spaces, + ); + } + @override List get props => [uuid, name, spaces]; } diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index a9af44d6..2949145a 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -1,28 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class CreateCommunityDialog extends StatelessWidget { - final void Function(CommunityModel community) onCreateCommunity; - final String? initialName; - final Widget title; - - const CreateCommunityDialog({ - super.key, - required this.onCreateCommunity, - required this.title, - this.initialName, - }); + const CreateCommunityDialog({super.key}); @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), - child: BlocListener( + create: (_) => CreateCommunityBloc( + RemoteCreateCommunityService(HTTPService()), + ), + child: BlocConsumer( listener: (context, state) { switch (state) { case CreateCommunityLoading(): @@ -39,7 +34,12 @@ class CreateCommunityDialog extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Community created successfully')), ); - onCreateCommunity.call(community); + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); break; case CreateCommunityFailure(): Navigator.of(context).pop(); @@ -48,10 +48,16 @@ class CreateCommunityDialog extends StatelessWidget { break; } }, - child: CreateCommunityDialogWidget( - title: title, - initialName: initialName, - ), + builder: (BuildContext context, CreateCommunityState state) { + return CommunityDialog( + title: const Text('Create Community'), + initialName: null, + onSubmit: (name) => context.read().add( + CreateCommunity(CreateCommunityParam(name: name)), + ), + errorMessage: state is CreateCommunityFailure ? state.message : null, + ); + }, ), ); } diff --git a/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart b/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart index 6c550673..f8f71afd 100644 --- a/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart +++ b/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -13,7 +12,7 @@ class RemoteUpdateCommunityService implements UpdateCommunityService { static const _defaultErrorMessage = 'Failed to update community'; @override - Future updateCommunity(UpdateCommunityParam param) async { + Future updateCommunity(CommunityModel param) async { try { final response = await _httpService.put( path: 'endpoint', diff --git a/lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart b/lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart deleted file mode 100644 index 69dfc4e2..00000000 --- a/lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class UpdateCommunityParam extends Equatable { - const UpdateCommunityParam({required this.name}); - - final String name; - - @override - List get props => [name]; -} diff --git a/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart b/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart index 9703fdc6..d32e79b6 100644 --- a/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart +++ b/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart @@ -1,6 +1,5 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart'; abstract class UpdateCommunityService { - Future updateCommunity(UpdateCommunityParam param); + Future updateCommunity(CommunityModel community); } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart index 4e913c22..6a4c2051 100644 --- a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -24,7 +23,7 @@ class UpdateCommunityBloc extends Bloc get props => [param]; + List get props => [communityModel]; } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart new file mode 100644 index 00000000..f42cea78 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class EditCommunityDialog extends StatelessWidget { + const EditCommunityDialog({required this.community, super.key}); + + final CommunityModel community; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => + UpdateCommunityBloc(RemoteUpdateCommunityService(HTTPService())), + child: BlocConsumer( + listener: (context, state) { + switch (state) { + case UpdateCommunityLoading(): + showDialog( + context: context, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + break; + case UpdateCommunitySuccess(:final community): + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${community.name} updated successfully'), + ), + ); + break; + case UpdateCommunityFailure(): + Navigator.of(context).pop(); + break; + default: + break; + } + }, + builder: (context, state) => CommunityDialog( + title: const Text('Edit Community'), + initialName: community.name, + errorMessage: state is UpdateCommunityFailure ? state.message : null, + onSubmit: (name) => context.read().add( + UpdateCommunity(community.copyWith(name: name)), + ), + ), + ), + ); + } +} From dd735032eab3b90333dc4f03f79a75547eb0bdbb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 11:00:01 +0300 Subject: [PATCH 074/105] Update SpaceSubSpacesDialog: Replace Text with SelectableText for title and error message, and add spacing in the content column to enhance user experience and maintainability. --- .../helpers/space_details_dialog_helper.dart | 4 ++-- .../presentation/widgets/space_details_form.dart | 14 +++++++------- .../widgets/space_sub_spaces_dialog.dart | 5 +++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index c8835716..6b95556a 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -16,7 +16,7 @@ abstract final class SpaceDetailsDialogHelper { ), child: SpaceDetailsDialog( context: context, - title: const Text('Create Space'), + title: const SelectableText('Create Space'), spaceModel: SpaceModel.empty(), onSave: print, ), @@ -36,7 +36,7 @@ abstract final class SpaceDetailsDialogHelper { ), child: SpaceDetailsDialog( context: context, - title: const Text('Edit Space'), + title: const SelectableText('Edit Space'), spaceModel: spaceModel, onSave: (space) {}, ), diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index f9a5ad64..d0495dd3 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -28,7 +28,7 @@ class SpaceDetailsForm extends StatelessWidget { create: (context) => SpaceDetailsModelBloc(initialState: space), child: BlocBuilder( buildWhen: (previous, current) => previous != current, - builder: (context, state) { + builder: (context, space) { return AlertDialog( title: title, backgroundColor: ColorsManager.whiteColors, @@ -39,7 +39,7 @@ class SpaceDetailsForm extends StatelessWidget { spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: SpaceIconPicker(iconPath: state.icon)), + Expanded(child: SpaceIconPicker(iconPath: space.icon)), Expanded( flex: 2, child: Column( @@ -47,17 +47,17 @@ class SpaceDetailsForm extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SpaceNameTextField( - initialValue: state.spaceName, - isNameFieldExist: (value) => state.subspaces.any( + initialValue: space.spaceName, + isNameFieldExist: (value) => space.subspaces.any( (subspace) => subspace.name == value, ), ), const Spacer(), SpaceSubSpacesBox( - subspaces: state.subspaces, + subspaces: space.subspaces, ), const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: state), + SpaceDetailsDevicesBox(space: space), ], ), ), @@ -66,7 +66,7 @@ class SpaceDetailsForm extends StatelessWidget { ), actions: [ SpaceDetailsActionButtons( - onSave: () => onSave(state), + onSave: () => onSave(space), onCancel: Navigator.of(context).pop, ), ], diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 4c537a8a..9e81c323 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -56,8 +56,9 @@ class _SpaceSubSpacesDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Create Sub Spaces'), + title: const SelectableText('Create Sub Spaces'), content: Column( + spacing: 12, mainAxisSize: MainAxisSize.min, children: [ SubSpacesInput( @@ -70,7 +71,7 @@ class _SpaceSubSpacesDialogState extends State { child: Visibility( key: ValueKey(_hasDuplicateNames), visible: _hasDuplicateNames, - child: const Text( + child: const SelectableText( 'Error: Duplicate subspace names are not allowed.', style: TextStyle(color: Colors.red), ), From 823d86fd80589829411347139a4739b21d0383fb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 11:17:28 +0300 Subject: [PATCH 075/105] Add loading and success snackbar helpers: Introduced `showLoadingDialog` and `showSuccessSnackBar` methods in `SpaceManagementCommunityDialogHelper` for consistent loading indicators and success messages across community dialogs. Updated `CreateCommunityDialog` and `EditCommunityDialog` to utilize these new helpers, enhancing user experience and maintainability. --- ...ce_management_community_dialog_helper.dart | 15 ++++++++++ .../presentation/bloc/communities_bloc.dart | 14 +++++++++ .../presentation/bloc/communities_event.dart | 9 ++++++ .../presentation/create_community_dialog.dart | 17 ++++------- .../bloc/update_community_state.dart | 6 ++-- .../presentation/edit_community_dialog.dart | 30 +++++++++---------- 6 files changed, 61 insertions(+), 30 deletions(-) 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 791ed10e..2446ae81 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 @@ -18,4 +18,19 @@ abstract final class SpaceManagementCommunityDialogHelper { builder: (_) => EditCommunityDialog(community: community), ); } + + static void showLoadingDialog(BuildContext context) => showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + + static void showSuccessSnackBar(BuildContext context, String message) => + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + ), + ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 9094a632..1b4f10a1 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -16,6 +16,7 @@ class CommunitiesBloc extends Bloc { on(_onLoadCommunities); on(_onLoadMoreCommunities); on(_onInsertCommunity); + on(_onCommunitiesUpdateCommunity); } final CommunitiesService _communitiesService; @@ -114,4 +115,17 @@ class CommunitiesBloc extends Bloc { ) { emit(state.copyWith(communities: [event.community, ...state.communities])); } + + void _onCommunitiesUpdateCommunity( + CommunitiesUpdateCommunity event, + Emitter emit, + ) { + emit( + state.copyWith( + communities: state.communities + .map((e) => e.uuid == event.community.uuid ? event.community : e) + .toList(), + ), + ); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index cd14fa3d..9f8d1126 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -31,3 +31,12 @@ final class InsertCommunity extends CommunitiesEvent { @override List get props => [community]; } + +final class CommunitiesUpdateCommunity extends CommunitiesEvent { + const CommunitiesUpdateCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index 2949145a..299c0078 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -1,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/shared/helpers/space_management_community_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; @@ -20,19 +21,15 @@ class CreateCommunityDialog extends StatelessWidget { child: BlocConsumer( listener: (context, state) { switch (state) { - case CreateCommunityLoading(): - showDialog( - context: context, - builder: (context) => const Center( - child: CircularProgressIndicator(), - ), - ); + case CreateCommunityLoading() || CreateCommunityInitial(): + SpaceManagementCommunityDialogHelper.showLoadingDialog(context); break; case CreateCommunitySuccess(:final community): Navigator.of(context).pop(); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Community created successfully')), + SpaceManagementCommunityDialogHelper.showSuccessSnackBar( + context, + '${community.name} community created successfully', ); context.read().add( InsertCommunity(community), @@ -44,8 +41,6 @@ class CreateCommunityDialog extends StatelessWidget { case CreateCommunityFailure(): Navigator.of(context).pop(); break; - default: - break; } }, builder: (BuildContext context, CreateCommunityState state) { diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart index 9126be0a..14ca7f6e 100644 --- a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart +++ b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart @@ -21,10 +21,10 @@ final class UpdateCommunitySuccess extends UpdateCommunityState { } final class UpdateCommunityFailure extends UpdateCommunityState { - final String message; + final String errorMessage; - const UpdateCommunityFailure(this.message); + const UpdateCommunityFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart index f42cea78..e4c13f3c 100644 --- a/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -14,39 +16,35 @@ class EditCommunityDialog extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - UpdateCommunityBloc(RemoteUpdateCommunityService(HTTPService())), + create: (_) => UpdateCommunityBloc( + RemoteUpdateCommunityService(HTTPService()), + ), child: BlocConsumer( listener: (context, state) { switch (state) { - case UpdateCommunityLoading(): - showDialog( - context: context, - builder: (context) => const Center( - child: CircularProgressIndicator(), - ), - ); + case UpdateCommunityInitial() || UpdateCommunityLoading(): + SpaceManagementCommunityDialogHelper.showLoadingDialog(context); break; case UpdateCommunitySuccess(:final community): Navigator.of(context).pop(); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${community.name} updated successfully'), - ), + SpaceManagementCommunityDialogHelper.showSuccessSnackBar( + context, + '${community.name} community updated successfully', ); + context.read().add( + CommunitiesUpdateCommunity(community), + ); break; case UpdateCommunityFailure(): Navigator.of(context).pop(); break; - default: - break; } }, builder: (context, state) => CommunityDialog( title: const Text('Edit Community'), initialName: community.name, - errorMessage: state is UpdateCommunityFailure ? state.message : null, + errorMessage: state is UpdateCommunityFailure ? state.errorMessage : null, onSubmit: (name) => context.read().add( UpdateCommunity(community.copyWith(name: name)), ), From fdea4b1cd054f2040e6c1843fa29d9baaee5ccfe Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 12:32:18 +0300 Subject: [PATCH 076/105] Refactor CommunityDialog: Extract error message display into a separate method `_buildErrorMessage` for improved readability and maintainability. This change enhances the structure of the dialog while ensuring consistent error handling. --- .../shared/widgets/community_dialog.dart | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart b/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart index f420abbd..32f6f39c 100644 --- a/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart +++ b/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart @@ -3,6 +3,7 @@ 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/presentation/create_community_name_text_field.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CommunityDialog extends StatefulWidget { final String? initialName; @@ -72,19 +73,8 @@ class _CommunityDialogState extends State { child: widget.title, ), const SizedBox(height: 18), - CreateCommunityNameTextField( - nameController: _nameController, - ), - if (widget.errorMessage != null) - Padding( - padding: const EdgeInsets.only(top: 18), - child: SelectableText( - '* ${widget.errorMessage}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - ), - ), - ), + CreateCommunityNameTextField(nameController: _nameController), + _buildErrorMessage(), const SizedBox(height: 24), _buildActionButtons(context), ], @@ -131,4 +121,19 @@ class _CommunityDialogState extends State { widget.onSubmit.call(_nameController.text.trim()); } } + + Widget _buildErrorMessage() { + return Visibility( + visible: widget.errorMessage != null, + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 18), + child: SelectableText( + '* ${widget.errorMessage}', + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + ); + } } From 826dea80546279e1300d74e2310b784d08b3f943 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 12:39:54 +0300 Subject: [PATCH 077/105] Implement community update endpoint: Refactor `RemoteUpdateCommunityService` to dynamically construct the update URL using the project UUID. This change enhances the service's flexibility and error handling by ensuring the correct endpoint is used for community updates. --- .../remote_update_community_service.dart | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart b/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart index f8f71afd..d6451a00 100644 --- a/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart +++ b/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart @@ -1,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/update_community/domain/services/update_community_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -13,14 +14,14 @@ class RemoteUpdateCommunityService implements UpdateCommunityService { @override Future updateCommunity(CommunityModel param) async { + final endpoint = await _makeUrl(param.uuid); try { - final response = await _httpService.put( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + await _httpService.put( + path: endpoint, + body: {'name': param.name}, + expectedResponseModel: (data) => null, ); - return response; + return param; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -35,4 +36,12 @@ class RemoteUpdateCommunityService implements UpdateCommunityService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl(String communityUuid) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities/$communityUuid'; + } } From 73de1e6ff9dc9e4cdad56647ba3feff257049205 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 12:52:35 +0300 Subject: [PATCH 078/105] Enhance EditCommunityDialog: Refactor to accept parent context and streamline community update handling. Introduced a new method `_onUpdateCommunitySuccess` for improved readability and maintainability. Updated `SpaceManagementCommunityDialogHelper` to pass the parent context for better state management in the community update flow. --- ...ce_management_community_dialog_helper.dart | 5 ++- .../presentation/bloc/communities_bloc.dart | 7 ++-- .../presentation/edit_community_dialog.dart | 36 +++++++++++++------ 3 files changed, 34 insertions(+), 14 deletions(-) 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 2446ae81..e1981208 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -15,7 +15,10 @@ abstract final class SpaceManagementCommunityDialogHelper { ) { showDialog( context: context, - builder: (_) => EditCommunityDialog(community: community), + builder: (_) => EditCommunityDialog( + community: community, + parentContext: context, + ), ); } 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 1b4f10a1..245448ea 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -120,11 +120,12 @@ class CommunitiesBloc extends Bloc { CommunitiesUpdateCommunity event, Emitter emit, ) { + final updatedCommunities = state.communities + .map((e) => e.uuid == event.community.uuid ? event.community : e) + .toList(); emit( state.copyWith( - communities: state.communities - .map((e) => e.uuid == event.community.uuid ? event.community : e) - .toList(), + communities: updatedCommunities, ), ); } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart index e4c13f3c..b566c02a 100644 --- a/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart @@ -4,14 +4,20 @@ import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class EditCommunityDialog extends StatelessWidget { - const EditCommunityDialog({required this.community, super.key}); + const EditCommunityDialog({ + required this.community, + required this.parentContext, + super.key, + }); final CommunityModel community; + final BuildContext parentContext; @override Widget build(BuildContext context) { @@ -26,15 +32,7 @@ class EditCommunityDialog extends StatelessWidget { SpaceManagementCommunityDialogHelper.showLoadingDialog(context); break; case UpdateCommunitySuccess(:final community): - Navigator.of(context).pop(); - Navigator.of(context).pop(); - SpaceManagementCommunityDialogHelper.showSuccessSnackBar( - context, - '${community.name} community updated successfully', - ); - context.read().add( - CommunitiesUpdateCommunity(community), - ); + _onUpdateCommunitySuccess(context, community); break; case UpdateCommunityFailure(): Navigator.of(context).pop(); @@ -52,4 +50,22 @@ class EditCommunityDialog extends StatelessWidget { ), ); } + + void _onUpdateCommunitySuccess( + BuildContext context, + CommunityModel community, + ) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + SpaceManagementCommunityDialogHelper.showSuccessSnackBar( + context, + '${community.name} community updated successfully', + ); + parentContext.read().add( + CommunitiesUpdateCommunity(community), + ); + parentContext.read().add( + SelectCommunityEvent(community: community), + ); + } } From 46a7add90de418a7f9b4c9e33b8bf763d336b8bb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 13:23:50 +0300 Subject: [PATCH 079/105] made subspaces unique and removed duplication from BE side. --- .../views/space_management_page.dart | 5 +++- .../services/unique_subspaces_decorator.dart | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.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 05768035..af6b1308 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 @@ -8,6 +8,7 @@ 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/space_details/data/services/remote_space_details_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; @@ -30,7 +31,9 @@ class SpaceManagementPage extends StatelessWidget { BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), BlocProvider( create: (context) => SpaceDetailsBloc( - RemoteSpaceDetailsService(httpService: HTTPService()), + UniqueSubspacesDecorator( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), ), ), ], diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart b/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart new file mode 100644 index 00000000..8309c545 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart @@ -0,0 +1,27 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; + +class UniqueSubspacesDecorator implements SpaceDetailsService { + final SpaceDetailsService _decoratee; + + const UniqueSubspacesDecorator(this._decoratee); + + @override + Future getSpaceDetails(LoadSpaceDetailsParam param) async { + final response = await _decoratee.getSpaceDetails(param); + + final uniqueSubspaces = {}; + + for (final subspace in response.subspaces) { + final normalizedName = subspace.name.trim().toLowerCase(); + if (!uniqueSubspaces.containsKey(normalizedName)) { + uniqueSubspaces[normalizedName] = subspace; + } + } + + return response.copyWith( + subspaces: uniqueSubspaces.values.toList(), + ); + } +} From 97e3fb68bfff825e658df659e024dd8873791ee3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 14:49:10 +0300 Subject: [PATCH 080/105] Enhance Product Model and SpaceDetailsDevicesBox: - Added 'productType' field to Product model for improved data representation. - Updated JSON parsing in Product model to handle 'prodType'. - Refactored SpaceDetailsDevicesBox to utilize productType for dynamic device icon rendering, enhancing UI clarity and maintainability. --- .../products/domain/models/product.dart | 11 ++- .../widgets/space_details_devices_box.dart | 87 ++++++++++++------- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/lib/pages/space_management_v2/modules/products/domain/models/product.dart b/lib/pages/space_management_v2/modules/products/domain/models/product.dart index cd837121..aac332d6 100644 --- a/lib/pages/space_management_v2/modules/products/domain/models/product.dart +++ b/lib/pages/space_management_v2/modules/products/domain/models/product.dart @@ -3,16 +3,18 @@ import 'package:equatable/equatable.dart'; class Product extends Equatable { final String uuid; final String name; - + final String productType; const Product({ required this.uuid, required this.name, + required this.productType, }); factory Product.fromJson(Map json) { return Product( - uuid: json['uuid'] as String, - name: json['name'] as String, + uuid: json['uuid'] as String? ?? '', + name: json['name'] as String? ?? '', + productType: json['prodType'] as String? ?? '', ); } @@ -20,9 +22,10 @@ class Product extends Equatable { return { 'uuid': uuid, 'name': name, + 'productType': productType, }; } @override - List get props => [uuid, name]; + List get props => [uuid, name, productType]; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 5b21bd61..fba905dc 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/enum/device_types.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDevicesBox extends StatelessWidget { const SpaceDetailsDevicesBox({ @@ -35,37 +38,30 @@ class SpaceDetailsDevicesBox extends StatelessWidget { spacing: 8.0, runSpacing: 8.0, children: [ - // Combine tags from spaceModel and subspaces - // ...TagHelper.groupTags([ - // ...?tags, - // ...?subspaces?.expand((subspace) => subspace.tags ?? []) - // ]).entries.map( - // (entry) => Chip( - // avatar: SizedBox( - // width: 24, - // height: 24, - // child: SvgPicture.asset( - // entry.key.icon ?? 'assets/icons/gateway.svg', - // fit: BoxFit.contain, - // ), - // ), - // label: Text( - // 'x${entry.value}', - // style: Theme.of(context) - // .textTheme - // .bodySmall - // ?.copyWith(color: ColorsManager.spaceColor), - // ), - // backgroundColor: ColorsManager.whiteColors, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(16), - // side: const BorderSide( - // color: ColorsManager.spaceColor, - // ), - // ), - // ), - // ), - + ...productAllocations.map( + (entry) => Chip( + avatar: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + _getDeviceIcon(entry.product.productType), + fit: BoxFit.contain, + ), + ), + label: Text( + entry.product.productType, + style: context.textTheme.bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor, + ), + ), + ), + ), EditChip( onTap: () {}, ), @@ -83,10 +79,37 @@ class SpaceDetailsDevicesBox extends StatelessWidget { child: ButtonContentWidget( svgAssets: Assets.addIcon, label: 'Add Devices', - // disabled: isTagsAndSubspaceModelDisabled, ), ), ); } } + + String _getDeviceIcon(String productType) => + switch (devicesTypesMap[productType]) { + DeviceType.LightBulb => Assets.lightBulb, + DeviceType.CeilingSensor => Assets.sensors, + DeviceType.AC => Assets.ac, + DeviceType.DoorLock => Assets.doorLock, + DeviceType.Curtain => Assets.curtain, + DeviceType.ThreeGang => Assets.gangSwitch, + DeviceType.Gateway => Assets.gateway, + DeviceType.OneGang => Assets.oneGang, + DeviceType.TwoGang => Assets.twoGang, + DeviceType.WH => Assets.waterHeater, + DeviceType.DoorSensor => Assets.openCloseDoor, + DeviceType.GarageDoor => Assets.openedDoor, + DeviceType.WaterLeak => Assets.waterLeakNormal, + DeviceType.Curtain2 => Assets.curtainIcon, + DeviceType.Blind => Assets.curtainIcon, + DeviceType.WallSensor => Assets.sensors, + DeviceType.DS => Assets.openCloseDoor, + DeviceType.OneTouch => Assets.gangSwitch, + DeviceType.TowTouch => Assets.gangSwitch, + DeviceType.ThreeTouch => Assets.gangSwitch, + DeviceType.NCPS => Assets.sensors, + DeviceType.PC => Assets.powerClamp, + DeviceType.Other => Assets.blackLogo, + null => Assets.blackLogo, + }; } From cebce2ce7f71b5f18684617f9f8bdb4d84e4a0f9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 14:50:24 +0300 Subject: [PATCH 081/105] Update SpaceDetailsModel: Change default icon from villa to location for improved representation of space details. --- .../space_details/domain/models/space_details_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 737d5060..5ea10e4a 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -21,7 +21,7 @@ class SpaceDetailsModel extends Equatable { factory SpaceDetailsModel.empty() => const SpaceDetailsModel( uuid: '', spaceName: '', - icon: Assets.villa, + icon: Assets.location, productAllocations: [], subspaces: [], ); From bcd0ae4a2abcdcc6fefcec82e60e77a979cc88f9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 16:44:26 +0300 Subject: [PATCH 082/105] Refactor Products Module: - Introduced ProductsBloc and updated ProductsService to remove LoadProductsParam, simplifying the product loading process. - Updated RemoteProductsService to utilize a new API endpoint for fetching products. - Adjusted ProductsEvent and ProductsState to reflect changes in the loading mechanism, enhancing maintainability and clarity in the products management flow. --- .../main_module/views/space_management_page.dart | 7 +++++++ .../data/services/remote_products_service.dart | 15 ++++++--------- .../domain/params/load_products_param.dart | 11 ----------- .../domain/services/products_service.dart | 3 +-- .../products/presentation/bloc/products_bloc.dart | 11 +++++------ .../presentation/bloc/products_event.dart | 7 +------ .../presentation/bloc/products_state.dart | 6 +++--- 7 files changed, 23 insertions(+), 37 deletions(-) delete mode 100644 lib/pages/space_management_v2/modules/products/domain/params/load_products_param.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 05768035..1672b821 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -33,6 +35,11 @@ class SpaceManagementPage extends StatelessWidget { RemoteSpaceDetailsService(httpService: HTTPService()), ), ), + BlocProvider( + create: (context) => ProductsBloc( + RemoteProductsService(HTTPService()), + ), + ), ], child: WebScaffold( appBarTitle: Text( diff --git a/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart b/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart index 6e501b44..a01419fe 100644 --- a/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart +++ b/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart @@ -1,9 +1,9 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteProductsService implements ProductsService { const RemoteProductsService(this._httpService); @@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService { static const _defaultErrorMessage = 'Failed to load devices'; @override - Future> getProducts(LoadProductsParam param) async { + Future> getProducts() async { try { final response = await _httpService.get( - path: 'devices', - queryParameters: { - 'spaceUuid': param.spaceUuid, - if (param.type != null) 'type': param.type, - if (param.status != null) 'status': param.status, - }, + path: ApiEndpoints.listProducts, expectedResponseModel: (data) { - return (data as List) + final json = data as Map; + final products = json['data'] as List; + return products .map((e) => Product.fromJson(e as Map)) .toList(); }, diff --git a/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart b/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart deleted file mode 100644 index 87194ae7..00000000 --- a/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart +++ /dev/null @@ -1,11 +0,0 @@ -class LoadProductsParam { - final String spaceUuid; - final String? type; - final String? status; - - const LoadProductsParam({ - required this.spaceUuid, - this.type, - this.status, - }); -} diff --git a/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart b/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart index 18554382..f6d41d0c 100644 --- a/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart +++ b/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart @@ -1,6 +1,5 @@ import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart'; abstract class ProductsService { - Future> getProducts(LoadProductsParam param); + Future> getProducts(); } diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart index 1ce6ae89..0e85f1c7 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -9,20 +8,20 @@ part 'products_event.dart'; part 'products_state.dart'; class ProductsBloc extends Bloc { - final ProductsService _deviceService; - - ProductsBloc(this._deviceService) : super(ProductsInitial()) { + ProductsBloc(this._productsService) : super(ProductsInitial()) { on(_onLoadProducts); } + final ProductsService _productsService; + Future _onLoadProducts( LoadProducts event, Emitter emit, ) async { emit(ProductsLoading()); try { - final devices = await _deviceService.getProducts(event.param); - emit(ProductsLoaded(devices)); + final products = await _productsService.getProducts(); + emit(ProductsLoaded(products)); } on APIException catch (e) { emit(ProductsFailure(e.message)); } catch (e) { diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart index 971b6d27..7bc14795 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart @@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable { } final class LoadProducts extends ProductsEvent { - const LoadProducts(this.param); - - final LoadProductsParam param; - - @override - List get props => [param]; + const LoadProducts(); } diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart index d5622cd3..78cee7ab 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart @@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState { } final class ProductsFailure extends ProductsState { - final String message; + final String errorMessage; - const ProductsFailure(this.message); + const ProductsFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } From e234c9f3b2a3bc5acaedde5bec7650d3523cf815 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 16:44:40 +0300 Subject: [PATCH 083/105] Enhance SpaceDetailsActionButtons: Introduced customizable button labels for save and cancel actions, improving flexibility and user experience. Updated button implementations to utilize these new labels, enhancing maintainability and adherence to Clean Architecture principles. --- .../widgets/space_details_action_buttons.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index 8518227f..8d7d2e29 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -8,10 +8,14 @@ class SpaceDetailsActionButtons extends StatelessWidget { super.key, required this.onSave, required this.onCancel, + this.saveButtonLabel = 'OK', + this.cancelButtonLabel = 'Cancel', }); final VoidCallback onCancel; final VoidCallback? onSave; + final String saveButtonLabel; + final String cancelButtonLabel; @override Widget build(BuildContext context) { @@ -27,10 +31,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { } Widget _buildCancelButton(BuildContext context) { - return CancelButton( - onPressed: onCancel, - label: 'Cancel', - ); + return CancelButton(onPressed: onCancel, label: cancelButtonLabel); } Widget _buildSaveButton() { @@ -39,7 +40,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { borderRadius: 10, backgroundColor: ColorsManager.secondaryColor, foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), + child: Text(saveButtonLabel), ); } } From bb846f797f7cad5ca811a6839d4614a5f21adbe0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 16:54:15 +0300 Subject: [PATCH 084/105] Implement Tag Assignment and Device Addition Features: - Introduced AssignTagsDialog for assigning tags to devices, enhancing user interaction and organization. - Added AddDeviceTypeWidget for adding new device types, improving the flexibility of device management. - Created ProductTypeCard and ProductTypeCardCounter for better representation and interaction with device types. - Enhanced AssignTagsTable for displaying and managing product allocations, improving maintainability and user experience. --- .../widgets/space_details_devices_box.dart | 12 +- .../widgets/add_device_type_widget.dart | 87 ++++++++ .../widgets/assign_tags_dialog.dart | 39 ++++ .../widgets/assign_tags_table.dart | 170 ++++++++++++++ .../widgets/product_tag_field.dart | 209 ++++++++++++++++++ .../widgets/product_type_card.dart | 61 +++++ .../widgets/product_type_card_counter.dart | 65 ++++++ 7 files changed, 641 insertions(+), 2 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index fba905dc..3dfd919c 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -3,6 +3,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -63,14 +64,14 @@ class SpaceDetailsDevicesBox extends StatelessWidget { ), ), EditChip( - onTap: () {}, + onTap: () => _showAssignTagsDialog(context), ), ], ), ); } else { return TextButton( - onPressed: () {}, + onPressed: () => _showAssignTagsDialog(context), style: TextButton.styleFrom( padding: EdgeInsets.zero, ), @@ -85,6 +86,13 @@ class SpaceDetailsDevicesBox extends StatelessWidget { } } + void _showAssignTagsDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AssignTagsDialog(space: space), + ); + } + String _getDeviceIcon(String productType) => switch (devicesTypesMap[productType]) { DeviceType.LightBulb => Assets.lightBulb, diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart new file mode 100644 index 00000000..aaef7a91 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AddDeviceTypeWidget extends StatelessWidget { + const AddDeviceTypeWidget({super.key}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final crossAxisCount = switch (context.screenWidth) { + > 1200 => 8, + > 800 => 5, + _ => 3, + }; + + return BlocProvider( + create: (_) => ProductsBloc(RemoteProductsService(HTTPService())) + ..add(const LoadProducts()), + child: Builder( + builder: (context) => AlertDialog( + title: const Text('Add Devices'), + backgroundColor: ColorsManager.whiteColors, + content: BlocBuilder( + builder: (context, state) { + return switch (state) { + ProductsInitial() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoading() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoaded(:final products) => SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + const SizedBox(height: 16), + Expanded( + child: GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + shrinkWrap: true, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: 0.8, + ), + itemCount: products.length, + itemBuilder: (context, index) => ProductTypeCard( + product: products[index], + ), + ), + ), + ], + ), + ), + ), + ProductsFailure(:final errorMessage) => Center( + child: Text( + errorMessage, + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + }; + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart new file mode 100644 index 00000000..706bc0a8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AssignTagsDialog extends StatelessWidget { + const AssignTagsDialog({required this.space, super.key}); + + final SpaceDetailsModel space; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Assign Tags'), + content: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: context.screenWidth * 0.6, + minWidth: context.screenWidth * 0.6, + maxHeight: context.screenHeight * 0.8, + ), + child: AssignTagsTable(productAllocations: space.productAllocations), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: () { + showDialog( + context: context, + builder: (context) => const AddDeviceTypeWidget(), + ); + }, + cancelButtonLabel: 'Add New Device', + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart new file mode 100644 index 00000000..03226b14 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/dialog_dropdown.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:uuid/uuid.dart'; + +class AssignTagsTable extends StatefulWidget { + const AssignTagsTable({ + required this.productAllocations, + super.key, + }); + + final List productAllocations; + + @override + State createState() => _AssignTagsTableState(); +} + +class _AssignTagsTableState extends State { + List _controllers = []; + + @override + void initState() { + super.initState(); + _controllers = List.generate( + widget.productAllocations.length, + (index) => TextEditingController( + text: widget.productAllocations[index].product.name, + ), + ); + } + + @override + void dispose() { + for (final controller in _controllers) { + controller.dispose(); + } + super.dispose(); + } + + DataColumn _buildDataColumn(String label) { + return DataColumn(label: Text(label, style: context.textTheme.bodyMedium)); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), + key: ValueKey(widget.productAllocations.length), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + _buildDataColumn('#'), + _buildDataColumn('Device'), + _buildDataColumn('Tag'), + _buildDataColumn('Location'), + ], + rows: widget.productAllocations.isEmpty + ? [ + DataRow( + cells: [ + DataCell( + Center( + child: Text( + 'No Devices Available', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + ), + ), + DataCell.empty, + DataCell.empty, + DataCell.empty, + ], + ), + ] + : List.generate(widget.productAllocations.length, (index) { + final productAllocation = widget.productAllocations[index]; + final controller = _controllers[index]; + + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + productAllocation.product.name, + overflow: TextOverflow.ellipsis, + )), + const SizedBox(width: 10), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1, + ), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: ColorsManager.lightGreyColor, + size: 16, + ), + onPressed: () { + // TODO: Delete the product allocation + }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + ], + ), + ), + DataCell( + Container( + alignment: Alignment.centerLeft, + width: double.infinity, + child: ProductTagField( + key: ValueKey('dropdown_${const Uuid().v4()}_$index'), + productName: productAllocation.product.uuid, + initialValue: null, + onSelected: (value) { + controller.text = value.name; + }, + items: const [ + Tag( + uuid: '', + name: 'Tag', + createdAt: '', + updatedAt: '', + ), + ], + ), + ), + ), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: const [], + // items: widget.locations, + selectedValue: productAllocation.tag.name.isEmpty + ? 'Main Space' + : productAllocation.tag.name, + onSelected: (value) {}, + )), + ), + ], + ); + }), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart new file mode 100644 index 00000000..04c82370 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ProductTagField extends StatefulWidget { + final List items; + final ValueChanged onSelected; + final Tag? initialValue; + final String productName; + + const ProductTagField({ + super.key, + required this.items, + required this.onSelected, + this.initialValue, + required this.productName, + }); + + @override + State createState() => _ProductTagFieldState(); +} + +class _ProductTagFieldState extends State { + bool _isOpen = false; + OverlayEntry? _overlayEntry; + final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + List _filteredItems = []; + + @override + void initState() { + super.initState(); + _controller.text = widget.initialValue?.name ?? ''; + + _filterItems(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final selectedTag = _filteredItems.firstWhere( + (tag) => tag.name == _controller.text, + orElse: () => Tag( + name: _controller.text, + uuid: '', + createdAt: '', + updatedAt: '', + ), + ); + widget.onSelected(selectedTag); + _closeDropdown(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + _overlayEntry = null; + _isOpen = false; + super.dispose(); + } + + void _filterItems() => setState(() => _filteredItems = widget.items); + + void _toggleDropdown() { + if (_isOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + } + + void _openDropdown() { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry!); + _isOpen = true; + } + + void _closeDropdown() { + if (_isOpen && _overlayEntry != null) { + _overlayEntry!.remove(); + _overlayEntry = null; + _isOpen = false; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.transparentColor), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextFormField( + controller: _controller, + focusNode: _focusNode, + onFieldSubmitted: (value) { + final selectedTag = _filteredItems.firstWhere( + (tag) => tag.name == value, + orElse: () => + Tag(name: value, uuid: '', createdAt: '', updatedAt: '')); + widget.onSelected(selectedTag); + _closeDropdown(); + }, + onTapOutside: (event) { + widget.onSelected(_filteredItems.firstWhere( + (tag) => tag.name == _controller.text, + orElse: () => Tag( + name: _controller.text, + uuid: '', + createdAt: '', + updatedAt: ''))); + _closeDropdown(); + }, + style: context.textTheme.bodyMedium, + decoration: const InputDecoration( + hintText: 'Enter or Select a tag', + border: InputBorder.none, + ), + ), + ), + GestureDetector( + onTap: _toggleDropdown, + child: const Icon(Icons.arrow_drop_down), + ), + ], + ), + ), + ); + } + + OverlayEntry _createOverlayEntry() { + final renderBox = context.findRenderObject()! as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (context) { + return GestureDetector( + onTap: _closeDropdown, + behavior: HitTestBehavior.translucent, + child: Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy + size.height, + width: size.width, + child: Material( + elevation: 4.0, + child: Container( + color: ColorsManager.whiteColors, + constraints: const BoxConstraints(maxHeight: 200.0), + child: StatefulBuilder( + builder: (context, setStateDropdown) { + return ListView.builder( + shrinkWrap: true, + itemCount: _filteredItems.length, + itemBuilder: (context, index) { + final tag = _filteredItems[index]; + + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 1.0, + ), + ), + ), + child: ListTile( + title: Text( + tag.name, + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textPrimaryColor, + ), + ), + onTap: () { + _controller.text = tag.name; + widget.onSelected(tag); + setState(() { + _filteredItems.remove(tag); + }); + _closeDropdown(); + }, + ), + ); + }, + ); + }, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart new file mode 100644 index 00000000..203e8502 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ProductTypeCard extends StatelessWidget { + const ProductTypeCard({super.key, required this.product}); + final Product product; + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + color: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: DeviceIconWidget( + icon: product.name, + ), + ), + _buildName(context, product.name), + CounterWidget( + isCreate: false, + initialCount: 0, + onCountChanged: (newCount) {}, + ), + const SizedBox(height: 4), + ], + ), + ), + ); + } + + Widget _buildName(BuildContext context, String name) { + return Expanded( + child: SizedBox( + height: 35, + child: Text( + name, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart new file mode 100644 index 00000000..3f9cb463 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ProductTypeCardCounter extends StatefulWidget { + const ProductTypeCardCounter({ + super.key, + required this.onIncrement, + required this.onDecrement, + required this.count, + }); + + final int count; + + final void Function() onIncrement; + final void Function() onDecrement; + + @override + State createState() => _ProductTypeCardCounterState(); +} + +class _ProductTypeCardCounterState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + _buildCounterButton( + Icons.remove, + widget.onDecrement, + ), + Text( + widget.count.toString(), + style: theme.textTheme.bodyLarge?.copyWith( + color: ColorsManager.spaceColor, + ), + ), + _buildCounterButton(Icons.add, widget.onIncrement), + ], + ), + ); + } + + Widget _buildCounterButton( + IconData icon, + VoidCallback onPressed, + ) { + return GestureDetector( + onTap: onPressed, + child: Icon( + icon, + color: ColorsManager.spaceColor.withValues(alpha: 0.3), + size: 18, + ), + ); + } +} From f0bfe085a4e145a2ade2c9f926a91494a46c4fed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 09:34:11 +0300 Subject: [PATCH 085/105] Enhance Product Model with Icon Mapping: - Added icon mapping functionality to the Product model, allowing dynamic icon retrieval based on product type. - Updated ProductTypeCard to utilize the new icon property, improving UI representation and maintainability. --- .../products/domain/models/product.dart | 37 +++++++++++++++++-- .../widgets/product_type_card.dart | 9 +++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/pages/space_management_v2/modules/products/domain/models/product.dart b/lib/pages/space_management_v2/modules/products/domain/models/product.dart index aac332d6..1a505bc5 100644 --- a/lib/pages/space_management_v2/modules/products/domain/models/product.dart +++ b/lib/pages/space_management_v2/modules/products/domain/models/product.dart @@ -1,15 +1,19 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class Product extends Equatable { - final String uuid; - final String name; - final String productType; const Product({ required this.uuid, required this.name, required this.productType, }); + final String uuid; + final String name; + final String productType; + + String get icon => _mapIconToProduct(productType); + factory Product.fromJson(Map json) { return Product( uuid: json['uuid'] as String? ?? '', @@ -26,6 +30,33 @@ class Product extends Equatable { }; } + static String _mapIconToProduct(String prodType) { + const iconMapping = { + '1G': Assets.Gang1SwitchIcon, + '1GT': Assets.oneTouchSwitch, + '2G': Assets.Gang2SwitchIcon, + '2GT': Assets.twoTouchSwitch, + '3G': Assets.Gang3SwitchIcon, + '3GT': Assets.threeTouchSwitch, + 'CUR': Assets.curtain, + 'CUR_2': Assets.curtain, + 'GD': Assets.garageDoor, + 'GW': Assets.SmartGatewayIcon, + 'DL': Assets.DoorLockIcon, + 'WL': Assets.waterLeakSensor, + 'WH': Assets.waterHeater, + 'WM': Assets.waterLeakSensor, + 'SOS': Assets.sos, + 'AC': Assets.ac, + 'CPS': Assets.presenceSensor, + 'PC': Assets.powerClamp, + 'WPS': Assets.presenceSensor, + 'DS': Assets.doorSensor + }; + + return iconMapping[prodType] ?? Assets.presenceSensor; + } + @override List get props => [uuid, name, productType]; } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart index 203e8502..d364a98a 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -6,7 +6,10 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ProductTypeCard extends StatelessWidget { - const ProductTypeCard({super.key, required this.product}); + const ProductTypeCard({ + required this.product, + super.key, + }); final Product product; @override @@ -18,7 +21,7 @@ class ProductTypeCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Padding( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(4), child: Column( spacing: 8, mainAxisAlignment: MainAxisAlignment.start, @@ -26,7 +29,7 @@ class ProductTypeCard extends StatelessWidget { children: [ Expanded( child: DeviceIconWidget( - icon: product.name, + icon: product.icon, ), ), _buildName(context, product.name), From df87e41d619cacc8aec2b6f02e2e74893de1da6f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:15:33 +0300 Subject: [PATCH 086/105] Refactor AddDeviceTypeWidget and ProductTypeCard Components: - Replaced ProductTypeCard with ProductsGrid in AddDeviceTypeWidget for improved layout and maintainability. - Converted ProductTypeCardCounter from StatefulWidget to StatelessWidget, simplifying its implementation. - Updated ProductTypeCard to accept count and increment/decrement callbacks, enhancing its reusability and interaction. - Introduced ProductsGrid to manage product display in a grid format, improving UI organization and responsiveness. --- .../widgets/add_device_type_widget.dart | 74 +++++-------------- .../widgets/product_type_card.dart | 23 +++--- .../widgets/product_type_card_counter.dart | 19 ++--- .../presentation/widgets/products_grid.dart | 57 ++++++++++++++ 4 files changed, 93 insertions(+), 80 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index aaef7a91..5cfceea8 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -12,13 +12,6 @@ class AddDeviceTypeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - final crossAxisCount = switch (context.screenWidth) { - > 1200 => 8, - > 800 => 5, - _ => 3, - }; - return BlocProvider( create: (_) => ProductsBloc(RemoteProductsService(HTTPService())) ..add(const LoadProducts()), @@ -27,57 +20,24 @@ class AddDeviceTypeWidget extends StatelessWidget { title: const Text('Add Devices'), backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( - builder: (context, state) { - return switch (state) { - ProductsInitial() => const Center( - child: CircularProgressIndicator(), - ), - ProductsLoading() => const Center( - child: CircularProgressIndicator(), - ), - ProductsLoaded(:final products) => SingleChildScrollView( - child: Container( - width: size.width * 0.9, - height: size.height * 0.65, - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - const SizedBox(height: 16), - Expanded( - child: GridView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - shrinkWrap: true, - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - mainAxisSpacing: 6, - crossAxisSpacing: 4, - childAspectRatio: 0.8, - ), - itemCount: products.length, - itemBuilder: (context, index) => ProductTypeCard( - product: products[index], - ), - ), - ), - ], - ), + builder: (context, state) => switch (state) { + ProductsInitial() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoading() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoaded(:final products) => ProductsGrid( + products: products, + ), + ProductsFailure(:final errorMessage) => Center( + child: Text( + errorMessage, + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, ), ), - ProductsFailure(:final errorMessage) => Center( - child: Text( - errorMessage, - style: context.textTheme.bodyMedium?.copyWith( - color: context.theme.colorScheme.error, - ), - ), - ), - }; + ), }, ), ), diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart index d364a98a..c3910aca 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart'; import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -8,9 +8,16 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ProductTypeCard extends StatelessWidget { const ProductTypeCard({ required this.product, + required this.count, + required this.onIncrement, + required this.onDecrement, super.key, }); + final Product product; + final int count; + final void Function() onIncrement; + final void Function() onDecrement; @override Widget build(BuildContext context) { @@ -27,16 +34,12 @@ class ProductTypeCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: DeviceIconWidget( - icon: product.icon, - ), - ), + Expanded(child: DeviceIconWidget(icon: product.icon)), _buildName(context, product.name), - CounterWidget( - isCreate: false, - initialCount: 0, - onCountChanged: (newCount) {}, + ProductTypeCardCounter( + onIncrement: onIncrement, + onDecrement: onDecrement, + count: count, ), const SizedBox(height: 4), ], diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart index 3f9cb463..605fde2f 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class ProductTypeCardCounter extends StatefulWidget { +class ProductTypeCardCounter extends StatelessWidget { const ProductTypeCardCounter({ super.key, required this.onIncrement, @@ -10,19 +11,11 @@ class ProductTypeCardCounter extends StatefulWidget { }); final int count; - final void Function() onIncrement; final void Function() onDecrement; - @override - State createState() => _ProductTypeCardCounterState(); -} - -class _ProductTypeCardCounterState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( @@ -35,15 +28,15 @@ class _ProductTypeCardCounterState extends State { children: [ _buildCounterButton( Icons.remove, - widget.onDecrement, + onDecrement, ), Text( - widget.count.toString(), - style: theme.textTheme.bodyLarge?.copyWith( + count.toString(), + style: context.textTheme.bodyLarge?.copyWith( color: ColorsManager.spaceColor, ), ), - _buildCounterButton(Icons.add, widget.onIncrement), + _buildCounterButton(Icons.add, onIncrement), ], ), ); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart new file mode 100644 index 00000000..b01fb10f --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ProductsGrid extends StatelessWidget { + const ProductsGrid({required this.products, super.key}); + + final List products; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final crossAxisCount = switch (context.screenWidth) { + > 1200 => 8, + > 800 => 5, + _ => 3, + }; + return SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + const SizedBox(height: 16), + Expanded( + child: GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: 0.8, + ), + itemCount: products.length, + itemBuilder: (context, index) => ProductTypeCard( + product: products[index], + count: 0, + onIncrement: () {}, + onDecrement: () {}, + ), + ), + ), + ], + ), + ), + ); + } +} From 47bd6ff89ed3f07e10236bb9d72c0ded7f1be9de Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:20:51 +0300 Subject: [PATCH 087/105] Refactor AddDeviceTypeWidget for Improved Error Handling and Loading States: - Extracted loading and error handling logic into separate methods for better readability and maintainability. - Updated UI to utilize centralized loading and failure widgets, enhancing user experience during data fetching. --- .../widgets/add_device_type_widget.dart | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index 5cfceea8..fa2af925 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -21,22 +21,14 @@ class AddDeviceTypeWidget extends StatelessWidget { backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( builder: (context, state) => switch (state) { - ProductsInitial() => const Center( - child: CircularProgressIndicator(), - ), - ProductsLoading() => const Center( - child: CircularProgressIndicator(), - ), + ProductsInitial() => _buildLoading(context), + ProductsLoading() => _buildLoading(context), ProductsLoaded(:final products) => ProductsGrid( products: products, ), - ProductsFailure(:final errorMessage) => Center( - child: Text( - errorMessage, - style: context.textTheme.bodyMedium?.copyWith( - color: context.theme.colorScheme.error, - ), - ), + ProductsFailure(:final errorMessage) => _buildFailure( + context, + errorMessage, ), }, ), @@ -44,4 +36,25 @@ class AddDeviceTypeWidget extends StatelessWidget { ), ); } + + Widget _buildLoading(BuildContext context) => SizedBox( + width: context.screenWidth * 0.9, + height: context.screenHeight * 0.65, + child: const Center(child: CircularProgressIndicator()), + ); + + Widget _buildFailure(BuildContext context, String errorMessage) { + return SizedBox( + width: context.screenWidth * 0.9, + height: context.screenHeight * 0.65, + child: Center( + child: SelectableText( + errorMessage, + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + ); + } } From e917225c3d1d170f4c5a7d24783ae38f638adea3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:36:42 +0300 Subject: [PATCH 088/105] Refactor Widgets for Improved UI Consistency and Usability: - Replaced Text with SelectableText in AddDeviceTypeWidget and AssignTagsTable for better text selection and accessibility. - Simplified onCancel action in AssignTagsDialog for improved readability. - Enhanced ProductsGrid layout by removing unnecessary Column widget, streamlining the widget structure for better performance and maintainability. --- .../widgets/add_device_type_widget.dart | 2 +- .../widgets/assign_tags_dialog.dart | 10 ++--- .../widgets/assign_tags_table.dart | 5 ++- .../presentation/widgets/products_grid.dart | 43 ++++++++----------- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index fa2af925..673efe7e 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -17,7 +17,7 @@ class AddDeviceTypeWidget extends StatelessWidget { ..add(const LoadProducts()), child: Builder( builder: (context) => AlertDialog( - title: const Text('Add Devices'), + title: const SelectableText('Add Devices'), backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( builder: (context, state) => switch (state) { diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart index 706bc0a8..25f08d30 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -25,12 +25,10 @@ class AssignTagsDialog extends StatelessWidget { actions: [ SpaceDetailsActionButtons( onSave: () {}, - onCancel: () { - showDialog( - context: context, - builder: (context) => const AddDeviceTypeWidget(), - ); - }, + onCancel: () => showDialog( + context: context, + builder: (context) => const AddDeviceTypeWidget(), + ), cancelButtonLabel: 'Add New Device', ), ], diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart index 03226b14..196b4102 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -42,7 +42,8 @@ class _AssignTagsTableState extends State { } DataColumn _buildDataColumn(String label) { - return DataColumn(label: Text(label, style: context.textTheme.bodyMedium)); + return DataColumn( + label: SelectableText(label, style: context.textTheme.bodyMedium)); } @override @@ -69,7 +70,7 @@ class _AssignTagsTableState extends State { cells: [ DataCell( Center( - child: Text( + child: SelectableText( 'No Devices Available', style: context.textTheme.bodyMedium?.copyWith( color: ColorsManager.lightGrayColor, diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart index b01fb10f..7fe5ec26 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart @@ -25,31 +25,26 @@ class ProductsGrid extends StatelessWidget { color: ColorsManager.textFieldGreyColor, borderRadius: BorderRadius.circular(8), ), - child: Column( - children: [ - const SizedBox(height: 16), - Expanded( - child: GridView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - mainAxisSpacing: 6, - crossAxisSpacing: 4, - childAspectRatio: 0.8, - ), - itemCount: products.length, - itemBuilder: (context, index) => ProductTypeCard( - product: products[index], - count: 0, - onIncrement: () {}, - onDecrement: () {}, - ), - ), + child: Expanded( + child: GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), - ], + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: 0.8, + ), + itemCount: products.length, + itemBuilder: (context, index) => ProductTypeCard( + product: products[index], + count: 0, + onIncrement: () {}, + onDecrement: () {}, + ), + ), ), ), ); From e523a8391225bbfe3645d3c5846f895868dd920d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:50:03 +0300 Subject: [PATCH 089/105] Refactor Tags Service and Bloc for Improved Data Handling: - Updated RemoteTagsService to remove LoadTagsParam and fetch project UUID internally, enhancing encapsulation and reducing parameter dependency. - Modified TagsService interface to reflect the new loading method signature. - Adjusted TagsBloc to align with the updated service method, simplifying the loading process. - Enhanced AssignTagsTable and AddDeviceTypeWidget to utilize the new data flow, improving maintainability and user experience. --- .../data/services/remote_tags_service.dart | 22 +- .../tags/domain/services/tags_service.dart | 3 +- .../tags/presentation/bloc/tags_bloc.dart | 3 +- .../tags/presentation/bloc/tags_event.dart | 7 +- .../widgets/add_device_type_widget.dart | 8 + .../widgets/assign_tags_table.dart | 246 ++++++++++-------- 6 files changed, 154 insertions(+), 135 deletions(-) diff --git a/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart b/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart index b5545bd3..76ceec71 100644 --- a/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart +++ b/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart @@ -1,10 +1,9 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; -import 'package:syncrow_web/utils/constants/api_const.dart'; final class RemoteTagsService implements TagsService { const RemoteTagsService(this._httpService); @@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService { static const _defaultErrorMessage = 'Failed to load tags'; @override - Future> loadTags(LoadTagsParam param) async { - if (param.projectUuid == null) { - throw Exception('Project UUID is required'); - } - + Future> loadTags() async { try { final response = await _httpService.get( - path: ApiEndpoints.listTags.replaceAll( - '{projectUuid}', - param.projectUuid!, - ), + path: await _makeUrl(), expectedResponseModel: (json) { final result = json as Map; final data = result['data'] as List; @@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is required'); + } + return '/projects/$projectUuid/tags'; + } } diff --git a/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart b/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart index ae097020..cf36527a 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart @@ -1,6 +1,5 @@ import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart'; abstract interface class TagsService { - Future> loadTags(LoadTagsParam param); + Future> loadTags(); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart index e51884cb..b81fcb76 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -21,7 +20,7 @@ class TagsBloc extends Bloc { ) async { emit(TagsLoading()); try { - final tags = await _tagsService.loadTags(event.param); + final tags = await _tagsService.loadTags(); emit(TagsLoaded(tags)); } on APIException catch (e) { emit(TagsFailure(e.message)); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart index 99134cab..8965b7b0 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart @@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable { } class LoadTags extends TagsEvent { - final LoadTagsParam param; - - const LoadTags(this.param); - - @override - List get props => [param]; + const LoadTags(); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index 673efe7e..f77f10b5 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -32,6 +33,13 @@ class AddDeviceTypeWidget extends StatelessWidget { ), }, ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: Navigator.of(context).pop, + saveButtonLabel: 'Next', + ), + ], ), ), ); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart index 196b4102..3d92109d 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/dialog_dropdown.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:uuid/uuid.dart'; @@ -48,123 +51,138 @@ class _AssignTagsTableState extends State { @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(20), - child: DataTable( - headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), - key: ValueKey(widget.productAllocations.length), - border: TableBorder.all( - color: ColorsManager.dataHeaderGrey, - width: 1, - borderRadius: BorderRadius.circular(20), - ), - columns: [ - _buildDataColumn('#'), - _buildDataColumn('Device'), - _buildDataColumn('Tag'), - _buildDataColumn('Location'), - ], - rows: widget.productAllocations.isEmpty - ? [ - DataRow( - cells: [ - DataCell( - Center( - child: SelectableText( - 'No Devices Available', - style: context.textTheme.bodyMedium?.copyWith( - color: ColorsManager.lightGrayColor, - ), - ), - ), - ), - DataCell.empty, - DataCell.empty, - DataCell.empty, + return BlocProvider( + create: (BuildContext context) => TagsBloc( + RemoteTagsService(HTTPService()), + )..add(const LoadTags()), + child: BlocBuilder( + builder: (context, state) { + return switch (state) { + TagsLoading() || TagsInitial() => const Center( + child: CircularProgressIndicator(), + ), + TagsFailure(:final message) => Center( + child: Text(message), + ), + TagsLoaded(:final tags) => ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + ColorsManager.dataHeaderGrey, + ), + key: ValueKey(widget.productAllocations.length), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + _buildDataColumn('#'), + _buildDataColumn('Device'), + _buildDataColumn('Tag'), + _buildDataColumn('Location'), ], - ), - ] - : List.generate(widget.productAllocations.length, (index) { - final productAllocation = widget.productAllocations[index]; - final controller = _controllers[index]; + rows: widget.productAllocations.isEmpty + ? [ + DataRow( + cells: [ + DataCell( + Center( + child: SelectableText( + 'No Devices Available', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + ), + ), + DataCell.empty, + DataCell.empty, + DataCell.empty, + ], + ), + ] + : List.generate(widget.productAllocations.length, (index) { + final productAllocation = widget.productAllocations[index]; + final controller = _controllers[index]; - return DataRow( - cells: [ - DataCell(Text((index + 1).toString())), - DataCell( - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - productAllocation.product.name, - overflow: TextOverflow.ellipsis, - )), - const SizedBox(width: 10), - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1, + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + productAllocation.product.name, + overflow: TextOverflow.ellipsis, + )), + const SizedBox(width: 10), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1, + ), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: ColorsManager.lightGreyColor, + size: 16, + ), + onPressed: () { + // TODO: Delete the product allocation + }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + ], + ), ), - ), - child: IconButton( - icon: const Icon( - Icons.close, - color: ColorsManager.lightGreyColor, - size: 16, + DataCell( + Container( + alignment: Alignment.centerLeft, + width: double.infinity, + child: ProductTagField( + key: ValueKey( + 'dropdown_${const Uuid().v4()}_$index'), + productName: productAllocation.product.uuid, + initialValue: null, + onSelected: (value) { + controller.text = value.name; + }, + items: tags, + ), + ), ), - onPressed: () { - // TODO: Delete the product allocation - }, - tooltip: 'Delete Tag', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ), - ], - ), - ), - DataCell( - Container( - alignment: Alignment.centerLeft, - width: double.infinity, - child: ProductTagField( - key: ValueKey('dropdown_${const Uuid().v4()}_$index'), - productName: productAllocation.product.uuid, - initialValue: null, - onSelected: (value) { - controller.text = value.name; - }, - items: const [ - Tag( - uuid: '', - name: 'Tag', - createdAt: '', - updatedAt: '', - ), - ], - ), - ), - ), - DataCell( - SizedBox( - width: double.infinity, - child: DialogDropdown( - items: const [], - // items: widget.locations, - selectedValue: productAllocation.tag.name.isEmpty - ? 'Main Space' - : productAllocation.tag.name, - onSelected: (value) {}, - )), - ), - ], - ); - }), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: const [], + // items: widget.locations, + selectedValue: + productAllocation.tag.name.isEmpty + ? 'Main Space' + : productAllocation.tag.name, + onSelected: (value) {}, + )), + ), + ], + ); + }), + ), + ), + _ => const SizedBox.shrink(), + }; + }, ), ); } From dc7064d142750e027c0aaa6618fd7fefda0429be Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:25:37 +0300 Subject: [PATCH 090/105] Add Factory Method for Empty Tag Instance in Tag Model. --- .../modules/tags/domain/models/tag.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart index 1044d888..370bdf47 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart @@ -13,6 +13,13 @@ class Tag extends Equatable { required this.updatedAt, }); + factory Tag.empty() => const Tag( + uuid: '', + name: '', + createdAt: '', + updatedAt: '', + ); + factory Tag.fromJson(Map json) { return Tag( uuid: json['uuid'] as String, From 7f0484eec6bb6c5cb351a5ee3f555f014d8409ce Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:18 +0300 Subject: [PATCH 091/105] Add UpdateSpaceDetails Event. --- .../space_details_model_bloc.dart | 8 ++++++++ .../space_details_model_event.dart | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart index 21a72557..15a22fda 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -13,6 +13,7 @@ class SpaceDetailsModelBloc extends Bloc(_onUpdateSpaceDetailsSubspaces); on( _onUpdateSpaceDetailsProductAllocations); + on(_onUpdateSpaceDetails); } void _onUpdateSpaceDetailsIcon( @@ -42,4 +43,11 @@ class SpaceDetailsModelBloc extends Bloc emit, + ) { + emit(event.space); + } } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart index d3e04bb9..abf9cd98 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart @@ -42,3 +42,12 @@ final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent @override List get props => [productAllocations]; } + +final class UpdateSpaceDetails extends SpaceDetailsModelEvent { + const UpdateSpaceDetails(this.space); + + final SpaceDetailsModel space; + + @override + List get props => [space]; +} From ddfd4ee153506f0b111f1d4ad77819ac592dfd92 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:39 +0300 Subject: [PATCH 092/105] removed print statement. --- .../presentation/helpers/space_details_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 6b95556a..de2c40f0 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -18,7 +18,7 @@ abstract final class SpaceDetailsDialogHelper { context: context, title: const SelectableText('Create Space'), spaceModel: SpaceModel.empty(), - onSave: print, + onSave: (space) {}, ), ), ); From 31019602016eb76ec1dd65fca3ed02e15fc19b44 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:59 +0300 Subject: [PATCH 093/105] Enhance Space Management Features with Tag Assignment Improvements: - Introduced UUID for ProductAllocation to ensure unique identification. - Refactored AssignTagsDialog to manage tag assignments and validation more effectively, including error handling for empty tags and duplicate tag usage. - Updated AssignTagsTable to support dynamic product allocation management and improved UI interactions. - Enhanced AddDeviceTypeWidget to maintain selected products and handle increment/decrement actions, improving user experience during device type selection. - Added AssignTagsErrorMessages widget for better error visibility in tag assignment process. --- .../domain/models/space_details_model.dart | 13 +- .../widgets/space_details_devices_box.dart | 52 +++-- .../widgets/add_device_type_widget.dart | 40 +++- .../widgets/assign_tags_dialog.dart | 212 +++++++++++++++++- .../widgets/assign_tags_error_messages.dart | 29 +++ .../widgets/assign_tags_table.dart | 121 +++++----- .../widgets/product_tag_field.dart | 129 +++++------ .../presentation/widgets/products_grid.dart | 50 +++-- 8 files changed, 463 insertions(+), 183 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 5ea10e4a..b3e436b1 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:uuid/uuid.dart'; class SpaceDetailsModel extends Equatable { final String uuid; @@ -31,8 +32,8 @@ class SpaceDetailsModel extends Equatable { spaceName: json['spaceName'] as String, icon: json['icon'] as String, productAllocations: (json['productAllocations'] as List) - .map((e) => ProductAllocation.fromJson(e as Map)) - .toList(), + .map((e) => ProductAllocation.fromJson(e as Map)) + .toList(), subspaces: (json['subspaces'] as List) .map((e) => Subspace.fromJson(e as Map)) .toList(), @@ -70,16 +71,19 @@ class SpaceDetailsModel extends Equatable { } class ProductAllocation extends Equatable { + final String uuid; final Product product; final Tag tag; const ProductAllocation({ + required this.uuid, required this.product, required this.tag, }); factory ProductAllocation.fromJson(Map json) { return ProductAllocation( + uuid: json['uuid'] as String? ?? const Uuid().v4(), product: Product.fromJson(json['product'] as Map), tag: Tag.fromJson(json['tag'] as Map), ); @@ -87,23 +91,26 @@ class ProductAllocation extends Equatable { Map toJson() { return { + 'uuid': uuid, 'product': product.toJson(), 'tag': tag.toJson(), }; } ProductAllocation copyWith({ + String? uuid, Product? product, Tag? tag, }) { return ProductAllocation( + uuid: uuid ?? this.uuid, product: product ?? this.product, tag: tag ?? this.tag, ); } @override - List get props => [product, tag]; + List get props => [uuid, product, tag]; } class Subspace extends Equatable { diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 3dfd919c..cf65dbb6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -19,11 +21,18 @@ class SpaceDetailsDevicesBox extends StatelessWidget { @override Widget build(BuildContext context) { - final productAllocations = space.productAllocations; - final subspaces = space.subspaces; - final isAnySubspaceHasProductAllocations = - subspaces.any((subspace) => subspace.productAllocations.isNotEmpty); - if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) { + final allAllocations = [ + ...space.productAllocations, + ...space.subspaces.expand((s) => s.productAllocations), + ]; + + if (allAllocations.isNotEmpty) { + final productCounts = {}; + for (final allocation in allAllocations) { + final productType = allocation.product.productType; + productCounts[productType] = (productCounts[productType] ?? 0) + 1; + } + return Container( width: double.infinity, padding: const EdgeInsets.all(8), @@ -39,20 +48,23 @@ class SpaceDetailsDevicesBox extends StatelessWidget { spacing: 8.0, runSpacing: 8.0, children: [ - ...productAllocations.map( - (entry) => Chip( + ...productCounts.entries.map((entry) { + final productType = entry.key; + final count = entry.value; + return Chip( avatar: SizedBox( width: 24, height: 24, child: SvgPicture.asset( - _getDeviceIcon(entry.product.productType), + _getDeviceIcon(productType), fit: BoxFit.contain, ), ), label: Text( - entry.product.productType, - style: context.textTheme.bodySmall - ?.copyWith(color: ColorsManager.spaceColor), + 'x$count', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.spaceColor, + ), ), backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( @@ -61,11 +73,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget { color: ColorsManager.spaceColor, ), ), - ), - ), - EditChip( - onTap: () => _showAssignTagsDialog(context), - ), + ); + }), + EditChip(onTap: () => _showAssignTagsDialog(context)), ], ), ); @@ -87,10 +97,16 @@ class SpaceDetailsDevicesBox extends StatelessWidget { } void _showAssignTagsDialog(BuildContext context) { - showDialog( + showDialog( context: context, builder: (context) => AssignTagsDialog(space: space), - ); + ).then((resultSpace) { + if (resultSpace != null) { + if (context.mounted) { + context.read().add(UpdateSpaceDetails(resultSpace)); + } + } + }); } String _getDeviceIcon(String productType) => diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index f77f10b5..4c9990ae 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart'; @@ -8,9 +9,33 @@ import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class AddDeviceTypeWidget extends StatelessWidget { +class AddDeviceTypeWidget extends StatefulWidget { const AddDeviceTypeWidget({super.key}); + @override + State createState() => _AddDeviceTypeWidgetState(); +} + +class _AddDeviceTypeWidgetState extends State { + final Map _selectedProducts = {}; + + void _onIncrement(Product product) { + setState(() { + _selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1; + }); + } + + void _onDecrement(Product product) { + setState(() { + if ((_selectedProducts[product] ?? 0) > 0) { + _selectedProducts[product] = _selectedProducts[product]! - 1; + if (_selectedProducts[product] == 0) { + _selectedProducts.remove(product); + } + } + }); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -22,10 +47,12 @@ class AddDeviceTypeWidget extends StatelessWidget { backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( builder: (context, state) => switch (state) { - ProductsInitial() => _buildLoading(context), - ProductsLoading() => _buildLoading(context), + ProductsInitial() || ProductsLoading() => _buildLoading(context), ProductsLoaded(:final products) => ProductsGrid( products: products, + selectedProducts: _selectedProducts, + onIncrement: _onIncrement, + onDecrement: _onDecrement, ), ProductsFailure(:final errorMessage) => _buildFailure( context, @@ -35,7 +62,12 @@ class AddDeviceTypeWidget extends StatelessWidget { ), actions: [ SpaceDetailsActionButtons( - onSave: () {}, + onSave: () { + final result = _selectedProducts.entries + .expand((entry) => List.generate(entry.value, (_) => entry.key)) + .toList(); + Navigator.of(context).pop(result); + }, onCancel: Navigator.of(context).pop, saveButtonLabel: 'Next', ), diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart index 25f08d30..3cab4abe 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -1,36 +1,230 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:uuid/uuid.dart'; -class AssignTagsDialog extends StatelessWidget { +class AssignTagsDialog extends StatefulWidget { const AssignTagsDialog({required this.space, super.key}); final SpaceDetailsModel space; + @override + State createState() => _AssignTagsDialogState(); +} + +class _AssignTagsDialogState extends State { + late SpaceDetailsModel _space; + final Map _validationErrors = {}; + + @override + void initState() { + super.initState(); + _space = widget.space.copyWith( + productAllocations: + widget.space.productAllocations.map((e) => e.copyWith()).toList(), + subspaces: widget.space.subspaces + .map( + (s) => s.copyWith( + productAllocations: + s.productAllocations.map((e) => e.copyWith()).toList(), + ), + ) + .toList(), + ); + _validateAllTags(); + } + + void _validateAllTags() { + final newErrors = {}; + final allAllocations = [ + ..._space.productAllocations, + ..._space.subspaces.expand((s) => s.productAllocations), + ]; + + final allocationsByProductType = >{}; + for (final allocation in allAllocations) { + (allocationsByProductType[allocation.product.productType] ??= []) + .add(allocation); + } + + for (final productType in allocationsByProductType.keys) { + final allocations = allocationsByProductType[productType]!; + final tagCounts = {}; + + for (final allocation in allocations) { + final tagName = allocation.tag.name.trim().toLowerCase(); + if (tagName.isEmpty) { + newErrors[allocation.uuid] = + 'Tag for ${allocation.product.name} cannot be empty.'; + } else { + tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1; + } + } + + for (final allocation in allocations) { + final tagName = allocation.tag.name.trim().toLowerCase(); + if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) { + newErrors[allocation.uuid] = + 'Tag "${allocation.tag.name}" is used by multiple $productType devices.'; + } + } + } + + setState(() { + _validationErrors + ..clear() + ..addAll(newErrors); + }); + } + + void _handleTagChange(String allocationUuid, Tag newTag) { + setState(() { + var index = + _space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + final allocation = _space.productAllocations[index]; + _space.productAllocations[index] = allocation.copyWith(tag: newTag); + } else { + for (final subspace in _space.subspaces) { + index = subspace.productAllocations + .indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + final allocation = subspace.productAllocations[index]; + subspace.productAllocations[index] = allocation.copyWith(tag: newTag); + break; + } + } + } + }); + _validateAllTags(); + } + + void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) { + setState(() { + ProductAllocation? allocationToMove; + + var index = + _space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + allocationToMove = _space.productAllocations.removeAt(index); + } else { + for (final subspace in _space.subspaces) { + index = subspace.productAllocations + .indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + allocationToMove = subspace.productAllocations.removeAt(index); + break; + } + } + } + + if (allocationToMove == null) return; + + if (newSubspaceUuid == null) { + _space.productAllocations.add(allocationToMove); + } else { + _space.subspaces + .firstWhere((s) => s.uuid == newSubspaceUuid) + .productAllocations + .add(allocationToMove); + } + }); + } + + void _handleProductDelete(String allocationUuid) { + setState(() { + _space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid); + + for (final subspace in _space.subspaces) { + subspace.productAllocations.removeWhere( + (pa) => pa.uuid == allocationUuid, + ); + } + }); + _validateAllTags(); + } + @override Widget build(BuildContext context) { + final allProductAllocations = [ + ..._space.productAllocations, + ..._space.subspaces.expand((s) => s.productAllocations), + ]; + + final productLocations = {}; + for (final pa in _space.productAllocations) { + productLocations[pa.uuid] = null; + } + for (final subspace in _space.subspaces) { + for (final pa in subspace.productAllocations) { + productLocations[pa.uuid] = subspace.uuid; + } + } + + final hasErrors = _validationErrors.isNotEmpty; + return AlertDialog( - title: const Text('Assign Tags'), + title: const SelectableText('Assign Tags'), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: context.screenWidth * 0.6, minWidth: context.screenWidth * 0.6, maxHeight: context.screenHeight * 0.8, ), - child: AssignTagsTable(productAllocations: space.productAllocations), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: AssignTagsTable( + productAllocations: allProductAllocations, + subspaces: _space.subspaces, + productLocations: productLocations, + onTagSelected: _handleTagChange, + onLocationSelected: _handleLocationChange, + onProductDeleted: _handleProductDelete, + ), + ), + if (hasErrors) + AssignTagsErrorMessages( + errorMessages: _validationErrors.values.toSet().toList(), + ), + ], + ), ), actions: [ SpaceDetailsActionButtons( - onSave: () {}, - onCancel: () => showDialog( - context: context, - builder: (context) => const AddDeviceTypeWidget(), - ), + onSave: hasErrors ? null : () => Navigator.of(context).pop(_space), + onCancel: () async { + final newProducts = await showDialog>( + context: context, + builder: (context) => const AddDeviceTypeWidget(), + ); + + if (newProducts == null || newProducts.isEmpty) return; + + setState(() { + for (final product in newProducts) { + _space.productAllocations.add( + ProductAllocation( + uuid: const Uuid().v4(), + product: product, + tag: Tag.empty(), + ), + ); + } + }); + _validateAllTags(); + }, cancelButtonLabel: 'Add New Device', - ), + ) ], ); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart new file mode 100644 index 00000000..9b0fd478 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AssignTagsErrorMessages extends StatelessWidget { + const AssignTagsErrorMessages({super.key, required this.errorMessages}); + + final List errorMessages; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: errorMessages + .map( + (error) => Text( + '- $error', + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart index 3d92109d..6e7e2097 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -3,50 +3,35 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/dialog_dropdown.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -import 'package:uuid/uuid.dart'; -class AssignTagsTable extends StatefulWidget { +class AssignTagsTable extends StatelessWidget { const AssignTagsTable({ required this.productAllocations, + required this.subspaces, + required this.productLocations, + required this.onTagSelected, + required this.onLocationSelected, + required this.onProductDeleted, super.key, }); final List productAllocations; + final List subspaces; + final Map productLocations; + final void Function(String, Tag) onTagSelected; + final void Function(String, String?) onLocationSelected; + final void Function(String) onProductDeleted; - @override - State createState() => _AssignTagsTableState(); -} - -class _AssignTagsTableState extends State { - List _controllers = []; - - @override - void initState() { - super.initState(); - _controllers = List.generate( - widget.productAllocations.length, - (index) => TextEditingController( - text: widget.productAllocations[index].product.name, - ), - ); - } - - @override - void dispose() { - for (final controller in _controllers) { - controller.dispose(); - } - super.dispose(); - } - - DataColumn _buildDataColumn(String label) { + DataColumn _buildDataColumn(BuildContext context, String label) { return DataColumn( - label: SelectableText(label, style: context.textTheme.bodyMedium)); + label: SelectableText(label, style: context.textTheme.bodyMedium), + ); } @override @@ -70,19 +55,19 @@ class _AssignTagsTableState extends State { headingRowColor: WidgetStateProperty.all( ColorsManager.dataHeaderGrey, ), - key: ValueKey(widget.productAllocations.length), + key: ValueKey(productAllocations.length), border: TableBorder.all( color: ColorsManager.dataHeaderGrey, width: 1, borderRadius: BorderRadius.circular(20), ), columns: [ - _buildDataColumn('#'), - _buildDataColumn('Device'), - _buildDataColumn('Tag'), - _buildDataColumn('Location'), + _buildDataColumn(context, '#'), + _buildDataColumn(context, 'Device'), + _buildDataColumn(context, 'Tag'), + _buildDataColumn(context, 'Location'), ], - rows: widget.productAllocations.isEmpty + rows: productAllocations.isEmpty ? [ DataRow( cells: [ @@ -102,11 +87,33 @@ class _AssignTagsTableState extends State { ], ), ] - : List.generate(widget.productAllocations.length, (index) { - final productAllocation = widget.productAllocations[index]; - final controller = _controllers[index]; + : List.generate(productAllocations.length, (index) { + final productAllocation = productAllocations[index]; + final allocationUuid = productAllocation.uuid; + + final availableTags = tags + .where( + (tag) => + !productAllocations + .where((p) => + p.product.productType == + productAllocation.product.productType) + .map((p) => p.tag.name.toLowerCase()) + .contains(tag.name.toLowerCase()) || + tag.uuid == productAllocation.tag.uuid, + ) + .toList(); + + final currentLocationUuid = + productLocations[allocationUuid]; + final currentLocationName = currentLocationUuid == null + ? 'Main Space' + : subspaces + .firstWhere((s) => s.uuid == currentLocationUuid) + .name; return DataRow( + key: ValueKey(allocationUuid), cells: [ DataCell(Text((index + 1).toString())), DataCell( @@ -136,7 +143,7 @@ class _AssignTagsTableState extends State { size: 16, ), onPressed: () { - // TODO: Delete the product allocation + onProductDeleted(allocationUuid); }, tooltip: 'Delete Tag', padding: EdgeInsets.zero, @@ -151,14 +158,13 @@ class _AssignTagsTableState extends State { alignment: Alignment.centerLeft, width: double.infinity, child: ProductTagField( - key: ValueKey( - 'dropdown_${const Uuid().v4()}_$index'), + key: ValueKey('dropdown_$allocationUuid'), productName: productAllocation.product.uuid, - initialValue: null, - onSelected: (value) { - controller.text = value.name; + initialValue: productAllocation.tag, + onSelected: (newTag) { + onTagSelected(allocationUuid, newTag); }, - items: tags, + items: availableTags, ), ), ), @@ -166,13 +172,22 @@ class _AssignTagsTableState extends State { SizedBox( width: double.infinity, child: DialogDropdown( - items: const [], - // items: widget.locations, - selectedValue: - productAllocation.tag.name.isEmpty - ? 'Main Space' - : productAllocation.tag.name, - onSelected: (value) {}, + items: [ + 'Main Space', + ...subspaces.map((s) => s.name) + ], + selectedValue: currentLocationName, + onSelected: (newLocationName) { + final newSubspaceUuid = newLocationName == + 'Main Space' + ? null + : subspaces + .firstWhere( + (s) => s.name == newLocationName) + .uuid; + onLocationSelected( + allocationUuid, newSubspaceUuid); + }, )), ), ], diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart index 04c82370..8bbf379d 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart @@ -26,42 +26,44 @@ class _ProductTagFieldState extends State { OverlayEntry? _overlayEntry; final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); - List _filteredItems = []; @override void initState() { super.initState(); _controller.text = widget.initialValue?.name ?? ''; - - _filterItems(); - - _focusNode.addListener(() { - if (!_focusNode.hasFocus) { - final selectedTag = _filteredItems.firstWhere( - (tag) => tag.name == _controller.text, - orElse: () => Tag( - name: _controller.text, - uuid: '', - createdAt: '', - updatedAt: '', - ), - ); - widget.onSelected(selectedTag); - _closeDropdown(); - } - }); + _focusNode.addListener(_handleFocusChange); } @override void dispose() { + _focusNode.removeListener(_handleFocusChange); _controller.dispose(); _focusNode.dispose(); + _overlayEntry?.remove(); _overlayEntry = null; - _isOpen = false; super.dispose(); } - void _filterItems() => setState(() => _filteredItems = widget.items); + void _handleFocusChange() { + if (!_focusNode.hasFocus) { + _submit(_controller.text); + } + } + + void _submit(String value) { + final lowerCaseValue = value.toLowerCase(); + final selectedTag = widget.items.firstWhere( + (tag) => tag.name.toLowerCase() == lowerCaseValue, + orElse: () => Tag( + name: value, + uuid: '', + createdAt: '', + updatedAt: '', + ), + ); + widget.onSelected(selectedTag); + _closeDropdown(); + } void _toggleDropdown() { if (_isOpen) { @@ -74,14 +76,14 @@ class _ProductTagFieldState extends State { void _openDropdown() { _overlayEntry = _createOverlayEntry(); Overlay.of(context).insert(_overlayEntry!); - _isOpen = true; + setState(() => _isOpen = true); } void _closeDropdown() { - if (_isOpen && _overlayEntry != null) { - _overlayEntry!.remove(); + if (_isOpen) { + _overlayEntry?.remove(); _overlayEntry = null; - _isOpen = false; + setState(() => _isOpen = false); } } @@ -103,24 +105,7 @@ class _ProductTagFieldState extends State { child: TextFormField( controller: _controller, focusNode: _focusNode, - onFieldSubmitted: (value) { - final selectedTag = _filteredItems.firstWhere( - (tag) => tag.name == value, - orElse: () => - Tag(name: value, uuid: '', createdAt: '', updatedAt: '')); - widget.onSelected(selectedTag); - _closeDropdown(); - }, - onTapOutside: (event) { - widget.onSelected(_filteredItems.firstWhere( - (tag) => tag.name == _controller.text, - orElse: () => Tag( - name: _controller.text, - uuid: '', - createdAt: '', - updatedAt: ''))); - _closeDropdown(); - }, + onFieldSubmitted: _submit, style: context.textTheme.bodyMedium, decoration: const InputDecoration( hintText: 'Enter or Select a tag', @@ -159,41 +144,33 @@ class _ProductTagFieldState extends State { child: Container( color: ColorsManager.whiteColors, constraints: const BoxConstraints(maxHeight: 200.0), - child: StatefulBuilder( - builder: (context, setStateDropdown) { - return ListView.builder( - shrinkWrap: true, - itemCount: _filteredItems.length, - itemBuilder: (context, index) { - final tag = _filteredItems[index]; - - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ColorsManager.lightGrayBorderColor, - width: 1.0, - ), - ), + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.items.length, + itemBuilder: (context, index) { + final tag = widget.items[index]; + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 1.0, ), - child: ListTile( - title: Text( - tag.name, - style: context.textTheme.bodyMedium?.copyWith( - color: ColorsManager.textPrimaryColor, - ), - ), - onTap: () { - _controller.text = tag.name; - widget.onSelected(tag); - setState(() { - _filteredItems.remove(tag); - }); - _closeDropdown(); - }, + ), + ), + child: ListTile( + title: Text( + tag.name, + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textPrimaryColor, ), - ); - }, + ), + onTap: () { + _controller.text = tag.name; + _submit(tag.name); + _closeDropdown(); + }, + ), ); }, ), diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart index 7fe5ec26..53e59bde 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart @@ -5,9 +5,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ProductsGrid extends StatelessWidget { - const ProductsGrid({required this.products, super.key}); + const ProductsGrid({ + required this.products, + required this.selectedProducts, + required this.onIncrement, + required this.onDecrement, + super.key, + }); final List products; + final Map selectedProducts; + final void Function(Product) onIncrement; + final void Function(Product) onDecrement; @override Widget build(BuildContext context) { @@ -25,26 +34,27 @@ class ProductsGrid extends StatelessWidget { color: ColorsManager.textFieldGreyColor, borderRadius: BorderRadius.circular(8), ), - child: Expanded( - child: GridView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - mainAxisSpacing: 6, - crossAxisSpacing: 4, - childAspectRatio: 0.8, - ), - itemCount: products.length, - itemBuilder: (context, index) => ProductTypeCard( - product: products[index], - count: 0, - onIncrement: () {}, - onDecrement: () {}, - ), + child: GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: 0.8, + ), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ProductTypeCard( + product: product, + count: selectedProducts[product] ?? 0, + onIncrement: () => onIncrement(product), + onDecrement: () => onDecrement(product), + ); + }, ), ), ); From 4c0647946963313722eb36c5558a8ba72cd52609 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:54:25 +0300 Subject: [PATCH 094/105] Replaced Column with ListView in SpaceDetailsForm to enhance scrolling behavior and accommodate dynamic content. --- .../presentation/widgets/space_details_form.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index d0495dd3..e4007511 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -42,9 +42,8 @@ class SpaceDetailsForm extends StatelessWidget { Expanded(child: SpaceIconPicker(iconPath: space.icon)), Expanded( flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + child: ListView( + shrinkWrap: true, children: [ SpaceNameTextField( initialValue: space.spaceName, @@ -52,7 +51,7 @@ class SpaceDetailsForm extends StatelessWidget { (subspace) => subspace.name == value, ), ), - const Spacer(), + const SizedBox(height: 32), SpaceSubSpacesBox( subspaces: space.subspaces, ), From 6534bfae5b05bd3fe4bc528a13e3d61b7c2c40a6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 09:31:55 +0300 Subject: [PATCH 095/105] Implement-Calendar-ui --- .../bloc/calendar/events_bloc.dart | 143 +++++++++++ .../bloc/calendar/events_event.dart | 25 ++ .../bloc/calendar/events_state.dart | 25 ++ .../date_selection/date_selection_bloc.dart | 37 +++ .../date_selection/date_selection_event.dart | 13 + .../date_selection/date_selection_state.dart | 21 ++ .../selected_bookable_space_bloc.dart | 12 + .../selected_bookable_space_event.dart | 11 + .../selected_bookable_space_state.dart | 7 + .../bloc/sidebar/sidebar_bloc.dart | 68 +++++ .../bloc/sidebar/sidebar_event.dart | 16 ++ .../bloc/sidebar/sidebar_state.dart | 37 +++ .../booking_system/model/bookable_room.dart | 13 + .../booking_system/view/booking_page.dart | 243 +++++++++++++++--- .../view/widgets/booking_sidebar.dart | 182 +++++++++++++ .../view/widgets/custom_calendar_page.dart | 56 ++++ .../view/widgets/room_list_item.dart | 51 ++++ .../view/widgets/weekly_calendar_page.dart | 236 +++++++++++++++++ pubspec.yaml | 3 + 19 files changed, 1169 insertions(+), 30 deletions(-) create mode 100644 lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart create mode 100644 lib/pages/access_management/booking_system/bloc/calendar/events_event.dart create mode 100644 lib/pages/access_management/booking_system/bloc/calendar/events_state.dart create mode 100644 lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart create mode 100644 lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart create mode 100644 lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart create mode 100644 lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart create mode 100644 lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart create mode 100644 lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart create mode 100644 lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart create mode 100644 lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart create mode 100644 lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart create mode 100644 lib/pages/access_management/booking_system/model/bookable_room.dart create mode 100644 lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart create mode 100644 lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart create mode 100644 lib/pages/access_management/booking_system/view/widgets/room_list_item.dart create mode 100644 lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart new file mode 100644 index 00000000..431720af --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; +part 'events_event.dart'; +part 'events_state.dart'; + +class CalendarEventsBloc extends Bloc { + final EventController eventController = EventController(); + + CalendarEventsBloc() : super(EventsInitial()) { + on(_onLoadEvents); + on(_onAddEvent); + on(_onStartTimer); + on(_onDisposeResources); + on(_onGoToWeek); + } + + Future _onLoadEvents( + LoadEvents event, + Emitter emit, + ) async { + emit(EventsLoading()); + try { + final events = _generateDummyEventsForWeek(event.weekStart); + eventController.addAll(events); + emit(EventsLoaded( + events: events, + initialDate: event.weekStart, + weekDays: _getWeekDays(event.weekStart), + )); + } catch (e) { + emit(EventsError('Failed to load events')); + } + } + + List _generateDummyEventsForWeek(DateTime weekStart) { + final events = []; + + for (int i = 0; i < 7; i++) { + final date = weekStart.add(Duration(days: i)); + + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 9, minute: 0), + endTime: date.copyWith(hour: 10, minute: 30), + title: 'Team Meeting', + description: 'Daily standup', + color: Colors.blue, + )); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 14, minute: 0), + endTime: date.copyWith(hour: 15, minute: 0), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + )); + } + + return events; + } + + void _onAddEvent(AddEvent event, Emitter emit) { + eventController.add(event.event); + if (state is EventsLoaded) { + final loaded = state as EventsLoaded; + emit(EventsLoaded( + events: [...eventController.events], + initialDate: loaded.initialDate, + weekDays: loaded.weekDays, + )); + } + } + + void _onStartTimer(StartTimer event, Emitter emit) {} + + void _onDisposeResources( + DisposeResources event, Emitter emit) { + eventController.dispose(); + } + + void _onGoToWeek(GoToWeek event, Emitter emit) { + if (state is EventsLoaded) { + final loaded = state as EventsLoaded; + final newWeekDays = _getWeekDays(event.weekDate); + emit(EventsLoaded( + events: loaded.events, + initialDate: event.weekDate, + weekDays: newWeekDays, + )); + } + } + + List _generateDummyEvents() { + final now = DateTime.now(); + return [ + CalendarEventData( + date: now, + startTime: now.copyWith(hour: 8, minute: 00, second: 0), + endTime: now.copyWith(hour: 9, minute: 00, second: 0), + title: 'Team Meeting', + description: 'Weekly team sync', + color: Colors.blue, + ), + CalendarEventData( + date: now, + startTime: now.copyWith(hour: 9, minute: 00, second: 0), + endTime: now.copyWith(hour: 10, minute: 30, second: 0), + title: 'Team Meeting', + description: 'Weekly team sync', + color: Colors.blue, + ), + CalendarEventData( + date: now.add(const Duration(days: 1)), + startTime: now.copyWith(hour: 14, day: now.day + 1), + endTime: now.copyWith(hour: 15, day: now.day + 1), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + ), + CalendarEventData( + date: now.add(const Duration(days: 2)), + startTime: now.copyWith(hour: 11, day: now.day + 2), + endTime: now.copyWith(hour: 12, day: now.day + 2), + title: 'Lunch with Team', + color: Colors.orange, + ), + ]; + } + + List _getWeekDays(DateTime date) { + final int weekday = date.weekday; + final DateTime monday = date.subtract(Duration(days: weekday - 1)); + return List.generate(7, (i) => monday.add(Duration(days: i))); + } + + @override + Future close() { + eventController.dispose(); + return super.close(); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart new file mode 100644 index 00000000..e23e65de --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart @@ -0,0 +1,25 @@ +part of 'events_bloc.dart'; + +@immutable +abstract class CalendarEventsEvent { + const CalendarEventsEvent(); +} + +class LoadEvents extends CalendarEventsEvent { + final DateTime weekStart; + const LoadEvents({required this.weekStart}); +} + +class AddEvent extends CalendarEventsEvent { + final CalendarEventData event; + AddEvent(this.event); +} + +class StartTimer extends CalendarEventsEvent {} + +class DisposeResources extends CalendarEventsEvent {} + +class GoToWeek extends CalendarEventsEvent { + final DateTime weekDate; + GoToWeek(this.weekDate); +} diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart new file mode 100644 index 00000000..b7263ec8 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart @@ -0,0 +1,25 @@ +part of 'events_bloc.dart'; + +@immutable +abstract class CalendarEventState {} + +class EventsInitial extends CalendarEventState {} + +class EventsLoading extends CalendarEventState {} + +class EventsLoaded extends CalendarEventState { + final List events; + final DateTime initialDate; + final List weekDays; + + EventsLoaded({ + required this.events, + required this.initialDate, + required this.weekDays, + }); +} + +class EventsError extends CalendarEventState { + final String message; + EventsError(this.message); +} diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart new file mode 100644 index 00000000..6cf56fc7 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart @@ -0,0 +1,37 @@ +import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; +import 'date_selection_state.dart'; + +class DateSelectionBloc extends Bloc { + DateSelectionBloc() : super(DateSelectionState.initial()) { + on((event, emit) { + final newWeekStart = _getStartOfWeek(event.selectedDate); + emit(DateSelectionState( + selectedDate: event.selectedDate, + weekStart: newWeekStart, + )); + }); + + on((event, emit) { + final newWeekStart = state.weekStart.add(const Duration(days: 7)); + final inNewWeek = state.selectedDate + .isAfter(newWeekStart.subtract(const Duration(days: 1))) && + state.selectedDate + .isBefore(newWeekStart.add(const Duration(days: 7))); + emit(DateSelectionState( + selectedDate: state.selectedDate, + weekStart: newWeekStart, + )); + }); + on((event, emit) { + emit(DateSelectionState( + selectedDate: state.selectedDate!.subtract(const Duration(days: 7)), + weekStart: state.weekStart.subtract(const Duration(days: 7)), + )); + }); + } + + static DateTime _getStartOfWeek(DateTime date) { + return date.subtract(Duration(days: date.weekday - 1)); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart new file mode 100644 index 00000000..8ed0a8a0 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart @@ -0,0 +1,13 @@ + +abstract class DateSelectionEvent { + const DateSelectionEvent(); +} + +class SelectDate extends DateSelectionEvent { + final DateTime selectedDate; + const SelectDate(this.selectedDate); +} + +class NextWeek extends DateSelectionEvent {} + +class PreviousWeek extends DateSelectionEvent {} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart new file mode 100644 index 00000000..3b35ce25 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart @@ -0,0 +1,21 @@ +class DateSelectionState { + final DateTime selectedDate; + final DateTime weekStart; + + const DateSelectionState({ + required this.selectedDate, + required this.weekStart, + }); + + factory DateSelectionState.initial() { + final now = DateTime.now(); + return DateSelectionState( + selectedDate: now, + weekStart: _getStartOfWeek(now), + ); + } + + static DateTime _getStartOfWeek(DateTime date) { + return date.subtract(Duration(days: date.weekday - 1)); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart new file mode 100644 index 00000000..23eaff61 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart @@ -0,0 +1,12 @@ +import 'package:bloc/bloc.dart'; +part 'selected_bookable_space_event.dart'; +part 'selected_bookable_space_state.dart'; + +class SelectedBookableSpaceBloc + extends Bloc { + SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) { + on((event, emit) { + emit(SelectedBookableSpaceState(selectedSpaceId: event.spaceId)); + }); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart new file mode 100644 index 00000000..d7fc931c --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart @@ -0,0 +1,11 @@ +part of 'selected_bookable_space_bloc.dart'; + +abstract class SelectedBookableSpaceEvent { + const SelectedBookableSpaceEvent(); +} + +class SelectBookableSpace extends SelectedBookableSpaceEvent { + final dynamic spaceId; + + const SelectBookableSpace(this.spaceId); +} diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart new file mode 100644 index 00000000..98d65fde --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart @@ -0,0 +1,7 @@ +part of 'selected_bookable_space_bloc.dart'; + +class SelectedBookableSpaceState { + final String? selectedSpaceId; + + const SelectedBookableSpaceState({this.selectedSpaceId}); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart new file mode 100644 index 00000000..9d1b4a5b --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart @@ -0,0 +1,68 @@ + + + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SidebarBloc extends Bloc { + SidebarBloc() : super(SidebarState( + allRooms: [], + displayedRooms: [], + isLoading: true, + )) { + on(_onLoadRooms); + on(_onSelectRoom); + on(_onSearchRooms); + } + + Future _onLoadRooms( + LoadRoomsEvent event, + Emitter emit, + ) async { + try { + emit(state.copyWith(isLoading: true)); + + await Future.delayed(const Duration(seconds: 1)); + final rooms = List.generate(15, (index) => BookableRoom( + id: index, + name: 'Meeting Room ${index + 1}', + capacity: [4, 6, 8, 10][index % 4], + iconAsset: Assets.AtoZIcon, + )); + + emit(state.copyWith( + allRooms: rooms, + displayedRooms: rooms, + isLoading: false, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load rooms', + )); + } + } + + void _onSelectRoom( + SelectRoomEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedRoomId: event.roomId)); + } + + void _onSearchRooms( + SearchRoomsEvent event, + Emitter emit, + ) { + if (event.query.isEmpty) { + emit(state.copyWith(displayedRooms: state.allRooms)); + } else { + final filtered = state.allRooms.where((room) => + room.name.toLowerCase().contains(event.query.toLowerCase())).toList(); + emit(state.copyWith(displayedRooms: filtered)); + } + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart new file mode 100644 index 00000000..3fa504ef --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart @@ -0,0 +1,16 @@ + +abstract class SidebarEvent {} + +class LoadRoomsEvent extends SidebarEvent {} + +class SelectRoomEvent extends SidebarEvent { + final int roomId; + + SelectRoomEvent(this.roomId); +} + +class SearchRoomsEvent extends SidebarEvent { + final String query; + + SearchRoomsEvent(this.query); +} diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart new file mode 100644 index 00000000..5b30a9a0 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart @@ -0,0 +1,37 @@ + + + + +import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; + +class SidebarState { + final List allRooms; + final List displayedRooms; + final int? selectedRoomId; + final bool isLoading; + final String? errorMessage; + + SidebarState({ + required this.allRooms, + required this.displayedRooms, + this.selectedRoomId, + this.isLoading = false, + this.errorMessage, + }); + + SidebarState copyWith({ + List? allRooms, + List? displayedRooms, + int? selectedRoomId, + bool? isLoading, + String? errorMessage, + }) { + return SidebarState( + allRooms: allRooms ?? this.allRooms, + displayedRooms: displayedRooms ?? this.displayedRooms, + selectedRoomId: selectedRoomId ?? this.selectedRoomId, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} diff --git a/lib/pages/access_management/booking_system/model/bookable_room.dart b/lib/pages/access_management/booking_system/model/bookable_room.dart new file mode 100644 index 00000000..9f85984e --- /dev/null +++ b/lib/pages/access_management/booking_system/model/bookable_room.dart @@ -0,0 +1,13 @@ +class BookableRoom { + final int id; + final String name; + final int capacity; + final String? iconAsset; + + BookableRoom({ + required this.id, + required this.name, + this.capacity = 4, + this.iconAsset, + }); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/view/booking_page.dart index 6fdb53bd..072dfcd7 100644 --- a/lib/pages/access_management/booking_system/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/view/booking_page.dart @@ -1,52 +1,235 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/booking_sidebar.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class BookingPage extends StatelessWidget { +class BookingPage extends StatefulWidget { const BookingPage({super.key}); + @override + State createState() => _BookingPageState(); +} + +class _BookingPageState extends State { + late final EventController _eventController; + + @override + void initState() { + super.initState(); + _eventController = EventController(); + } + + @override + void dispose() { + _eventController.dispose(); + super.dispose(); + } + + List _generateDummyEventsForWeek(DateTime weekStart) { + final List events = []; + for (int i = 0; i < 7; i++) { + final date = weekStart.add(Duration(days: i)); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 9, minute: 0), + endTime: date.copyWith(hour: 10, minute: 30), + title: 'Team Meeting', + description: 'Daily standup', + color: Colors.blue, + )); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 14, minute: 0), + endTime: date.copyWith(hour: 15, minute: 0), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + )); + } + return events; + } + + void _loadEventsForWeek(DateTime weekStart) { + _eventController.removeWhere((_) => true); + _eventController.addAll(_generateDummyEventsForWeek(weekStart)); + } + @override Widget build(BuildContext context) { - return Container( - child: Row( - children: [ - Expanded( - child: Container( - color: Colors.blueGrey[100], - child: const Center( - child: Text( - 'Side bar', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => SelectedBookableSpaceBloc()), + BlocProvider(create: (_) => DateSelectionBloc()), + ], + child: BlocListener( + listenWhen: (previous, current) => + previous.weekStart != current.weekStart, + listener: (context, state) { + _loadEventsForWeek(state.weekStart); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return BookingSidebar( + onRoomSelected: (id) { + context + .read() + .add(SelectBookableSpace(id)); + }, + ); + }, + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return Container( + color: Colors.grey[300], + child: CustomCalendarPage( + selectedDate: dateState.selectedDate, + onDateChanged: (day, month, year) { + final newDate = DateTime(year, month, day); + context + .read() + .add(SelectDate(newDate)); + }, + ), + ); + }, + ), + ), + ], ), ), - )), - Expanded( + Expanded( flex: 4, child: Padding( padding: const EdgeInsets.all(20.0), - child: SizedBox( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SvgTextButton( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgTextButton( svgAsset: Assets.homeIcon, label: 'Manage Bookable Spaces', - onPressed: () {}), - SizedBox(width: 20), - SvgTextButton( + onPressed: () {}, + ), + const SizedBox(width: 20), + SvgTextButton( svgAsset: Assets.groupIcon, label: 'Manage Users', - onPressed: () {}) - ], - ) - ], - ), + onPressed: () {}, + ), + ], + ), + BlocBuilder( + builder: (context, state) { + final weekStart = state.weekStart; + final weekEnd = + weekStart.add(const Duration(days: 6)); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: ColorsManager.textGray, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios, + color: Colors.black), + onPressed: () { + context + .read() + .add(PreviousWeek()); + }, + ), + Text( + _getMonthYearText(weekStart, weekEnd), + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios, + color: Colors.black), + onPressed: () { + context + .read() + .add(NextWeek()); + }, + ), + ], + ), + ); + }, + ), + ], + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return WeeklyCalendarPage( + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + ); + }, + ), + ), + ], ), - )) - ], + ), + ), + ], + ), ), ); } + + String _getMonthYearText(DateTime start, DateTime end) { + final startMonth = DateFormat('MMM').format(start); + final endMonth = DateFormat('MMM').format(end); + final year = start.year == end.year + ? start.year.toString() + : '${start.year}-${end.year}'; + + if (start.month == end.month) { + return '$startMonth $year'; + } else { + return '$startMonth - $endMonth $year'; + } + } } diff --git a/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart new file mode 100644 index 00000000..2849da5d --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/room_list_item.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BookingSidebar extends StatelessWidget { + final void Function(int) onRoomSelected; + + const BookingSidebar({ + super.key, + required this.onRoomSelected, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SidebarBloc()..add(LoadRoomsEvent()), + child: _SidebarContent(onRoomSelected: onRoomSelected), + ); + } +} + +class _SidebarContent extends StatelessWidget { + final void Function(int) onRoomSelected; + + const _SidebarContent({ + required this.onRoomSelected, + }); + + @override + Widget build(BuildContext context) { + final TextEditingController searchController = TextEditingController(); + + return Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, + ), + ], + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + const _SidebarHeader(title: 'Spaces'), + Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, -2), + blurRadius: 4, + spreadRadius: 0, + ), + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Container( + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(8.0), + ), + child: TextField( + style: + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.blackColor, + ), + controller: searchController, + onChanged: (value) { + context + .read() + .add(SearchRoomsEvent(value)); + }, + decoration: InputDecoration( + hintText: 'Search', + suffixIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 20, + height: 20, + child: SvgPicture.asset( + Assets.searchIconUser, + color: ColorsManager.primaryTextColor, + ), + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none), + ), + ), + ), + ), + ), + ), + ), + if (state.isLoading) + const Expanded( + child: Center(child: CircularProgressIndicator()), + ) + else if (state.errorMessage != null) + Expanded( + child: Center(child: Text(state.errorMessage!)), + ) + else + Expanded( + child: ListView.builder( + itemCount: state.displayedRooms.length, + itemBuilder: (context, index) { + final room = state.displayedRooms[index]; + return RoomListItem( + room: room, + isSelected: state.selectedRoomId == room.id, + onTap: () { + context + .read() + .add(SelectRoomEvent(room.id)); + onRoomSelected(room.id); + }, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _SidebarHeader extends StatelessWidget { + final String title; + + const _SidebarHeader({ + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.primaryTextColor, + fontSize: 20, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart new file mode 100644 index 00000000..a523ae61 --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class CustomCalendarPage extends StatefulWidget { + final DateTime selectedDate; + final Function(int day, int month, int year) onDateChanged; + + const CustomCalendarPage({ + super.key, + required this.selectedDate, + required this.onDateChanged, + }); + + @override + State createState() => _CustomCalendarPageState(); +} + +class _CustomCalendarPageState extends State { + late DateTime _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.selectedDate; + } + + @override + void didUpdateWidget(CustomCalendarPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedDate != oldWidget.selectedDate) { + setState(() { + _selectedDate = widget.selectedDate; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Column( + children: [ + Expanded( + child: CalendarDatePicker( + initialDate: _selectedDate, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + onDateChanged: (date) { + widget.onDateChanged(date.day, date.month, date.year); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart new file mode 100644 index 00000000..40f148ff --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart @@ -0,0 +1,51 @@ + +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoomListItem extends StatelessWidget { + final BookableRoom room; + final bool isSelected; + final VoidCallback onTap; + + const RoomListItem({ + required this.room, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + hoverColor: ColorsManager.primaryColor.withOpacity(0.05), + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + IgnorePointer( + child: Radio( + value: room.id, + groupValue: isSelected ? room.id : null, + onChanged: (value) {}, + activeColor: ColorsManager.primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + room.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart new file mode 100644 index 00000000..c6eb2f40 --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeeklyCalendarPage extends StatelessWidget { + final DateTime weekStart; + final DateTime selectedDate; + final EventController eventController; + + const WeeklyCalendarPage({ + super.key, + required this.weekStart, + required this.selectedDate, + required this.eventController, + }); + + @override + Widget build(BuildContext context) { + final weekDays = _getWeekDays(weekStart); + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + + return LayoutBuilder( + builder: (context, constraints) { + final double calendarWidth = constraints.maxWidth; + const double timeLineWidth = 80; + const int totalDays = 7; + final double dayColumnWidth = + (calendarWidth - timeLineWidth) / totalDays; + final selectedDayIndex = (selectedDate != null) + ? weekDays.indexWhere((d) => isSameDay(d, selectedDate)) + : -1; + return Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), + child: Stack( + children: [ + WeekView( + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: 7, + endHour: 18, + heightPerMinute: 1.1, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -80, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, + ), + weekDayBuilder: (date) { + final weekDays = _getWeekDays(weekStart); + final selectedDayIndex = + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + final index = weekDays.indexWhere((d) => isSameDay(d, date)); + final isSelectedDay = index == selectedDayIndex; + final isToday = isSameDay(date, DateTime.now()); + + return Container( + decoration: isSelectedDay + ? BoxDecoration( + color: ColorsManager.blue1.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + ) + : isToday + ? BoxDecoration( + color: ColorsManager.blue1.withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + ) + : null, + child: Column( + children: [ + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, + ), + ), + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: isSelectedDay + ? ColorsManager.blue1 + : ColorsManager.blackColor, + ), + ), + ], + ), + ); + }, + timeLineBuilder: (date) { + int hour = date.hour == 0 + ? 12 + : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$hour', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: ColorsManager.blackColor, + ), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.blackColor, + letterSpacing: 1, + ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + ), + ], + ), + ), + ); + }, + timeLineWidth: timeLineWidth, + weekPageHeaderBuilder: (start, end) => Container(), + weekTitleHeight: 60, + weekNumberBuilder: (firstDayOfWeek) => Text( + firstDayOfWeek.timeZoneName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + eventTileBuilder: (date, events, boundary, start, end) { + return Container( + margin: + const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = event.endTime != null && + event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.grayColor + : ColorsManager.lightGrayColor + .withOpacity(0.25), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a').format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + }, + ), + // Highlight the selected day column + if (selectedDayIndex >= 0) + Positioned( + left: timeLineWidth + dayColumnWidth * selectedDayIndex, + top: 0, + bottom: 0, + width: dayColumnWidth, + child: IgnorePointer( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 0, horizontal: 2), + color: ColorsManager.blue1.withOpacity(0.1), + ), + ), + ), + Positioned( + right: 0, + top: 50, + bottom: 0, + child: IgnorePointer( + child: Container( + width: 1, + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + ), + ], + ), + ); + }, + ); + } + + List _getWeekDays(DateTime date) { + final int weekday = date.weekday; + final DateTime monday = date.subtract(Duration(days: weekday - 1)); + return List.generate(7, (i) => monday.add(Duration(days: i))); + } +} + +bool isSameDay(DateTime d1, DateTime d2) { + return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day; +} diff --git a/pubspec.yaml b/pubspec.yaml index c4692ac4..cba59019 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,9 @@ dependencies: bloc: ^9.0.0 geocoding: ^4.0.0 gauge_indicator: ^0.4.3 + # syncfusion_flutter_calendar: ^30.1.38 + calendar_view: ^1.4.0 + dev_dependencies: From 9f28e1ccef51ea10cbc1c48fc3978e7d6df23e85 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 16:18:10 +0300 Subject: [PATCH 096/105] Refactor booking system: remove unused classes, update dependencies, and implement date selection logic --- .../access_management/bloc/access_bloc.dart | 2 +- .../access_management/bloc/access_state.dart | 2 +- .../selected_bookable_space_state.dart | 7 - .../bloc/sidebar/sidebar_bloc.dart | 68 ----- .../bloc/sidebar/sidebar_event.dart | 16 -- .../bloc/sidebar/sidebar_state.dart | 37 --- .../services/bookable_spaces_service.dart | 50 ++++ .../domain/models/bookable_room.dart | 53 ++++ .../models/paginated_bookable_spaces.dart | 40 +++ .../booking_system/domain/models/product.dart | 0 .../DebouncedBookingSystemService.dart | 58 ++++ .../services/booking_system_service.dart | 10 + .../booking_system/model/bookable_room.dart | 13 - .../bloc/calendar/events_bloc.dart | 0 .../bloc/calendar/events_event.dart | 0 .../bloc/calendar/events_state.dart | 0 .../date_selection/date_selection_bloc.dart | 2 +- .../date_selection/date_selection_event.dart | 0 .../date_selection/date_selection_state.dart | 0 .../selected_bookable_space_bloc.dart | 4 +- .../selected_bookable_space_event.dart | 4 +- .../selected_bookable_space_state.dart | 9 + .../bloc/sidebar/sidebar_bloc.dart | 190 +++++++++++++ .../bloc/sidebar/sidebar_event.dart | 27 ++ .../bloc/sidebar/sidebar_state.dart | 49 ++++ .../presentation}/model/password_model.dart | 0 .../{ => presentation}/view/booking_page.dart | 123 +++++---- .../view/widgets/booking_sidebar.dart | 251 ++++++++++++++++++ .../view/widgets/custom_calendar_page.dart | 83 ++++++ .../view/widgets/icon_text_button.dart | 13 +- .../view/widgets/room_list_item.dart | 42 +++ .../view/widgets/weekly_calendar_page.dart | 38 ++- .../view/widgets/booking_sidebar.dart | 182 ------------- .../view/widgets/custom_calendar_page.dart | 56 ---- .../view/widgets/room_list_item.dart | 51 ---- .../view/access_management.dart | 2 +- lib/services/access_mang_api.dart | 2 +- lib/utils/color_manager.dart | 1 + lib/utils/constants/api_const.dart | 5 +- pubspec.yaml | 2 +- 40 files changed, 989 insertions(+), 503 deletions(-) delete mode 100644 lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart delete mode 100644 lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart delete mode 100644 lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart delete mode 100644 lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart create mode 100644 lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart create mode 100644 lib/pages/access_management/booking_system/domain/models/bookable_room.dart create mode 100644 lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart create mode 100644 lib/pages/access_management/booking_system/domain/models/product.dart create mode 100644 lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart create mode 100644 lib/pages/access_management/booking_system/domain/services/booking_system_service.dart delete mode 100644 lib/pages/access_management/booking_system/model/bookable_room.dart rename lib/pages/access_management/booking_system/{ => presentation}/bloc/calendar/events_bloc.dart (100%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/calendar/events_event.dart (100%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/calendar/events_state.dart (100%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/date_selection/date_selection_bloc.dart (95%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/date_selection/date_selection_event.dart (100%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/date_selection/date_selection_state.dart (100%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart (64%) rename lib/pages/access_management/booking_system/{ => presentation}/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart (68%) create mode 100644 lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart create mode 100644 lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart create mode 100644 lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart create mode 100644 lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart rename lib/pages/access_management/{ => booking_system/presentation}/model/password_model.dart (100%) rename lib/pages/access_management/booking_system/{ => presentation}/view/booking_page.dart (66%) create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart rename lib/pages/access_management/booking_system/{ => presentation}/view/widgets/icon_text_button.dart (87%) create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart rename lib/pages/access_management/booking_system/{ => presentation}/view/widgets/weekly_calendar_page.dart (90%) delete mode 100644 lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart delete mode 100644 lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart delete mode 100644 lib/pages/access_management/booking_system/view/widgets/room_list_item.dart diff --git a/lib/pages/access_management/bloc/access_bloc.dart b/lib/pages/access_management/bloc/access_bloc.dart index dd82d739..11c42f3b 100644 --- a/lib/pages/access_management/bloc/access_bloc.dart +++ b/lib/pages/access_management/bloc/access_bloc.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; -import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/common/hour_picker_dialog.dart'; import 'package:syncrow_web/services/access_mang_api.dart'; diff --git a/lib/pages/access_management/bloc/access_state.dart b/lib/pages/access_management/bloc/access_state.dart index 0790a735..cdaf9aad 100644 --- a/lib/pages/access_management/bloc/access_state.dart +++ b/lib/pages/access_management/bloc/access_state.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart'; abstract class AccessState extends Equatable { const AccessState(); diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart deleted file mode 100644 index 98d65fde..00000000 --- a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart +++ /dev/null @@ -1,7 +0,0 @@ -part of 'selected_bookable_space_bloc.dart'; - -class SelectedBookableSpaceState { - final String? selectedSpaceId; - - const SelectedBookableSpaceState({this.selectedSpaceId}); -} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart deleted file mode 100644 index 9d1b4a5b..00000000 --- a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart +++ /dev/null @@ -1,68 +0,0 @@ - - - -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; - -class SidebarBloc extends Bloc { - SidebarBloc() : super(SidebarState( - allRooms: [], - displayedRooms: [], - isLoading: true, - )) { - on(_onLoadRooms); - on(_onSelectRoom); - on(_onSearchRooms); - } - - Future _onLoadRooms( - LoadRoomsEvent event, - Emitter emit, - ) async { - try { - emit(state.copyWith(isLoading: true)); - - await Future.delayed(const Duration(seconds: 1)); - final rooms = List.generate(15, (index) => BookableRoom( - id: index, - name: 'Meeting Room ${index + 1}', - capacity: [4, 6, 8, 10][index % 4], - iconAsset: Assets.AtoZIcon, - )); - - emit(state.copyWith( - allRooms: rooms, - displayedRooms: rooms, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( - isLoading: false, - errorMessage: 'Failed to load rooms', - )); - } - } - - void _onSelectRoom( - SelectRoomEvent event, - Emitter emit, - ) { - emit(state.copyWith(selectedRoomId: event.roomId)); - } - - void _onSearchRooms( - SearchRoomsEvent event, - Emitter emit, - ) { - if (event.query.isEmpty) { - emit(state.copyWith(displayedRooms: state.allRooms)); - } else { - final filtered = state.allRooms.where((room) => - room.name.toLowerCase().contains(event.query.toLowerCase())).toList(); - emit(state.copyWith(displayedRooms: filtered)); - } - } -} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart deleted file mode 100644 index 3fa504ef..00000000 --- a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart +++ /dev/null @@ -1,16 +0,0 @@ - -abstract class SidebarEvent {} - -class LoadRoomsEvent extends SidebarEvent {} - -class SelectRoomEvent extends SidebarEvent { - final int roomId; - - SelectRoomEvent(this.roomId); -} - -class SearchRoomsEvent extends SidebarEvent { - final String query; - - SearchRoomsEvent(this.query); -} diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart deleted file mode 100644 index 5b30a9a0..00000000 --- a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart +++ /dev/null @@ -1,37 +0,0 @@ - - - - -import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; - -class SidebarState { - final List allRooms; - final List displayedRooms; - final int? selectedRoomId; - final bool isLoading; - final String? errorMessage; - - SidebarState({ - required this.allRooms, - required this.displayedRooms, - this.selectedRoomId, - this.isLoading = false, - this.errorMessage, - }); - - SidebarState copyWith({ - List? allRooms, - List? displayedRooms, - int? selectedRoomId, - bool? isLoading, - String? errorMessage, - }) { - return SidebarState( - allRooms: allRooms ?? this.allRooms, - displayedRooms: displayedRooms ?? this.displayedRooms, - selectedRoomId: selectedRoomId ?? this.selectedRoomId, - isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage ?? this.errorMessage, - ); - } -} diff --git a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart new file mode 100644 index 00000000..5cc0decb --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart @@ -0,0 +1,50 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class BookableSpacesService implements BookingSystemService { + const BookableSpacesService(this._httpService); + + final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load bookable spaces'; + + @override + Future getBookableSpaces({ + required int page, + required int size, + required String search, + }) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getBookableSpaces, + queryParameters: { + 'page': page, + 'size': size, + 'active': true, + 'configured': true, + if (search.isNotEmpty && search != 'null') 'search': search, + }, + expectedResponseModel: (json) { + print('Response JSON: $json'); + return PaginatedBookableSpaces.fromJson( + json as Map, + ); + }); + return response; + } on DioException catch (e) { + final responseData = e.response?.data; + if (responseData is Map) { + final errorMessage = responseData['error']?['message'] as String? ?? + responseData['message'] as String? ?? + _defaultErrorMessage; + throw APIException(errorMessage); + } + throw APIException(_defaultErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: ${e.toString()}'); + } + } +} diff --git a/lib/pages/access_management/booking_system/domain/models/bookable_room.dart b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart new file mode 100644 index 00000000..b8aa58b9 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart @@ -0,0 +1,53 @@ +// bookable_space_model.dart +class BookableSpaceModel { + final String uuid; + final String spaceName; + final String virtualLocation; + final BookableConfig bookableConfig; + + BookableSpaceModel({ + required this.uuid, + required this.spaceName, + required this.virtualLocation, + required this.bookableConfig, + }); + + factory BookableSpaceModel.fromJson(Map json) { + return BookableSpaceModel( + uuid: json['uuid'] as String, + spaceName: json['spaceName'] as String, + virtualLocation: json['virtualLocation'] as String, + bookableConfig: BookableConfig.fromJson( + json['bookableConfig'] as Map), + ); + } +} + +class BookableConfig { + final String uuid; + final List daysAvailable; + final String startTime; + final String endTime; + final bool active; + final int points; + + BookableConfig({ + required this.uuid, + required this.daysAvailable, + required this.startTime, + required this.endTime, + required this.active, + required this.points, + }); + + factory BookableConfig.fromJson(Map json) { + return BookableConfig( + uuid: json['uuid'] as String, + daysAvailable: (json['daysAvailable'] as List).cast(), + startTime: json['startTime'] as String, + endTime: json['endTime'] as String, + active: json['active'] as bool, + points: json['points'] as int, + ); + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart b/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart new file mode 100644 index 00000000..b4b79bc2 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart @@ -0,0 +1,40 @@ + + +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; + +class PaginatedBookableSpaces { + final List data; + final String message; + final int page; + final int size; + final int totalItem; + final int totalPage; + final bool hasNext; + final bool hasPrevious; + + PaginatedBookableSpaces({ + required this.data, + required this.message, + required this.page, + required this.size, + required this.totalItem, + required this.totalPage, + required this.hasNext, + required this.hasPrevious, + }); + + factory PaginatedBookableSpaces.fromJson(Map json) { + return PaginatedBookableSpaces( + data: (json['data'] as List) + .map((item) => BookableSpaceModel.fromJson(item)) + .toList(), + message: json['message'] as String, + page: json['page'] as int, + size: json['size'] as int, + totalItem: json['totalItem'] as int, + totalPage: json['totalPage'] as int, + hasNext: json['hasNext'] as bool, + hasPrevious: json['hasPrevious'] as bool, + ); + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/models/product.dart b/lib/pages/access_management/booking_system/domain/models/product.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart new file mode 100644 index 00000000..40e4e5ab --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; + +class DebouncedBookingSystemService implements BookingSystemService { + final BookingSystemService _inner; + final Duration debounceDuration; + + Timer? _debounceTimer; + Completer? _lastCompleter; + + // Store last parameters + int? _lastPage; + int? _lastSize; + bool? _lastIncludeSpaces; + String? _lastSearch; + + DebouncedBookingSystemService( + this._inner, { + this.debounceDuration = const Duration(milliseconds: 500), + }); + + @override + Future getBookableSpaces({ + required int page, + required int size, + required String search, + }) { + _debounceTimer?.cancel(); + _lastCompleter?.completeError(StateError("Cancelled by new search")); + + final completer = Completer(); + _lastCompleter = completer; + + _lastPage = page; + _lastSize = size; + _lastSearch = search; + + _debounceTimer = Timer(debounceDuration, () async { + try { + final result = await _inner.getBookableSpaces( + page: _lastPage!, + size: _lastSize!, + search: _lastSearch!, + ); + if (!completer.isCompleted) { + completer.complete(result); + } + } catch (e, st) { + if (!completer.isCompleted) { + completer.completeError(e, st); + } + } + }); + + return completer.future; + } +} diff --git a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart new file mode 100644 index 00000000..40a9a8e4 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart @@ -0,0 +1,10 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; + +abstract class BookingSystemService { + Future getBookableSpaces({ + required int page, + required int size, + required String search, + + }); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/model/bookable_room.dart b/lib/pages/access_management/booking_system/model/bookable_room.dart deleted file mode 100644 index 9f85984e..00000000 --- a/lib/pages/access_management/booking_system/model/bookable_room.dart +++ /dev/null @@ -1,13 +0,0 @@ -class BookableRoom { - final int id; - final String name; - final int capacity; - final String? iconAsset; - - BookableRoom({ - required this.id, - required this.name, - this.capacity = 4, - this.iconAsset, - }); -} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart rename to lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/calendar/events_event.dart rename to lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/calendar/events_state.dart rename to lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart similarity index 95% rename from lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart rename to lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart index 6cf56fc7..d6687087 100644 --- a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart @@ -1,5 +1,5 @@ import 'package:bloc/bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'date_selection_state.dart'; class DateSelectionBloc extends Bloc { diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart rename to lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart rename to lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart similarity index 64% rename from lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart rename to lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart index 23eaff61..70b46c1a 100644 --- a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart @@ -1,4 +1,5 @@ import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; part 'selected_bookable_space_event.dart'; part 'selected_bookable_space_state.dart'; @@ -6,7 +7,8 @@ class SelectedBookableSpaceBloc extends Bloc { SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) { on((event, emit) { - emit(SelectedBookableSpaceState(selectedSpaceId: event.spaceId)); + emit(SelectedBookableSpaceState( + selectedBookableSpace: event.bookableSpace)); }); } } diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart similarity index 68% rename from lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart rename to lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart index d7fc931c..c74c13df 100644 --- a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart @@ -5,7 +5,7 @@ abstract class SelectedBookableSpaceEvent { } class SelectBookableSpace extends SelectedBookableSpaceEvent { - final dynamic spaceId; + final BookableSpaceModel bookableSpace; - const SelectBookableSpace(this.spaceId); + const SelectBookableSpace(this.bookableSpace); } diff --git a/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart new file mode 100644 index 00000000..8509d5c3 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart @@ -0,0 +1,9 @@ +part of 'selected_bookable_space_bloc.dart'; + +class SelectedBookableSpaceState { + final BookableSpaceModel? selectedBookableSpace; + + const SelectedBookableSpaceState( + { this.selectedBookableSpace,} + ); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart new file mode 100644 index 00000000..9abf032a --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart'; + +class SidebarBloc extends Bloc { + final BookingSystemService _bookingService; + Timer? _searchDebounce; + int _currentPage = 1; + final int _pageSize = 20; + String _currentSearch = ''; + + SidebarBloc(this._bookingService) + : super(SidebarState( + allRooms: [], + displayedRooms: [], + isLoading: true, + hasMore: true, + )) { + on(_onLoadBookableSpaces); + on(_onLoadMoreSpaces); + on(_onSelectRoom); + on(_onSearchRooms); + on(_onResetSearch); + } + + Future _onLoadBookableSpaces( + LoadBookableSpaces event, + Emitter emit, + ) async { + try { + emit(state.copyWith(isLoading: true, errorMessage: null)); + _currentPage = 1; + _currentSearch = ''; + + final paginatedSpaces = await _bookingService.getBookableSpaces( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ); + + final rooms = paginatedSpaces.data.map((space) { + return BookableSpaceModel( + uuid: space.uuid, + spaceName: space.spaceName, + virtualLocation: space.virtualLocation, + bookableConfig: BookableConfig( + uuid: space.bookableConfig.uuid, + daysAvailable: space.bookableConfig.daysAvailable, + startTime: space.bookableConfig.startTime, + endTime: space.bookableConfig.endTime, + active: space.bookableConfig.active, + points: space.bookableConfig.points, + ), + ); + }).toList(); + + emit(state.copyWith( + allRooms: rooms, + displayedRooms: rooms, + isLoading: false, + hasMore: paginatedSpaces.hasNext, + totalPages: paginatedSpaces.totalPage, + currentPage: _currentPage, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load rooms: ${e.toString()}', + )); + } + } + + Future _onLoadMoreSpaces( + LoadMoreSpaces event, + Emitter emit, + ) async { + if (!state.hasMore || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + _currentPage++; + + final paginatedSpaces = await _bookingService.getBookableSpaces( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ); + + final newRooms = paginatedSpaces.data.map((space) { + return BookableSpaceModel( + uuid: space.uuid, + spaceName: space.spaceName, + virtualLocation: space.virtualLocation, + bookableConfig: BookableConfig( + uuid: space.bookableConfig.uuid, + daysAvailable: space.bookableConfig.daysAvailable, + startTime: space.bookableConfig.startTime, + endTime: space.bookableConfig.endTime, + active: space.bookableConfig.active, + points: space.bookableConfig.points, + ), + ); + }).toList(); + + final updatedRooms = [...state.allRooms, ...newRooms]; + + emit(state.copyWith( + allRooms: updatedRooms, + displayedRooms: updatedRooms, + isLoadingMore: false, + hasMore: paginatedSpaces.hasNext, + currentPage: _currentPage, + )); + } catch (e) { + _currentPage--; + emit(state.copyWith( + isLoadingMore: false, + errorMessage: 'Failed to load more rooms: ${e.toString()}', + )); + } + } + + Future _onSearchRooms( + SearchRoomsEvent event, + Emitter emit, + ) async { + try { + _currentSearch = event.query; + _currentPage = 1; + emit(state.copyWith(isLoading: true, errorMessage: null)); + final paginatedSpaces = await _bookingService.getBookableSpaces( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ); + final rooms = paginatedSpaces.data.map((space) { + return BookableSpaceModel( + uuid: space.uuid, + spaceName: space.spaceName, + virtualLocation: space.virtualLocation, + bookableConfig: BookableConfig( + uuid: space.bookableConfig.uuid, + daysAvailable: space.bookableConfig.daysAvailable, + startTime: space.bookableConfig.startTime, + endTime: space.bookableConfig.endTime, + active: space.bookableConfig.active, + points: space.bookableConfig.points, + ), + ); + }).toList(); + emit(state.copyWith( + allRooms: rooms, + displayedRooms: rooms, + isLoading: false, + hasMore: paginatedSpaces.hasNext, + totalPages: paginatedSpaces.totalPage, + currentPage: _currentPage, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Search failed: ${e.toString()}', + )); + } + } + + void _onResetSearch( + ResetSearch event, + Emitter emit, + ) { + _currentSearch = ''; + add(LoadBookableSpaces()); + } + + void _onSelectRoom( + SelectRoomEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedRoomId: event.roomId)); + } + + @override + Future close() { + _searchDebounce?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart new file mode 100644 index 00000000..5dd1a3c8 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart @@ -0,0 +1,27 @@ +abstract class SidebarEvent {} + +class LoadBookableSpaces extends SidebarEvent {} + +class SelectRoomEvent extends SidebarEvent { + final String roomId; + + SelectRoomEvent(this.roomId); +} + +class SearchRoomsEvent extends SidebarEvent { + final String query; + + SearchRoomsEvent(this.query); +} + +// Add these to your sidebar_event.dart file +class LoadMoreSpaces extends SidebarEvent {} + +class ResetSearch extends SidebarEvent {} + +// Add to sidebar_event.dart +class ExecuteSearch extends SidebarEvent { + final String query; + + ExecuteSearch(this.query); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart new file mode 100644 index 00000000..7b35474e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart @@ -0,0 +1,49 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; + +class SidebarState { + final List allRooms; + final List displayedRooms; + final bool isLoading; + final bool isLoadingMore; + final String? errorMessage; + final String? selectedRoomId; + final bool hasMore; + final int totalPages; + final int currentPage; + + SidebarState({ + required this.allRooms, + required this.displayedRooms, + required this.isLoading, + this.isLoadingMore = false, + this.errorMessage, + this.selectedRoomId, + this.hasMore = true, + this.totalPages = 0, + this.currentPage = 1, + }); + + SidebarState copyWith({ + List? allRooms, + List? displayedRooms, + bool? isLoading, + bool? isLoadingMore, + String? errorMessage, + String? selectedRoomId, + bool? hasMore, + int? totalPages, + int? currentPage, + }) { + return SidebarState( + allRooms: allRooms ?? this.allRooms, + displayedRooms: displayedRooms ?? this.displayedRooms, + isLoading: isLoading ?? this.isLoading, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + errorMessage: errorMessage ?? this.errorMessage, + selectedRoomId: selectedRoomId ?? this.selectedRoomId, + hasMore: hasMore ?? this.hasMore, + totalPages: totalPages ?? this.totalPages, + currentPage: currentPage ?? this.currentPage, + ); + } +} diff --git a/lib/pages/access_management/model/password_model.dart b/lib/pages/access_management/booking_system/presentation/model/password_model.dart similarity index 100% rename from lib/pages/access_management/model/password_model.dart rename to lib/pages/access_management/booking_system/presentation/model/password_model.dart diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart similarity index 66% rename from lib/pages/access_management/booking_system/view/booking_page.dart rename to lib/pages/access_management/booking_system/presentation/view/booking_page.dart index 072dfcd7..522b0f25 100644 --- a/lib/pages/access_management/booking_system/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:calendar_view/calendar_view.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/booking_sidebar.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -81,29 +81,39 @@ class _BookingPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - children: [ - Expanded( - flex: 2, - child: BlocBuilder( - builder: (context, state) { - return BookingSidebar( - onRoomSelected: (id) { - context - .read() - .add(SelectBookableSpace(id)); - }, - ); - }, + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, ), - ), - Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return Container( - color: Colors.grey[300], - child: CustomCalendarPage( + ], + ), + child: Column( + children: [ + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return BookingSidebar( + onRoomSelected: (selectedRoom) { + context + .read() + .add(SelectBookableSpace(selectedRoom)); + }, + ); + }, + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return CustomCalendarPage( selectedDate: dateState.selectedDate, onDateChanged: (day, month, year) { final newDate = DateTime(year, month, day); @@ -111,12 +121,12 @@ class _BookingPageState extends State { .read() .add(SelectDate(newDate)); }, - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), ), Expanded( @@ -157,34 +167,38 @@ class _BookingPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: const [ BoxShadow( - color: ColorsManager.textGray, - blurRadius: 12, - offset: Offset(0, 4), + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), ), ], ), child: Row( children: [ IconButton( + iconSize: 15, icon: const Icon(Icons.arrow_back_ios, - color: Colors.black), + color: ColorsManager.lightGrayColor), onPressed: () { context .read() .add(PreviousWeek()); }, ), + const SizedBox(width: 10), Text( _getMonthYearText(weekStart, weekEnd), style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, + color: ColorsManager.lightGrayColor, + fontSize: 14, + fontWeight: FontWeight.w400, ), ), + const SizedBox(width: 10), IconButton( + iconSize: 15, icon: const Icon(Icons.arrow_forward_ios, - color: Colors.black), + color: ColorsManager.lightGrayColor), onPressed: () { context .read() @@ -199,12 +213,23 @@ class _BookingPageState extends State { ], ), Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return WeeklyCalendarPage( - weekStart: dateState.weekStart, - selectedDate: dateState.selectedDate, - eventController: _eventController, + child: BlocBuilder( + builder: (context, roomState) { + // NOTE: Assuming `SelectedBookableSpaceState` has a `selectedBookableSpace` property. + final selectedRoom = roomState.selectedBookableSpace; + return BlocBuilder( + builder: (context, dateState) { + return WeeklyCalendarPage( + startTime: + selectedRoom?.bookableConfig.startTime, + endTime: selectedRoom?.bookableConfig.endTime, + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + ); + }, ); }, ), diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart new file mode 100644 index 00000000..1796f331 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -0,0 +1,251 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/data/services/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BookingSidebar extends StatelessWidget { + final void Function(BookableSpaceModel) onRoomSelected; + + const BookingSidebar({ + super.key, + required this.onRoomSelected, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SidebarBloc(BookableSpacesService( + HTTPService(), + )) + ..add(LoadBookableSpaces()), + child: _SidebarContent(onRoomSelected: onRoomSelected), + ); + } +} + +class _SidebarContent extends StatefulWidget { + final void Function(BookableSpaceModel) onRoomSelected; + + const _SidebarContent({ + required this.onRoomSelected, + }); + + @override + State<_SidebarContent> createState() => __SidebarContentState(); +} + +class __SidebarContentState extends State<_SidebarContent> { + final TextEditingController searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + Timer? _searchDebounce; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchDebounce?.cancel(); + super.dispose(); + } + + void _scrollListener() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + context.read().add(LoadMoreSpaces()); + } + } + + void _handleSearch(String value) { + // Cancel previous debounce timer + _searchDebounce?.cancel(); + + // Set up new debounce timer + _searchDebounce = Timer(const Duration(milliseconds: 300), () { + context.read().add(SearchRoomsEvent(value)); + }); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state.currentPage == 1 && searchController.text.isNotEmpty) { + searchController.clear(); + } + }, + builder: (context, state) { + return Column( + children: [ + const _SidebarHeader(title: 'Spaces'), + Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, -2), + blurRadius: 4, + spreadRadius: 0, + ), + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Container( + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + children: [ + Expanded( + child: TextField( + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager.blackColor, + ), + controller: searchController, + onChanged: _handleSearch, + decoration: InputDecoration( + hintText: 'Search', + suffixIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 20, + height: 20, + child: SvgPicture.asset( + Assets.searchIconUser, + color: ColorsManager.primaryTextColor, + ), + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none), + ), + ), + ), + if (searchController.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + context.read().add(ResetSearch()); + }, + ), + ], + ), + ), + ), + ), + ), + ), + if (state.isLoading) + const Expanded( + child: Center(child: CircularProgressIndicator()), + ) + else if (state.errorMessage != null) + Expanded( + child: Center(child: Text(state.errorMessage!)), + ) + else + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: + state.displayedRooms.length + (state.hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == state.displayedRooms.length) { + return _buildLoadMoreIndicator(state); + } + + final room = state.displayedRooms[index]; + return RoomListItem( + room: room, + isSelected: state.selectedRoomId == room.uuid, + onTap: () { + context + .read() + .add(SelectRoomEvent(room.uuid)); + widget.onRoomSelected(room); + }, + ); + }, + ), + ), + ], + ); + }, + ); + } + + Widget _buildLoadMoreIndicator(SidebarState state) { + if (state.isLoadingMore) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: CircularProgressIndicator()), + ); + } else if (state.hasMore) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: Text('Scroll to load more')), + ); + } else { + return const SizedBox.shrink(); + } + } +} + +class _SidebarHeader extends StatelessWidget { + final String title; + + const _SidebarHeader({ + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.primaryTextColor, + fontSize: 20, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart new file mode 100644 index 00000000..eb758311 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CustomCalendarPage extends StatefulWidget { + final DateTime selectedDate; + final Function(int day, int month, int year) onDateChanged; + + const CustomCalendarPage({ + super.key, + required this.selectedDate, + required this.onDateChanged, + }); + + @override + State createState() => _CustomCalendarPageState(); +} + +class _CustomCalendarPageState extends State { + late DateTime _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.selectedDate; + } + + @override + void didUpdateWidget(CustomCalendarPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedDate != oldWidget.selectedDate) { + setState(() { + _selectedDate = widget.selectedDate; + }); + } + } + + @override + Widget build(BuildContext context) { + final config = CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.single, + selectedDayHighlightColor: const Color(0xFF3B82F6), + selectedDayTextStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + dayTextStyle: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + weekdayLabelTextStyle: const TextStyle( + color: ColorsManager.grey50, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + controlsTextStyle: const TextStyle( + color: Color(0xFF232D3A), + fontWeight: FontWeight.w400, + fontSize: 18, + ), + centerAlignModePicker: false, + disableMonthPicker: true, + firstDayOfWeek: 1, + weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], + ); + + return CalendarDatePicker2( + config: config, + value: [_selectedDate], + onValueChanged: (dates) { + final picked = dates.first; + if (picked != null) { + setState(() { + _selectedDate = picked; + }); + widget.onDateChanged(picked.day, picked.month, picked.year); + } + }, + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart similarity index 87% rename from lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart rename to lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart index afccafdb..c7c660c1 100644 --- a/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart @@ -20,13 +20,13 @@ class SvgTextButton extends StatelessWidget { required this.onPressed, this.backgroundColor = ColorsManager.circleRolesBackground, this.svgColor = const Color(0xFF496EFF), - this.labelColor = Colors.black87, + this.labelColor = Colors.black, this.borderRadius = 10.0, this.boxShadow = const [ BoxShadow( - color: ColorsManager.textGray, - blurRadius: 12, - offset: Offset(0, 4), + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), ), ], this.svgSize = 24.0, @@ -53,15 +53,14 @@ class SvgTextButton extends StatelessWidget { svgAsset, width: svgSize, height: svgSize, - color: svgColor, ), const SizedBox(width: 12), Text( label, style: TextStyle( color: labelColor, - fontSize: 16, - fontWeight: FontWeight.w500, + fontSize: 12, + fontWeight: FontWeight.w400, ), ), ], diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart new file mode 100644 index 00000000..4a4b608d --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoomListItem extends StatelessWidget { + final BookableSpaceModel room; + final bool isSelected; + final VoidCallback onTap; + + const RoomListItem({ + required this.room, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return RadioListTile( + value: room.uuid, + contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16), + groupValue: isSelected ? room.uuid : null, + visualDensity: const VisualDensity(vertical: -4), + onChanged: (value) => onTap(), + activeColor: ColorsManager.primaryColor, + title: Text( + room.spaceName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + fontWeight: FontWeight.w700, + fontSize: 12), + ), + subtitle: Text( + room.virtualLocation, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 10, + fontWeight: FontWeight.w400, + color: ColorsManager.textGray, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart similarity index 90% rename from lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart rename to lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index c6eb2f40..d815ee2e 100644 --- a/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -7,18 +7,32 @@ class WeeklyCalendarPage extends StatelessWidget { final DateTime weekStart; final DateTime selectedDate; final EventController eventController; + final String? startTime; + final String? endTime; const WeeklyCalendarPage({ super.key, required this.weekStart, required this.selectedDate, required this.eventController, + this.startTime, + this.endTime, }); @override Widget build(BuildContext context) { + final startHour = _parseHour(startTime, defaultValue: 0); + final endHour = _parseHour(endTime, defaultValue: 24); + if (endTime == null || endTime!.isEmpty) { + return const Center( + child: Text( + 'Please select a bookable space to view the calendar.', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + ); + } + final weekDays = _getWeekDays(weekStart); - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return LayoutBuilder( builder: (context, constraints) { @@ -27,9 +41,8 @@ class WeeklyCalendarPage extends StatelessWidget { const int totalDays = 7; final double dayColumnWidth = (calendarWidth - timeLineWidth) / totalDays; - final selectedDayIndex = (selectedDate != null) - ? weekDays.indexWhere((d) => isSameDay(d, selectedDate)) - : -1; + final selectedDayIndex = + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return Padding( padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( @@ -38,8 +51,8 @@ class WeeklyCalendarPage extends StatelessWidget { key: ValueKey(weekStart), controller: eventController, initialDay: weekStart, - startHour: 7, - endHour: 18, + startHour: startHour - 1, + endHour: endHour, heightPerMinute: 1.1, showLiveTimeLineInAllDays: false, showVerticalLines: true, @@ -191,7 +204,6 @@ class WeeklyCalendarPage extends StatelessWidget { ); }, ), - // Highlight the selected day column if (selectedDayIndex >= 0) Positioned( left: timeLineWidth + dayColumnWidth * selectedDayIndex, @@ -234,3 +246,15 @@ class WeeklyCalendarPage extends StatelessWidget { bool isSameDay(DateTime d1, DateTime d2) { return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day; } + +int _parseHour(String? time, {required int defaultValue}) { + if (time == null || time.isEmpty || !time.contains(':')) { + return defaultValue; + } + try { + return int.parse(time.split(':')[0]); + } catch (e) { + // Optionally log the error, e.g., print('Error parsing time: $e'); + return defaultValue; + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart deleted file mode 100644 index 2849da5d..00000000 --- a/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/room_list_item.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; - -class BookingSidebar extends StatelessWidget { - final void Function(int) onRoomSelected; - - const BookingSidebar({ - super.key, - required this.onRoomSelected, - }); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SidebarBloc()..add(LoadRoomsEvent()), - child: _SidebarContent(onRoomSelected: onRoomSelected), - ); - } -} - -class _SidebarContent extends StatelessWidget { - final void Function(int) onRoomSelected; - - const _SidebarContent({ - required this.onRoomSelected, - }); - - @override - Widget build(BuildContext context) { - final TextEditingController searchController = TextEditingController(); - - return Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(3, 0), - blurRadius: 6, - spreadRadius: 0, - ), - ], - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - const _SidebarHeader(title: 'Spaces'), - Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(0, -2), - blurRadius: 4, - spreadRadius: 0, - ), - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(0, 2), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 8.0), - child: Container( - decoration: BoxDecoration( - color: ColorsManager.counterBackgroundColor, - borderRadius: BorderRadius.circular(8.0), - ), - child: TextField( - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - color: ColorsManager.blackColor, - ), - controller: searchController, - onChanged: (value) { - context - .read() - .add(SearchRoomsEvent(value)); - }, - decoration: InputDecoration( - hintText: 'Search', - suffixIcon: Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: 20, - height: 20, - child: SvgPicture.asset( - Assets.searchIconUser, - color: ColorsManager.primaryTextColor, - ), - ), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 12), - border: const OutlineInputBorder( - borderSide: BorderSide.none), - ), - ), - ), - ), - ), - ), - ), - if (state.isLoading) - const Expanded( - child: Center(child: CircularProgressIndicator()), - ) - else if (state.errorMessage != null) - Expanded( - child: Center(child: Text(state.errorMessage!)), - ) - else - Expanded( - child: ListView.builder( - itemCount: state.displayedRooms.length, - itemBuilder: (context, index) { - final room = state.displayedRooms[index]; - return RoomListItem( - room: room, - isSelected: state.selectedRoomId == room.id, - onTap: () { - context - .read() - .add(SelectRoomEvent(room.id)); - onRoomSelected(room.id); - }, - ); - }, - ), - ), - ], - ); - }, - ), - ); - } -} - -class _SidebarHeader extends StatelessWidget { - final String title; - - const _SidebarHeader({ - required this.title, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.primaryTextColor, - fontSize: 20, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart deleted file mode 100644 index a523ae61..00000000 --- a/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomCalendarPage extends StatefulWidget { - final DateTime selectedDate; - final Function(int day, int month, int year) onDateChanged; - - const CustomCalendarPage({ - super.key, - required this.selectedDate, - required this.onDateChanged, - }); - - @override - State createState() => _CustomCalendarPageState(); -} - -class _CustomCalendarPageState extends State { - late DateTime _selectedDate; - - @override - void initState() { - super.initState(); - _selectedDate = widget.selectedDate; - } - - @override - void didUpdateWidget(CustomCalendarPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.selectedDate != oldWidget.selectedDate) { - setState(() { - _selectedDate = widget.selectedDate; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - Expanded( - child: CalendarDatePicker( - initialDate: _selectedDate, - firstDate: DateTime(2000), - lastDate: DateTime(2100), - onDateChanged: (date) { - widget.onDateChanged(date.day, date.month, date.year); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart deleted file mode 100644 index 40f148ff..00000000 --- a/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart +++ /dev/null @@ -1,51 +0,0 @@ - -import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class RoomListItem extends StatelessWidget { - final BookableRoom room; - final bool isSelected; - final VoidCallback onTap; - - const RoomListItem({ - required this.room, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - hoverColor: ColorsManager.primaryColor.withOpacity(0.05), - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - IgnorePointer( - child: Radio( - value: room.id, - groupValue: isSelected ? room.id : null, - onChanged: (value) {}, - activeColor: ColorsManager.primaryColor, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - room.name, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: ColorsManager.textGray, - fontWeight: - isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index e035d252..4e31f23f 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart'; import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/services/access_mang_api.dart b/lib/services/access_mang_api.dart index a780a12b..db3cb554 100644 --- a/lib/services/access_mang_api.dart +++ b/lib/services/access_mang_api.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 40fca1fa..a36d1193 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -84,4 +84,5 @@ abstract class ColorsManager { static const Color minBlue = Color(0xFF93AAFD); static const Color minBlueDot = Color(0xFF023DFE); static const Color grey25 = Color(0xFFF9F9F9); + static const Color grey50 = Color(0xFF718096); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index eb7b6a3e..f908db85 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,7 +46,8 @@ 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 getCommunityListv2 = + '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity = @@ -138,4 +139,6 @@ abstract class ApiEndpoints { static const String assignDeviceToRoom = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}'; + + static const String getBookableSpaces = '/bookable-spaces'; } diff --git a/pubspec.yaml b/pubspec.yaml index cba59019..67bf5328 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,8 +63,8 @@ dependencies: bloc: ^9.0.0 geocoding: ^4.0.0 gauge_indicator: ^0.4.3 - # syncfusion_flutter_calendar: ^30.1.38 calendar_view: ^1.4.0 + calendar_date_picker2: ^2.0.1 From 6cac94a1c4f16b45db1927e691e1fa6909648327 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 16:23:39 +0300 Subject: [PATCH 097/105] Clean up booking system code: remove commented-out code and unnecessary variables for improved readability --- .../booking_system/domain/models/bookable_room.dart | 1 - .../booking_system/domain/models/product.dart | 0 .../domain/services/DebouncedBookingSystemService.dart | 1 - .../presentation/bloc/sidebar/sidebar_event.dart | 2 -- .../booking_system/presentation/view/booking_page.dart | 1 - .../presentation/view/widgets/booking_sidebar.dart | 3 --- .../presentation/view/widgets/weekly_calendar_page.dart | 2 +- 7 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 lib/pages/access_management/booking_system/domain/models/product.dart diff --git a/lib/pages/access_management/booking_system/domain/models/bookable_room.dart b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart index b8aa58b9..c7dbd0aa 100644 --- a/lib/pages/access_management/booking_system/domain/models/bookable_room.dart +++ b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart @@ -1,4 +1,3 @@ -// bookable_space_model.dart class BookableSpaceModel { final String uuid; final String spaceName; diff --git a/lib/pages/access_management/booking_system/domain/models/product.dart b/lib/pages/access_management/booking_system/domain/models/product.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart index 40e4e5ab..252810e2 100644 --- a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart +++ b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart @@ -9,7 +9,6 @@ class DebouncedBookingSystemService implements BookingSystemService { Timer? _debounceTimer; Completer? _lastCompleter; - // Store last parameters int? _lastPage; int? _lastSize; bool? _lastIncludeSpaces; diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart index 5dd1a3c8..770e7b7e 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart @@ -14,12 +14,10 @@ class SearchRoomsEvent extends SidebarEvent { SearchRoomsEvent(this.query); } -// Add these to your sidebar_event.dart file class LoadMoreSpaces extends SidebarEvent {} class ResetSearch extends SidebarEvent {} -// Add to sidebar_event.dart class ExecuteSearch extends SidebarEvent { final String query; diff --git a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart index 522b0f25..c1c81ef4 100644 --- a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -216,7 +216,6 @@ class _BookingPageState extends State { child: BlocBuilder( builder: (context, roomState) { - // NOTE: Assuming `SelectedBookableSpaceState` has a `selectedBookableSpace` property. final selectedRoom = roomState.selectedBookableSpace; return BlocBuilder( diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart index 1796f331..778822f8 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -69,10 +69,7 @@ class __SidebarContentState extends State<_SidebarContent> { } void _handleSearch(String value) { - // Cancel previous debounce timer _searchDebounce?.cancel(); - - // Set up new debounce timer _searchDebounce = Timer(const Duration(milliseconds: 300), () { context.read().add(SearchRoomsEvent(value)); }); diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index d815ee2e..9b521368 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -253,8 +253,8 @@ int _parseHour(String? time, {required int defaultValue}) { } try { return int.parse(time.split(':')[0]); + } catch (e) { - // Optionally log the error, e.g., print('Error parsing time: $e'); return defaultValue; } } From d6a48850a7847df64a75e037de1d117d268c56e7 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 16:25:21 +0300 Subject: [PATCH 098/105] Remove debug print statement from BookableSpacesService response handling --- .../booking_system/data/services/bookable_spaces_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart index 5cc0decb..2f3e08b8 100644 --- a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart +++ b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart @@ -28,7 +28,6 @@ class BookableSpacesService implements BookingSystemService { if (search.isNotEmpty && search != 'null') 'search': search, }, expectedResponseModel: (json) { - print('Response JSON: $json'); return PaginatedBookableSpaces.fromJson( json as Map, ); From 5c90d5f6b939157d102876a8de19bc7a56109a46 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 16:40:57 +0300 Subject: [PATCH 099/105] Refactor SidebarBloc: simplify room data handling by directly using paginatedSpaces.data --- .../bloc/sidebar/sidebar_bloc.dart | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart index 9abf032a..3aa13192 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -41,25 +41,9 @@ class SidebarBloc extends Bloc { search: _currentSearch, ); - final rooms = paginatedSpaces.data.map((space) { - return BookableSpaceModel( - uuid: space.uuid, - spaceName: space.spaceName, - virtualLocation: space.virtualLocation, - bookableConfig: BookableConfig( - uuid: space.bookableConfig.uuid, - daysAvailable: space.bookableConfig.daysAvailable, - startTime: space.bookableConfig.startTime, - endTime: space.bookableConfig.endTime, - active: space.bookableConfig.active, - points: space.bookableConfig.points, - ), - ); - }).toList(); - emit(state.copyWith( - allRooms: rooms, - displayedRooms: rooms, + allRooms: paginatedSpaces.data, + displayedRooms: paginatedSpaces.data, isLoading: false, hasMore: paginatedSpaces.hasNext, totalPages: paginatedSpaces.totalPage, @@ -89,23 +73,7 @@ class SidebarBloc extends Bloc { search: _currentSearch, ); - final newRooms = paginatedSpaces.data.map((space) { - return BookableSpaceModel( - uuid: space.uuid, - spaceName: space.spaceName, - virtualLocation: space.virtualLocation, - bookableConfig: BookableConfig( - uuid: space.bookableConfig.uuid, - daysAvailable: space.bookableConfig.daysAvailable, - startTime: space.bookableConfig.startTime, - endTime: space.bookableConfig.endTime, - active: space.bookableConfig.active, - points: space.bookableConfig.points, - ), - ); - }).toList(); - - final updatedRooms = [...state.allRooms, ...newRooms]; + final updatedRooms = [...state.allRooms, ...paginatedSpaces.data]; emit(state.copyWith( allRooms: updatedRooms, From 2d16bda61d22dc01353bcb77f4c5054d1c8cb94c Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 16:41:31 +0300 Subject: [PATCH 100/105] Refactor SidebarBloc: streamline room data handling by using paginatedSpaces.data directly --- .../bloc/sidebar/sidebar_bloc.dart | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart index 3aa13192..0b9c9295 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart'; @@ -104,24 +103,10 @@ class SidebarBloc extends Bloc { size: _pageSize, search: _currentSearch, ); - final rooms = paginatedSpaces.data.map((space) { - return BookableSpaceModel( - uuid: space.uuid, - spaceName: space.spaceName, - virtualLocation: space.virtualLocation, - bookableConfig: BookableConfig( - uuid: space.bookableConfig.uuid, - daysAvailable: space.bookableConfig.daysAvailable, - startTime: space.bookableConfig.startTime, - endTime: space.bookableConfig.endTime, - active: space.bookableConfig.active, - points: space.bookableConfig.points, - ), - ); - }).toList(); + emit(state.copyWith( - allRooms: rooms, - displayedRooms: rooms, + allRooms: paginatedSpaces.data, + displayedRooms: paginatedSpaces.data, isLoading: false, hasMore: paginatedSpaces.hasNext, totalPages: paginatedSpaces.totalPage, From 3e95bf4473adc96298de910d6f327c02a884ab09 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 10:56:10 +0300 Subject: [PATCH 101/105] Refactor booking system: replace individual parameters with LoadBookableSpacesParam for improved clarity and maintainability --- .../services/bookable_spaces_service.dart | 37 ++- .../domain/load_bookable_spaces_param.dart | 36 +++ .../DebouncedBookingSystemService.dart | 26 +- .../services/booking_system_service.dart | 8 +- .../bloc/sidebar/sidebar_bloc.dart | 31 +- .../view/widgets/weekly_calendar_page.dart | 305 +++++++++--------- 6 files changed, 244 insertions(+), 199 deletions(-) create mode 100644 lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart diff --git a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart index 2f3e08b8..72efcc1b 100644 --- a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart +++ b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.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'; @@ -13,25 +14,29 @@ class BookableSpacesService implements BookingSystemService { @override Future getBookableSpaces({ - required int page, - required int size, - required String search, + required LoadCommunitiesParam param, }) async { try { final response = await _httpService.get( - path: ApiEndpoints.getBookableSpaces, - queryParameters: { - 'page': page, - 'size': size, - 'active': true, - 'configured': true, - if (search.isNotEmpty && search != 'null') 'search': search, - }, - expectedResponseModel: (json) { - return PaginatedBookableSpaces.fromJson( - json as Map, - ); - }); + path: ApiEndpoints.getBookableSpaces, + queryParameters: { + 'page': param.page, + 'size': param.size, + 'active': true, + 'configured': true, + if (param.search != null && + param.search.isNotEmpty && + param.search != 'null') + 'search': param.search, + if (param.includeSpaces != null) + 'includeSpaces': param.includeSpaces, + }, + expectedResponseModel: (json) { + return PaginatedBookableSpaces.fromJson( + json as Map, + ); + }, + ); return response; } on DioException catch (e) { final responseData = e.response?.data; diff --git a/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart b/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart new file mode 100644 index 00000000..f2b2e5fe --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +class LoadBookableSpacesParam extends Equatable { + const LoadBookableSpacesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.active = true, + this.configured = true, + }); + + final int page; + final int size; + final String search; + final bool active; + final bool configured; + + LoadBookableSpacesParam copyWith({ + int? page, + int? size, + String? search, + bool? active, + bool? configured, + }) { + return LoadBookableSpacesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + active: active ?? this.active, + configured: configured ?? this.configured, + ); + } + + @override + List get props => [page, size, search, active, configured]; +} diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart index 252810e2..66a2c01f 100644 --- a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart +++ b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; class DebouncedBookingSystemService implements BookingSystemService { final BookingSystemService _inner; @@ -9,11 +11,6 @@ class DebouncedBookingSystemService implements BookingSystemService { Timer? _debounceTimer; Completer? _lastCompleter; - int? _lastPage; - int? _lastSize; - bool? _lastIncludeSpaces; - String? _lastSearch; - DebouncedBookingSystemService( this._inner, { this.debounceDuration = const Duration(milliseconds: 500), @@ -21,27 +18,20 @@ class DebouncedBookingSystemService implements BookingSystemService { @override Future getBookableSpaces({ - required int page, - required int size, - required String search, + required LoadCommunitiesParam param, }) { _debounceTimer?.cancel(); - _lastCompleter?.completeError(StateError("Cancelled by new search")); + if (_lastCompleter != null && !_lastCompleter!.isCompleted) { + _lastCompleter! + .completeError(StateError("Cancelled by new search")); + } final completer = Completer(); _lastCompleter = completer; - _lastPage = page; - _lastSize = size; - _lastSearch = search; - _debounceTimer = Timer(debounceDuration, () async { try { - final result = await _inner.getBookableSpaces( - page: _lastPage!, - size: _lastSize!, - search: _lastSearch!, - ); + final result = await _inner.getBookableSpaces(param: param); if (!completer.isCompleted) { completer.complete(result); } diff --git a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart index 40a9a8e4..b6d82d23 100644 --- a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart +++ b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart @@ -1,10 +1,8 @@ import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; abstract class BookingSystemService { Future getBookableSpaces({ - required int page, - required int size, - required String search, - + required LoadCommunitiesParam param, }); -} \ No newline at end of file +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart index 0b9c9295..874971de 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -3,10 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; class SidebarBloc extends Bloc { final BookingSystemService _bookingService; - Timer? _searchDebounce; int _currentPage = 1; final int _pageSize = 20; String _currentSearch = ''; @@ -35,9 +35,11 @@ class SidebarBloc extends Bloc { _currentSearch = ''; final paginatedSpaces = await _bookingService.getBookableSpaces( - page: _currentPage, - size: _pageSize, - search: _currentSearch, + param: LoadCommunitiesParam( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ), ); emit(state.copyWith( @@ -67,9 +69,12 @@ class SidebarBloc extends Bloc { _currentPage++; final paginatedSpaces = await _bookingService.getBookableSpaces( - page: _currentPage, - size: _pageSize, - search: _currentSearch, + param: LoadCommunitiesParam( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + // Add any other required params + ), ); final updatedRooms = [...state.allRooms, ...paginatedSpaces.data]; @@ -79,6 +84,7 @@ class SidebarBloc extends Bloc { displayedRooms: updatedRooms, isLoadingMore: false, hasMore: paginatedSpaces.hasNext, + totalPages: paginatedSpaces.totalPage, currentPage: _currentPage, )); } catch (e) { @@ -99,11 +105,13 @@ class SidebarBloc extends Bloc { _currentPage = 1; emit(state.copyWith(isLoading: true, errorMessage: null)); final paginatedSpaces = await _bookingService.getBookableSpaces( - page: _currentPage, - size: _pageSize, - search: _currentSearch, + param: LoadCommunitiesParam( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + // Add other fields if required + ), ); - emit(state.copyWith( allRooms: paginatedSpaces.data, displayedRooms: paginatedSpaces.data, @@ -137,7 +145,6 @@ class SidebarBloc extends Bloc { @override Future close() { - _searchDebounce?.cancel(); return super.close(); } } diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index 9b521368..fd086de5 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -40,181 +40,191 @@ class WeeklyCalendarPage extends StatelessWidget { const double timeLineWidth = 80; const int totalDays = 7; final double dayColumnWidth = - (calendarWidth - timeLineWidth) / totalDays; + (calendarWidth - timeLineWidth) / totalDays - 0.1; final selectedDayIndex = weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return Padding( padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( children: [ - WeekView( - key: ValueKey(weekStart), - controller: eventController, - initialDay: weekStart, - startHour: startHour - 1, - endHour: endHour, - heightPerMinute: 1.1, - showLiveTimeLineInAllDays: false, - showVerticalLines: true, - emulateVerticalOffsetBy: -80, - startDay: WeekDays.monday, - liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( - showBullet: false, - height: 0, - ), - weekDayBuilder: (date) { - final weekDays = _getWeekDays(weekStart); - final selectedDayIndex = - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); - final index = weekDays.indexWhere((d) => isSameDay(d, date)); - final isSelectedDay = index == selectedDayIndex; - final isToday = isSameDay(date, DateTime.now()); - - return Container( - decoration: isSelectedDay - ? BoxDecoration( - color: ColorsManager.blue1.withOpacity(0.2), - borderRadius: BorderRadius.circular(6), - ) - : isToday - ? BoxDecoration( - color: ColorsManager.blue1.withOpacity(0.08), - borderRadius: BorderRadius.circular(6), - ) - : null, - child: Column( - children: [ - Text( - DateFormat('EEE').format(date).toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - color: isSelectedDay ? Colors.blue : Colors.black, - ), - ), - Text( - DateFormat('d').format(date), - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20, - color: isSelectedDay - ? ColorsManager.blue1 - : ColorsManager.blackColor, - ), - ), - ], + AbsorbPointer( + absorbing: false, + ignoringSemantics: false, + child: WeekView( + scrollPhysics: const NeverScrollableScrollPhysics(), + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: startHour - 1, + endHour: endHour, + heightPerMinute: 1.1, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -80, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, ), - ); - }, - timeLineBuilder: (date) { - int hour = date.hour == 0 - ? 12 - : (date.hour > 12 ? date.hour - 12 : date.hour); - String period = date.hour >= 12 ? 'PM' : 'AM'; - return Container( - height: 60, - alignment: Alignment.center, - child: RichText( - text: TextSpan( + weekDayBuilder: (date) { + final weekDays = _getWeekDays(weekStart); + final selectedDayIndex = weekDays + .indexWhere((d) => isSameDay(d, selectedDate)); + final index = + weekDays.indexWhere((d) => isSameDay(d, date)); + final isSelectedDay = index == selectedDayIndex; + final isToday = isSameDay(date, DateTime.now()); + + return Column( children: [ - TextSpan( - text: '$hour', - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 24, - color: ColorsManager.blackColor, + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, ), ), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(left: 2, top: 6), - child: Text( - period, + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: isSelectedDay + ? ColorsManager.blue1 + : ColorsManager.blackColor, + ), + ), + ], + ); + }, + timeLineBuilder: (date) { + int hour = date.hour == 0 + ? 12 + : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$hour', style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 12, + fontWeight: FontWeight.w700, + fontSize: 24, color: ColorsManager.blackColor, - letterSpacing: 1, ), ), - ), - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, + WidgetSpan( + child: Padding( + padding: + const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.blackColor, + letterSpacing: 1, + ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + ), + ], + ), + ), + ); + }, + timeLineWidth: timeLineWidth, + weekPageHeaderBuilder: (start, end) => Container(), + weekTitleHeight: 60, + weekNumberBuilder: (firstDayOfWeek) => Padding( + padding: const EdgeInsets.only( + right: 15, + bottom: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + firstDayOfWeek.timeZoneName.replaceAll(':00', ''), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + fontSize: 12, + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + ), ), ], ), ), - ); - }, - timeLineWidth: timeLineWidth, - weekPageHeaderBuilder: (start, end) => Container(), - weekTitleHeight: 60, - weekNumberBuilder: (firstDayOfWeek) => Text( - firstDayOfWeek.timeZoneName, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), - eventTileBuilder: (date, events, boundary, start, end) { - return Container( - margin: - const EdgeInsets.symmetric(vertical: 2, horizontal: 2), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: events.map((event) { - final bool isEventEnded = event.endTime != null && - event.endTime!.isBefore(DateTime.now()); - return Expanded( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: isEventEnded - ? ColorsManager.grayColor - : ColorsManager.lightGrayColor - .withOpacity(0.25), - borderRadius: BorderRadius.circular(6), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('h:mm a').format(event.startTime!), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.black87, - ), + eventTileBuilder: (date, events, boundary, start, end) { + return Container( + margin: const EdgeInsets.symmetric( + vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = event.endTime != null && + event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.lightGrayBorderColor + : ColorsManager.blue1.withOpacity(0.25), + borderRadius: BorderRadius.circular(6), ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a') + .format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], ), - ], - ), - ), - ); - }).toList(), - ), - ); - }, - ), + ), + ); + }).toList(), + ), + ); + }, + )), if (selectedDayIndex >= 0) Positioned( - left: timeLineWidth + dayColumnWidth * selectedDayIndex, + left: (timeLineWidth + 3) + + (dayColumnWidth - 8) * (selectedDayIndex - 0.01), top: 0, bottom: 0, width: dayColumnWidth, child: IgnorePointer( child: Container( margin: const EdgeInsets.symmetric( - vertical: 0, horizontal: 2), - color: ColorsManager.blue1.withOpacity(0.1), + vertical: 0, horizontal: 4), + color: ColorsManager.spaceColor.withOpacity(0.07), ), ), ), @@ -253,7 +263,6 @@ int _parseHour(String? time, {required int defaultValue}) { } try { return int.parse(time.split(':')[0]); - } catch (e) { return defaultValue; } From 2b638940aef59e18a2b32490d8fb01dbdc0401a6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 11:14:30 +0300 Subject: [PATCH 102/105] Refactor booking system: enhance parameter handling for improved clarity and maintainability --- .../view/widgets/weekly_calendar_page.dart | 297 +++++++++--------- 1 file changed, 144 insertions(+), 153 deletions(-) diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index fd086de5..8a6d898f 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -47,172 +47,163 @@ class WeeklyCalendarPage extends StatelessWidget { padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( children: [ - AbsorbPointer( - absorbing: false, - ignoringSemantics: false, - child: WeekView( - scrollPhysics: const NeverScrollableScrollPhysics(), - key: ValueKey(weekStart), - controller: eventController, - initialDay: weekStart, - startHour: startHour - 1, - endHour: endHour, - heightPerMinute: 1.1, - showLiveTimeLineInAllDays: false, - showVerticalLines: true, - emulateVerticalOffsetBy: -80, - startDay: WeekDays.monday, - liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( - showBullet: false, - height: 0, - ), - weekDayBuilder: (date) { - final weekDays = _getWeekDays(weekStart); - final selectedDayIndex = weekDays - .indexWhere((d) => isSameDay(d, selectedDate)); - final index = - weekDays.indexWhere((d) => isSameDay(d, date)); - final isSelectedDay = index == selectedDayIndex; - final isToday = isSameDay(date, DateTime.now()); + WeekView( + pageViewPhysics: const NeverScrollableScrollPhysics(), + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: startHour - 1, + endHour: endHour, + heightPerMinute: 1.1, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -80, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, + ), + weekDayBuilder: (date) { + final weekDays = _getWeekDays(weekStart); + final selectedDayIndex = + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + final index = weekDays.indexWhere((d) => isSameDay(d, date)); + final isSelectedDay = index == selectedDayIndex; + final isToday = isSameDay(date, DateTime.now()); - return Column( - children: [ - Text( - DateFormat('EEE').format(date).toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - color: isSelectedDay ? Colors.blue : Colors.black, - ), - ), - Text( - DateFormat('d').format(date), - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20, - color: isSelectedDay - ? ColorsManager.blue1 - : ColorsManager.blackColor, - ), - ), - ], - ); - }, - timeLineBuilder: (date) { - int hour = date.hour == 0 - ? 12 - : (date.hour > 12 ? date.hour - 12 : date.hour); - String period = date.hour >= 12 ? 'PM' : 'AM'; - return Container( - height: 60, - alignment: Alignment.center, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: '$hour', - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 24, - color: ColorsManager.blackColor, - ), - ), - WidgetSpan( - child: Padding( - padding: - const EdgeInsets.only(left: 2, top: 6), - child: Text( - period, - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 12, - color: ColorsManager.blackColor, - letterSpacing: 1, - ), - ), - ), - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, - ), - ], - ), + return Column( + children: [ + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, ), - ); - }, - timeLineWidth: timeLineWidth, - weekPageHeaderBuilder: (start, end) => Container(), - weekTitleHeight: 60, - weekNumberBuilder: (firstDayOfWeek) => Padding( - padding: const EdgeInsets.only( - right: 15, - bottom: 10, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: isSelectedDay + ? ColorsManager.blue1 + : ColorsManager.blackColor, + ), + ), + ], + ); + }, + timeLineBuilder: (date) { + int hour = date.hour == 0 + ? 12 + : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( children: [ - Text( - firstDayOfWeek.timeZoneName.replaceAll(':00', ''), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( + TextSpan( + text: '$hour', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: ColorsManager.blackColor, + ), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, fontSize: 12, color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, + letterSpacing: 1, ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, ), ], ), ), - eventTileBuilder: (date, events, boundary, start, end) { - return Container( - margin: const EdgeInsets.symmetric( - vertical: 2, horizontal: 2), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: events.map((event) { - final bool isEventEnded = event.endTime != null && - event.endTime!.isBefore(DateTime.now()); - return Expanded( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: isEventEnded - ? ColorsManager.lightGrayBorderColor - : ColorsManager.blue1.withOpacity(0.25), - borderRadius: BorderRadius.circular(6), + ); + }, + timeLineWidth: timeLineWidth, + weekPageHeaderBuilder: (start, end) => Container(), + weekTitleHeight: 60, + weekNumberBuilder: (firstDayOfWeek) => Padding( + padding: const EdgeInsets.only( + right: 15, + bottom: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + firstDayOfWeek.timeZoneName.replaceAll(':00', ''), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 12, + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + eventTileBuilder: (date, events, boundary, start, end) { + return Container( + margin: + const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = event.endTime != null && + event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.lightGrayBorderColor + : ColorsManager.blue1.withOpacity(0.25), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a').format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('h:mm a') - .format(event.startTime!), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.black87, - ), - ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - ], + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), ), - ), - ); - }).toList(), - ), - ); - }, - )), + ], + ), + ), + ); + }).toList(), + ), + ); + }, + ), if (selectedDayIndex >= 0) Positioned( left: (timeLineWidth + 3) + From bfd6b5c3a07697184f6af8d4fb406c973c72b18a Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 11:25:35 +0300 Subject: [PATCH 103/105] Refactor booking system: replace BookingSystemService with BookableSystemService and update parameter handling for improved clarity --- ...ce.dart => remote_bookable_spaces_service.dart} | 12 +++++------- .../services/DebouncedBookingSystemService.dart | 14 ++++++-------- .../domain/services/bookable_system_service.dart | 8 ++++++++ .../domain/services/booking_system_service.dart | 8 -------- .../presentation/bloc/sidebar/sidebar_bloc.dart | 14 ++++++-------- .../presentation/view/widgets/booking_sidebar.dart | 4 ++-- 6 files changed, 27 insertions(+), 33 deletions(-) rename lib/pages/access_management/booking_system/data/services/{bookable_spaces_service.dart => remote_bookable_spaces_service.dart} (80%) create mode 100644 lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart delete mode 100644 lib/pages/access_management/booking_system/domain/services/booking_system_service.dart diff --git a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart similarity index 80% rename from lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart rename to lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart index 72efcc1b..3c2610db 100644 --- a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart +++ b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart @@ -1,20 +1,20 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; -class BookableSpacesService implements BookingSystemService { - const BookableSpacesService(this._httpService); +class RemoteBookableSpacesService implements BookableSystemService { + const RemoteBookableSpacesService(this._httpService); final HTTPService _httpService; static const _defaultErrorMessage = 'Failed to load bookable spaces'; @override Future getBookableSpaces({ - required LoadCommunitiesParam param, + required LoadBookableSpacesParam param, }) async { try { final response = await _httpService.get( @@ -28,8 +28,6 @@ class BookableSpacesService implements BookingSystemService { param.search.isNotEmpty && param.search != 'null') 'search': param.search, - if (param.includeSpaces != null) - 'includeSpaces': param.includeSpaces, }, expectedResponseModel: (json) { return PaginatedBookableSpaces.fromJson( diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart index 66a2c01f..a251a9c1 100644 --- a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart +++ b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:meta/meta.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart'; -class DebouncedBookingSystemService implements BookingSystemService { - final BookingSystemService _inner; +class DebouncedBookingSystemService implements BookableSystemService { + final BookableSystemService _inner; final Duration debounceDuration; Timer? _debounceTimer; @@ -18,12 +17,11 @@ class DebouncedBookingSystemService implements BookingSystemService { @override Future getBookableSpaces({ - required LoadCommunitiesParam param, + required LoadBookableSpacesParam param, }) { _debounceTimer?.cancel(); if (_lastCompleter != null && !_lastCompleter!.isCompleted) { - _lastCompleter! - .completeError(StateError("Cancelled by new search")); + _lastCompleter!.completeError(StateError("Cancelled by new search")); } final completer = Completer(); diff --git a/lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart b/lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart new file mode 100644 index 00000000..c3b0bfb7 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; + +abstract class BookableSystemService { + Future getBookableSpaces({ + required LoadBookableSpacesParam param, + }); +} diff --git a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart deleted file mode 100644 index b6d82d23..00000000 --- a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; - -abstract class BookingSystemService { - Future getBookableSpaces({ - required LoadCommunitiesParam param, - }); -} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart index 874971de..1b6f41fa 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; class SidebarBloc extends Bloc { - final BookingSystemService _bookingService; + final BookableSystemService _bookingService; int _currentPage = 1; final int _pageSize = 20; String _currentSearch = ''; @@ -35,7 +35,7 @@ class SidebarBloc extends Bloc { _currentSearch = ''; final paginatedSpaces = await _bookingService.getBookableSpaces( - param: LoadCommunitiesParam( + param: LoadBookableSpacesParam( page: _currentPage, size: _pageSize, search: _currentSearch, @@ -69,11 +69,10 @@ class SidebarBloc extends Bloc { _currentPage++; final paginatedSpaces = await _bookingService.getBookableSpaces( - param: LoadCommunitiesParam( + param: LoadBookableSpacesParam( page: _currentPage, size: _pageSize, search: _currentSearch, - // Add any other required params ), ); @@ -105,11 +104,10 @@ class SidebarBloc extends Bloc { _currentPage = 1; emit(state.copyWith(isLoading: true, errorMessage: null)); final paginatedSpaces = await _bookingService.getBookableSpaces( - param: LoadCommunitiesParam( + param: LoadBookableSpacesParam( page: _currentPage, size: _pageSize, search: _currentSearch, - // Add other fields if required ), ); emit(state.copyWith( diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart index 778822f8..894e2030 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/data/services/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; @@ -23,7 +23,7 @@ class BookingSidebar extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => SidebarBloc(BookableSpacesService( + create: (context) => SidebarBloc(RemoteBookableSpacesService( HTTPService(), )) ..add(LoadBookableSpaces()), From 885ef61114f44a0eedbb278fc6a3205a42e92ffb Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 12:10:28 +0300 Subject: [PATCH 104/105] Refactor booking system: replace DebouncedBookingSystemService with DebouncedBookableSpacesService and streamline search handling --- ...ervice.dart => debounced_bookable_spaces_service.dart} | 4 ++-- .../presentation/view/widgets/booking_sidebar.dart | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) rename lib/pages/access_management/booking_system/domain/services/{DebouncedBookingSystemService.dart => debounced_bookable_spaces_service.dart} (92%) diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/debounced_bookable_spaces_service.dart similarity index 92% rename from lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart rename to lib/pages/access_management/booking_system/domain/services/debounced_bookable_spaces_service.dart index a251a9c1..bea3c103 100644 --- a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart +++ b/lib/pages/access_management/booking_system/domain/services/debounced_bookable_spaces_service.dart @@ -3,14 +3,14 @@ import 'package:syncrow_web/pages/access_management/booking_system/domain/load_b import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart'; -class DebouncedBookingSystemService implements BookableSystemService { +class DebouncedBookableSpacesService implements BookableSystemService { final BookableSystemService _inner; final Duration debounceDuration; Timer? _debounceTimer; Completer? _lastCompleter; - DebouncedBookingSystemService( + DebouncedBookableSpacesService( this._inner, { this.debounceDuration = const Duration(milliseconds: 500), }); diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart index 894e2030..e3d84924 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -46,7 +45,6 @@ class _SidebarContent extends StatefulWidget { class __SidebarContentState extends State<_SidebarContent> { final TextEditingController searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); - Timer? _searchDebounce; @override void initState() { @@ -57,7 +55,6 @@ class __SidebarContentState extends State<_SidebarContent> { @override void dispose() { _scrollController.dispose(); - _searchDebounce?.cancel(); super.dispose(); } @@ -69,10 +66,7 @@ class __SidebarContentState extends State<_SidebarContent> { } void _handleSearch(String value) { - _searchDebounce?.cancel(); - _searchDebounce = Timer(const Duration(milliseconds: 300), () { - context.read().add(SearchRoomsEvent(value)); - }); + context.read().add(SearchRoomsEvent(value)); } @override From 645a07287efac6d24061737ee237970d60b8b55c Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 14:15:57 +0300 Subject: [PATCH 105/105] Refactor date selection: add SelectDateFromSidebarCalendar event and update state management for improved clarity --- .../date_selection/date_selection_bloc.dart | 23 ++++--- .../date_selection/date_selection_event.dart | 7 +- .../date_selection/date_selection_state.dart | 21 ++++-- .../presentation/view/booking_page.dart | 7 ++ .../view/widgets/weekly_calendar_page.dart | 64 ++++++++++++++----- 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart index d6687087..8592433f 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart @@ -2,11 +2,12 @@ import 'package:bloc/bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'date_selection_state.dart'; + class DateSelectionBloc extends Bloc { DateSelectionBloc() : super(DateSelectionState.initial()) { on((event, emit) { final newWeekStart = _getStartOfWeek(event.selectedDate); - emit(DateSelectionState( + emit(state.copyWith( selectedDate: event.selectedDate, weekStart: newWeekStart, )); @@ -14,19 +15,21 @@ class DateSelectionBloc extends Bloc { on((event, emit) { final newWeekStart = state.weekStart.add(const Duration(days: 7)); - final inNewWeek = state.selectedDate - .isAfter(newWeekStart.subtract(const Duration(days: 1))) && - state.selectedDate - .isBefore(newWeekStart.add(const Duration(days: 7))); - emit(DateSelectionState( - selectedDate: state.selectedDate, + emit(state.copyWith( weekStart: newWeekStart, )); }); + on((event, emit) { - emit(DateSelectionState( - selectedDate: state.selectedDate!.subtract(const Duration(days: 7)), - weekStart: state.weekStart.subtract(const Duration(days: 7)), + final newWeekStart = state.weekStart.subtract(const Duration(days: 7)); + emit(state.copyWith( + weekStart: newWeekStart, + )); + }); + + on((event, emit) { + emit(state.copyWith( + selectedDateFromSideBarCalender: event.selectedDate, )); }); } diff --git a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart index 8ed0a8a0..058c0db5 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart @@ -10,4 +10,9 @@ class SelectDate extends DateSelectionEvent { class NextWeek extends DateSelectionEvent {} -class PreviousWeek extends DateSelectionEvent {} \ No newline at end of file +class PreviousWeek extends DateSelectionEvent {} + +class SelectDateFromSidebarCalendar extends DateSelectionEvent { + final DateTime selectedDate; + SelectDateFromSidebarCalendar(this.selectedDate); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart index 3b35ce25..8c839c72 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart @@ -1,21 +1,34 @@ + class DateSelectionState { final DateTime selectedDate; final DateTime weekStart; + final DateTime? selectedDateFromSideBarCalender; - const DateSelectionState({ + DateSelectionState({ required this.selectedDate, required this.weekStart, + this.selectedDateFromSideBarCalender, }); factory DateSelectionState.initial() { final now = DateTime.now(); + final weekStart = now.subtract(Duration(days: now.weekday - 1)); return DateSelectionState( selectedDate: now, - weekStart: _getStartOfWeek(now), + weekStart: weekStart, + selectedDateFromSideBarCalender: null, ); } - static DateTime _getStartOfWeek(DateTime date) { - return date.subtract(Duration(days: date.weekday - 1)); + DateSelectionState copyWith({ + DateTime? selectedDate, + DateTime? weekStart, + DateTime? selectedDateFromSideBarCalender, + }) { + return DateSelectionState( + selectedDate: selectedDate ?? this.selectedDate, + weekStart: weekStart ?? this.weekStart, + selectedDateFromSideBarCalender: selectedDateFromSideBarCalender ?? this.selectedDateFromSideBarCalender, + ); } } diff --git a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart index c1c81ef4..357cac41 100644 --- a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -120,6 +120,9 @@ class _BookingPageState extends State { context .read() .add(SelectDate(newDate)); + context + .read() + .add(SelectDateFromSidebarCalendar(newDate)); }, ); }, @@ -227,6 +230,10 @@ class _BookingPageState extends State { weekStart: dateState.weekStart, selectedDate: dateState.selectedDate, eventController: _eventController, + selectedDateFromSideBarCalender: context + .watch() + .state + .selectedDateFromSideBarCalender, ); }, ); diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index 8a6d898f..5c38e2fc 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -9,6 +9,7 @@ class WeeklyCalendarPage extends StatelessWidget { final EventController eventController; final String? startTime; final String? endTime; + final DateTime? selectedDateFromSideBarCalender; const WeeklyCalendarPage({ super.key, @@ -17,32 +18,55 @@ class WeeklyCalendarPage extends StatelessWidget { required this.eventController, this.startTime, this.endTime, + this.selectedDateFromSideBarCalender, }); @override Widget build(BuildContext context) { final startHour = _parseHour(startTime, defaultValue: 0); final endHour = _parseHour(endTime, defaultValue: 24); + if (endTime == null || endTime!.isEmpty) { return const Center( - child: Text( - 'Please select a bookable space to view the calendar.', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.calendar_today, + color: ColorsManager.lightGrayColor, + size: 80, + ), + SizedBox(height: 20), + Text( + 'Please select a bookable space to view the calendar.', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: ColorsManager.lightGrayColor), + ), + ], ), ); } final weekDays = _getWeekDays(weekStart); + final selectedDayIndex = + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + final selectedSidebarIndex = selectedDateFromSideBarCalender == null + ? -1 + : weekDays + .indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!)); + + const double timeLineWidth = 80; + const int totalDays = 7; + return LayoutBuilder( builder: (context, constraints) { final double calendarWidth = constraints.maxWidth; - const double timeLineWidth = 80; - const int totalDays = 7; final double dayColumnWidth = (calendarWidth - timeLineWidth) / totalDays - 0.1; - final selectedDayIndex = - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + return Padding( padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( @@ -64,13 +88,8 @@ class WeeklyCalendarPage extends StatelessWidget { height: 0, ), weekDayBuilder: (date) { - final weekDays = _getWeekDays(weekStart); - final selectedDayIndex = - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); final index = weekDays.indexWhere((d) => isSameDay(d, date)); final isSelectedDay = index == selectedDayIndex; - final isToday = isSameDay(date, DateTime.now()); - return Column( children: [ Text( @@ -138,10 +157,7 @@ class WeeklyCalendarPage extends StatelessWidget { weekPageHeaderBuilder: (start, end) => Container(), weekTitleHeight: 60, weekNumberBuilder: (firstDayOfWeek) => Padding( - padding: const EdgeInsets.only( - right: 15, - bottom: 10, - ), + padding: const EdgeInsets.only(right: 15, bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, @@ -219,6 +235,22 @@ class WeeklyCalendarPage extends StatelessWidget { ), ), ), + if (selectedSidebarIndex >= 0 && + selectedSidebarIndex != selectedDayIndex) + Positioned( + left: (timeLineWidth + 3) + + (dayColumnWidth - 8) * (selectedSidebarIndex - 0.01), + top: 0, + bottom: 0, + width: dayColumnWidth, + child: IgnorePointer( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 0, horizontal: 4), + color: Colors.orange.withOpacity(0.14), + ), + ), + ), Positioned( right: 0, top: 50,