From dcf1df9b4a82f21e9db1bc35a58298fc1de5a685 Mon Sep 17 00:00:00 2001 From: Rafeek Alkhoudare Date: Wed, 21 May 2025 07:25:34 -0500 Subject: [PATCH 01/89] sp1613 delete condition word --- .../flush_presence_sensor/flush_presence_sensor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart index bf2146ad..35cee5e7 100644 --- a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart +++ b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart @@ -96,7 +96,7 @@ class _WallPresenceSensorState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('Presence Sensor Condition'), + const DialogHeader('Presence Sensor'), Expanded(child: _buildMainContent(context, state)), _buildDialogFooter(context, state), ], From f33b3e8bd2692aa11e6da43efad76aefcc2c9f4c Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Tue, 1 Jul 2025 11:19:35 +0300 Subject: [PATCH 02/89] 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 03/89] 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 b06e4bd2ba7500fac6202908c6efe4025807b7c3 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 2 Jul 2025 08:27:09 +0300 Subject: [PATCH 04/89] 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 05/89] 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 06/89] 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 07/89] 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 08/89] 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 09/89] 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 10/89] 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 11/89] 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 12/89] 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 13/89] 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 14/89] 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 15/89] 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 16/89] 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 17/89] 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 18/89] 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 19/89] 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 20/89] 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 21/89] 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 22/89] 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 23/89] 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 24/89] 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 25/89] 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 26/89] 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 27/89] 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 28/89] 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 29/89] 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 30/89] 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 31/89] 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 32/89] 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 33/89] 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 34/89] 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 35/89] 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 36/89] 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 37/89] 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 38/89] 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 39/89] 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 40/89] 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 41/89] 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 42/89] 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 43/89] 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 44/89] 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 45/89] 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 46/89] 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 47/89] 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 48/89] 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 49/89] 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 50/89] 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 51/89] 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 52/89] 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 53/89] 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 54/89] 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 55/89] 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 56/89] 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 57/89] 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 58/89] 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 fa1eaa570c9158852a394dc5f4a957bf2717580a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 10:01:43 +0300 Subject: [PATCH 59/89] Refactor Space Update Logic: Introduced UpdateSpaceParam for better parameter handling in update operations. Enhanced SpaceDetailsDialogHelper to manage loading and error states during space updates. Updated RemoteUpdateSpaceService to construct dynamic URLs for space updates based on community UUID. Improved CommunitiesTreeFailureWidget UI with SelectableText and added spacing for better layout. --- .../widgets/community_structure_header.dart | 1 + .../communities_tree_failure_widget.dart | 6 +- .../helpers/space_details_dialog_helper.dart | 112 +++++++++++++++--- .../services/remote_update_space_service.dart | 28 ++++- .../domain/params/update_space_param.dart | 11 ++ .../domain/services/update_space_service.dart | 5 +- .../presentation/bloc/update_space_bloc.dart | 3 +- .../presentation/bloc/update_space_event.dart | 6 +- .../presentation/bloc/update_space_state.dart | 6 +- 9 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart 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 4f71075b..5b790514 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 @@ -97,6 +97,7 @@ class CommunityStructureHeader extends StatelessWidget { SpaceDetailsDialogHelper.showEdit( context, spaceModel: selectedSpace!, + communityUuid: selectedCommunity.uuid, ); }, selectedSpace: selectedSpace, diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart index cfd32f52..277347df 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget { return Expanded( child: Center( child: Column( + spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + SelectableText( errorMessage ?? 'Something went wrong', textAlign: TextAlign.center, ), - const SizedBox(height: 16), - ElevatedButton( + FilledButton( onPressed: () => context.read().add( LoadCommunities( LoadCommunitiesParam( 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..229d0dca 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 @@ -2,23 +2,38 @@ 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/domain/models/space_details_model.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/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; abstract final class SpaceDetailsDialogHelper { static void showCreate(BuildContext context) { showDialog( context: context, - builder: (_) => BlocProvider( - create: (context) => SpaceDetailsBloc( - RemoteSpaceDetailsService(httpService: HTTPService()), - ), - child: SpaceDetailsDialog( - context: context, - title: const SelectableText('Create Space'), - spaceModel: SpaceModel.empty(), - onSave: print, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), + BlocProvider( + create: (context) => UpdateSpaceBloc( + RemoteUpdateSpaceService(HTTPService()), + ), + ), + ], + child: Builder( + builder: (context) => SpaceDetailsDialog( + context: context, + title: const SelectableText('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: (space) {}, + ), ), ), ); @@ -27,20 +42,81 @@ abstract final class SpaceDetailsDialogHelper { static void showEdit( BuildContext context, { required SpaceModel spaceModel, + required String communityUuid, }) { showDialog( context: context, - builder: (_) => BlocProvider( - create: (context) => SpaceDetailsBloc( - RemoteSpaceDetailsService(httpService: HTTPService()), - ), - child: SpaceDetailsDialog( - context: context, - title: const SelectableText('Edit Space'), - spaceModel: spaceModel, - onSave: (space) {}, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), + BlocProvider( + create: (context) => UpdateSpaceBloc( + RemoteUpdateSpaceService(HTTPService()), + ), + ), + ], + child: Builder( + builder: (context) => BlocListener( + listener: _updateListener, + child: SpaceDetailsDialog( + context: context, + title: const SelectableText('Edit Space'), + spaceModel: spaceModel, + onSave: (space) => context.read().add( + UpdateSpace( + UpdateSpaceParam( + communityUuid: communityUuid, + space: space, + ), + ), + ), + ), + ), ), ), ); } + + static void _updateListener(BuildContext context, UpdateSpaceState state) { + return switch (state) { + UpdateSpaceInitial() => null, + UpdateSpaceLoading() => _onLoading(context), + UpdateSpaceSuccess(:final space) => _onUpdateSuccess(context, space), + UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage), + }; + } + + static void _onUpdateSuccess(BuildContext context, SpaceDetailsModel space) { + Navigator.of(context).pop(); + } + + static void _onLoading(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ); + } + + static void _onError(BuildContext context, String errorMessage) { + Navigator.of(context).pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + title: const Text('Error'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text('OK'), + ), + ], + ), + ); + } } diff --git a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart index b15e6095..9f6f65a6 100644 --- a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart @@ -1,5 +1,7 @@ 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/update_space/domain/params/update_space_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -12,14 +14,19 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { static const _defaultErrorMessage = 'Failed to update space'; @override - Future updateSpace(SpaceDetailsModel space) async { + Future updateSpace(UpdateSpaceParam param) async { try { + final path = await _makeUrl(param); final response = await _httpService.put( - path: 'endpoint', - body: space.toJson(), - expectedResponseModel: (data) => SpaceDetailsModel.fromJson( - data as Map, - ), + path: path, + body: param.space.toJson(), + expectedResponseModel: (data) { + final response = data as Map; + final space = SpaceDetailsModel.fromJson( + response['data'] as Map, + ); + return space; + }, ); return response; @@ -37,4 +44,13 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl(UpdateSpaceParam 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.space.uuid}'; + } } diff --git a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart new file mode 100644 index 00000000..884976f7 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -0,0 +1,11 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; + +class UpdateSpaceParam { + UpdateSpaceParam({ + required this.space, + required this.communityUuid, + }); + + final SpaceDetailsModel space; + final String communityUuid; +} diff --git a/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart index 29bc9419..c75fc0d4 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart @@ -1,5 +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/update_space/domain/params/update_space_param.dart'; -abstract class UpdateSpaceService { - Future updateSpace(SpaceDetailsModel space); +abstract interface class UpdateSpaceService { + Future updateSpace(UpdateSpaceParam param); } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart index 3bc4e187..0920b547 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart @@ -1,6 +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/update_space/domain/params/update_space_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc { ) async { emit(UpdateSpaceLoading()); try { - final updatedSpace = await _updateSpaceService.updateSpace(event.space); + final updatedSpace = await _updateSpaceService.updateSpace(event.param); emit(UpdateSpaceSuccess(updatedSpace)); } on APIException catch (e) { emit(UpdateSpaceFailure(e.message)); diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart index b7d476af..ec08cdd2 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart @@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable { } final class UpdateSpace extends UpdateSpaceEvent { - const UpdateSpace(this.space); + const UpdateSpace(this.param); - final SpaceDetailsModel space; + final UpdateSpaceParam param; @override - List get props => [space]; + List get props => [param]; } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart index f0bc5a2b..437cca60 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart @@ -21,10 +21,10 @@ final class UpdateSpaceSuccess extends UpdateSpaceState { } final class UpdateSpaceFailure extends UpdateSpaceState { - final String message; + final String errorMessage; - const UpdateSpaceFailure(this.message); + const UpdateSpaceFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } From b001713ce439a392f6d5a1b6e83d16a30f4dfb26 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 11:10:22 +0300 Subject: [PATCH 60/89] Enhance Community Structure Widgets: Updated SpaceDetailsDialogHelper to accept community UUID for space creation and editing. Refactored CreateSpaceButton and CommunityStructureHeader to pass community UUID, improving data handling and consistency across the community structure features. --- .../widgets/community_structure_canvas.dart | 5 ++- .../widgets/community_structure_header.dart | 22 ++++++------ .../widgets/create_space_button.dart | 12 +++++-- .../space_management_community_structure.dart | 10 ++++-- .../helpers/space_details_dialog_helper.dart | 7 +++- .../widgets/space_details_dialog.dart | 9 ++--- .../domain/params/update_space_param.dart | 34 +++++++++++++++++++ 7 files changed, 76 insertions(+), 23 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 4aea103a..8ab0c97b 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -241,7 +241,10 @@ class _CommunityStructureCanvasState extends State ), ); }, - onTap: () => SpaceDetailsDialogHelper.showCreate(context), + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: widget.community.uuid, + ), ), ), ); 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 5b790514..f27dc8b9 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 @@ -55,8 +55,9 @@ class CommunityStructureHeader extends StatelessWidget { children: [ Text( 'Community Structure', - style: theme.textTheme.headlineLarge - ?.copyWith(color: ColorsManager.blackColor), + style: theme.textTheme.headlineLarge?.copyWith( + color: ColorsManager.blackColor, + ), ), if (selectedCommunity != null) Row( @@ -67,8 +68,9 @@ class CommunityStructureHeader extends StatelessWidget { Flexible( child: SelectableText( selectedCommunity.name, - style: theme.textTheme.bodyLarge - ?.copyWith(color: ColorsManager.blackColor), + style: theme.textTheme.bodyLarge?.copyWith( + color: ColorsManager.blackColor, + ), maxLines: 1, ), ), @@ -93,13 +95,11 @@ class CommunityStructureHeader extends StatelessWidget { CommunityStructureHeaderActionButtons( onDelete: (space) {}, onDuplicate: (space) {}, - onEdit: (space) { - SpaceDetailsDialogHelper.showEdit( - context, - spaceModel: selectedSpace!, - communityUuid: selectedCommunity.uuid, - ); - }, + onEdit: (space) => SpaceDetailsDialogHelper.showEdit( + context, + spaceModel: selectedSpace!, + communityUuid: selectedCommunity.uuid, + ), selectedSpace: selectedSpace, ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index 4cbfd7fd..b7259d21 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -3,12 +3,20 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/pres import 'package:syncrow_web/utils/color_manager.dart'; class CreateSpaceButton extends StatelessWidget { - const CreateSpaceButton({super.key}); + const CreateSpaceButton({ + required this.communityUuid, + super.key, + }); + + final String communityUuid; @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => SpaceDetailsDialogHelper.showCreate(context), + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: communityUuid, + ), child: Container( height: 60, decoration: BoxDecoration( diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index e1f1fc00..4c588ec7 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -16,8 +16,14 @@ class SpaceManagementCommunityStructure extends StatelessWidget { const spacer = Spacer(flex: 10); return Visibility( visible: selectedCommunity!.spaces.isNotEmpty, - replacement: const Row( - children: [spacer, Expanded(child: CreateSpaceButton()), spacer], + replacement: Row( + children: [ + spacer, + Expanded( + child: CreateSpaceButton(communityUuid: selectedCommunity.uuid), + ), + spacer + ], ), child: Column( mainAxisSize: MainAxisSize.min, 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 229d0dca..d66d28f4 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 @@ -11,7 +11,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/update_space/prese import 'package:syncrow_web/services/api/http_service.dart'; abstract final class SpaceDetailsDialogHelper { - static void showCreate(BuildContext context) { + static void showCreate( + BuildContext context, { + required String communityUuid, + }) { showDialog( context: context, builder: (_) => MultiBlocProvider( @@ -33,6 +36,7 @@ abstract final class SpaceDetailsDialogHelper { title: const SelectableText('Create Space'), spaceModel: SpaceModel.empty(), onSave: (space) {}, + communityUuid: communityUuid, ), ), ), @@ -74,6 +78,7 @@ abstract final class SpaceDetailsDialogHelper { ), ), ), + communityUuid: communityUuid, ), ), ), 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 ae772036..d97442ec 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,7 +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/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'; @@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget { required this.spaceModel, required this.onSave, required this.context, + required this.communityUuid, super.key, }); @@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget { final SpaceModel spaceModel; final void Function(SpaceDetailsModel space) onSave; final BuildContext context; + final String communityUuid; @override State createState() => _SpaceDetailsDialogState(); @@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State { if (!isCreateMode) { final param = LoadSpaceDetailsParam( spaceUuid: widget.spaceModel.uuid, - communityUuid: widget.context - .read() - .state - .selectedCommunity! - .uuid, + communityUuid: widget.communityUuid, ); widget.context.read().add(LoadSpaceDetails(param)); } diff --git a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart index 884976f7..884cd581 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -8,4 +8,38 @@ class UpdateSpaceParam { final SpaceDetailsModel space; final String communityUuid; + + Map toJson() { + return { + 'spaceName': space.spaceName, + 'icon': space.icon, + 'subspaces': space.subspaces + .map( + (e) => { + 'subspaceName': e.name, + 'productAllocations': e.productAllocations + .map( + (e) => { + 'name': e.tag.name, + 'productUuid': e.product.uuid, + 'uuid': e.uuid, + }, + ) + .toList(), + 'uuid': e.uuid, + }, + ) + .toList(), + 'productAllocations': space.productAllocations + .map( + (e) => { + 'tagName': e.tag.name, + 'tagUuid': e.tag.uuid, + 'productUuid': e.product.uuid, + }, + ) + .toList(), + 'spaceModelUuid': space.uuid, + }; + } } From bcf62027bc38a171be12c5f1d6475fa4e846dd44 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 11:12:12 +0300 Subject: [PATCH 61/89] Validate UUIDs in RemoteUpdateSpaceService: Added checks for empty space and community UUIDs before constructing the update URL, improving error handling and robustness in the update space process. --- .../data/services/remote_update_space_service.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart index 9f6f65a6..452a7375 100644 --- a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart @@ -51,6 +51,16 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { throw APIException('Project UUID is not set'); } - return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.space.uuid}'; + final spaceUuid = param.space.uuid; + if (spaceUuid.isEmpty) { + throw APIException('Space UUID is not set'); + } + + final communityUuid = param.communityUuid; + if (communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid'; } } From 7c2aed2d580cf4898421a8f511b32209aec2f5cd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 12:20:10 +0300 Subject: [PATCH 62/89] Refactor RemoteUpdateSpaceService: Improved error handling in updateSpace method by checking API response success before returning the updated space. This enhances robustness and ensures proper error propagation for failed updates. --- .../data/services/remote_update_space_service.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart index 452a7375..b595e2b9 100644 --- a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart @@ -17,19 +17,20 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { Future updateSpace(UpdateSpaceParam param) async { try { final path = await _makeUrl(param); - final response = await _httpService.put( + await _httpService.put( path: path, body: param.space.toJson(), expectedResponseModel: (data) { final response = data as Map; - final space = SpaceDetailsModel.fromJson( - response['data'] as Map, - ); - return space; + final isSuccess = response['success'] as bool; + if (!isSuccess) { + throw APIException(response['error'] as String); + } + return isSuccess; }, ); - return response; + return param.space; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; From 9e6b14737f5a438f15ac74d20523c73d69a70c3f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 13:07:26 +0300 Subject: [PATCH 63/89] Refactor CreateSpaceButton: Changed from StatelessWidget to StatefulWidget to manage hover state and added tooltip for improved user experience. Enhanced button styling and interaction feedback for better visual cues during space creation. --- .../widgets/create_space_button.dart | 80 ++++++++++++------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index b7259d21..90d359e2 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -class CreateSpaceButton extends StatelessWidget { +class CreateSpaceButton extends StatefulWidget { const CreateSpaceButton({ required this.communityUuid, super.key, @@ -10,38 +10,58 @@ class CreateSpaceButton extends StatelessWidget { final String communityUuid; + @override + State createState() => _CreateSpaceButtonState(); +} + +class _CreateSpaceButtonState extends State { + bool _isHovered = false; + @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => SpaceDetailsDialogHelper.showCreate( - context, - communityUuid: communityUuid, - ), - child: Container( - height: 60, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.5), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset(0, 3), - ), - ], + return Tooltip( + margin: const EdgeInsets.symmetric(vertical: 24), + message: 'Create a new space', + child: GestureDetector( + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: widget.communityUuid, ), - child: Center( - child: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.add, - color: Colors.blue, + child: MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: _isHovered ? 1.0 : 0.45, + child: Container( + width: 150, + height: 90, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.2), + spreadRadius: 3, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.borderColor, width: 2), + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), ), ), ), From 9e0ea4ad6f9e916dacb5e5e1e8b1fd10634f41d1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 13:07:39 +0300 Subject: [PATCH 64/89] Adjust spacer flex in SpaceManagementCommunityStructure widget for improved layout consistency. --- .../widgets/space_management_community_structure.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4c588ec7..050eac87 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 @@ -13,7 +13,7 @@ class SpaceManagementCommunityStructure extends StatelessWidget { final selectionBloc = context.watch().state; final selectedCommunity = selectionBloc.selectedCommunity; final selectedSpace = selectionBloc.selectedSpace; - const spacer = Spacer(flex: 10); + const spacer = Spacer(flex: 6); return Visibility( visible: selectedCommunity!.spaces.isNotEmpty, replacement: Row( From 03c45ed8d0292d44d1a3e74630b39296fc8e5f6e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 13:07:55 +0300 Subject: [PATCH 65/89] Refactor SpaceCardWidget: Simplified widget structure by removing unnecessary SizedBox. --- .../widgets/space_card_widget.dart | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart index e91e577f..54902280 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -22,22 +22,20 @@ class _SpaceCardWidgetState extends State { return MouseRegion( onEnter: (_) => setState(() => isHovered = true), onExit: (_) => setState(() => isHovered = false), - child: SizedBox( - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - widget.buildSpaceContainer(), - if (isHovered) - Positioned( - bottom: 0, - child: PlusButtonWidget( - offset: Offset.zero, - onButtonTap: widget.onTap, - ), + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + widget.buildSpaceContainer(), + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + offset: Offset.zero, + onButtonTap: widget.onTap, ), - ], - ), + ), + ], ), ); } From 707cb4791f170563041b994ffe34d4c69b61eb84 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 13:08:43 +0300 Subject: [PATCH 66/89] Added CreateSpaceButton for improved user interaction and updated layout calculations to utilize context extensions for better responsiveness. --- .../widgets/community_structure_canvas.dart | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 8ab0c97b..f23405bf 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ @@ -31,8 +33,8 @@ class _CommunityStructureCanvasState extends State final double _horizontalSpacing = 150.0; final double _verticalSpacing = 120.0; - late TransformationController _transformationController; - late AnimationController _animationController; + late final TransformationController _transformationController; + late final AnimationController _animationController; @override void initState() { @@ -182,7 +184,8 @@ class _CommunityStructureCanvasState extends State _positions.clear(); final community = widget.community; - _calculateLayout(community.spaces, 0, {}); + final levelXOffset = {}; + _calculateLayout(community.spaces, 0, levelXOffset); final selectedSpace = widget.selectedSpace; final highlightedUuids = {}; @@ -195,6 +198,17 @@ class _CommunityStructureCanvasState extends State final connections = []; _generateWidgets(community.spaces, widgets, connections, highlightedUuids); + final createButtonX = levelXOffset[0] ?? 0.0; + const createButtonY = 0.0; + + widgets.add( + Positioned( + left: createButtonX, + top: createButtonY, + child: CreateSpaceButton(communityUuid: widget.community.uuid), + ), + ); + return [ CustomPaint( painter: SpacesConnectionsArrowPainter( @@ -264,8 +278,8 @@ class _CommunityStructureCanvasState extends State return InteractiveViewer( transformationController: _transformationController, boundaryMargin: EdgeInsets.symmetric( - horizontal: MediaQuery.sizeOf(context).width * 0.3, - vertical: MediaQuery.sizeOf(context).height * 0.3, + horizontal: context.screenWidth * 0.3, + vertical: context.screenHeight * 0.3, ), minScale: 0.5, maxScale: 3.0, @@ -273,8 +287,8 @@ class _CommunityStructureCanvasState extends State child: GestureDetector( onTap: _resetSelectionAndZoom, child: SizedBox( - width: MediaQuery.sizeOf(context).width * 5, - height: MediaQuery.sizeOf(context).height * 5, + width: context.screenWidth * 5, + height: context.screenHeight * 5, child: Stack(children: treeWidgets), ), ), From 2b8d987c69e4b6f6231297429e7261defafa4024 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 8 Jul 2025 16:00:57 +0300 Subject: [PATCH 67/89] Add SpaceReorderDataModel and integrate drag-and-drop functionality in CommunityStructureCanvas for improved space management. --- .../models/space_reorder_data_model.dart | 14 ++ .../widgets/community_structure_canvas.dart | 199 +++++++++++++++--- 2 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart diff --git a/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart b/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart new file mode 100644 index 00000000..d05f22c7 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart @@ -0,0 +1,14 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class SpaceReorderDataModel { + const SpaceReorderDataModel({ + required this.space, + this.parent, + this.community, + }); + + final SpaceModel space; + final SpaceModel? parent; + final CommunityModel? community; +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index f23405bf..3cf761ad 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/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/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -35,6 +37,7 @@ class _CommunityStructureCanvasState extends State late final TransformationController _transformationController; late final AnimationController _animationController; + SpaceReorderDataModel? _draggedData; @override void initState() { @@ -99,7 +102,7 @@ class _CommunityStructureCanvasState extends State final position = _positions[space.uuid]; if (position == null) return; - const scale = 1.5; + const scale = 1; final viewSize = context.size; if (viewSize == null) return; @@ -114,16 +117,33 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _onReorder(SpaceReorderDataModel data, int newIndex) { + final newCommunity = widget.community.copyWith(); + final children = data.parent?.children ?? newCommunity.spaces; + final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid); + if (oldIndex != -1) { + final item = children.removeAt(oldIndex); + if (newIndex > oldIndex) { + children.insert(newIndex - 1, item); + } else { + children.insert(newIndex, item); + } + } + context.read().add( + CommunitiesUpdateCommunity(newCommunity), + ); + } + void _onSpaceTapped(SpaceModel? space) { context.read().add( SelectSpaceEvent(community: widget.community, space: space), ); } - void _resetSelectionAndZoom() { + void _resetSelectionAndZoom([CommunityModel? community]) { context.read().add( SelectSpaceEvent( - community: widget.community, + community: community ?? widget.community, space: null, ), ); @@ -196,7 +216,13 @@ class _CommunityStructureCanvasState extends State final widgets = []; final connections = []; - _generateWidgets(community.spaces, widgets, connections, highlightedUuids); + _generateWidgets( + widget.community.spaces, + widgets, + connections, + highlightedUuids, + community: widget.community, + ); final createButtonX = levelXOffset[0] ?? 0.0; const createButtonY = 0.0; @@ -225,53 +251,170 @@ class _CommunityStructureCanvasState extends State List spaces, List widgets, List connections, - Set highlightedUuids, - ) { - for (final space in spaces) { + Set highlightedUuids, { + CommunityModel? community, + SpaceModel? parent, + }) { + if (spaces.isNotEmpty) { + final firstChildPos = _positions[spaces.first.uuid]!; + final targetPos = Offset( + firstChildPos.dx - (_horizontalSpacing / 4), + firstChildPos.dy, + ); + widgets.add(_buildDropTarget(parent, community, 0, targetPos)); + } + + for (var i = 0; i < spaces.length; i++) { + final space = spaces[i]; final position = _positions[space.uuid]; - if (position == null) continue; + if (position == null) { + continue; + } final isHighlighted = highlightedUuids.contains(space.uuid); final hasNoSelectedSpace = widget.selectedSpace == null; + final spaceCard = SpaceCardWidget( + buildSpaceContainer: () { + return Opacity( + opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, + child: Tooltip( + message: space.spaceName, + preferBelow: false, + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, + ), + ), + ); + }, + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: widget.community.uuid, + ), + ); + + final reorderData = SpaceReorderDataModel( + space: space, + parent: parent, + community: community, + ); + widgets.add( Positioned( left: position.dx, top: position.dy, width: _cardWidth, height: _cardHeight, - child: SpaceCardWidget( - buildSpaceContainer: () { - return Opacity( - opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, - child: Tooltip( - message: space.spaceName, - preferBelow: false, - child: SpaceCell( - onTap: () => _onSpaceTapped(space), - icon: space.icon, - name: space.spaceName, - ), + child: Draggable( + data: reorderData, + feedback: Material( + color: Colors.transparent, + child: Opacity( + opacity: 0.2, + child: SizedBox( + width: _cardWidth, + height: _cardHeight, + child: spaceCard, ), - ); - }, - onTap: () => SpaceDetailsDialogHelper.showCreate( - context, - communityUuid: widget.community.uuid, + ), ), + onDragStarted: () => setState(() => _draggedData = reorderData), + onDragEnd: (_) => setState(() => _draggedData = null), + onDraggableCanceled: (_, __) => setState(() => _draggedData = null), + childWhenDragging: Opacity(opacity: 0.4, child: spaceCard), + child: spaceCard, ), ), ); + final targetPos = Offset( + position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, + position.dy, + ); + widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); + for (final child in space.children) { - connections.add( - SpaceConnectionModel(from: space.uuid, to: child.uuid), + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + + if (space.children.isNotEmpty) { + _generateWidgets( + space.children, + widgets, + connections, + highlightedUuids, + parent: space, ); } - _generateWidgets(space.children, widgets, connections, highlightedUuids); } } + Widget _buildDropTarget( + SpaceModel? parent, + CommunityModel? community, + int index, + Offset position, + ) { + return Positioned( + left: position.dx, + top: position.dy, + width: 40, + height: _cardHeight, + child: DragTarget( + builder: (context, candidateData, rejectedData) { + if (_draggedData == null) { + return const SizedBox(); + } + + final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && + _draggedData?.community == null) || + (_draggedData?.community?.uuid == community?.uuid && + _draggedData?.parent == null); + + if (!isTargetForDragged) { + return const SizedBox(); + } + + return Container( + width: 40, + height: _cardHeight, + decoration: BoxDecoration( + color: context.theme.colorScheme.primary.withValues( + alpha: candidateData.isNotEmpty ? 0.7 : 0.3, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.add, + color: context.theme.colorScheme.onPrimary, + ), + ); + }, + onWillAcceptWithDetails: (data) { + final children = parent?.children ?? community?.spaces ?? []; + final isSameParent = (data.data.parent?.uuid == parent?.uuid && + data.data.community == null) || + (data.data.community?.uuid == community?.uuid && + data.data.parent == null); + + if (!isSameParent) { + return false; + } + + final oldIndex = + children.indexWhere((s) => s.uuid == data.data.space.uuid); + if (oldIndex == index || oldIndex == index - 1) { + return false; + } + return true; + }, + onAcceptWithDetails: (data) => _onReorder(data.data, index), + ), + ); + } + @override Widget build(BuildContext context) { final treeWidgets = _buildTreeWidgets(); From 6534bfae5b05bd3fe4bc528a13e3d61b7c2c40a6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 09:31:55 +0300 Subject: [PATCH 68/89] 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 5cd083a37b007eaa093ed70b2dc8ac458276ee42 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 9 Jul 2025 15:08:49 +0300 Subject: [PATCH 69/89] Refactor Space and Tag Models: Removed unused JSON serialization methods from SpaceDetailsModel, ProductAllocation, and Subspace. Updated Tag model to eliminate unnecessary fields. Enhanced UpdateSpaceParam to streamline JSON conversion for subspaces and product allocations, improving data handling during updates. --- .../domain/models/space_details_model.dart | 26 ---------- .../widgets/space_sub_spaces_dialog.dart | 2 +- .../modules/tags/domain/models/tag.dart | 24 +-------- .../widgets/assign_tags_dialog.dart | 7 ++- .../widgets/product_tag_field.dart | 10 ++-- .../services/remote_update_space_service.dart | 2 +- .../domain/params/update_space_param.dart | 50 +++++++++---------- 7 files changed, 34 insertions(+), 87 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 b3e436b1..ec3c9f81 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 @@ -40,16 +40,6 @@ class SpaceDetailsModel extends Equatable { ); } - Map toJson() { - return { - 'uuid': uuid, - 'spaceName': spaceName, - 'icon': icon, - 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), - 'subspaces': subspaces.map((e) => e.toJson()).toList(), - }; - } - SpaceDetailsModel copyWith({ String? uuid, String? spaceName, @@ -89,14 +79,6 @@ class ProductAllocation extends Equatable { ); } - Map toJson() { - return { - 'uuid': uuid, - 'product': product.toJson(), - 'tag': tag.toJson(), - }; - } - ProductAllocation copyWith({ String? uuid, Product? product, @@ -134,14 +116,6 @@ class Subspace extends Equatable { ); } - Map toJson() { - return { - 'uuid': uuid, - 'name': name, - 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), - }; - } - Subspace copyWith({ String? uuid, String? name, 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 9e81c323..8faac548 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 @@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State { ..._subspaces, Subspace( name: name, - uuid: const Uuid().v4(), + uuid: '${const Uuid().v4()}-NewTag', productAllocations: const [], ), ]; 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 370bdf47..c5bccdbb 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 @@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart'; class Tag extends Equatable { final String uuid; final String name; - final String createdAt; - final String updatedAt; const Tag({ required this.uuid, required this.name, - required this.createdAt, - required this.updatedAt, }); - factory Tag.empty() => const Tag( - uuid: '', - name: '', - createdAt: '', - updatedAt: '', - ); - factory Tag.fromJson(Map json) { return Tag( uuid: json['uuid'] as String, name: json['name'] as String, - createdAt: json['createdAt'] as String, - updatedAt: json['updatedAt'] as String, ); } - Map toJson() { - return { - 'uuid': uuid, - 'name': name, - 'createdAt': createdAt, - 'updatedAt': updatedAt, - }; - } - @override - List get props => [uuid, name, createdAt, updatedAt]; + List get props => [uuid, name]; } 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 3cab4abe..3f6d42ab 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 @@ -214,9 +214,12 @@ class _AssignTagsDialogState extends State { for (final product in newProducts) { _space.productAllocations.add( ProductAllocation( - uuid: const Uuid().v4(), + uuid: '${const Uuid().v4()}-NewProductUuid', product: product, - tag: Tag.empty(), + tag: Tag( + uuid: '${const Uuid().v4()}-NewTag', + name: '', + ), ), ); } 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 8bbf379d..30282123 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 @@ -2,6 +2,7 @@ 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'; +import 'package:uuid/uuid.dart'; class ProductTagField extends StatefulWidget { final List items; @@ -53,13 +54,8 @@ class _ProductTagFieldState extends State { 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: '', - ), + (e) => e.name.toLowerCase() == lowerCaseValue, + orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value), ); widget.onSelected(selectedTag); _closeDropdown(); diff --git a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart index b595e2b9..a70d3b85 100644 --- a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart @@ -19,7 +19,7 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { final path = await _makeUrl(param); await _httpService.put( path: path, - body: param.space.toJson(), + body: param.toJson(), expectedResponseModel: (data) { final response = data as Map; final isSuccess = response['success'] as bool; diff --git a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart index 884cd581..97fefe03 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -13,33 +13,29 @@ class UpdateSpaceParam { return { 'spaceName': space.spaceName, 'icon': space.icon, - 'subspaces': space.subspaces - .map( - (e) => { - 'subspaceName': e.name, - 'productAllocations': e.productAllocations - .map( - (e) => { - 'name': e.tag.name, - 'productUuid': e.product.uuid, - 'uuid': e.uuid, - }, - ) - .toList(), - 'uuid': e.uuid, - }, - ) - .toList(), - 'productAllocations': space.productAllocations - .map( - (e) => { - 'tagName': e.tag.name, - 'tagUuid': e.tag.uuid, - 'productUuid': e.product.uuid, - }, - ) - .toList(), - 'spaceModelUuid': space.uuid, + 'subspaces': space.subspaces.map((e) => e._toJson()).toList(), + 'productAllocations': + space.productAllocations.map((e) => e._toJson()).toList(), + }; + } +} + +extension _ProductAllocationToJson on ProductAllocation { + Map _toJson() { + final isNewTag = tag.uuid.isEmpty; + return { + if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid, + 'productUuid': product.uuid, + }; + } +} + +extension _SubspaceToJson on Subspace { + Map _toJson() { + final isNewSubspace = uuid.endsWith('-NewTag'); + return { + if (isNewSubspace) 'subspaceName': name else 'uuid': uuid, + 'productAllocations': productAllocations.map((e) => e._toJson()).toList(), }; } } From d87739f1fd10221b448fe3040069ad348af23ebc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 9 Jul 2025 15:25:41 +0300 Subject: [PATCH 70/89] Refactor JSON Serialization in UpdateSpaceParam: Adjusted the _toJson method for Subspace to ensure 'subspaceName' is always included and 'uuid' is only added when applicable, enhancing clarity and consistency in data representation. --- .../modules/update_space/domain/params/update_space_param.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart index 97fefe03..5dd9106d 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -34,7 +34,8 @@ extension _SubspaceToJson on Subspace { Map _toJson() { final isNewSubspace = uuid.endsWith('-NewTag'); return { - if (isNewSubspace) 'subspaceName': name else 'uuid': uuid, + if (!isNewSubspace) 'uuid': uuid, + 'subspaceName': name, 'productAllocations': productAllocations.map((e) => e._toJson()).toList(), }; } From 83202204b094cf8c05c1acf05d4430b3ad5e3557 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 9 Jul 2025 15:58:17 +0300 Subject: [PATCH 71/89] Remove BlocProvider for UpdateSpaceBloc in SpaceDetailsDialogHelper to streamline dependency management and improve code clarity. --- .../presentation/helpers/space_details_dialog_helper.dart | 5 ----- 1 file changed, 5 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 d66d28f4..031e0399 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 @@ -24,11 +24,6 @@ abstract final class SpaceDetailsDialogHelper { RemoteSpaceDetailsService(httpService: HTTPService()), ), ), - BlocProvider( - create: (context) => UpdateSpaceBloc( - RemoteUpdateSpaceService(HTTPService()), - ), - ), ], child: Builder( builder: (context) => SpaceDetailsDialog( From 9f28e1ccef51ea10cbc1c48fc3978e7d6df23e85 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 9 Jul 2025 16:18:10 +0300 Subject: [PATCH 72/89] 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 73/89] 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 74/89] 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 75/89] 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 76/89] 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 77/89] 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 78/89] 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 79/89] 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 80/89] 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 df29aab111bd9ec4bab378336fe70f047593dada Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 10 Jul 2025 12:18:45 +0300 Subject: [PATCH 81/89] Setup new firebase project in the web platform. --- .env.development | 3 +- .env.production | 3 +- .env.staging | 3 +- .vscode/launch.json | 112 ++++++++++++++------------------- firebase.json | 2 +- lib/firebase_options.dart | 16 +++++ lib/firebase_options_dev.dart | 93 --------------------------- lib/firebase_options_prod.dart | 77 ----------------------- lib/main.dart | 8 ++- lib/main_dev.dart | 8 ++- lib/main_staging.dart | 8 ++- 11 files changed, 85 insertions(+), 248 deletions(-) create mode 100644 lib/firebase_options.dart delete mode 100644 lib/firebase_options_dev.dart delete mode 100644 lib/firebase_options_prod.dart diff --git a/.env.development b/.env.development index e77609dc..1983668b 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,3 @@ ENV_NAME=development -BASE_URL=https://syncrow-dev.azurewebsites.net \ No newline at end of file +BASE_URL=https://syncrow-dev.azurewebsites.net +RTDB_URL=https://syncrow-dev-79446.asia-southeast1.firebasedatabase.app/ diff --git a/.env.production b/.env.production index 4e9dcb81..3ab08819 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,3 @@ ENV_NAME=production -BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file +BASE_URL=https://syncrow-staging.azurewebsites.net +RTDB_URL=https://syncrow-prod-79446.asia-southeast1.firebasedatabase.app/ diff --git a/.env.staging b/.env.staging index 9565b426..bfd878b1 100644 --- a/.env.staging +++ b/.env.staging @@ -1,2 +1,3 @@ ENV_NAME=staging -BASE_URL=https://syncrow-staging.azurewebsites.net \ No newline at end of file +BASE_URL=https://syncrow-staging.azurewebsites.net +RTDB_URL=https://syncrow-staging-79446.asia-southeast1.firebasedatabase.app/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 4aceb26d..4fd4333b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,67 +1,49 @@ { - "configurations": [ - - { - - "name": "DEVELOPMENT", - - "request": "launch", - - "type": "dart", - - "args": [ - "-d", - "chrome", - "--web-port", - "3000", - "-t", - "lib/main_dev.dart", - "--web-experimental-hot-reload", - ], - - "flutterMode": "debug" - - },{ - - "name": "STAGING", - - "request": "launch", - - "type": "dart", - - "args": [ - "-d", - "chrome", - "--web-port", - "3000", - "-t", - "lib/main_staging.dart", - "--web-experimental-hot-reload", - ], - - "flutterMode": "debug" - - },{ - - "name": "PRODUCTION", - - "request": "launch", - - "type": "dart", - - "args": [ - "-d", - "chrome", - "--web-port", - "3000", - "-t", - "lib/main.dart", - "--web-experimental-hot-reload", - ], - - "flutterMode": "debug" - - }, - - ] + "configurations": [ + { + "name": "DEVELOPMENT", + "request": "launch", + "type": "dart", + "args": [ + "-d", + "chrome", + "--web-port", + "3000", + "-t", + "lib/main_dev.dart", + "--web-experimental-hot-reload" + ], + "flutterMode": "debug" + }, + { + "name": "STAGING", + "request": "launch", + "type": "dart", + "args": [ + "-d", + "chrome", + "--web-port", + "3000", + "-t", + "lib/main_staging.dart", + "--web-experimental-hot-reload" + ], + "flutterMode": "debug" + }, + { + "name": "PRODUCTION", + "request": "launch", + "type": "dart", + "args": [ + "-d", + "chrome", + "--web-port", + "3000", + "-t", + "lib/main.dart", + "--web-experimental-hot-reload" + ], + "flutterMode": "debug" + } + ] } \ No newline at end of file diff --git a/firebase.json b/firebase.json index fa1105a3..e22eee5e 100644 --- a/firebase.json +++ b/firebase.json @@ -1 +1 @@ -{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"test2-8a3d2","configurations":{"android":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","ios":"1:427332280600:ios:14346b200780dc760c7e6d","macos":"1:427332280600:ios:14346b200780dc760c7e6d","web":"1:427332280600:web:ad50516a87a35a1a0c7e6d","windows":"1:427332280600:web:f7a25537ccd5a7bd0c7e6d"}}}}}} \ No newline at end of file +{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"syncrow-prod-79446","configurations":{"web":"1:255001682464:web:a03e2d6214c13101561245"}}}}}} \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 00000000..0f5fbf8c --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,16 @@ +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; + +final class DefaultFirebaseOptions extends FirebaseOptions { + const DefaultFirebaseOptions({ + required String databaseUrl, + }) : super( + apiKey: 'AIzaSyDgq5ywsnFVbbQO-Xz1Z4sR5bBcuiDaS9g', + appId: '1:255001682464:web:a03e2d6214c13101561245', + messagingSenderId: '255001682464', + projectId: 'syncrow-prod-79446', + authDomain: 'syncrow-prod-79446.firebaseapp.com', + storageBucket: 'syncrow-prod-79446.firebasestorage.app', + databaseURL: databaseUrl, + measurementId: 'G-1850Q89RMK', + ); +} diff --git a/lib/firebase_options_dev.dart b/lib/firebase_options_dev.dart deleted file mode 100644 index 93e8600c..00000000 --- a/lib/firebase_options_dev.dart +++ /dev/null @@ -1,93 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: type=lint -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptionsDev { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - return windows; - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyCVEvKsJYzhWDFM-9Od68FE0nPpP933st0', - appId: '1:427332280600:web:ad50516a87a35a1a0c7e6d', - messagingSenderId: '427332280600', - projectId: 'test2-8a3d2', - authDomain: 'test2-8a3d2.firebaseapp.com', - databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com', - storageBucket: 'test2-8a3d2.firebasestorage.app', - measurementId: 'G-Z1RTTTV5H9', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0', - appId: '1:427332280600:android:2bc36fbe82994a3e0c7e6d', - messagingSenderId: '427332280600', - projectId: 'test2-8a3d2', - databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com', - storageBucket: 'test2-8a3d2.firebasestorage.app', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw', - appId: '1:427332280600:ios:14346b200780dc760c7e6d', - messagingSenderId: '427332280600', - projectId: 'test2-8a3d2', - databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com', - storageBucket: 'test2-8a3d2.firebasestorage.app', - iosBundleId: 'com.example.syncrowWeb', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw', - appId: '1:427332280600:ios:14346b200780dc760c7e6d', - messagingSenderId: '427332280600', - projectId: 'test2-8a3d2', - databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com', - storageBucket: 'test2-8a3d2.firebasestorage.app', - iosBundleId: 'com.example.syncrowWeb', - ); - - static const FirebaseOptions windows = FirebaseOptions( - apiKey: 'AIzaSyDizKjPC5rdkEjDxwXjM-RU5unB0Ziq3iw', - appId: '1:427332280600:web:f7a25537ccd5a7bd0c7e6d', - messagingSenderId: '427332280600', - projectId: 'test2-8a3d2', - authDomain: 'test2-8a3d2.firebaseapp.com', - databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com', - storageBucket: 'test2-8a3d2.firebasestorage.app', - measurementId: 'G-4LFVXEXWKY', - ); -} diff --git a/lib/firebase_options_prod.dart b/lib/firebase_options_prod.dart deleted file mode 100644 index 485696b8..00000000 --- a/lib/firebase_options_prod.dart +++ /dev/null @@ -1,77 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: type=lint -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptionsStaging { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for macos - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyDP9GpYfLE8gHTj3kZ1hW8fx_FkJqOqSQk', - appId: '1:786692570726:android:0ef7079c2b978d4417b7a7', - messagingSenderId: '786692570726', - projectId: 'syncrow-staging', - databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com', - storageBucket: 'syncrow-staging.appspot.com', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyAWlRiuJ75FMlf2_UDdri1voWKvkaSHtRg', - appId: '1:786692570726:ios:455a6fcff77e130f17b7a7', - messagingSenderId: '786692570726', - projectId: 'syncrow-staging', - databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com', - storageBucket: 'syncrow-staging.appspot.com', - iosBundleId: 'com.example.syncrow.app', - ); - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyDyGaQ3sZhb4meaY6sGke-YglhdhJ2is8Q', - appId: '1:786692570726:web:93c931e6701797b317b7a7', - messagingSenderId: '786692570726', - projectId: 'syncrow-staging', - authDomain: 'syncrow-staging.firebaseapp.com', - databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com', - storageBucket: 'syncrow-staging.appspot.com', - measurementId: 'G-CZ3J3G6LMQ', - ); -} diff --git a/lib/main.dart b/lib/main.dart index 219fc41d..a50d2615 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; -import 'package:syncrow_web/firebase_options_prod.dart'; +import 'package:syncrow_web/firebase_options.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; @@ -27,7 +27,9 @@ Future main() async { await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( - options: DefaultFirebaseOptionsStaging.currentPlatform, + options: DefaultFirebaseOptions( + databaseUrl: dotenv.env['RTDB_URL']!, + ), ); initialSetup(); } catch (_) {} @@ -59,7 +61,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => CreateRoutineBloc(), ), - BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())), + BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), ), diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 410110d1..284e2f30 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; -import 'package:syncrow_web/firebase_options_dev.dart'; +import 'package:syncrow_web/firebase_options.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; @@ -27,7 +27,9 @@ Future main() async { await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( - options: DefaultFirebaseOptionsDev.currentPlatform, + options: DefaultFirebaseOptions( + databaseUrl: dotenv.env['RTDB_URL']!, + ), ); initialSetup(); } catch (_) {} @@ -59,7 +61,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => CreateRoutineBloc(), ), - BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())), + BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), ), diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 70c968c0..6389c53a 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; -import 'package:syncrow_web/firebase_options_prod.dart'; +import 'package:syncrow_web/firebase_options.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; @@ -24,7 +24,9 @@ Future main() async { await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( - options: DefaultFirebaseOptionsStaging.currentPlatform, + options: DefaultFirebaseOptions( + databaseUrl: dotenv.env['RTDB_URL']!, + ), ); initialSetup(); } catch (_) {} @@ -56,7 +58,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => CreateRoutineBloc(), ), - BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())), + BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), ), From 645a07287efac6d24061737ee237970d60b8b55c Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 14:15:57 +0300 Subject: [PATCH 82/89] 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, From b6664ec1ba7160198f26b54468dc0bda1bed2665 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 11 Jul 2025 10:53:04 +0300 Subject: [PATCH 83/89] fix Redundant API calls on Routines page when selecting a community from the tree --- .../device_managment_bloc.dart | 14 ++-- .../bloc/routine_bloc/routine_bloc.dart | 81 +++++++++---------- lib/services/devices_mang_api.dart | 17 ++-- lib/utils/constants/api_const.dart | 6 +- 4 files changed, 56 insertions(+), 62 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..eeb5e45c 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 @@ -43,16 +43,15 @@ class DeviceManagementBloc final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (spaceBloc.state.selectedCommunities.isEmpty) { - devices = - await DevicesManagementApi().fetchDevices('', '', projectUuid); + devices = await DevicesManagementApi().fetchDevices( + projectUuid, + ); } else { for (var community in spaceBloc.state.selectedCommunities) { - List spacesList = + final spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; - for (var space in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(community, space, projectUuid)); - } + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } @@ -270,6 +269,7 @@ class DeviceManagementBloc return 'All'; } } + void _onSearchDevices( SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 3fd07834..971f4f8c 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -170,45 +170,45 @@ class RoutineBloc extends Bloc { } } -Future _onLoadScenes( - LoadScenes event, Emitter emit) async { - emit(state.copyWith(isLoading: true, errorMessage: null)); - List scenes = []; - try { - BuildContext context = NavigationService.navigatorKey.currentContext!; - var createRoutineBloc = context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (createRoutineBloc.selectedSpaceId == '' && - createRoutineBloc.selectedCommunityId == '') { - var spaceBloc = context.read(); - for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = - spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - scenes.addAll( - await SceneApi.getScenes(spaceId, communityId, projectUuid)); + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + List scenes = []; + try { + BuildContext context = NavigationService.navigatorKey.currentContext!; + var createRoutineBloc = context.read(); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { + var spaceBloc = context.read(); + for (var communityId in spaceBloc.state.selectedCommunities) { + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + for (var spaceId in spacesList) { + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); + } } + } else { + scenes.addAll(await SceneApi.getScenes( + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } - } else { - scenes.addAll(await SceneApi.getScenes( - createRoutineBloc.selectedSpaceId, - createRoutineBloc.selectedCommunityId, - projectUuid)); - } - emit(state.copyWith( - scenes: scenes, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( + emit(state.copyWith( + scenes: scenes, isLoading: false, - loadScenesErrorMessage: 'Failed to load scenes', - errorMessage: '', - loadAutomationErrorMessage: '', - scenes: scenes)); + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', + scenes: scenes)); + } } -} Future _onLoadAutomation( LoadAutomation event, Emitter emit) async { @@ -936,16 +936,15 @@ Future _onLoadScenes( for (var communityId in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(communityId, spaceId, projectUuid)); - } + + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } else { devices.addAll(await DevicesManagementApi().fetchDevices( - createRoutineBloc.selectedCommunityId, - createRoutineBloc.selectedSpaceId, - projectUuid)); + projectUuid, + spacesId: [createRoutineBloc.selectedSpaceId], + )); } emit(state.copyWith(isLoading: false, devices: devices)); diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 709d6855..8c74dbb1 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -12,20 +12,16 @@ import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; class DevicesManagementApi { - Future> fetchDevices( - String communityId, String spaceId, String projectId) async { + Future> fetchDevices(String projectId, + {List? spacesId}) async { try { final response = await HTTPService().get( - path: communityId.isNotEmpty && spaceId.isNotEmpty - ? ApiEndpoints.getSpaceDevices - .replaceAll('{spaceUuid}', spaceId) - .replaceAll('{communityUuid}', communityId) - .replaceAll('{projectId}', projectId) - : ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), + path: ApiEndpoints.getSpaceDevices.replaceAll('{projectId}', projectId), + queryParameters: {if (spacesId != null) 'spaces': spacesId}, showServerMessage: true, expectedResponseModel: (json) { - List jsonData = json['data']; - List devicesList = jsonData.map((jsonItem) { + final List jsonData = json['data'] as List; + final List devicesList = jsonData.map((jsonItem) { return AllDevicesModel.fromJson(jsonItem); }).toList(); return devicesList; @@ -416,5 +412,4 @@ class DevicesManagementApi { ); return response; } - } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index eb7b6a3e..c3e8b1d2 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -17,8 +17,7 @@ abstract class ApiEndpoints { ////// Devices Management //////////////// static const String getAllDevices = '/projects/{projectId}/devices'; - static const String getSpaceDevices = - '/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices'; + static const String getSpaceDevices = '/projects/{projectId}/devices'; static const String getDeviceStatus = '/devices/{uuid}/functions/status'; static const String getBatchStatus = '/devices/batch'; @@ -46,7 +45,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 = From 2681c837f511227f025293fa2687707c3135e4d3 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 11 Jul 2025 12:10:53 +0300 Subject: [PATCH 84/89] add company name and replace it with job title --- .../model/edit_user_model.dart | 3 ++ .../model/roles_user_model.dart | 4 ++- .../add_user_dialog/bloc/users_bloc.dart | 12 ++++---- .../add_user_dialog/view/basics_view.dart | 6 ++-- .../users_table/view/users_page.dart | 4 +-- lib/services/user_permission.dart | 30 ++++++++++++------- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lib/pages/roles_and_permission/model/edit_user_model.dart b/lib/pages/roles_and_permission/model/edit_user_model.dart index 81430fa3..6f5564d4 100644 --- a/lib/pages/roles_and_permission/model/edit_user_model.dart +++ b/lib/pages/roles_and_permission/model/edit_user_model.dart @@ -153,6 +153,7 @@ class EditUserModel { final String? jobTitle; // can be empty final String roleType; // e.g. "ADMIN" final List spaces; + final String? companyName; EditUserModel({ required this.uuid, @@ -167,6 +168,7 @@ class EditUserModel { required this.jobTitle, required this.roleType, required this.spaces, + this.companyName, }); /// Create a [UserData] from JSON data @@ -182,6 +184,7 @@ class EditUserModel { invitedBy: json['invitedBy'] as String, phoneNumber: json['phoneNumber'] ?? '', jobTitle: json['jobTitle'] ?? '', + companyName: json['companyName'] as String?, roleType: json['roleType'] as String, spaces: (json['spaces'] as List) .map((e) => UserSpaceModel.fromJson(e as Map)) diff --git a/lib/pages/roles_and_permission/model/roles_user_model.dart b/lib/pages/roles_and_permission/model/roles_user_model.dart index e502370a..ce88e200 100644 --- a/lib/pages/roles_and_permission/model/roles_user_model.dart +++ b/lib/pages/roles_and_permission/model/roles_user_model.dart @@ -12,7 +12,7 @@ class RolesUserModel { final dynamic jobTitle; final dynamic createdDate; final dynamic createdTime; - + final String? companyName; RolesUserModel({ required this.uuid, required this.createdAt, @@ -27,6 +27,7 @@ class RolesUserModel { this.jobTitle, required this.createdDate, required this.createdTime, + this.companyName, }); factory RolesUserModel.fromJson(Map json) { @@ -47,6 +48,7 @@ class RolesUserModel { : json['jobTitle'], createdDate: json['createdDate'], createdTime: json['createdTime'], + companyName: json['companyName'] as String?, ); } } 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 72c4501c..cda61499 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 @@ -52,7 +52,7 @@ class UsersBloc extends Bloc { final TextEditingController lastNameController = TextEditingController(); final TextEditingController emailController = TextEditingController(); final TextEditingController phoneController = TextEditingController(); - final TextEditingController jobTitleController = TextEditingController(); + final TextEditingController companyNameController = TextEditingController(); final TextEditingController roleSearchController = TextEditingController(); bool? isCompleteBasics; @@ -352,7 +352,7 @@ class UsersBloc extends Bloc { bool res = await UserPermissionApi().sendInviteUser( email: emailController.text, firstName: firstNameController.text, - jobTitle: jobTitleController.text, + companyName: companyNameController.text, lastName: lastNameController.text, phoneNumber: phoneController.text, roleUuid: roleSelected, @@ -405,7 +405,7 @@ class UsersBloc extends Bloc { bool res = await UserPermissionApi().editInviteUser( userId: event.userId, firstName: firstNameController.text, - jobTitle: jobTitleController.text, + companyName: companyNameController.text, lastName: lastNameController.text, phoneNumber: phoneController.text, roleUuid: roleSelected, @@ -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!; @@ -529,7 +529,7 @@ class UsersBloc extends Bloc { lastNameController.text = res.lastName; emailController.text = res.email; phoneController.text = res.phoneNumber ?? ''; - jobTitleController.text = res.jobTitle ?? ''; + companyNameController.text = res.companyName ?? ''; res.roleType; res.spaces.map((space) { selectedIds.add(space.uuid); @@ -645,7 +645,7 @@ class UsersBloc extends Bloc { lastNameController.dispose(); emailController.dispose(); phoneController.dispose(); - jobTitleController.dispose(); + companyNameController.dispose(); roleSearchController.dispose(); return super.close(); } 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 14022cab..250bba3f 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 @@ -317,7 +317,7 @@ class BasicsView extends StatelessWidget { child: Row( children: [ Text( - 'Job Title', + 'Company Name', style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w400, fontSize: 13, @@ -328,11 +328,11 @@ class BasicsView extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( - controller: _blocRole.jobTitleController, + controller: _blocRole.companyNameController, style: const TextStyle(color: ColorsManager.blackColor), decoration: inputTextFormDeco( - hintText: "Job Title (Optional)") + hintText: "Comapny Name (Optional)") .copyWith( hintStyle: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w400, diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index da159d94..0a7e3714 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -411,7 +411,7 @@ class UsersPage extends StatelessWidget { titles: const [ "Full Name", "Email Address", - "Job Title", + "Company Name", "Role", "Creation Date", "Creation Time", @@ -424,7 +424,7 @@ class UsersPage extends StatelessWidget { return [ Text('${user.firstName} ${user.lastName}'), Text(user.email), - Text(user.jobTitle), + Center(child: Text(user.companyName ?? '-')), Text(user.roleType ?? ''), Text(user.createdDate ?? ''), Text(user.createdTime ?? ''), diff --git a/lib/services/user_permission.dart b/lib/services/user_permission.dart index 90a82921..59d8dfcc 100644 --- a/lib/services/user_permission.dart +++ b/lib/services/user_permission.dart @@ -34,8 +34,9 @@ class UserPermissionApi { path: ApiEndpoints.roleTypes, showServerMessage: true, expectedResponseModel: (json) { - final List fetchedRoles = - (json['data'] as List).map((item) => RoleTypeModel.fromJson(item)).toList(); + final List fetchedRoles = (json['data'] as List) + .map((item) => RoleTypeModel.fromJson(item)) + .toList(); return fetchedRoles; }, ); @@ -47,7 +48,9 @@ class UserPermissionApi { path: ApiEndpoints.permission.replaceAll("roleUuid", roleUuid), showServerMessage: true, expectedResponseModel: (json) { - return (json as List).map((data) => PermissionOption.fromJson(data)).toList(); + return (json as List) + .map((data) => PermissionOption.fromJson(data)) + .toList(); }, ); return response ?? []; @@ -57,7 +60,7 @@ class UserPermissionApi { String? firstName, String? lastName, String? email, - String? jobTitle, + String? companyName, String? phoneNumber, String? roleUuid, List? spaceUuids, @@ -68,7 +71,7 @@ class UserPermissionApi { "firstName": firstName, "lastName": lastName, "email": email, - "jobTitle": jobTitle != '' ? jobTitle : null, + "companyName": companyName != '' ? companyName : null, "phoneNumber": phoneNumber != '' ? phoneNumber : null, "roleUuid": roleUuid, "projectUuid": projectUuid, @@ -140,7 +143,7 @@ class UserPermissionApi { String? firstName, String? userId, String? lastName, - String? jobTitle, + String? companyName, String? phoneNumber, String? roleUuid, List? spaceUuids, @@ -150,8 +153,8 @@ class UserPermissionApi { final body = { "firstName": firstName, "lastName": lastName, - "jobTitle": jobTitle != '' ? jobTitle : " ", - "phoneNumber": phoneNumber != '' ? phoneNumber : " ", + "companyName": companyName != '' ? companyName : ' ', + "phoneNumber": phoneNumber != '' ? phoneNumber : ' ', "roleUuid": roleUuid, "projectUuid": projectUuid, "spaceUuids": spaceUuids, @@ -190,12 +193,17 @@ class UserPermissionApi { } } - Future changeUserStatusById(userUuid, status, String projectUuid) async { + Future changeUserStatusById( + userUuid, status, String projectUuid) async { try { - Map bodya = {"disable": status, "projectUuid": projectUuid}; + Map bodya = { + "disable": status, + "projectUuid": projectUuid + }; final response = await _httpService.put( - path: ApiEndpoints.changeUserStatus.replaceAll("{invitedUserUuid}", userUuid), + path: ApiEndpoints.changeUserStatus + .replaceAll("{invitedUserUuid}", userUuid), body: bodya, expectedResponseModel: (json) { return json['success']; From 7331c8440b567e8413c525ad8a5bbba51a193c5a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 14 Jul 2025 10:27:22 +0300 Subject: [PATCH 85/89] Refactor SpaceManagementPage to use StatefulWidget and initialize CommunitiesBloc in initState. Update CommunityStructureHeader to handle community updates and improve state management in CommunitiesTreeSelectionBloc with new event for community state updates. --- .../views/space_management_page.dart | 30 +++++-- .../widgets/community_structure_header.dart | 36 +++++++++ .../domain/models/space_model.dart | 19 +++++ .../communities_tree_selection_bloc.dart | 79 ++++++++++++++++++- .../communities_tree_selection_event.dart | 9 +++ .../communities_tree_selection_state.dart | 12 +-- .../helpers/space_details_dialog_helper.dart | 24 +++++- 7 files changed, 189 insertions(+), 20 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 106b9a3a..40a37891 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 @@ -16,21 +16,37 @@ 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'; -class SpaceManagementPage extends StatelessWidget { +class SpaceManagementPage extends StatefulWidget { const SpaceManagementPage({super.key}); + @override + State createState() => _SpaceManagementPageState(); +} + +class _SpaceManagementPageState extends State { + late final CommunitiesBloc communitiesBloc; + + @override + void initState() { + communitiesBloc = CommunitiesBloc( + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), + )..add(const LoadCommunities(LoadCommunitiesParam())); + + super.initState(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ + BlocProvider.value(value: communitiesBloc), BlocProvider( - create: (context) => CommunitiesBloc( - communitiesService: DebouncedCommunitiesService( - RemoteCommunitiesService(HTTPService()), - ), - )..add(const LoadCommunities(LoadCommunitiesParam())), + create: (context) => CommunitiesTreeSelectionBloc( + communitiesBloc: communitiesBloc, + ), ), - BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), BlocProvider( create: (context) => SpaceDetailsBloc( UniqueSubspacesDecorator( 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 f27dc8b9..cb6271d1 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 @@ -3,7 +3,10 @@ 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/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.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'; @@ -11,6 +14,26 @@ import 'package:syncrow_web/utils/constants/assets.dart'; class CommunityStructureHeader extends StatelessWidget { const CommunityStructureHeader({super.key}); + List _updateRecursive( + List spaces, + SpaceDetailsModel updatedSpace, + ) { + return spaces.map((space) { + if (space.uuid == updatedSpace.uuid) { + return space.copyWith( + spaceName: updatedSpace.spaceName, + icon: updatedSpace.icon, + ); + } + if (space.children.isNotEmpty) { + return space.copyWith( + children: _updateRecursive(space.children, updatedSpace), + ); + } + return space; + }).toList(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -99,6 +122,19 @@ class CommunityStructureHeader extends StatelessWidget { context, spaceModel: selectedSpace!, communityUuid: selectedCommunity.uuid, + onSuccess: (updatedSpaceDetails) { + final communitiesBloc = context.read(); + final updatedSpaces = _updateRecursive( + selectedCommunity.spaces, + updatedSpaceDetails, + ); + + final community = selectedCommunity.copyWith( + spaces: updatedSpaces, + ); + + communitiesBloc.add(CommunitiesUpdateCommunity(community)); + }, ), selectedSpace: selectedSpace, ), 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 ddcc6a86..bd5a2e50 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 @@ -46,6 +46,25 @@ class SpaceModel extends Equatable { ); } + SpaceModel copyWith({ + String? uuid, + DateTime? createdAt, + DateTime? updatedAt, + String? spaceName, + String? icon, + List? children, + }) { + return SpaceModel( + uuid: uuid ?? this.uuid, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + spaceName: spaceName ?? this.spaceName, + icon: icon ?? this.icon, + children: children ?? this.children, + parent: parent, + ); + } + @override List get props => [uuid, spaceName, icon, children]; } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart index bdda04ee..25263d35 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -1,17 +1,39 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; part 'communities_tree_selection_event.dart'; part 'communities_tree_selection_state.dart'; class CommunitiesTreeSelectionBloc extends Bloc { - CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + CommunitiesTreeSelectionBloc({ + required CommunitiesBloc communitiesBloc, + }) : _communitiesBloc = communitiesBloc, + super(const CommunitiesTreeSelectionState()) { on(_onSelectCommunity); on(_onSelectSpace); on(_onClearSelection); + on<_CommunitiesStateUpdated>(_onCommunitiesStateUpdated); + + _communitiesSubscription = _communitiesBloc.stream.listen((communitiesState) { + if (state.selectedCommunity != null) { + add(_CommunitiesStateUpdated(communitiesState)); + } + }); + } + + final CommunitiesBloc _communitiesBloc; + late final StreamSubscription _communitiesSubscription; + + @override + Future close() { + _communitiesSubscription.cancel(); + return super.close(); } void _onSelectCommunity( @@ -44,4 +66,59 @@ class CommunitiesTreeSelectionBloc ) { emit(const CommunitiesTreeSelectionState()); } + + void _onCommunitiesStateUpdated( + _CommunitiesStateUpdated event, + Emitter emit, + ) { + if (state.selectedCommunity == null) return; + + final communities = event.communitiesState.communities; + try { + final updatedCommunity = communities.firstWhere( + (c) => c.uuid == state.selectedCommunity!.uuid, + ); + + var updatedSelectedSpace = state.selectedSpace; + if (state.selectedSpace != null) { + updatedSelectedSpace = _findSpaceInCommunity( + updatedCommunity, + state.selectedSpace!.uuid, + ); + } + emit( + state.copyWith( + selectedCommunity: updatedCommunity, + selectedSpace: updatedSelectedSpace, + clearSelectedSpace: updatedSelectedSpace == null, + ), + ); + } catch (_) { + add(const ClearCommunitiesTreeSelectionEvent()); + } + } + + SpaceModel? _findSpaceInCommunity(CommunityModel community, String spaceUuid) { + try { + return _findSpaceRecursive(community.spaces, spaceUuid); + } catch (_) { + return null; + } + } + + SpaceModel _findSpaceRecursive(List spaces, String spaceUuid) { + for (final space in spaces) { + if (space.uuid == spaceUuid) { + return space; + } + if (space.children.isNotEmpty) { + try { + return _findSpaceRecursive(space.children, spaceUuid); + } catch (_) { + // not found in this branch + } + } + } + throw Exception('Space not found'); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 21088632..43a69e05 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -29,3 +29,12 @@ final class ClearCommunitiesTreeSelectionEvent extends CommunitiesTreeSelectionEvent { const ClearCommunitiesTreeSelectionEvent(); } + +final class _CommunitiesStateUpdated extends CommunitiesTreeSelectionEvent { + const _CommunitiesStateUpdated(this.communitiesState); + + final CommunitiesState communitiesState; + + @override + List get props => [communitiesState]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart index b14d330b..4c36f778 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -12,18 +12,14 @@ final class CommunitiesTreeSelectionState extends Equatable { CommunitiesTreeSelectionState copyWith({ CommunityModel? selectedCommunity, SpaceModel? selectedSpace, - List? expandedCommunities, - List? expandedSpaces, + bool clearSelectedSpace = false, }) { return CommunitiesTreeSelectionState( selectedCommunity: selectedCommunity ?? this.selectedCommunity, - selectedSpace: selectedSpace ?? this.selectedSpace, + selectedSpace: clearSelectedSpace ? null : selectedSpace ?? this.selectedSpace, ); } @override - List get props => [ - selectedCommunity, - selectedSpace, - ]; - } + List get props => [selectedCommunity, selectedSpace]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 031e0399..c5de7dad 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 @@ -42,6 +42,7 @@ abstract final class SpaceDetailsDialogHelper { BuildContext context, { required SpaceModel spaceModel, required String communityUuid, + required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess, }) { showDialog( context: context, @@ -60,7 +61,11 @@ abstract final class SpaceDetailsDialogHelper { ], child: Builder( builder: (context) => BlocListener( - listener: _updateListener, + listener: (context, state) => _updateListener( + context, + state, + onSuccess, + ), child: SpaceDetailsDialog( context: context, title: const SelectableText('Edit Space'), @@ -81,17 +86,28 @@ abstract final class SpaceDetailsDialogHelper { ); } - static void _updateListener(BuildContext context, UpdateSpaceState state) { + static void _updateListener( + BuildContext context, + UpdateSpaceState state, + void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess, + ) { return switch (state) { UpdateSpaceInitial() => null, UpdateSpaceLoading() => _onLoading(context), - UpdateSpaceSuccess(:final space) => _onUpdateSuccess(context, space), + UpdateSpaceSuccess(:final space) => + _onUpdateSuccess(context, space, onSuccess), UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage), }; } - static void _onUpdateSuccess(BuildContext context, SpaceDetailsModel space) { + static void _onUpdateSuccess( + BuildContext context, + SpaceDetailsModel space, + void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess, + ) { Navigator.of(context).pop(); + Navigator.of(context).pop(); + onSuccess?.call(space); } static void _onLoading(BuildContext context) { From 65d541d59408ed0fd9b167968060520d499061e3 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 14 Jul 2025 10:46:12 +0300 Subject: [PATCH 86/89] Add calendar event management features and UI components and Implement Calendar logic --- .../services/remote_calendar_service.dart | 170 ++++++++ .../domain/models/calendar_event_booking.dart | 134 +++++++ .../services/calendar_system_service.dart | 7 + .../bloc/calendar/events_bloc.dart | 118 +++--- .../bloc/calendar/events_event.dart | 16 +- .../bloc/calendar/events_state.dart | 4 - .../presentation/view/booking_page.dart | 368 ++++++++---------- .../view/widgets/event_tile_widget.dart | 60 +++ .../widgets/hatched_column_background.dart | 91 +++++ .../view/widgets/time_line_widget.dart | 49 +++ .../view/widgets/week_day_header.dart | 39 ++ .../view/widgets/week_navigation.dart | 76 ++++ .../view/widgets/weekly_calendar_page.dart | 138 ++----- lib/utils/color_manager.dart | 2 +- lib/utils/constants/api_const.dart | 1 + 15 files changed, 890 insertions(+), 383 deletions(-) create mode 100644 lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart create mode 100644 lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart create mode 100644 lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart create mode 100644 lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart diff --git a/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart new file mode 100644 index 00000000..aa3307d3 --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart @@ -0,0 +1,170 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_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 RemoteCalendarService implements CalendarSystemService { + const RemoteCalendarService(this._httpService); + + final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load Calendar'; + + @override + Future getCalendarEvents({ + required String spaceId, + }) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getCalendarEvents, + queryParameters: { + 'spaceId': spaceId, + }, + expectedResponseModel: (json) { + return CalendarEventsResponse.fromJson( + json as Map, + ); + }, + ); + + return CalendarEventsResponse.fromJson(response as Map); + } 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()}'); + } + } +} + +class FakeRemoteCalendarService implements CalendarSystemService { + const FakeRemoteCalendarService(this._httpService, {this.useDummy = false}); + + final HTTPService _httpService; + final bool useDummy; + static const _defaultErrorMessage = 'Failed to load Calendar'; + + @override + Future getCalendarEvents({ + required String spaceId, + }) async { + if (useDummy) { + final dummyJson = { + 'statusCode': 200, + 'message': 'Successfully fetched all bookings', + 'data': [ + { + 'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80', + 'date': '2025-07-11T10:22:00.626Z', + 'startTime': '09:00:00', + 'endTime': '12:00:00', + 'cost': 10, + 'user': { + 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', + 'firstName': 'salsabeel', + 'lastName': 'abuzaid', + 'email': 'test@test.com', + 'companyName': null + }, + 'space': { + 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', + 'spaceName': '2(1)' + } + }, + { + 'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561', + 'date': '2025-07-11T10:22:00.626Z', + 'startTime': '12:00:00', + 'endTime': '13:00:00', + 'cost': 10, + 'user': { + 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', + 'firstName': 'salsabeel', + 'lastName': 'abuzaid', + 'email': 'test@test.com', + 'companyName': null + }, + 'space': { + 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', + 'spaceName': '2(1)' + } + }, + { + 'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561', + 'date': '2025-07-13T10:22:00.626Z', + 'startTime': '15:30:00', + 'endTime': '19:00:00', + 'cost': 20, + 'user': { + 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', + 'firstName': 'salsabeel', + 'lastName': 'abuzaid', + 'email': 'test@test.com', + 'companyName': null + }, + 'space': { + 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', + 'spaceName': '2(1)' + } + } + ], + 'success': true + }; + final response = CalendarEventsResponse.fromJson(dummyJson); + + // Filter events by spaceId + final filteredData = response.data.where((event) { + return event.space.uuid == spaceId; + }).toList(); + print('Filtering events for spaceId: $spaceId'); + print('Found ${filteredData.length} matching events'); + return filteredData.isNotEmpty + ? CalendarEventsResponse( + statusCode: response.statusCode, + message: response.message, + data: filteredData, + success: response.success, + ) + : CalendarEventsResponse( + statusCode: 404, + message: 'No events found for spaceId: $spaceId', + data: [], + success: false, + ); + } + + try { + final response = await _httpService.get( + path: ApiEndpoints.getCalendarEvents, + queryParameters: { + 'spaceId': spaceId, + }, + expectedResponseModel: (json) { + return CalendarEventsResponse.fromJson( + json as Map, + ); + }, + ); + + return CalendarEventsResponse.fromJson(response as Map); + } 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/calendar_event_booking.dart b/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart new file mode 100644 index 00000000..4b8f1ba1 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart @@ -0,0 +1,134 @@ +class CalendarEventBooking { + final String uuid; + final DateTime date; + final String startTime; + final String endTime; + final int cost; + final BookingUser user; + final BookingSpace space; + + CalendarEventBooking({ + required this.uuid, + required this.date, + required this.startTime, + required this.endTime, + required this.cost, + required this.user, + required this.space, + }); + + factory CalendarEventBooking.fromJson(Map json) { + return CalendarEventBooking( + uuid: json['uuid'] as String? ?? '', + date: json['date'] != null + ? DateTime.parse(json['date'] as String) + : DateTime.now(), + startTime: json['startTime'] as String? ?? '', + endTime: json['endTime'] as String? ?? '', + cost: _parseInt(json['cost']), + user: json['user'] != null + ? BookingUser.fromJson(json['user'] as Map) + : BookingUser.empty(), + space: json['space'] != null + ? BookingSpace.fromJson(json['space'] as Map) + : BookingSpace.empty(), + ); + } + + static int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; + } +} + +class BookingUser { + final String uuid; + final String firstName; + final String lastName; + final String email; + final String? companyName; + + BookingUser({ + required this.uuid, + required this.firstName, + required this.lastName, + required this.email, + this.companyName, + }); + + factory BookingUser.fromJson(Map json) { + return BookingUser( + uuid: json['uuid'] as String? ?? '', + firstName: json['firstName'] as String? ?? '', + lastName: json['lastName'] as String? ?? '', + email: json['email'] as String? ?? '', + companyName: json['companyName'] as String?, + ); + } + + factory BookingUser.empty() { + return BookingUser( + uuid: '', + firstName: '', + lastName: '', + email: '', + companyName: null, + ); + } +} + +class BookingSpace { + final String uuid; + final String spaceName; + + BookingSpace({ + required this.uuid, + required this.spaceName, + }); + + factory BookingSpace.fromJson(Map json) { + return BookingSpace( + uuid: json['uuid'] as String? ?? '', + spaceName: json['spaceName'] as String? ?? '', + ); + } + + factory BookingSpace.empty() { + return BookingSpace( + uuid: '', + spaceName: '', + ); + } +} + +class CalendarEventsResponse { + final int statusCode; + final String message; + final List data; + final bool success; + + CalendarEventsResponse({ + required this.statusCode, + required this.message, + required this.data, + required this.success, + }); + + factory CalendarEventsResponse.fromJson(Map json) { + return CalendarEventsResponse( + statusCode: _parseInt(json['statusCode']), + message: json['message'] as String? ?? '', + data: (json['data'] as List? ?? []) + .map((e) => CalendarEventBooking.fromJson(e as Map)) + .toList(), + success: json['success'] as bool? ?? false, + ); + } +} + +int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; +} diff --git a/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart new file mode 100644 index 00000000..9e178040 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart @@ -0,0 +1,7 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; + +abstract class CalendarSystemService { + Future getCalendarEvents({ + required String spaceId, + }); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart index 431720af..da782d74 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart @@ -2,13 +2,17 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; + part 'events_event.dart'; part 'events_state.dart'; class CalendarEventsBloc extends Bloc { final EventController eventController = EventController(); + final CalendarSystemService calendarService; - CalendarEventsBloc() : super(EventsInitial()) { + CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) { on(_onLoadEvents); on(_onAddEvent); on(_onStartTimer); @@ -22,53 +26,24 @@ class CalendarEventsBloc extends Bloc { ) async { emit(EventsLoading()); try { - final events = _generateDummyEventsForWeek(event.weekStart); + final response = await calendarService.getCalendarEvents( + spaceId: event.spaceId, + ); + final events = + response.data.map(_toCalendarEventData).toList(); eventController.addAll(events); - emit(EventsLoaded( - events: events, - initialDate: event.weekStart, - weekDays: _getWeekDays(event.weekStart), - )); + emit(EventsLoaded(events: events)); } 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, )); } } @@ -86,47 +61,44 @@ class CalendarEventsBloc extends Bloc { 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, - ), - ]; + CalendarEventData _toCalendarEventData(CalendarEventBooking booking) { + final date = booking.date; + + final localDate = date.toLocal(); + + final startParts = booking.startTime.split(':').map(int.parse).toList(); + final endParts = booking.endTime.split(':').map(int.parse).toList(); + + final startTime = DateTime( + localDate.year, + localDate.month, + localDate.day, + startParts[0], + startParts[1], + ); + + final endTime = DateTime( + localDate.year, + localDate.month, + localDate.day, + endParts[0], + endParts[1], + ); + + return CalendarEventData( + date: startTime, + startTime: startTime, + endTime: endTime, + title: + '${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}', + description: 'Cost: ${booking.cost}', + color: Colors.blue, + event: booking, + ); } List _getWeekDays(DateTime date) { diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart index e23e65de..4f4cafcf 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart @@ -6,13 +6,20 @@ abstract class CalendarEventsEvent { } class LoadEvents extends CalendarEventsEvent { + final String spaceId; final DateTime weekStart; - const LoadEvents({required this.weekStart}); + final DateTime weekEnd; + + const LoadEvents({ + required this.spaceId, + required this.weekStart, + required this.weekEnd, + }); } class AddEvent extends CalendarEventsEvent { final CalendarEventData event; - AddEvent(this.event); + const AddEvent(this.event); } class StartTimer extends CalendarEventsEvent {} @@ -23,3 +30,8 @@ class GoToWeek extends CalendarEventsEvent { final DateTime weekDate; GoToWeek(this.weekDate); } + +class CheckWeekHasEvents extends CalendarEventsEvent { + final DateTime weekStart; + const CheckWeekHasEvents(this.weekStart); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart index b7263ec8..bc0c2e31 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart @@ -9,13 +9,9 @@ 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, }); } 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 357cac41..0ff9aaf6 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 @@ -1,7 +1,8 @@ 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/data/services/remote_calendar_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.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'; @@ -9,7 +10,9 @@ import 'package:syncrow_web/pages/access_management/booking_system/presentation/ 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/week_navigation.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.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'; @@ -35,33 +38,20 @@ class _BookingPageState extends State { 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 _dispatchLoadEvents(BuildContext context) { + final selectedRoom = + context.read().state.selectedBookableSpace; + final dateState = context.read().state; - void _loadEventsForWeek(DateTime weekStart) { - _eventController.removeWhere((_) => true); - _eventController.addAll(_generateDummyEventsForWeek(weekStart)); + if (selectedRoom != null) { + context.read().add( + LoadEvents( + spaceId: selectedRoom.uuid, + weekStart: dateState.weekStart, + weekEnd: dateState.weekStart.add(const Duration(days: 6)), + ), + ); + } } @override @@ -70,197 +60,181 @@ class _BookingPageState extends State { providers: [ BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => DateSelectionBloc()), + BlocProvider( + create: (_) => CalendarEventsBloc( + calendarService: + FakeRemoteCalendarService(HTTPService(), useDummy: true), + ), + ), ], - child: BlocListener( - listenWhen: (previous, current) => - previous.weekStart != current.weekStart, - listener: (context, state) { - _loadEventsForWeek(state.weekStart); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(3, 0), - blurRadius: 6, - spreadRadius: 0, - ), - ], - ), - child: Column( - children: [ - Expanded( - flex: 2, - child: BlocBuilder( - builder: (context, state) { - return BookingSidebar( - onRoomSelected: (selectedRoom) { - context - .read() - .add(SelectBookableSpace(selectedRoom)); - }, - ); - }, + child: Builder( + builder: (context) => + BlocListener( + listenWhen: (prev, curr) => curr is EventsLoaded, + listener: (context, state) { + if (state is EventsLoaded) { + _eventController.removeWhere((_) => true); + _eventController.addAll(state.events); + } + }, + child: BlocListener( + listener: (context, state) => _dispatchLoadEvents(context), + child: BlocListener( + listener: (context, state) => _dispatchLoadEvents(context), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, + ), + ], + ), + 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); + context + .read() + .add(SelectDate(newDate)); + context.read().add( + SelectDateFromSidebarCalendar(newDate)); + }, + ); + }, + ), + ), + ], ), ), - Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return CustomCalendarPage( - selectedDate: dateState.selectedDate, - onDateChanged: (day, month, year) { - final newDate = DateTime(year, month, day); - context - .read() - .add(SelectDate(newDate)); - context - .read() - .add(SelectDateFromSidebarCalendar(newDate)); - }, - ); - }, - ), - ), - ], - ), - ), - ), - Expanded( - flex: 4, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgTextButton( - svgAsset: Assets.homeIcon, - label: 'Manage Bookable Spaces', - onPressed: () {}, - ), - const SizedBox(width: 20), - SvgTextButton( - svgAsset: Assets.groupIcon, - label: 'Manage Users', - 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.lightGrayColor, - blurRadius: 4, - offset: Offset(0, 1), + ), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () {}, + ), + const SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}, ), ], ), - child: Row( - children: [ - IconButton( - iconSize: 15, - icon: const Icon(Icons.arrow_back_ios, - color: ColorsManager.lightGrayColor), - onPressed: () { + BlocBuilder( + builder: (context, state) { + final weekStart = state.weekStart; + final weekEnd = + weekStart.add(const Duration(days: 6)); + return WeekNavigation( + weekStart: weekStart, + weekEnd: weekEnd, + onPreviousWeek: () { context .read() .add(PreviousWeek()); }, - ), - const SizedBox(width: 10), - Text( - _getMonthYearText(weekStart, weekEnd), - style: const TextStyle( - color: ColorsManager.lightGrayColor, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(width: 10), - IconButton( - iconSize: 15, - icon: const Icon(Icons.arrow_forward_ios, - color: ColorsManager.lightGrayColor), - onPressed: () { + onNextWeek: () { context .read() .add(NextWeek()); }, - ), - ], + ); + }, ), - ); - }, - ), - ], - ), - Expanded( - child: BlocBuilder( - builder: (context, roomState) { - 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, - selectedDateFromSideBarCalender: context - .watch() - .state - .selectedDateFromSideBarCalender, - ); - }, - ); - }, + ], + ), + Expanded( + child: BlocBuilder( + builder: (context, roomState) { + final selectedRoom = + roomState.selectedBookableSpace; + return BlocBuilder( + builder: (context, dateState) { + return BlocListener( + listenWhen: (prev, curr) => + curr is EventsLoaded, + listener: (context, state) { + if (state is EventsLoaded) { + _eventController + .removeWhere((_) => true); + _eventController.addAll(state.events); + } + }, + child: WeeklyCalendarPage( + startTime: selectedRoom + ?.bookableConfig.startTime, + endTime: selectedRoom + ?.bookableConfig.endTime, + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + selectedDateFromSideBarCalender: context + .watch() + .state + .selectedDateFromSideBarCalender, + ), + ); + }, + ); + }, + ), + ), + ], ), ), - ], - ), + ), + ], ), ), - ], + ), ), ), ); } - - 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/presentation/view/widgets/event_tile_widget.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart new file mode 100644 index 00000000..6c0f9cb2 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart @@ -0,0 +1,60 @@ +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EventTileWidget extends StatelessWidget { + final List> events; + + const EventTileWidget({ + super.key, + required this.events, + }); + + @override + Widget build(BuildContext context) { + 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, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart new file mode 100644 index 00000000..da74d07f --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class HatchedColumnBackground extends StatelessWidget { + final Color backgroundColor; + final Color lineColor; + final double opacity; + final double stripeSpacing; + final BorderRadius? borderRadius; + + const HatchedColumnBackground({ + super.key, + required this.backgroundColor, + required this.lineColor, + this.opacity = 0.15, + this.stripeSpacing = 12, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _HatchedBackgroundPainter( + backgroundColor: backgroundColor, + opacity: opacity, + lineColor: lineColor, + stripeSpacing: stripeSpacing, + borderRadius: borderRadius, + ), + size: Size.infinite, + ); + } +} + +class _HatchedBackgroundPainter extends CustomPainter { + final Color backgroundColor; + final double opacity; + final Color lineColor; + final double stripeSpacing; + final BorderRadius? borderRadius; + + _HatchedBackgroundPainter({ + required this.backgroundColor, + required this.opacity, + required this.lineColor, + required this.stripeSpacing, + this.borderRadius, + }); + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final RRect rrect = borderRadius?.toRRect(rect) ?? + RRect.fromRectAndRadius(rect, Radius.zero); + final backgroundPaint = Paint() + ..color = backgroundColor.withOpacity(0.02) + ..style = PaintingStyle.fill; + canvas.drawRRect(rrect, backgroundPaint); + canvas.save(); + canvas.clipRRect(rrect); + final linePaint = Paint() + ..color = lineColor + ..strokeWidth = 0.5 + ..style = PaintingStyle.stroke; + final maxExtent = + math.sqrt(size.width * size.width + size.height * size.height); + + canvas.translate(0, size.height); + canvas.rotate(-math.pi / 4); + double y = -maxExtent; + while (y < maxExtent) { + canvas.drawLine( + Offset(-maxExtent, y), + Offset(maxExtent, y), + linePaint, + ); + y += stripeSpacing; + } + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) { + return backgroundColor != oldDelegate.backgroundColor || + opacity != oldDelegate.opacity || + lineColor != oldDelegate.lineColor || + stripeSpacing != oldDelegate.stripeSpacing || + borderRadius != oldDelegate.borderRadius; + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart new file mode 100644 index 00000000..eada3b97 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class TimeLineWidget extends StatelessWidget { + final DateTime date; + + const TimeLineWidget({Key? key, required this.date}) : super(key: key); + + @override + Widget build(BuildContext context) { + 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, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart new file mode 100644 index 00000000..57e35c6d --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeekDayHeader extends StatelessWidget { + final DateTime date; + final bool isSelectedDay; + + const WeekDayHeader({ + Key? key, + required this.date, + required this.isSelectedDay, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + 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, + ), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart new file mode 100644 index 00000000..bdc65b8e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeekNavigation extends StatelessWidget { + final DateTime weekStart; + final DateTime weekEnd; + final VoidCallback onPreviousWeek; + final VoidCallback onNextWeek; + + const WeekNavigation({ + Key? key, + required this.weekStart, + required this.weekEnd, + required this.onPreviousWeek, + required this.onNextWeek, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_back_ios, + color: ColorsManager.lightGrayColor), + onPressed: onPreviousWeek, + ), + const SizedBox(width: 10), + Text( + _getMonthYearText(weekStart, weekEnd), + style: const TextStyle( + color: ColorsManager.lightGrayColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 10), + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_forward_ios, + color: ColorsManager.lightGrayColor), + onPressed: onNextWeek, + ), + ], + ), + ); + } + + 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/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index 5c38e2fc..0dd343a7 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 @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:calendar_view/calendar_view.dart'; -import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class WeeklyCalendarPage extends StatelessWidget { @@ -60,18 +63,39 @@ class WeeklyCalendarPage extends StatelessWidget { const double timeLineWidth = 80; const int totalDays = 7; - + final DateTime highlightStart = DateTime(2025, 7, 10); + final DateTime highlightEnd = DateTime(2025, 7, 19); return LayoutBuilder( builder: (context, constraints) { final double calendarWidth = constraints.maxWidth; final double dayColumnWidth = (calendarWidth - timeLineWidth) / totalDays - 0.1; + bool isInRange(DateTime date, DateTime start, DateTime end) { + return !date.isBefore(start) && !date.isAfter(end); + } return Padding( padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( children: [ WeekView( + weekDetectorBuilder: ({ + required date, + required height, + required heightPerMinute, + required minuteSlotSize, + required width, + }) { + return isInRange(date, highlightStart, highlightEnd) + ? HatchedColumnBackground( + backgroundColor: ColorsManager.grey800, + lineColor: ColorsManager.textGray, + opacity: 0.3, + stripeSpacing: 12, + borderRadius: BorderRadius.circular(8), + ) + : const SizedBox(); + }, pageViewPhysics: const NeverScrollableScrollPhysics(), key: ValueKey(weekStart), controller: eventController, @@ -88,70 +112,13 @@ class WeeklyCalendarPage extends StatelessWidget { height: 0, ), weekDayBuilder: (date) { - final index = weekDays.indexWhere((d) => isSameDay(d, date)); - final isSelectedDay = index == selectedDayIndex; - 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, - ), - ), - ], + return WeekDayHeader( + date: date, + isSelectedDay: isSameDay(date, selectedDate), ); }, 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 TimeLineWidget(date: date); }, timeLineWidth: timeLineWidth, weekPageHeaderBuilder: (start, end) => Container(), @@ -174,49 +141,8 @@ class WeeklyCalendarPage extends StatelessWidget { ), ), 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, - ), - ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ); - }).toList(), - ), + return EventTileWidget( + events: events, ); }, ), diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index a36d1193..55bfef1d 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -69,7 +69,6 @@ abstract class ColorsManager { static const Color invitedOrange = Color(0xFFFFE193); static const Color invitedOrangeText = Color(0xFFFFBF00); static const Color lightGrayBorderColor = Color(0xB2D5D5D5); - //background: #F8F8F8; static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); @@ -85,4 +84,5 @@ abstract class ColorsManager { static const Color minBlueDot = Color(0xFF023DFE); static const Color grey25 = Color(0xFFF9F9F9); static const Color grey50 = Color(0xFF718096); + static const Color grey800 = Color(0xffF8F8F8); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index f908db85..8797f0cd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -141,4 +141,5 @@ abstract class ApiEndpoints { static const String saveSchedule = '/schedule/{deviceUuid}'; static const String getBookableSpaces = '/bookable-spaces'; + static const String getCalendarEvents = '/api'; } From c112cde63490aa5ca9a90d03a1444859270d3b46 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 14 Jul 2025 11:04:29 +0300 Subject: [PATCH 87/89] Uses Inkwell instead of Gesture Detector for canvas widgets. --- .../main_module/widgets/create_space_button.dart | 2 +- .../space_management_v2/main_module/widgets/space_cell.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index 90d359e2..e6dfbb15 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -22,7 +22,7 @@ class _CreateSpaceButtonState extends State { return Tooltip( margin: const EdgeInsets.symmetric(vertical: 24), message: 'Create a new space', - child: GestureDetector( + child: InkWell( onTap: () => SpaceDetailsDialogHelper.showCreate( context, communityUuid: widget.communityUuid, diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart index bcde6560..80b18526 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -17,7 +17,7 @@ class SpaceCell extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( + return InkWell( onTap: onTap, child: Container( width: 150, From 81679266209c8a325f97e92d34d3916c12160a99 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 14 Jul 2025 15:14:56 +0300 Subject: [PATCH 88/89] Enhance UI components: update color management, adjust button styles, and improve text formatting for better readability --- .../schedule_widgets/count_down_button.dart | 10 ++- .../count_down_inching_view.dart | 4 +- .../schedule_widgets/schedual_view.dart | 10 +-- .../schedule_widgets/schedule_header.dart | 6 +- .../schedule_managment_ui.dart | 2 +- .../schedule_mode_buttons.dart | 6 +- .../schedule_mode_selector.dart | 3 +- .../helper/add_schedule_dialog_helper.dart | 63 ++++++++----------- lib/utils/color_manager.dart | 1 + 9 files changed, 55 insertions(+), 50 deletions(-) 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 b28a6a23..8fa1e290 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 @@ -29,7 +29,9 @@ class CountdownModeButtons extends StatelessWidget { children: [ Expanded( child: DefaultButton( + elevation: 2.5, height: 40, + borderRadius: 8, onPressed: () => Navigator.pop(context), backgroundColor: ColorsManager.boxColor, child: Text('Cancel', style: context.textTheme.bodyMedium), @@ -39,6 +41,8 @@ class CountdownModeButtons extends StatelessWidget { Expanded( child: isActive ? DefaultButton( + elevation: 2.5, + borderRadius: 8, height: 40, onPressed: () { context.read().add( @@ -49,10 +53,12 @@ class CountdownModeButtons extends StatelessWidget { ), ); }, - backgroundColor: Colors.red, + backgroundColor: ColorsManager.red100, child: const Text('Stop'), ) : DefaultButton( + elevation: 2.5, + borderRadius: 8, height: 40, onPressed: () { context.read().add( @@ -63,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget { countDownCode: countDownCode), ); }, - backgroundColor: ColorsManager.primaryColor, + backgroundColor: ColorsManager.primaryColorWithOpacity, child: const Text('Save'), ), ), 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 e64b7cf7..c6a35bb6 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 @@ -226,6 +226,7 @@ class _CountdownInchingViewState extends State { index.toString().padLeft(2, '0'), style: TextStyle( fontSize: 24, + fontWeight: FontWeight.w400, color: isActive ? ColorsManager.grayColor : Colors.black, ), ), @@ -240,7 +241,8 @@ class _CountdownInchingViewState extends State { label, style: const TextStyle( color: ColorsManager.grayColor, - fontSize: 18, + fontSize: 24, + fontWeight: FontWeight.w400, ), ), ], 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 b654698d..d5194f35 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 @@ -31,12 +31,11 @@ class BuildScheduleView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ScheduleBloc( - deviceId: deviceUuid, - ) + create: (_) => ScheduleBloc(deviceId: deviceUuid,) ..add(ScheduleGetEvent(category: category)) ..add(ScheduleFetchStatusEvent( - deviceId: deviceUuid, countdownCode: countdownCode ?? '')), + deviceId: deviceUuid, + countdownCode: countdownCode ?? '')), child: Dialog( backgroundColor: Colors.white, insetPadding: const EdgeInsets.all(20), @@ -77,7 +76,8 @@ class BuildScheduleView extends StatelessWidget { category: category, time: '', function: Status( - code: code.toString(), value: null), + code: code.toString(), + value: true), days: [], ), isEdit: false, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart index 87afe430..06f785eb 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart @@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget { Text( 'Scheduling', style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, - color: ColorsManager.dialogBlueTitle, + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.w700, + fontSize: 30, ), ), Container( 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 1a89c1ee..39899fe5 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 @@ -27,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget { width: 170, height: 40, child: DefaultButton( - borderColor: ColorsManager.boxColor, + borderColor: ColorsManager.grayColor.withOpacity(0.5), padding: 2, backgroundColor: ColorsManager.graysColor, borderRadius: 15, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart index f1307d5f..f1df1f20 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart @@ -19,6 +19,8 @@ class ScheduleModeButtons extends StatelessWidget { children: [ Expanded( child: DefaultButton( + elevation: 2.5, + borderRadius: 8, height: 40, onPressed: () { Navigator.pop(context); @@ -33,9 +35,11 @@ class ScheduleModeButtons extends StatelessWidget { const SizedBox(width: 20), Expanded( child: DefaultButton( + elevation: 2.5, + borderRadius: 8, height: 40, onPressed: onSave, - backgroundColor: ColorsManager.primaryColor, + backgroundColor: ColorsManager.primaryColorWithOpacity, child: const Text('Save'), ), ), 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 200d8c66..3b2f6502 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 @@ -35,12 +35,12 @@ class ScheduleModeSelector extends StatelessWidget { ), const SizedBox(height: 4), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildRadioTile( context, 'Countdown', ScheduleModes.countdown, currentMode), _buildRadioTile( context, 'Schedule', ScheduleModes.schedule, currentMode), + const Spacer(flex: 1), // _buildRadioTile( // context, 'Circulate', ScheduleModes.circulate, currentMode), // _buildRadioTile( @@ -65,6 +65,7 @@ class ScheduleModeSelector extends StatelessWidget { style: context.textTheme.bodySmall!.copyWith( fontSize: 13, color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, ), ), leading: Radio( 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 0a65595e..e5695b9e 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class ScheduleDialogHelper { static const List allDays = [ @@ -56,8 +58,9 @@ class ScheduleDialogHelper { Text( isEdit ? 'Edit Schedule' : 'Add Schedule', style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Colors.blue, - fontWeight: FontWeight.bold, + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.w700, + fontSize: 30, ), ), const SizedBox(), @@ -69,9 +72,9 @@ class ScheduleDialogHelper { height: 40, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[200], + backgroundColor: ColorsManager.boxColor, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + borderRadius: BorderRadius.circular(8), ), ), onPressed: () async { @@ -110,39 +113,27 @@ class ScheduleDialogHelper { ], ), actions: [ - SizedBox( - width: 100, - child: OutlinedButton( - onPressed: () { - Navigator.pop(ctx, null); - }, - child: const Text('Cancel'), - ), + ScheduleModeButtons( + onSave: () { + 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); + }, ), - SizedBox( - 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/utils/color_manager.dart b/lib/utils/color_manager.dart index a36d1193..f2123046 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -85,4 +85,5 @@ abstract class ColorsManager { static const Color minBlueDot = Color(0xFF023DFE); static const Color grey25 = Color(0xFFF9F9F9); static const Color grey50 = Color(0xFF718096); + static const Color red100 = Color(0xFFFE0202); } From 338d4f5737d5f6fdba39d117b2f3d025279458cf Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Tue, 15 Jul 2025 08:19:43 +0300 Subject: [PATCH 89/89] fix typo --- .../users_page/add_user_dialog/view/basics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 250bba3f..7128ef2c 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 @@ -332,7 +332,7 @@ class BasicsView extends StatelessWidget { style: const TextStyle(color: ColorsManager.blackColor), decoration: inputTextFormDeco( - hintText: "Comapny Name (Optional)") + hintText: 'Company Name (Optional)') .copyWith( hintStyle: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w400,