Compare commits

..

48 Commits

Author SHA1 Message Date
acefe6b433 fixed repeated duplication 2025-04-29 10:13:11 +04:00
63bc7a56de - Refactor the code in _RoutinesViewState to improve readability and maintainability.
- Update the indentation and add padding to the child widgets in the Column.
- Add a bottom padding to the empty state text in _buildEmptyState.
2025-04-28 16:49:22 +03:00
7b3635deae Merge pull request #166 from SyncrowIOT/bugfix/clear-search
fixed community search caching
2025-04-28 16:27:38 +04:00
58755eafe1 Merge branch 'dev' of https://github.com/SyncrowIOT/web into bugfix/clear-search 2025-04-28 16:26:47 +04:00
ce225818fb fixed community search caching 2025-04-28 16:25:43 +04:00
8762a7aaa8 Merge pull request #165 from SyncrowIOT/bugfix/white-page-rendering 2025-04-28 15:22:37 +04:00
8dc833b2c3 fixed blank page rendering 2025-04-28 15:21:28 +04:00
13cef151aa Merge pull request #164 from SyncrowIOT/bugfix/space-model-with-tags 2025-04-28 14:37:26 +04:00
ab23be9828 fixed the issue in selecting space model and tag 2025-04-28 14:36:27 +04:00
687b68ab22 Merge pull request #163 from SyncrowIOT/fix/duplication-flatten
Fix/duplication-flatten
2025-04-28 13:00:17 +04:00
25614c3dd0 fix the flatten 2025-04-28 12:59:16 +04:00
7cbe20ae88 remove unused code 2025-04-28 12:56:29 +04:00
349fe6c555 realignment 2025-04-28 11:58:41 +04:00
9779f3783c Merge branch 'dev' of https://github.com/SyncrowIOT/web into dev 2025-04-28 10:03:31 +04:00
fe3db663b6 realign initially 2025-04-28 10:03:27 +04:00
888d444752 Merge pull request #160 from SyncrowIOT/SP-1478-FE-On-devices-management-page-when-we-open-power-clamp-device-loading-indicator-remains-loading-and-no-data-is-displayed
Sp 1478 fe on devices management page when we open power clamp device loading indicator remains loading and no data is displayed
2025-04-28 08:59:17 +03:00
bab5e06968 Merge pull request #159 from SyncrowIOT/SP-1463-rework
Sp 1463 rework
2025-04-28 08:58:39 +03:00
d7b6174dee Merge pull request #162 from SyncrowIOT:bugfix/save-spaces
fixed the save issue
2025-04-28 00:37:33 +04:00
6ef0b2f9d1 fixed the save issue 2025-04-28 00:36:58 +04:00
3ceb03826e Merge pull request #161 from SyncrowIOT/bugfix/searchquery 2025-04-27 22:44:57 +04:00
52608b1f35 fixed search query 2025-04-27 22:42:57 +04:00
ac2996629e resolved an exception that was thrown when resizing the DeviceSearchFilters. 2025-04-27 15:42:50 +03:00
51c52c66cb SP-1478-FE-On-devices-management-page-when-we-open-power-clamp-device-loading-indicator-remains-loading-and-no-data-is-displayed 2025-04-27 15:18:19 +03:00
0f56273d99 SP-1408 2025-04-27 12:10:38 +03:00
34d4d892d9 refactor: streamline value calculations in FlushMountedPresenceSensorControlView 2025-04-27 11:11:38 +03:00
3193fd26fe refactor: update presence delay value and enhance request handling in DebouncedBatchControlDevicesService 2025-04-27 11:04:54 +03:00
43802a9fad refactor: update detection value calculations and adjust parameters for presence delay 2025-04-27 10:57:44 +03:00
6e0b1775f0 fix: reorder constructor parameters for consistency in FlushMountedPresenceSensorControlView 2025-04-27 10:55:18 +03:00
233fb2ee2c refactor: improve formatting of clamp method for near and far detection values 2025-04-27 10:55:03 +03:00
b26928b3d5 fixed ui bugs. 2025-04-27 10:14:35 +03:00
6fc35a7b9a enhanced device debouncing to accomodate for multiple calls from the different devices functions calls. 2025-04-27 10:14:19 +03:00
756457927c removed unnecessary isBatch flag from FlushMountedPresenceSensorChangeValueEvent. 2025-04-27 10:13:53 +03:00
f30d7c0117 Merge pull request #158 from SyncrowIOT:bugfix/duplicate-space
Bugfix/duplicate-space
2025-04-27 11:13:07 +04:00
976d6e385a duplicated space 2025-04-27 11:12:03 +04:00
ff07e7509d fixed the issue in aligning child space 2025-04-26 16:04:41 +04:00
17a582ab99 Merge pull request #157 from SyncrowIOT/SP-1415 2025-04-25 10:50:06 +04:00
09fb604acc added filtering 2025-04-25 10:49:25 +04:00
2068df173d Merge pull request #155 from SyncrowIOT/SP-1441-rework-FE-On-routine-creation-page-When-the-user-drags-a-card-that-has-signs-and-selects-a-sign-without-a-number-then-confirms-the-value-appears-to-be-Null
Sp 1441 rework fe on routine creation page when the user drags a card that has signs and selects a sign without a number then confirms the value appears to be null
2025-04-24 16:25:39 +03:00
bfc2a381d2 Merge pull request #156 from SyncrowIOT/SP-1464-FE-implement-Batch-Control-Dialog
Sp 1464 fe implement batch control dialog
2025-04-24 16:25:17 +03:00
778257644d reduced debounce duration. 2025-04-24 15:13:10 +03:00
c8e540e938 Remove unnecessary event dispatch in FlushMountedPresenceSensorBlocFactory creation 2025-04-24 14:29:18 +03:00
ba20998067 disabled realtime on batch control. 2025-04-24 14:21:38 +03:00
75b0b24543 Add Flush Mounted Presence Sensor support and update event handling 2025-04-24 14:13:13 +03:00
c03b8f290c refactor function tap handlers to use RoutineTapFunctionHelper for improved code reuse and readability, and to remove code duplication. 2025-04-24 10:25:41 +03:00
2c684a9495 SP-1441 rework. 2025-04-23 16:58:50 +03:00
fbc45b465f Merge pull request #153 from SyncrowIOT/SP-1344-FE-Real-Time-Issues-Ceiling-Sensor-AC-and-Garage-Door-Sensor
Refactor widget lifecycle methods for temperature control and presenc…
2025-04-23 16:29:36 +03:00
c9c939c59f Merge pull request #154 from SyncrowIOT/Add-Flush-Mounted-Presence-Sensor-Single-Control
Add flush mounted presence sensor single control
2025-04-23 16:21:17 +03:00
d1bb7b129f Refactor widget lifecycle methods for temperature control and presence sensor 2025-04-23 10:46:56 +03:00
37 changed files with 844 additions and 525 deletions

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.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'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SearchResetButtons extends StatelessWidget { class SearchResetButtons extends StatelessWidget {
const SearchResetButtons({ const SearchResetButtons({
@ -17,8 +17,10 @@ class SearchResetButtons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Column( Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const SizedBox(height: 25), const SizedBox(height: 25),

View File

@ -60,7 +60,15 @@ class _CurrentTempState extends State<CurrentTemp> {
); );
}); });
} }
@override
void didUpdateWidget(CurrentTemp oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.tempSet != widget.tempSet) {
setState(() {
_adjustedValue = _initialAdjustedValue(widget.tempSet);
});
}
}
@override @override
void dispose() { void dispose() {
_debounce?.cancel(); _debounce?.cancel();

View File

@ -9,6 +9,7 @@ import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_st
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_control_view.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_control_view.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_control_view.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_control_view.dart';
@ -198,6 +199,10 @@ mixin RouteControlsBasedCode {
return SOSBatchControlView( return SOSBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(), deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(),
); );
case 'NCPS':
return FlushMountedPresenceSensorBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(),
);
default: default:
return const SizedBox(); return const SizedBox();
} }

View File

@ -108,7 +108,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
return DeviceManagementBody( return DeviceManagementBody(
devices: deviceState.filteredDevices); devices: deviceState.filteredDevices);
} else { } else {
return const Center(child: Text('Error fetching Devices')); return const DeviceManagementBody(devices: []);
} }
}, },
); );

View File

@ -72,6 +72,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
child: state is DeviceManagementLoading child: state is DeviceManagementLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Column( : Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: isLargeScreenSize(context) padding: isLargeScreenSize(context)

View File

@ -14,29 +14,29 @@ class DeviceSearchFilters extends StatefulWidget {
class _DeviceSearchFiltersState extends State<DeviceSearchFilters> class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
with HelperResponsiveLayout { with HelperResponsiveLayout {
final _unitNameController = TextEditingController(); late final TextEditingController _unitNameController;
final _productNameController = TextEditingController(); late final TextEditingController _productNameController;
List<Widget> get _widgets => [ @override
_buildSearchField("Space Name", _unitNameController, 200), void initState() {
_buildSearchField("Device Name / Product Name", _productNameController, 300), _unitNameController = TextEditingController();
_buildSearchResetButtons(), _productNameController = TextEditingController();
]; super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isExtraLargeScreenSize(context)) {
return Row(
children: _widgets
.map((e) => Padding(padding: const EdgeInsets.all(10), child: e))
.toList(),
);
}
return Wrap( return Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 20, spacing: 20,
runSpacing: 10, runSpacing: 10,
children: _widgets, children: [
_buildSearchField("Space Name", _unitNameController, 200),
_buildSearchField("Device Name / Product Name", _productNameController, 300),
_buildSearchResetButtons(),
],
); );
} }

View File

@ -16,7 +16,8 @@ import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dar
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLayout { class CeilingSensorControlsView extends StatelessWidget
with HelperResponsiveLayout {
const CeilingSensorControlsView({super.key, required this.device}); const CeilingSensorControlsView({super.key, required this.device});
final AllDevicesModel device; final AllDevicesModel device;
@ -31,29 +32,35 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
..add(CeilingInitialEvent(device.uuid ?? '')), ..add(CeilingInitialEvent(device.uuid ?? '')),
child: BlocBuilder<CeilingSensorBloc, CeilingSensorState>( child: BlocBuilder<CeilingSensorBloc, CeilingSensorState>(
builder: (context, state) { builder: (context, state) {
if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) { if (state is CeilingLoadingInitialState ||
state is CeilingReportsLoadingState) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (state is CeilingUpdateState) { } else if (state is CeilingUpdateState) {
return _buildGridView( return _buildGridView(context, state.ceilingSensorModel,
context, state.ceilingSensorModel, isExtraLarge, isLarge, isMedium); isExtraLarge, isLarge, isMedium);
} else if (state is CeilingReportsState) { } else if (state is CeilingReportsState) {
return ReportsTable( return ReportsTable(
report: state.deviceReport, report: state.deviceReport,
onRowTap: (index) {}, onRowTap: (index) {},
onClose: () { onClose: () {
context.read<CeilingSensorBloc>().add(BackToCeilingGridViewEvent()); context
.read<CeilingSensorBloc>()
.add(BackToCeilingGridViewEvent());
}, },
); );
} else if (state is ShowCeilingDescriptionState) { } else if (state is ShowCeilingDescriptionState) {
return DescriptionView( return DescriptionView(
description: state.description, description: state.description,
onClose: () { onClose: () {
context.read<CeilingSensorBloc>().add(BackToCeilingGridViewEvent()); context
.read<CeilingSensorBloc>()
.add(BackToCeilingGridViewEvent());
}, },
); );
} else if (state is CeilingReportsFailedState) { } else if (state is CeilingReportsFailedState) {
final model = context.read<CeilingSensorBloc>().deviceStatus; final model = context.read<CeilingSensorBloc>().deviceStatus;
return _buildGridView(context, model, isExtraLarge, isLarge, isMedium); return _buildGridView(
context, model, isExtraLarge, isLarge, isMedium);
} }
return const Center(child: Text('Error fetching status')); return const Center(child: Text('Error fetching status'));
}, },
@ -61,8 +68,8 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
); );
} }
Widget _buildGridView(BuildContext context, CeilingSensorModel model, bool isExtraLarge, Widget _buildGridView(BuildContext context, CeilingSensorModel model,
bool isLarge, bool isMedium) { bool isExtraLarge, bool isLarge, bool isMedium) {
return GridView( return GridView(
padding: const EdgeInsets.symmetric(horizontal: 50), padding: const EdgeInsets.symmetric(horizontal: 50),
shrinkWrap: true, shrinkWrap: true,
@ -143,8 +150,8 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context.read<CeilingSensorBloc>().add( context.read<CeilingSensorBloc>().add(GetCeilingDeviceReportsEvent(
GetCeilingDeviceReportsEvent(code: 'presence_state', deviceUuid: device.uuid!)); code: 'presence_state', deviceUuid: device.uuid!));
}, },
child: const PresenceStaticWidget( child: const PresenceStaticWidget(
icon: Assets.illuminanceRecordIcon, icon: Assets.illuminanceRecordIcon,
@ -153,9 +160,8 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context context.read<CeilingSensorBloc>().add(GetCeilingDeviceReportsEvent(
.read<CeilingSensorBloc>() code: '', deviceUuid: device.uuid!));
.add(GetCeilingDeviceReportsEvent(code: '', deviceUuid: device.uuid!));
}, },
child: const PresenceStaticWidget( child: const PresenceStaticWidget(
icon: Assets.helpDescriptionIcon, icon: Assets.helpDescriptionIcon,

View File

@ -49,6 +49,9 @@ class FlushMountedPresenceSensorBloc
on<FlushMountedPresenceSensorFactoryResetEvent>( on<FlushMountedPresenceSensorFactoryResetEvent>(
_onFlushMountedPresenceSensorFactoryResetEvent, _onFlushMountedPresenceSensorFactoryResetEvent,
); );
on<FlushMountedPresenceSensorStatusUpdatedEvent>(
_onFlushMountedPresenceSensorStatusUpdatedEvent,
);
} }
void _onFlushMountedPresenceSensorFetchStatusEvent( void _onFlushMountedPresenceSensorFetchStatusEvent(
@ -60,7 +63,7 @@ class FlushMountedPresenceSensorBloc
final response = await DevicesManagementApi().getDeviceStatus(deviceId); final response = await DevicesManagementApi().getDeviceStatus(deviceId);
deviceStatus = FlushMountedPresenceSensorModel.fromJson(response.status); deviceStatus = FlushMountedPresenceSensorModel.fromJson(response.status);
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus)); emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
_listenToChanges(emit, deviceId); _listenToChanges(deviceId);
} catch (e) { } catch (e) {
emit(FlushMountedPresenceSensorFailedState(error: e.toString())); emit(FlushMountedPresenceSensorFailedState(error: e.toString()));
return; return;
@ -81,31 +84,33 @@ class FlushMountedPresenceSensorBloc
} }
} }
Future<void> _listenToChanges( void _listenToChanges(String deviceId) {
Emitter<FlushMountedPresenceSensorState> emit, try {
String deviceId, final ref = FirebaseDatabase.instance.ref(
) async { 'device-status/$deviceId',
final ref = FirebaseDatabase.instance.ref( );
'device-status/$deviceId',
); ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
await ref.onValue.listen(
(DatabaseEvent event) async {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = []; List<Status> statusList = [];
eventsMap['status'].forEach((element) {
(usersMap['status'] as List<dynamic>?)?.forEach((element) { statusList.add(
statusList.add(Status(code: element['code'], value: element['value'])); Status(code: element['code'], value: element['value']),
);
}); });
deviceStatus = FlushMountedPresenceSensorModel.fromJson(statusList); deviceStatus = FlushMountedPresenceSensorModel.fromJson(statusList);
if (!emit.isDone) { if (!isClosed) {
emit(FlushMountedPresenceSensorLoadingNewSate(model: deviceStatus)); add(FlushMountedPresenceSensorStatusUpdatedEvent(deviceStatus));
} }
}, });
onError: (error) => log(error.toString(), name: 'FirebaseDatabaseError'), } catch (_) {
).asFuture(); log(
'Error listening to changes',
name: 'FlushMountedPresenceSensorBloc._listenToChanges',
);
}
} }
void _onFlushMountedPresenceSensorChangeValueEvent( void _onFlushMountedPresenceSensorChangeValueEvent(
@ -234,4 +239,12 @@ class FlushMountedPresenceSensorBloc
emit(FlushMountedPresenceSensorFailedState(error: e.toString())); emit(FlushMountedPresenceSensorFailedState(error: e.toString()));
} }
} }
void _onFlushMountedPresenceSensorStatusUpdatedEvent(
FlushMountedPresenceSensorStatusUpdatedEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) {
deviceStatus = event.model;
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
}
} }

View File

@ -10,15 +10,24 @@ sealed class FlushMountedPresenceSensorEvent extends Equatable {
class FlushMountedPresenceSensorFetchStatusEvent class FlushMountedPresenceSensorFetchStatusEvent
extends FlushMountedPresenceSensorEvent {} extends FlushMountedPresenceSensorEvent {}
class FlushMountedPresenceSensorStatusUpdatedEvent
extends FlushMountedPresenceSensorEvent {
const FlushMountedPresenceSensorStatusUpdatedEvent(this.model);
final FlushMountedPresenceSensorModel model;
@override
List<Object> get props => [model];
}
class FlushMountedPresenceSensorChangeValueEvent class FlushMountedPresenceSensorChangeValueEvent
extends FlushMountedPresenceSensorEvent { extends FlushMountedPresenceSensorEvent {
final int value; final int value;
final String code; final String code;
final bool isBatchControl;
const FlushMountedPresenceSensorChangeValueEvent({ const FlushMountedPresenceSensorChangeValueEvent({
required this.value, required this.value,
required this.code, required this.code,
this.isBatchControl = false,
}); });
@override @override

View File

@ -16,6 +16,6 @@ abstract final class FlushMountedPresenceSensorBlocFactory {
batchControlDevicesService: DebouncedBatchControlDevicesService( batchControlDevicesService: DebouncedBatchControlDevicesService(
decoratee: RemoteBatchControlDevicesService(), decoratee: RemoteBatchControlDevicesService(),
), ),
)..add(FlushMountedPresenceSensorFetchStatusEvent()); );
} }
} }

View File

@ -22,7 +22,7 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
return BlocProvider( return BlocProvider(
create: (context) => FlushMountedPresenceSensorBlocFactory.create( create: (context) => FlushMountedPresenceSensorBlocFactory.create(
deviceId: devicesIds.first, deviceId: devicesIds.first,
), )..add(FlushMountedPresenceSensorFetchBatchStatusEvent(devicesIds)),
child: BlocBuilder<FlushMountedPresenceSensorBloc, child: BlocBuilder<FlushMountedPresenceSensorBloc,
FlushMountedPresenceSensorState>( FlushMountedPresenceSensorState>(
builder: (context, state) { builder: (context, state) {
@ -67,14 +67,15 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
maxValue: 9, maxValue: 9,
steps: 1, steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeSensitivity, code: FlushMountedPresenceSensorModel.codeSensitivity,
value: value, value: value,
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.nearDetection / 100).toDouble(), value: (model.nearDetection / 100).clamp(0.0, double.infinity),
title: 'Nearest Detect Dist:', title: 'Nearest Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -83,14 +84,15 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
valuesPercision: 1, valuesPercision: 1,
action: (double value) => action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add( context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeNearDetection, code: FlushMountedPresenceSensorModel.codeNearDetection,
value: (value * 100).toInt(), value: (value * 100).toInt(),
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.farDetection / 100).toDouble(), value: (model.farDetection / 100).clamp(0.0, double.infinity),
title: 'Max Detect Dist:', title: 'Max Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -99,51 +101,57 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
valuesPercision: 1, valuesPercision: 1,
action: (double value) => action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add( context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeFarDetection, code: FlushMountedPresenceSensorModel.codeFarDetection,
value: (value * 100).toInt(), value: (value * 100).toInt(),
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: model.presenceDelay.toDouble(), value: model.sensiReduce.toDouble(),
title: 'Trigger Level:', title: 'Trigger Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
steps: 1, steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorBatchControlEvent(
code: FlushMountedPresenceSensorModel.codePresenceDelay, deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeSensiReduce,
value: value, value: value,
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.occurDistReduce.toDouble()), value: model.occurDistReduce.toDouble(),
title: 'Indent Level:', title: 'Indent Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
steps: 1, steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeOccurDistReduce, code: FlushMountedPresenceSensorModel.codeOccurDistReduce,
value: value, value: value,
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.sensiReduce.toDouble()), value: (model.presenceDelay / 10).toDouble(),
title: 'Target Confirm Time:', title: 'Target Confirm Time:',
description: 's', description: 's',
minValue: 0, minValue: 0.0,
maxValue: 3, maxValue: 0.5,
steps: 1, steps: 0.1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( valuesPercision: 1,
FlushMountedPresenceSensorChangeValueEvent( action: (double value) =>
code: FlushMountedPresenceSensorModel.codeSensiReduce, context.read<FlushMountedPresenceSensorBloc>().add(
value: value, FlushMountedPresenceSensorBatchControlEvent(
), deviceIds: devicesIds,
), code: FlushMountedPresenceSensorModel.codePresenceDelay,
value: (value * 10).toInt(),
),
),
), ),
PresenceUpdateData( PresenceUpdateData(
value: ((model.noneDelay / 10).toDouble()), value: ((model.noneDelay / 10).toDouble()),
@ -154,7 +162,8 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
steps: 1, steps: 1,
action: (double value) => action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add( context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeNoneDelay, code: FlushMountedPresenceSensorModel.codeNoneDelay,
value: (value * 10).round(), value: (value * 10).round(),
), ),

View File

@ -15,7 +15,7 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la
class FlushMountedPresenceSensorControlView extends StatelessWidget class FlushMountedPresenceSensorControlView extends StatelessWidget
with HelperResponsiveLayout { with HelperResponsiveLayout {
const FlushMountedPresenceSensorControlView({super.key, required this.device}); const FlushMountedPresenceSensorControlView({required this.device, super.key});
final AllDevicesModel device; final AllDevicesModel device;
@ -24,7 +24,7 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
return BlocProvider( return BlocProvider(
create: (context) => FlushMountedPresenceSensorBlocFactory.create( create: (context) => FlushMountedPresenceSensorBlocFactory.create(
deviceId: device.uuid ?? '-1', deviceId: device.uuid ?? '-1',
), )..add(FlushMountedPresenceSensorFetchStatusEvent()),
child: BlocBuilder<FlushMountedPresenceSensorBloc, child: BlocBuilder<FlushMountedPresenceSensorBloc,
FlushMountedPresenceSensorState>( FlushMountedPresenceSensorState>(
builder: (context, state) { builder: (context, state) {
@ -113,7 +113,7 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.nearDetection / 100).toDouble(), value: (model.nearDetection / 100).clamp(0.0, double.infinity),
title: 'Nearest Detect Dist:', title: 'Nearest Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -129,7 +129,7 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.farDetection / 100).toDouble(), value: (model.farDetection / 100).clamp(0.0, double.infinity),
title: 'Max Detect Dist:', title: 'Max Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -145,20 +145,20 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.presenceDelay.toDouble()), value: model.sensiReduce.toDouble(),
title: 'Trigger Level:', title: 'Trigger Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
steps: 1, steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorChangeValueEvent(
code: FlushMountedPresenceSensorModel.codePresenceDelay, code: FlushMountedPresenceSensorModel.codeSensiReduce,
value: value, value: value,
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.occurDistReduce.toDouble()), value: model.occurDistReduce.toDouble(),
title: 'Indent Level:', title: 'Indent Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
@ -171,21 +171,23 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.sensiReduce.toDouble()), value: (model.presenceDelay / 10).toDouble(),
valuesPercision: 1,
title: 'Target Confirm Time:', title: 'Target Confirm Time:',
description: 's', description: 's',
minValue: 0, minValue: 0.0,
maxValue: 3, maxValue: 0.5,
steps: 1, steps: 0.1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (double value) =>
FlushMountedPresenceSensorChangeValueEvent( context.read<FlushMountedPresenceSensorBloc>().add(
code: FlushMountedPresenceSensorModel.codeSensiReduce, FlushMountedPresenceSensorChangeValueEvent(
value: value, code: FlushMountedPresenceSensorModel.codePresenceDelay,
), value: (value * 10).toInt(),
), ),
),
), ),
PresenceUpdateData( PresenceUpdateData(
value: ((model.noneDelay / 10).toDouble()), value: (model.noneDelay / 10).toDouble(),
description: 's', description: 's',
title: 'Disappe Delay:', title: 'Disappe Delay:',
minValue: 20, minValue: 20,

View File

@ -217,29 +217,31 @@ class SmartPowerBloc extends Bloc<SmartPowerEvent, SmartPowerState> {
try { try {
var status = var status =
await DevicesManagementApi().getPowerClampInfo(event.deviceId); await DevicesManagementApi().getPowerClampInfo(event.deviceId);
deviceStatus = PowerClampModel.fromJson(status); deviceStatus = PowerClampModel.fromJson(status as Map<String, Object?>? ??{});
final phaseADataPoints = deviceStatus.status.phaseA.dataPoints;
final phaseBDataPoints = deviceStatus.status.phaseB.dataPoints;
final phaseCDataPoints = deviceStatus.status.phaseC.dataPoints;
phaseData = [ phaseData = [
{ {
'name': 'Phase A', 'name': 'Phase A',
'voltage': '${deviceStatus.status.phaseA.dataPoints[0].value / 10} V', 'voltage': '${(phaseADataPoints.elementAtOrNull(0)?.value as num? ?? 0) / 10} V',
'current': '${deviceStatus.status.phaseA.dataPoints[1].value / 10} A', 'current': '${(phaseADataPoints.elementAtOrNull(1)?.value as num? ?? 0) / 10} A',
'activePower': '${deviceStatus.status.phaseA.dataPoints[2].value} W', 'activePower': '${phaseADataPoints.elementAtOrNull(2)?.value??'N/A'} W',
'powerFactor': '${deviceStatus.status.phaseA.dataPoints[3].value}', 'powerFactor': '${phaseADataPoints.elementAtOrNull(3)?.value??'N/A'}',
}, },
{ {
'name': 'Phase B', 'name': 'Phase B',
'voltage': '${deviceStatus.status.phaseB.dataPoints[0].value / 10} V', 'voltage': '${(phaseBDataPoints .elementAtOrNull(0)?.value as num? ?? 0) / 10} V',
'current': '${deviceStatus.status.phaseB.dataPoints[1].value / 10} A', 'current': '${(phaseBDataPoints .elementAtOrNull(1)?.value as num? ?? 0) / 10} A',
'activePower': '${deviceStatus.status.phaseB.dataPoints[2].value} W', 'activePower': '${phaseBDataPoints.elementAtOrNull(2)?.value??'N/A'} W',
'powerFactor': '${deviceStatus.status.phaseB.dataPoints[3].value}', 'powerFactor': '${phaseBDataPoints.elementAtOrNull(3)?.value??'N/A'}',
}, },
{ {
'name': 'Phase C', 'name': 'Phase C',
'voltage': '${deviceStatus.status.phaseC.dataPoints[0].value / 10} V', 'voltage': '${(phaseCDataPoints.elementAtOrNull(0)?.value as num? ?? 0) / 10} V',
'current': '${deviceStatus.status.phaseC.dataPoints[1].value / 10} A', 'current': '${(phaseCDataPoints.elementAtOrNull(1)?.value as num? ?? 0) / 10} A',
'activePower': '${deviceStatus.status.phaseC.dataPoints[2].value} W', 'activePower': '${phaseCDataPoints.elementAtOrNull(2)?.value ?? 'N/A'} W',
'powerFactor': '${deviceStatus.status.phaseC.dataPoints[3].value}', 'powerFactor': '${phaseCDataPoints.elementAtOrNull(3)?.value ?? 'N/A'}',
}, },
]; ];
emit(GetDeviceStatus()); emit(GetDeviceStatus());
@ -785,7 +787,7 @@ class SmartPowerBloc extends Bloc<SmartPowerEvent, SmartPowerState> {
void selectDateRange() async { void selectDateRange() async {
DateTime startDate = dateTime!; DateTime startDate = dateTime!;
DateTime endDate = DateTime(startDate.year, startDate.month + 1, 1) DateTime endDate = DateTime(startDate.year, startDate.month + 1, 1)
.subtract(Duration(days: 1)); .subtract(const Duration(days: 1));
String formattedEndDate = DateFormat('dd/MM/yyyy').format(endDate); String formattedEndDate = DateFormat('dd/MM/yyyy').format(endDate);
endChartDate = ' - $formattedEndDate'; endChartDate = ' - $formattedEndDate';
} }

View File

@ -12,9 +12,9 @@ class PowerClampModel {
factory PowerClampModel.fromJson(Map<String, dynamic> json) { factory PowerClampModel.fromJson(Map<String, dynamic> json) {
return PowerClampModel( return PowerClampModel(
productUuid: json['productUuid'], productUuid: json['productUuid'] as String? ?? '',
productType: json['productType'], productType: json['productType'] as String? ?? '',
status: PowerStatus.fromJson(json['status']), status: PowerStatus.fromJson(json['status'] as Map<String, dynamic>? ?? {}),
); );
} }
@ -26,7 +26,7 @@ class PowerClampModel {
return PowerClampModel( return PowerClampModel(
productUuid: productUuid ?? this.productUuid, productUuid: productUuid ?? this.productUuid,
productType: productType ?? this.productType, productType: productType ?? this.productType,
status: statusPower ?? this.status, status: statusPower ?? status,
); );
} }
} }
@ -46,12 +46,10 @@ class PowerStatus {
factory PowerStatus.fromJson(Map<String, dynamic> json) { factory PowerStatus.fromJson(Map<String, dynamic> json) {
return PowerStatus( return PowerStatus(
phaseA: Phase.fromJson(json['phaseA']), phaseA: Phase.fromJson(json['phaseA']as List<dynamic>? ?? []),
phaseB: Phase.fromJson(json['phaseB']), phaseB: Phase.fromJson(json['phaseB']as List<dynamic>? ?? []),
phaseC: Phase.fromJson(json['phaseC']), phaseC: Phase.fromJson(json['phaseC']as List<dynamic>? ?? []),
general: Phase.fromJson(json['general'] general: Phase.fromJson(json['general']as List<dynamic>? ?? []
// List<DataPoint>.from(
// json['general'].map((x) => DataPoint.fromJson(x))),
)); ));
} }
} }
@ -69,30 +67,30 @@ class Phase {
} }
class DataPoint { class DataPoint {
dynamic code; final String? code;
dynamic customName; final String? customName;
dynamic dpId; final int? dpId;
dynamic time; final int? time;
dynamic type; final String? type;
dynamic value; final dynamic value;
DataPoint({ DataPoint({
required this.code, this.code,
required this.customName, this.customName,
required this.dpId, this.dpId,
required this.time, this.time,
required this.type, this.type,
required this.value, this.value,
}); });
factory DataPoint.fromJson(Map<String, dynamic> json) { factory DataPoint.fromJson(Map<String, dynamic> json) {
return DataPoint( return DataPoint(
code: json['code'], code: json['code'] as String?,
customName: json['customName'], customName: json['customName'] as String?,
dpId: json['dpId'], dpId: json['dpId'] as int?,
time: json['time'], time: json['time'] as int?,
type: json['type'], type: json['type'] as String?,
value: json['value'], value: json['value'] as dynamic,
); );
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class EnergyConsumptionPage extends StatefulWidget { class EnergyConsumptionPage extends StatefulWidget {
@ -10,7 +10,8 @@ class EnergyConsumptionPage extends StatefulWidget {
final Widget widget; final Widget widget;
final Function()? onTap; final Function()? onTap;
EnergyConsumptionPage({ const EnergyConsumptionPage({
super.key,
required this.chartData, required this.chartData,
required this.totalConsumption, required this.totalConsumption,
required this.date, required this.date,
@ -91,11 +92,12 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
], ],
), ),
Column( Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 10), padding: const EdgeInsets.only(top: 10),
child: SizedBox( child: SizedBox(
height: MediaQuery.of(context).size.height * 0.11, height: MediaQuery.sizeOf(context).height * 0.09,
child: LineChart( child: LineChart(
LineChartData( LineChartData(
lineTouchData: LineTouchData( lineTouchData: LineTouchData(
@ -151,7 +153,7 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
child: RotatedBox( child: RotatedBox(
quarterTurns: -1, quarterTurns: -1,
child: Text(_chartData[index].time, child: Text(_chartData[index].time,
style: TextStyle(fontSize: 10)), style: const TextStyle(fontSize: 10)),
), ),
); );
} }
@ -190,8 +192,8 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
spots: _chartData spots: _chartData
.asMap() .asMap()
.entries .entries
.map((entry) => FlSpot(entry.key.toDouble(), .map((entry) => FlSpot(
entry.value.consumption)) entry.key.toDouble(), entry.value.consumption))
.toList(), .toList(),
isCurved: true, isCurved: true,
color: ColorsManager.primaryColor.withOpacity(0.6), color: ColorsManager.primaryColor.withOpacity(0.6),
@ -218,7 +220,7 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
borderData: FlBorderData( borderData: FlBorderData(
show: false, show: false,
border: Border.all( border: Border.all(
color: Color(0xff023DFE).withOpacity(0.7), color: const Color(0xff023DFE).withOpacity(0.7),
width: 10, width: 10,
), ),
), ),
@ -253,11 +255,9 @@ class _EnergyConsumptionPageState extends State<EnergyConsumptionPage> {
child: InkWell( child: InkWell(
onTap: widget.onTap, onTap: widget.onTap,
child: Center( child: Center(
child: SizedBox( child: Padding(
child: Padding( padding: const EdgeInsets.all(5),
padding: const EdgeInsets.all(5), child: Text(widget.date),
child: Text(widget.date),
),
), ),
), ),
), ),

View File

@ -12,8 +12,7 @@ import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
//Smart Power Clamp //Smart Power Clamp
class SmartPowerDeviceControl extends StatelessWidget class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout {
with HelperResponsiveLayout {
final String deviceId; final String deviceId;
const SmartPowerDeviceControl({super.key, required this.deviceId}); const SmartPowerDeviceControl({super.key, required this.deviceId});
@ -25,27 +24,27 @@ class SmartPowerDeviceControl extends StatelessWidget
..add(SmartPowerFetchDeviceEvent(deviceId)), ..add(SmartPowerFetchDeviceEvent(deviceId)),
child: BlocBuilder<SmartPowerBloc, SmartPowerState>( child: BlocBuilder<SmartPowerBloc, SmartPowerState>(
builder: (context, state) { builder: (context, state) {
final _blocProvider = BlocProvider.of<SmartPowerBloc>(context); final blocProvider = BlocProvider.of<SmartPowerBloc>(context);
if (state is SmartPowerLoading) { if (state is SmartPowerLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (state is FakeState) { } else if (state is FakeState) {
return _buildStatusControls( return _buildStatusControls(
currentPage: _blocProvider.currentPage, currentPage: blocProvider.currentPage,
context: context, context: context,
blocProvider: _blocProvider, blocProvider: blocProvider,
); );
} else if (state is GetDeviceStatus) { } else if (state is GetDeviceStatus) {
return _buildStatusControls( return _buildStatusControls(
currentPage: _blocProvider.currentPage, currentPage: blocProvider.currentPage,
context: context, context: context,
blocProvider: _blocProvider, blocProvider: blocProvider,
); );
} else if (state is FilterRecordsState) { } else if (state is FilterRecordsState) {
return _buildStatusControls( return _buildStatusControls(
currentPage: _blocProvider.currentPage, currentPage: blocProvider.currentPage,
context: context, context: context,
blocProvider: _blocProvider, blocProvider: blocProvider,
); );
} }
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -60,7 +59,7 @@ class SmartPowerDeviceControl extends StatelessWidget
required SmartPowerBloc blocProvider, required SmartPowerBloc blocProvider,
required int currentPage, required int currentPage,
}) { }) {
PageController _pageController = PageController(initialPage: currentPage); PageController pageController = PageController(initialPage: currentPage);
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 50), padding: const EdgeInsets.symmetric(horizontal: 50),
child: DeviceControlsContainer( child: DeviceControlsContainer(
@ -85,25 +84,31 @@ class SmartPowerDeviceControl extends StatelessWidget
PowerClampInfoCard( PowerClampInfoCard(
iconPath: Assets.powerActiveIcon, iconPath: Assets.powerActiveIcon,
title: 'Active', title: 'Active',
value: blocProvider value: blocProvider.deviceStatus.status.general.dataPoints
.deviceStatus.status.general.dataPoints[2].value .elementAtOrNull(2)
.toString(), ?.value
.toString() ??
'',
unit: '', unit: '',
), ),
PowerClampInfoCard( PowerClampInfoCard(
iconPath: Assets.voltMeterIcon, iconPath: Assets.voltMeterIcon,
title: 'Current', title: 'Current',
value: blocProvider value: blocProvider.deviceStatus.status.general.dataPoints
.deviceStatus.status.general.dataPoints[1].value .elementAtOrNull(1)
.toString(), ?.value
.toString() ??
'',
unit: ' A', unit: ' A',
), ),
PowerClampInfoCard( PowerClampInfoCard(
iconPath: Assets.frequencyIcon, iconPath: Assets.frequencyIcon,
title: 'Frequency', title: 'Frequency',
value: blocProvider value: blocProvider.deviceStatus.status.general.dataPoints
.deviceStatus.status.general.dataPoints[4].value .elementAtOrNull(4)
.toString(), ?.value
.toString() ??
'',
unit: ' Hz', unit: ' Hz',
), ),
], ],
@ -142,7 +147,7 @@ class SmartPowerDeviceControl extends StatelessWidget
icon: const Icon(Icons.arrow_left), icon: const Icon(Icons.arrow_left),
onPressed: () { onPressed: () {
blocProvider.add(SmartPowerArrowPressedEvent(-1)); blocProvider.add(SmartPowerArrowPressedEvent(-1));
_pageController.previousPage( pageController.previousPage(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -162,7 +167,7 @@ class SmartPowerDeviceControl extends StatelessWidget
icon: const Icon(Icons.arrow_right), icon: const Icon(Icons.arrow_right),
onPressed: () { onPressed: () {
blocProvider.add(SmartPowerArrowPressedEvent(1)); blocProvider.add(SmartPowerArrowPressedEvent(1));
_pageController.nextPage( pageController.nextPage(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -177,7 +182,7 @@ class SmartPowerDeviceControl extends StatelessWidget
Expanded( Expanded(
flex: 2, flex: 2,
child: PageView( child: PageView(
controller: _pageController, controller: pageController,
onPageChanged: (int page) { onPageChanged: (int page) {
blocProvider.add(SmartPowerPageChangedEvent(page)); blocProvider.add(SmartPowerPageChangedEvent(page));
}, },
@ -190,8 +195,8 @@ class SmartPowerDeviceControl extends StatelessWidget
blocProvider.add(SelectDateEvent(context: context)); blocProvider.add(SelectDateEvent(context: context));
blocProvider.add(FilterRecordsByDateEvent( blocProvider.add(FilterRecordsByDateEvent(
selectedDate: blocProvider.dateTime!, selectedDate: blocProvider.dateTime!,
viewType: blocProvider viewType:
.views[blocProvider.currentIndex])); blocProvider.views[blocProvider.currentIndex]));
}, },
widget: blocProvider.dateSwitcher(), widget: blocProvider.dateSwitcher(),
chartData: blocProvider.energyDataList.isNotEmpty chartData: blocProvider.energyDataList.isNotEmpty

View File

@ -84,6 +84,16 @@ class _PresenceUpdateDataState extends State<PresenceNoBodyTime> {
} }
} }
@override
void didUpdateWidget(PresenceNoBodyTime oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
setState(() {
_currentValue = widget.value;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DeviceControlsContainer( return DeviceControlsContainer(

View File

@ -21,6 +21,7 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
on<ShowDescriptionEvent>(_showDescription); on<ShowDescriptionEvent>(_showDescription);
on<BackToGridViewEvent>(_backToGridView); on<BackToGridViewEvent>(_backToGridView);
on<WallSensorFactoryResetEvent>(_onFactoryReset); on<WallSensorFactoryResetEvent>(_onFactoryReset);
on<WallSensorRealtimeUpdateEvent>(_onRealtimeUpdate);
} }
void _fetchWallSensorStatus( void _fetchWallSensorStatus(
@ -30,7 +31,7 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
var response = await DevicesManagementApi().getDeviceStatus(deviceId); var response = await DevicesManagementApi().getDeviceStatus(deviceId);
deviceStatus = WallSensorModel.fromJson(response.status); deviceStatus = WallSensorModel.fromJson(response.status);
emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
_listenToChanges(emit, deviceId); _listenToChanges(deviceId);
} catch (e) { } catch (e) {
emit(WallSensorFailedState(error: e.toString())); emit(WallSensorFailedState(error: e.toString()));
return; return;
@ -52,28 +53,27 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
} }
} }
_listenToChanges(Emitter<WallSensorState> emit, deviceId) { void _listenToChanges(String deviceId) {
try { DatabaseReference ref =
DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
FirebaseDatabase.instance.ref('device-status/$deviceId'); ref.onValue.listen((DatabaseEvent event) {
Stream<DatabaseEvent> stream = ref.onValue; final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
stream.listen((DatabaseEvent event) { final statusList = (data['status'] as List?)
Map<dynamic, dynamic> usersMap = ?.map((e) => Status(code: e['code'], value: e['value']))
event.snapshot.value as Map<dynamic, dynamic>; .toList();
List<Status> statusList = [];
usersMap['status'].forEach((element) { if (statusList != null) {
statusList final updatedDeviceStatus = WallSensorModel.fromJson(statusList);
.add(Status(code: element['code'], value: element['value'])); if (!isClosed) {
}); add(WallSensorRealtimeUpdateEvent(updatedDeviceStatus));
}
deviceStatus = WallSensorModel.fromJson(statusList); }
emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); });
});
} catch (_) {}
} }
void _changeValue( void _changeValue(
WallSensorChangeValueEvent event, Emitter<WallSensorState> emit) async { WallSensorChangeValueEvent event, Emitter<WallSensorState> emit) async {
emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus));
@ -195,4 +195,12 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
emit(WallSensorFailedState(error: e.toString())); emit(WallSensorFailedState(error: e.toString()));
} }
} }
void _onRealtimeUpdate(
WallSensorRealtimeUpdateEvent event,
Emitter<WallSensorState> emit,
) {
deviceStatus = event.deviceStatus;
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
}
} }

View File

@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart';
abstract class WallSensorEvent extends Equatable { abstract class WallSensorEvent extends Equatable {
const WallSensorEvent(); const WallSensorEvent();
@ -70,3 +71,8 @@ class WallSensorFactoryResetEvent extends WallSensorEvent {
required this.factoryReset, required this.factoryReset,
}); });
} }
class WallSensorRealtimeUpdateEvent extends WallSensorEvent {
final WallSensorModel deviceStatus;
const WallSensorRealtimeUpdateEvent(this.deviceStatus);
}

View File

@ -30,8 +30,8 @@ class _RoutinesViewState extends State<RoutinesView> {
final spaceId = result['space']; final spaceId = result['space'];
final bloc = BlocProvider.of<CreateRoutineBloc>(context); final bloc = BlocProvider.of<CreateRoutineBloc>(context);
final routineBloc = context.read<RoutineBloc>(); final routineBloc = context.read<RoutineBloc>();
bloc.add( bloc.add(SaveCommunityIdAndSpaceIdEvent(
SaveCommunityIdAndSpaceIdEvent(communityID: communityId, spaceID: spaceId)); communityID: communityId, spaceID: spaceId));
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
routineBloc.add(const CreateNewRoutineViewEvent(createRoutineView: true)); routineBloc.add(const CreateNewRoutineViewEvent(createRoutineView: true));
} }
@ -61,34 +61,38 @@ class _RoutinesViewState extends State<RoutinesView> {
width: context.screenWidth, width: context.screenWidth,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsetsDirectional.all(16), padding: const EdgeInsetsDirectional.all(16),
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only(left: 20),
mainAxisAlignment: MainAxisAlignment.start, child: Column(
spacing: 16, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisAlignment: MainAxisAlignment.start,
Text( spacing: 16,
"Create New Routines", children: [
style: Theme.of(context).textTheme.titleLarge?.copyWith( Text(
color: ColorsManager.grayColor, "Create New Routines",
fontWeight: FontWeight.bold, style:
), Theme.of(context).textTheme.titleLarge?.copyWith(
), color: ColorsManager.grayColor,
RoutineViewCard( fontWeight: FontWeight.bold,
isLoading: false, ),
onChanged: (v) {}, ),
status: '', RoutineViewCard(
spaceId: '', isLoading: false,
automationId: '', onChanged: (v) {},
communityId: '', status: '',
sceneId: '', spaceId: '',
cardType: '', automationId: '',
spaceName: '', communityId: '',
onTap: () => _handleRoutineCreation(context), sceneId: '',
icon: Icons.add, cardType: '',
textString: '', spaceName: '',
), onTap: () => _handleRoutineCreation(context),
const FetchRoutineScenesAutomation(), icon: Icons.add,
], textString: '',
),
const FetchRoutineScenesAutomation(),
],
),
), ),
), ),
), ),

View File

@ -16,7 +16,8 @@ class FetchRoutineScenesAutomation extends StatelessWidget
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<RoutineBloc, RoutineState>( return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) { builder: (context, state) {
if (state.isLoading) return const Center(child: CircularProgressIndicator()); if (state.isLoading)
return const Center(child: CircularProgressIndicator());
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
@ -40,7 +41,8 @@ class FetchRoutineScenesAutomation extends StatelessWidget
const SizedBox(height: 3), const SizedBox(height: 3),
Visibility( Visibility(
visible: state.automations.isNotEmpty, visible: state.automations.isNotEmpty,
replacement: _buildEmptyState(context, "No automations found"), replacement:
_buildEmptyState(context, "No automations found"),
child: SizedBox( child: SizedBox(
height: 200, height: 200,
child: _buildAutomations(state), child: _buildAutomations(state),
@ -59,7 +61,8 @@ class FetchRoutineScenesAutomation extends StatelessWidget
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: state.automations.length, itemCount: state.automations.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isLoading = state.automations.contains(state.automations[index].id); final isLoading =
state.automations.contains(state.automations[index].id);
return Column( return Column(
children: [ children: [
@ -179,10 +182,13 @@ class FetchRoutineScenesAutomation extends StatelessWidget
} }
Widget _buildEmptyState(BuildContext context, String title) { Widget _buildEmptyState(BuildContext context, String title) {
return Text( return Padding(
title, padding: const EdgeInsets.only(bottom: 100),
style: context.textTheme.bodyMedium?.copyWith( child: Text(
color: ColorsManager.grayColor, title,
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.grayColor,
),
), ),
); );
} }

View File

@ -9,6 +9,7 @@ import 'package:syncrow_web/pages/routines/models/ac/ac_operational_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -74,11 +75,26 @@ class ACHelper {
child: _buildFunctionsList( child: _buildFunctionsList(
context: context, context: context,
acFunctions: acFunctions, acFunctions: acFunctions,
onFunctionSelected: (functionCode, operationName) => device: device,
context.read<FunctionBloc>().add(SelectFunction( onFunctionSelected: (functionCode, operationName) {
functionCode: functionCode, RoutineTapFunctionHelper.onTapFunction(
operationName: operationName, context,
)), functionCode: functionCode,
functionOperationName: operationName,
functionValueDescription:
selectedFunctionData.valueDescription,
deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'temp_set',
'temp_current',
],
defaultValue: functionCode == 'temp_set'
? 200
: functionCode == 'temp_current'
? -100
: 0,
);
},
), ),
), ),
// Value selector // Value selector
@ -137,6 +153,7 @@ class ACHelper {
required BuildContext context, required BuildContext context,
required List<ACFunction> acFunctions, required List<ACFunction> acFunctions,
required Function(String, String) onFunctionSelected, required Function(String, String) onFunctionSelected,
required AllDevicesModel? device,
}) { }) {
return ListView.separated( return ListView.separated(
shrinkWrap: false, shrinkWrap: false,

View File

@ -131,7 +131,12 @@ class _CeilingSensorDialogState extends State<CeilingSensorDialog> {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
CpsFunctionsList(cpsFunctions: _cpsFunctions), CpsFunctionsList(
cpsFunctions: _cpsFunctions,
device: widget.device,
selectedFunctionData: selectedFunctionData,
dialogType: widget.dialogType,
),
if (state.selectedFunction != null) if (state.selectedFunction != null)
Expanded( Expanded(
child: isToggleFunction child: isToggleFunction

View File

@ -1,14 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart'; import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialog_function_list_tile.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialog_function_list_tile.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class CpsFunctionsList extends StatelessWidget { class CpsFunctionsList extends StatelessWidget {
const CpsFunctionsList({required this.cpsFunctions, super.key}); const CpsFunctionsList({
required this.cpsFunctions,
required this.device,
required this.selectedFunctionData,
required this.dialogType,
super.key,
});
final List<CpsFunctions> cpsFunctions; final List<CpsFunctions> cpsFunctions;
final AllDevicesModel? device;
final DeviceFunctionData? selectedFunctionData;
final String dialogType;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,12 +36,27 @@ class CpsFunctionsList extends StatelessWidget {
return RoutineDialogFunctionListTile( return RoutineDialogFunctionListTile(
iconPath: function.icon, iconPath: function.icon,
operationName: function.operationName, operationName: function.operationName,
onTap: () => context.read<FunctionBloc>().add( onTap: () => RoutineTapFunctionHelper.onTapFunction(
SelectFunction( context,
functionCode: function.code, functionCode: function.code,
operationName: function.operationName, functionOperationName: function.operationName,
), functionValueDescription: selectedFunctionData?.valueDescription,
), deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'static_max_dis',
'presence_reference',
'moving_reference',
'perceptual_boundary',
'moving_boundary',
'moving_rigger_time',
'moving_static_time',
'none_body_time',
'moving_max_dis',
'moving_range',
'presence_range',
if (dialogType == "IF") 'sensitivity',
],
),
); );
}, },
), ),

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
abstract final class RoutineTapFunctionHelper {
const RoutineTapFunctionHelper._();
static void onTapFunction(
BuildContext context, {
required String functionCode,
required String functionOperationName,
required String? functionValueDescription,
required String? deviceUuid,
required List<String> codesToAddIntoFunctionsWithDefaultValue,
int defaultValue = 0,
}) {
final functionsBloc = context.read<FunctionBloc>();
functionsBloc.add(
SelectFunction(
functionCode: functionCode,
operationName: functionOperationName,
),
);
final addedFunctions = functionsBloc.state.addedFunctions;
final isFunctionAlreadyAdded = addedFunctions.any(
(e) => e.functionCode == functionCode,
);
final shouldAddFunction =
codesToAddIntoFunctionsWithDefaultValue.contains(functionCode);
if (!isFunctionAlreadyAdded && shouldAddFunction) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: deviceUuid ?? '',
functionCode: functionCode,
operationName: functionOperationName,
value: defaultValue,
condition: '==',
valueDescription: functionValueDescription,
),
),
);
}
}
}

View File

@ -11,6 +11,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -24,21 +25,23 @@ class OneGangSwitchHelper {
required String uniqueCustomId, required String uniqueCustomId,
required bool removeComparetors, required bool removeComparetors,
}) async { }) async {
List<BaseSwitchFunction> oneGangFunctions = functions.whereType<BaseSwitchFunction>().toList(); List<BaseSwitchFunction> oneGangFunctions =
functions.whereType<BaseSwitchFunction>().toList();
return showDialog<Map<String, dynamic>?>( return showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => FunctionBloc()..add(InitializeFunctions(deviceSelectedFunctions ?? [])), create: (_) => FunctionBloc()
..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
child: AlertDialog( child: AlertDialog(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>( content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) { builder: (context, state) {
final selectedFunction = state.selectedFunction; final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName; final selectedOperationName = state.selectedOperationName;
final selectedFunctionData = final selectedFunctionData = state.addedFunctions
state.addedFunctions.firstWhere((f) => f.functionCode == selectedFunction, .firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData( orElse: () => DeviceFunctionData(
entityId: '', entityId: '',
functionCode: selectedFunction ?? '', functionCode: selectedFunction ?? '',
@ -84,12 +87,19 @@ class OneGangSwitchHelper {
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () { onTap: () =>
context.read<FunctionBloc>().add(SelectFunction( RoutineTapFunctionHelper.onTapFunction(
functionCode: function.code, context,
operationName: function.operationName, functionCode: function.code,
)); functionOperationName:
}, function.operationName,
functionValueDescription:
selectedFunctionData.valueDescription,
deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'countdown_1',
],
),
); );
}, },
), ),
@ -220,11 +230,11 @@ class OneGangSwitchHelper {
selectedFunctionData, selectedFunctionData,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildCountDownDisplay( _buildCountDownDisplay(context, initialValue, device, operationName,
context, initialValue, device, operationName, selectedFunctionData, selectCode), selectedFunctionData, selectCode),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildCountDownSlider( _buildCountDownSlider(context, initialValue, device, operationName,
context, initialValue, device, operationName, selectedFunctionData, selectCode), selectedFunctionData, selectCode),
], ],
); );
} }
@ -314,9 +324,10 @@ class OneGangSwitchHelper {
value: (initialValue ?? 0).toDouble(), value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0, min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0, max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) / divisions:
(operationalValues.stepValue ?? 1)) (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
.round(), (operationalValues.stepValue ?? 1))
.round(),
onChanged: (value) { onChanged: (value) {
context.read<FunctionBloc>().add( context.read<FunctionBloc>().add(
AddFunction( AddFunction(
@ -368,7 +379,9 @@ class OneGangSwitchHelper {
trailing: Icon( trailing: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
size: 24, size: 24,
color: isSelected ? ColorsManager.primaryColorWithOpacity : ColorsManager.textGray, color: isSelected
? ColorsManager.primaryColorWithOpacity
: ColorsManager.textGray,
), ),
onTap: () { onTap: () {
if (!isSelected) { if (!isSelected) {

View File

@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_func
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -85,15 +86,21 @@ class ThreeGangSwitchHelper {
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () { onTap: () =>
context RoutineTapFunctionHelper.onTapFunction(
.read<FunctionBloc>() context,
.add(SelectFunction( functionCode: function.code,
functionCode: function.code, functionOperationName:
operationName: function.operationName,
function.operationName, functionValueDescription:
)); selectedFunctionData.valueDescription,
}, deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'countdown_1',
'countdown_2',
'countdown_3',
],
),
); );
}, },
), ),
@ -183,8 +190,7 @@ class ThreeGangSwitchHelper {
); );
} }
final selectedFn = final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction);
switchFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues(); final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList( return _buildOperationalValuesList(
@ -266,8 +272,7 @@ class ThreeGangSwitchHelper {
minHeight: 40.0, minHeight: 40.0,
minWidth: 40.0, minWidth: 40.0,
), ),
isSelected: isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(), children: conditions.map((c) => Text(c)).toList(),
); );
} }
@ -316,10 +321,10 @@ class ThreeGangSwitchHelper {
value: (initialValue ?? 0).toDouble(), value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0, min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0, max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - divisions:
(operationalValues.minValue ?? 0)) / (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1)) (operationalValues.stepValue ?? 1))
.round(), .round(),
onChanged: (value) { onChanged: (value) {
context.read<FunctionBloc>().add( context.read<FunctionBloc>().add(
AddFunction( AddFunction(
@ -369,9 +374,7 @@ class ThreeGangSwitchHelper {
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: Icon( trailing: Icon(
isSelected isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 24, size: 24,
color: isSelected color: isSelected
? ColorsManager.primaryColorWithOpacity ? ColorsManager.primaryColorWithOpacity
@ -387,8 +390,7 @@ class ThreeGangSwitchHelper {
operationName: operationName, operationName: operationName,
value: value.value, value: value.value,
condition: selectedFunctionData?.condition, condition: selectedFunctionData?.condition,
valueDescription: valueDescription: selectedFunctionData?.valueDescription,
selectedFunctionData?.valueDescription,
), ),
), ),
); );

View File

@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_func
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -85,15 +86,20 @@ class TwoGangSwitchHelper {
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () { onTap: () =>
context RoutineTapFunctionHelper.onTapFunction(
.read<FunctionBloc>() context,
.add(SelectFunction( functionCode: function.code,
functionCode: function.code, functionOperationName:
operationName: function.operationName,
function.operationName, functionValueDescription:
)); selectedFunctionData.valueDescription,
}, deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'countdown_1',
'countdown_2',
],
),
); );
}, },
), ),
@ -167,8 +173,7 @@ class TwoGangSwitchHelper {
required String operationName, required String operationName,
required bool removeComparetors, required bool removeComparetors,
}) { }) {
if (selectedFunction == 'countdown_1' || if (selectedFunction == 'countdown_1' || selectedFunction == 'countdown_2') {
selectedFunction == 'countdown_2') {
final initialValue = selectedFunctionData?.value ?? 0; final initialValue = selectedFunctionData?.value ?? 0;
return _buildTemperatureSelector( return _buildTemperatureSelector(
context: context, context: context,
@ -182,8 +187,7 @@ class TwoGangSwitchHelper {
); );
} }
final selectedFn = final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction);
switchFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues(); final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList( return _buildOperationalValuesList(
@ -265,8 +269,7 @@ class TwoGangSwitchHelper {
minHeight: 40.0, minHeight: 40.0,
minWidth: 40.0, minWidth: 40.0,
), ),
isSelected: isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(), children: conditions.map((c) => Text(c)).toList(),
); );
} }
@ -315,10 +318,10 @@ class TwoGangSwitchHelper {
value: (initialValue ?? 0).toDouble(), value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0, min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0, max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - divisions:
(operationalValues.minValue ?? 0)) / (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1)) (operationalValues.stepValue ?? 1))
.round(), .round(),
onChanged: (value) { onChanged: (value) {
context.read<FunctionBloc>().add( context.read<FunctionBloc>().add(
AddFunction( AddFunction(
@ -368,9 +371,7 @@ class TwoGangSwitchHelper {
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
trailing: Icon( trailing: Icon(
isSelected isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 24, size: 24,
color: isSelected color: isSelected
? ColorsManager.primaryColorWithOpacity ? ColorsManager.primaryColorWithOpacity
@ -386,8 +387,7 @@ class TwoGangSwitchHelper {
operationName: operationName, operationName: operationName,
value: value.value, value: value.value,
condition: selectedFunctionData?.condition, condition: selectedFunctionData?.condition,
valueDescription: valueDescription: selectedFunctionData?.valueDescription,
selectedFunctionData?.valueDescription,
), ),
), ),
); );

View File

@ -8,6 +8,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -111,13 +112,23 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildFunctionList(context), _buildFunctionList(context, state),
if (state.selectedFunction != null) _buildValueSelector(context, state), if (state.selectedFunction != null) _buildValueSelector(context, state),
], ],
); );
} }
Widget _buildFunctionList(BuildContext context) { Widget _buildFunctionList(BuildContext context, FunctionBlocState state) {
final selectedFunction = state.selectedFunction;
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
),
);
return SizedBox( return SizedBox(
width: 360, width: 360,
child: ListView.separated( child: ListView.separated(
@ -149,12 +160,18 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
size: 16, size: 16,
color: ColorsManager.textGray, color: ColorsManager.textGray,
), ),
onTap: () => context.read<FunctionBloc>().add( onTap: () => RoutineTapFunctionHelper.onTapFunction(
SelectFunction( context,
functionCode: function.code, functionCode: function.code,
operationName: function.operationName, functionOperationName: function.operationName,
), functionValueDescription: selectedFunctionData.valueDescription,
), deviceUuid: widget.device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'dis_current',
'presence_time',
'illuminance_value',
],
),
); );
}, },
), ),

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
@ -246,7 +247,9 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
final previousState = state; final previousState = state;
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = event.context.read<SpaceTreeBloc>(); var spaceBloc = event.context.read<SpaceTreeBloc>();
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc); var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
await fetchSpaceModels(); await fetchSpaceModels();
await fetchTags(); await fetchTags();
@ -277,11 +280,13 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
LoadCommunityAndSpacesEvent event, LoadCommunityAndSpacesEvent event,
Emitter<SpaceManagementState> emit, Emitter<SpaceManagementState> emit,
) async { ) async {
var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
var spaceBloc = event.context.read<SpaceTreeBloc>(); var spaceBloc = event.context.read<SpaceTreeBloc>();
_onloadProducts(); _onloadProducts();
await fetchTags(); await fetchTags();
// Wait until `communityList` is loaded // Wait until `communityList` is loaded
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc); List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
// Fetch space models after communities are available // Fetch space models after communities are available
final prevSpaceModels = await fetchSpaceModels(); final prevSpaceModels = await fetchSpaceModels();
@ -292,23 +297,38 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
allTags: _cachedTags ?? [])); allTags: _cachedTags ?? []));
} }
Future<List<CommunityModel>> _waitForCommunityList(SpaceTreeBloc spaceBloc) async { Future<List<CommunityModel>> _waitForCommunityList(
SpaceTreeBloc spaceBloc, SpaceTreeState spaceTreeState) async {
// Check if communityList is already populated // Check if communityList is already populated
if (spaceBloc.state.communityList.isNotEmpty) { final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
return spaceBloc.state.communityList; ? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
if (filteredCommunities.isNotEmpty) {
return filteredCommunities;
} }
final completer = Completer<List<CommunityModel>>(); final completer = Completer<List<CommunityModel>>();
final subscription = spaceBloc.stream.listen((state) { final subscription = spaceBloc.stream.listen((state) {
if (state.communityList.isNotEmpty) { if (!completer.isCompleted && state.communityList.isNotEmpty) {
completer.complete(state.communityList); completer
.complete(state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList);
} }
}); });
try {
final communities = await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
if (!completer.isCompleted) {
completer.complete([]);
}
return [];
},
);
// Return the list once available, then cancel the listener return communities;
final communities = await completer.future; } finally {
await subscription.cancel(); await subscription.cancel();
return communities; }
} }
void _onCommunityDelete( void _onCommunityDelete(
@ -446,6 +466,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded) {
await _updateLoadedState( await _updateLoadedState(
event.context,
previousState, previousState,
allSpaces, allSpaces,
event.communityUuid, event.communityUuid,
@ -462,6 +483,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} }
Future<void> _updateLoadedState( Future<void> _updateLoadedState(
BuildContext context,
SpaceManagementLoaded previousState, SpaceManagementLoaded previousState,
List<SpaceModel> allSpaces, List<SpaceModel> allSpaces,
String communityUuid, String communityUuid,
@ -469,7 +491,10 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
) async { ) async {
var prevSpaceModels = await fetchSpaceModels(); var prevSpaceModels = await fetchSpaceModels();
await fetchTags(); await fetchTags();
final communities = List<CommunityModel>.from(previousState.communities); final spaceTreeState = context.read<SpaceTreeBloc>().state;
final communities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
for (var community in communities) { for (var community in communities) {
if (community.uuid == communityUuid) { if (community.uuid == communityUuid) {
@ -484,6 +509,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
spaceModels: prevSpaceModels, spaceModels: prevSpaceModels,
allTags: _cachedTags ?? [])); allTags: _cachedTags ?? []));
return; return;
} else {
print("Community not found");
} }
} }
} }
@ -491,12 +518,21 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
Future<List<SpaceModel>> saveSpacesHierarchically( Future<List<SpaceModel>> saveSpacesHierarchically(
BuildContext context, List<SpaceModel> spaces, String communityUuid) async { BuildContext context, List<SpaceModel> spaces, String communityUuid) async {
final orderedSpaces = flattenHierarchy(spaces); final orderedSpaces = flattenHierarchy(spaces);
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = context.read<SpaceTreeBloc>(); CommunityModel? selectedCommunity;
List<CommunityModel> communities = spaceBloc.state.communityList; try {
CommunityModel? selectedCommunity = communities.firstWhere( final spaceTreeState = context.read<SpaceTreeBloc>().state;
(community) => community.uuid == communityUuid, final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
); ? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
selectedCommunity = filteredCommunities.firstWhere(
(community) => community.uuid == communityUuid,
);
} catch (e) {
return [];
}
final parentsToDelete = orderedSpaces.where((space) => final parentsToDelete = orderedSpaces.where((space) =>
space.status == SpaceStatus.deleted && space.status == SpaceStatus.deleted &&
@ -605,11 +641,11 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
projectId: projectUuid); projectId: projectUuid);
} else { } else {
// Call create if the space does not have a UUID // Call create if the space does not have a UUID
final List<CreateTagBodyModel> tagBodyModels = space.tags != null List<CreateTagBodyModel> tagBodyModels = space.tags != null
? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList() ? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList()
: []; : [];
final createSubspaceBodyModels = space.subspaces?.map((subspace) { var createSubspaceBodyModels = space.subspaces?.map((subspace) {
final tagBodyModels = final tagBodyModels =
subspace.tags?.map((tag) => tag.toCreateTagBodyModel()).toList() ?? []; subspace.tags?.map((tag) => tag.toCreateTagBodyModel()).toList() ?? [];
return CreateSubspaceModel() return CreateSubspaceModel()
@ -618,6 +654,11 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
}).toList() ?? }).toList() ??
[]; [];
if (space.spaceModel?.uuid != null) {
tagBodyModels = [];
createSubspaceBodyModels = [];
}
final response = await _api.createSpace( final response = await _api.createSpace(
communityId: communityUuid, communityId: communityUuid,
name: space.name, name: space.name,
@ -669,9 +710,12 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
try { try {
await fetchTags(); await fetchTags();
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final spaceTreeState = event.context.read<SpaceTreeBloc>().state;
var spaceBloc = event.context.read<SpaceTreeBloc>(); final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
List<CommunityModel> communities = spaceBloc.state.communityList; ? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
List<CommunityModel> communities = filteredCommunities;
var prevSpaceModels = await fetchSpaceModels(); var prevSpaceModels = await fetchSpaceModels();

View File

@ -1,4 +1,6 @@
// Flutter imports // Flutter imports
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -70,6 +72,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_nameController = TextEditingController( _nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '', text: widget.selectedCommunity?.name ?? '',
); );
realignTree();
_transformationController = TransformationController(); _transformationController = TransformationController();
if (widget.selectedSpace != null) { if (widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -94,6 +98,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces(); _adjustCanvasSizeForSpaces();
realignTree();
}); });
} }
@ -336,6 +341,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
spaces.add(newSpace); spaces.add(newSpace);
_updateNodePosition(newSpace, newSpace.position); _updateNodePosition(newSpace, newSpace.position);
realignTree();
}); });
}, },
); );
@ -401,12 +407,15 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
List<SpaceModel> flattenSpaces(List<SpaceModel> spaces) { List<SpaceModel> flattenSpaces(List<SpaceModel> spaces) {
List<SpaceModel> result = []; List<SpaceModel> result = [];
Map<String, SpaceModel> idToSpace = {};
void flatten(SpaceModel space) { void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) { if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) {
return; return;
} }
result.add(space); result.add(space);
idToSpace[space.internalId] = space;
for (var child in space.children) { for (var child in space.children) {
flatten(child); flatten(child);
} }
@ -416,6 +425,12 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
flatten(space); flatten(space);
} }
for (var space in result) {
if (space.parent != null) {
space.parent = idToSpace[space.parent!.internalId];
}
}
return result; return result;
} }
@ -450,7 +465,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _saveSpaces() { void _saveSpaces() {
if (widget.selectedCommunity == null) { if (widget.selectedCommunity == null) {
debugPrint("No community selected for saving spaces.");
return; return;
} }
@ -465,7 +479,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
String communityUuid = widget.selectedCommunity!.uuid; String communityUuid = widget.selectedCommunity!.uuid;
context.read<SpaceManagementBloc>().add(SaveSpacesEvent( context.read<SpaceManagementBloc>().add(SaveSpacesEvent(
context, context,
spaces: spacesToSave, spaces: spacesToSave,
@ -530,35 +543,83 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
Offset getBalancedChildPosition(SpaceModel parent) { Offset getBalancedChildPosition(SpaceModel parent) {
int totalSiblings = parent.children.length + 1; const double nodeWidth = 200;
double totalWidth = (totalSiblings - 1) * 250; // Horizontal spacing const double verticalGap = 180;
double startX = parent.position.dx - (totalWidth / 2);
Offset position = Offset(startX + (parent.children.length * 250), parent.position.dy + 180); if (parent.children.isEmpty) {
// First child → exactly center
return Offset(parent.position.dx, parent.position.dy + verticalGap);
} else {
// More children → arrange them spaced horizontally
double totalWidth = (parent.children.length) * (nodeWidth + 60);
double startX = parent.position.dx - (totalWidth / 2);
// Check for overlaps & adjust double childX = startX + (parent.children.length * (nodeWidth + 60));
while (spaces.any((s) => (s.position - position).distance < 250)) { return Offset(childX, parent.position.dy + verticalGap);
position = Offset(position.dx + 250, position.dy);
} }
return position;
} }
void realignTree() { void realignTree() {
void updatePositions(SpaceModel node, double x, double y) { const double nodeWidth = 200;
node.position = Offset(x, y); const double nodeHeight = 100;
const double horizontalGap = 60;
const double verticalGap = 180;
const double rootGap = 400; // extra space between different roots
int numChildren = node.children.length; double canvasRightEdge = 1000;
double childStartX = x - ((numChildren - 1) * 250) / 2; double canvasBottomEdge = 1000;
for (int i = 0; i < numChildren; i++) { double calculateSubtreeWidth(SpaceModel node) {
updatePositions(node.children[i], childStartX + (i * 250), y + 180); if (node.children.isEmpty) return nodeWidth;
double totalWidth = 0;
for (var child in node.children) {
totalWidth += calculateSubtreeWidth(child) + horizontalGap;
}
return totalWidth - horizontalGap;
}
void layoutSubtree(SpaceModel node, double startX, double y) {
double subtreeWidth = calculateSubtreeWidth(node);
double centerX = startX + subtreeWidth / 2 - nodeWidth / 2;
node.position = Offset(centerX, y);
canvasRightEdge = max(canvasRightEdge, centerX + nodeWidth);
canvasBottomEdge = max(canvasBottomEdge, y + nodeHeight);
if (node.children.length == 1) {
final child = node.children.first;
layoutSubtree(child, centerX, y + verticalGap);
} else {
double childX = startX;
for (var child in node.children) {
double childWidth = calculateSubtreeWidth(child);
layoutSubtree(child, childX, y + verticalGap);
childX += childWidth + horizontalGap;
}
} }
} }
if (spaces.isNotEmpty) { // ⚡ New: layout each root separately
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy); final List<SpaceModel> roots = spaces
.where((s) =>
s.parent == null &&
s.status != SpaceStatus.deleted &&
s.status != SpaceStatus.parentDeleted)
.toList();
double currentX = 100; // start some margin from left
double currentY = 100; // top margin
for (var root in roots) {
layoutSubtree(root, currentX, currentY);
double rootWidth = calculateSubtreeWidth(root);
currentX += rootWidth + rootGap;
} }
setState(() {
canvasWidth = canvasRightEdge + 400;
canvasHeight = canvasBottomEdge + 400;
});
} }
void _onDuplicate(BuildContext parentContext) { void _onDuplicate(BuildContext parentContext) {
@ -642,116 +703,72 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
void _duplicateSpace(SpaceModel space) { void _duplicateSpace(SpaceModel space) {
final Map<SpaceModel, SpaceModel> originalToDuplicate = {}; setState(() {
double horizontalGap = 250.0; // Increased spacing SpaceModel? parent = space.parent;
double verticalGap = 180.0; // Adjusted for better visualization
print("🟢 Duplicating: ${space.name}"); SpaceModel duplicated = _deepCloneSpaceTree(space, parent: parent);
/// **Find a new position ensuring no overlap** duplicated.position = Offset(space.position.dx + 300, space.position.dy + 100);
Offset getBalancedChildPosition(SpaceModel parent) { List<SpaceModel> duplicatedSubtree = [];
int totalSiblings = parent.children.length + 1; void collectSubtree(SpaceModel node) {
double totalWidth = (totalSiblings - 1) * horizontalGap; duplicatedSubtree.add(node);
double startX = parent.position.dx - (totalWidth / 2); for (var child in node.children) {
Offset position = Offset( collectSubtree(child);
startX + (parent.children.length * horizontalGap), parent.position.dy + verticalGap);
// **Check for overlaps & adjust**
while (spaces.any((s) => (s.position - position).distance < horizontalGap)) {
position = Offset(position.dx + horizontalGap, position.dy);
}
print("🔹 New position for ${parent.name}: (${position.dx}, ${position.dy})");
return position;
}
/// **Realign the entire tree after duplication**
void realignTree() {
void updatePositions(SpaceModel node, double x, double y) {
node.position = Offset(x, y);
print("✅ Adjusted ${node.name} to (${x}, ${y})");
int numChildren = node.children.length;
double childStartX = x - ((numChildren - 1) * horizontalGap) / 2;
for (int i = 0; i < numChildren; i++) {
updatePositions(node.children[i], childStartX + (i * horizontalGap), y + verticalGap);
} }
} }
if (spaces.isNotEmpty) { collectSubtree(duplicated);
print("🔄 Realigning tree..."); spaces.addAll(duplicatedSubtree);
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy);
if (parent != null) {
parent.children.add(duplicated);
final newConnection = Connection(
startSpace: parent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
parent.addOutgoingConnection(newConnection);
} }
}
/// **Recursive duplication logic** realignTree();
SpaceModel duplicateRecursive(SpaceModel original, SpaceModel? duplicatedParent) { connections = createConnections(spaces);
Offset newPosition = duplicatedParent == null });
? Offset(original.position.dx + horizontalGap, original.position.dy) }
: getBalancedChildPosition(duplicatedParent);
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces); SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) {
print( final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces);
"🟡 Duplicating ${original.name}${duplicatedName} at (${newPosition.dx}, ${newPosition.dy})");
final duplicated = SpaceModel( final newSpace = SpaceModel(
name: duplicatedName, name: duplicatedName,
icon: original.icon, icon: original.icon,
position: newPosition, position: original.position,
isPrivate: original.isPrivate, isPrivate: original.isPrivate,
children: [], children: [],
status: SpaceStatus.newSpace, status: SpaceStatus.newSpace,
parent: duplicatedParent, spaceModel: original.spaceModel,
spaceModel: original.spaceModel, subspaces: original.subspaces,
subspaces: original.subspaces, tags: original.tags,
tags: original.tags, parent: parent,
);
for (var child in original.children) {
final duplicatedChild = _deepCloneSpaceTree(child, parent: newSpace);
newSpace.children.add(duplicatedChild);
final newConnection = Connection(
startSpace: newSpace,
endSpace: duplicatedChild,
direction: "down",
); );
connections.add(newConnection);
setState(() { duplicatedChild.incomingConnection = newConnection;
spaces.add(duplicated); newSpace.addOutgoingConnection(newConnection);
_updateNodePosition(duplicated, duplicated.position);
if (duplicatedParent != null) {
final newConnection = Connection(
startSpace: duplicatedParent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
duplicatedParent.addOutgoingConnection(newConnection);
duplicatedParent.children.add(duplicated);
print("🔗 Created connection: ${duplicatedParent.name}${duplicated.name}");
}
// **Recalculate the whole tree to avoid overlaps**
realignTree();
});
// Recursively duplicate children
for (var child in original.children) {
duplicateRecursive(child, duplicated);
}
return duplicated;
} }
/// **Handle root duplication** return newSpace;
if (space.parent == null) {
print("🟠 Duplicating root node: ${space.name}");
SpaceModel duplicatedRoot = duplicateRecursive(space, null);
setState(() {
spaces.add(duplicatedRoot);
realignTree();
});
print("✅ Root duplication successful: ${duplicatedRoot.name}");
} else {
duplicateRecursive(space, space.parent);
}
print("🟢 Finished duplication process for: ${space.name}");
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart'; import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
@ -45,7 +46,6 @@ class _LoadedSpaceViewState extends State<LoadedSpaceView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_spaceModels = List.from(widget.spaceModels ?? []); _spaceModels = List.from(widget.spaceModels ?? []);
} }
@ -106,9 +106,8 @@ class _LoadedSpaceViewState extends State<LoadedSpaceView> {
children: [ children: [
SidebarWidget( SidebarWidget(
communities: widget.communities, communities: widget.communities,
selectedSpaceUuid: widget.selectedSpace?.uuid ?? selectedSpaceUuid:
widget.selectedCommunity?.uuid ?? widget.selectedSpace?.uuid ?? widget.selectedCommunity?.uuid ?? '',
'',
onCreateCommunity: (name, description) { onCreateCommunity: (name, description) {
context.read<SpaceManagementBloc>().add( context.read<SpaceManagementBloc>().add(
CreateCommunityEvent(name, description, context), CreateCommunityEvent(name, description, context),

View File

@ -1,8 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/empty_search_result_widget.dart'; import 'package:syncrow_web/common/widgets/empty_search_result_widget.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart'; import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
@ -15,6 +18,8 @@ import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/cent
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart'; import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
import '../../../space_tree/bloc/space_tree_event.dart';
class SidebarWidget extends StatefulWidget { class SidebarWidget extends StatefulWidget {
final List<CommunityModel> communities; final List<CommunityModel> communities;
final String? selectedSpaceUuid; final String? selectedSpaceUuid;
@ -33,6 +38,7 @@ class SidebarWidget extends StatefulWidget {
class _SidebarWidgetState extends State<SidebarWidget> { class _SidebarWidgetState extends State<SidebarWidget> {
late final ScrollController _scrollController; late final ScrollController _scrollController;
Timer? _debounce;
String _searchQuery = ''; String _searchQuery = '';
String? _selectedSpaceUuid; String? _selectedSpaceUuid;
@ -40,17 +46,48 @@ class _SidebarWidgetState extends State<SidebarWidget> {
@override @override
void initState() { void initState() {
_selectedId = widget.selectedSpaceUuid;
_scrollController = ScrollController();
super.initState(); super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
_selectedId = widget.selectedSpaceUuid;
_searchQuery = '';
context.read<SpaceTreeBloc>().add(ClearCachedData());
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
// Trigger pagination event
final bloc = context.read<SpaceTreeBloc>();
if (!bloc.state.paginationIsLoading && bloc.state.paginationModel?.hasNext == true) {
bloc.add(
PaginationEvent(
bloc.state.paginationModel!,
bloc.state.communityList,
),
);
}
}
} }
@override @override
void dispose() { void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose(); _scrollController.dispose();
_debounce?.cancel();
super.dispose(); super.dispose();
} }
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
setState(() {
_searchQuery = query;
});
context.read<SpaceTreeBloc>().add(SearchQueryEvent(query));
});
}
@override @override
void didUpdateWidget(covariant SidebarWidget oldWidget) { void didUpdateWidget(covariant SidebarWidget oldWidget) {
if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) { if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) {
@ -59,38 +96,6 @@ class _SidebarWidgetState extends State<SidebarWidget> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
List<CommunityModel> _filteredCommunities() {
if (_searchQuery.isEmpty) {
_selectedSpaceUuid = null;
return widget.communities;
}
return widget.communities.where((community) {
final containsQueryInCommunity =
community.name.toLowerCase().contains(_searchQuery.toLowerCase());
final containsQueryInSpaces = community.spaces.any((space) =>
_containsQuery(space: space, query: _searchQuery.toLowerCase()));
return containsQueryInCommunity || containsQueryInSpaces;
}).toList();
}
bool _containsQuery({
required SpaceModel space,
required String query,
}) {
final matchesSpace = space.name.toLowerCase().contains(query);
final matchesChildren = space.children.any(
(child) => _containsQuery(space: child, query: query),
);
if (matchesSpace || matchesChildren) {
_selectedSpaceUuid = space.uuid;
}
return matchesSpace || matchesChildren;
}
bool _isSpaceOrChildSelected(SpaceModel space) { bool _isSpaceOrChildSelected(SpaceModel space) {
final isSpaceSelected = _selectedSpaceUuid == space.uuid; final isSpaceSelected = _selectedSpaceUuid == space.uuid;
final anySubSpaceIsSelected = space.children.any(_isSpaceOrChildSelected); final anySubSpaceIsSelected = space.children.any(_isSpaceOrChildSelected);
@ -101,7 +106,10 @@ class _SidebarWidgetState extends State<SidebarWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final filteredCommunities = _filteredCommunities(); final spaceTreeState = context.watch<SpaceTreeBloc>().state;
final filteredCommunities = spaceTreeState.searchQuery.isNotEmpty
? spaceTreeState.filteredCommunity
: spaceTreeState.communityList;
return Container( return Container(
width: _width, width: _width,
@ -112,7 +120,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
children: [ children: [
SidebarHeader(onAddCommunity: _onAddCommunity), SidebarHeader(onAddCommunity: _onAddCommunity),
CustomSearchBar( CustomSearchBar(
onSearchChanged: (query) => setState(() => _searchQuery = query), onSearchChanged: _onSearchChanged,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Expanded( Expanded(
@ -120,14 +128,23 @@ class _SidebarWidgetState extends State<SidebarWidget> {
visible: filteredCommunities.isNotEmpty, visible: filteredCommunities.isNotEmpty,
replacement: const EmptySearchResultWidget(), replacement: const EmptySearchResultWidget(),
child: SidebarCommunitiesList( child: SidebarCommunitiesList(
scrollController: _scrollController, scrollController: _scrollController,
onScrollToEnd: () {}, onScrollToEnd: () {},
communities: filteredCommunities, communities: filteredCommunities,
itemBuilder: (context, index) => _buildCommunityTile( itemBuilder: (context, index) {
context, if (index == filteredCommunities.length) {
filteredCommunities[index], final spaceTreeState = context.read<SpaceTreeBloc>().state;
), if (spaceTreeState.paginationIsLoading) {
), return const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),
);
} else {
return const SizedBox.shrink();
}
}
return _buildCommunityTile(context, filteredCommunities[index]);
}),
), ),
), ),
], ],
@ -205,9 +222,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
); );
} }
void _onAddCommunity() => _selectedId?.isNotEmpty ?? true void _onAddCommunity() =>
? _clearSelection() _selectedId?.isNotEmpty ?? true ? _clearSelection() : _showCreateCommunityDialog();
: _showCreateCommunityDialog();
void _clearSelection() { void _clearSelection() {
setState(() => _selectedId = ''); setState(() => _selectedId = '');

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
@ -27,6 +29,8 @@ class SpaceModelPage extends StatelessWidget {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (state is SpaceModelLoaded) { } else if (state is SpaceModelLoaded) {
final spaceModels = state.spaceModels; final spaceModels = state.spaceModels;
context.read<SpaceTreeBloc>().add(ClearCachedData());
final allTagValues = _getAllTagValues(spaceModels); final allTagValues = _getAllTagValues(spaceModels);
final allSpaceModelNames = _getAllSpaceModelName(spaceModels); final allSpaceModelNames = _getAllSpaceModelName(spaceModels);

View File

@ -46,14 +46,14 @@ final class DebouncedBatchControlDevicesService
final BatchControlDevicesService decoratee; final BatchControlDevicesService decoratee;
final Duration debounceDuration; final Duration debounceDuration;
final _pendingRequests = <(List<String> uuids, String code, Object value)>[];
var _isProcessing = false;
DebouncedBatchControlDevicesService({ DebouncedBatchControlDevicesService({
required this.decoratee, required this.decoratee,
this.debounceDuration = const Duration(milliseconds: 1500), this.debounceDuration = const Duration(milliseconds: 1500),
}); });
final _pendingRequests = <(List<String> uuids, String code, Object value)>[];
var _isProcessing = false;
@override @override
Future<bool> batchControlDevices({ Future<bool> batchControlDevices({
required List<String> uuids, required List<String> uuids,
@ -68,16 +68,26 @@ final class DebouncedBatchControlDevicesService
await Future.delayed(debounceDuration); await Future.delayed(debounceDuration);
final lastRequest = _pendingRequests.last; final groupedRequests =
<String, (List<String> uuids, String code, Object value)>{};
for (final request in _pendingRequests) {
final (_, requestCode, requestValue) = request;
groupedRequests[requestCode] = request;
}
_pendingRequests.clear(); _pendingRequests.clear();
try { try {
final (lastRequestUuids, lastRequestCode, lastRequestValue) = lastRequest; var allSuccessful = true;
return decoratee.batchControlDevices( for (final request in groupedRequests.values) {
uuids: lastRequestUuids, final (lastRequestUuids, lastRequestCode, lastRequestValue) = request;
code: lastRequestCode, final success = await decoratee.batchControlDevices(
value: lastRequestValue, uuids: lastRequestUuids,
); code: lastRequestCode,
value: lastRequestValue,
);
if (!success) allSuccessful = false;
}
return allSuccessful;
} finally { } finally {
_isProcessing = false; _isProcessing = false;
} }

View File

@ -59,15 +59,24 @@ final class DebouncedControlDeviceService implements ControlDeviceService {
await Future.delayed(debounceDuration); await Future.delayed(debounceDuration);
final lastRequest = _pendingRequests.last; final groupedRequests = <String, (String deviceUuid, Status status)>{};
for (final request in _pendingRequests) {
final (_, requestStatus) = request;
groupedRequests[requestStatus.code] = request;
}
_pendingRequests.clear(); _pendingRequests.clear();
try { try {
final (lastRequestDeviceUuid, lastRequestStatus) = lastRequest; var allSuccessful = true;
return decoratee.controlDevice( for (final request in groupedRequests.values) {
deviceUuid: lastRequestDeviceUuid, final (lastRequestDeviceUuid, lastRequestStatus) = request;
status: lastRequestStatus, final success = await decoratee.controlDevice(
); deviceUuid: lastRequestDeviceUuid,
status: lastRequestStatus,
);
if (!success) allSuccessful = false;
}
return allSuccessful;
} finally { } finally {
_isProcessing = false; _isProcessing = false;
} }

View File

@ -281,6 +281,7 @@ class CommunitySpaceManagementApi {
return json['success'] ?? false; return json['success'] ?? false;
}, },
); );
return response; return response;
} catch (e) { } catch (e) {
debugPrint('Error updating space: $e'); debugPrint('Error updating space: $e');