Compare commits

..

39 Commits

Author SHA1 Message Date
c07b53107e SP-1506-FE-chart per phase api integration. 2025-05-15 10:51:09 +03:00
5279020d08 Merge pull request #188 from SyncrowIOT/1495-energy-consumption-per-device-api-integration
1495-energy-consumption-per-device-api-integration.
2025-05-15 09:32:15 +03:00
da481536c4 1495-energy-consumption-per-device-api-integration. 2025-05-14 16:55:28 +03:00
f21366268a Merge pull request #187 from SyncrowIOT/SP-1509-FE-Implement-devices-status-based-on-the-selected-device-from-the-dropdown-list
Sp 1509 fe implement devices status based on the selected device from the dropdown list
2025-05-14 16:18:51 +03:00
c3aef736fd Merge pull request #186 from SyncrowIOT/1511-occupancy-heat-map-tooltip
1511-occupancy-heat-map-tooltip.
2025-05-14 16:18:08 +03:00
887ac58f40 fixed import. 2025-05-14 15:59:40 +03:00
c709477500 some refactors to further clarify intent. 2025-05-14 15:55:12 +03:00
63e7b3faa2 resets selection and clears data. 2025-05-14 15:47:07 +03:00
0e61e52bf8 Connected devices to widgets, and is currently making the necessary and correct api calls for everything to function properly. 2025-05-14 15:35:22 +03:00
7515b347ce analytics devices integtation. 2025-05-14 15:03:30 +03:00
3dfbcb5935 connect device dropdown to bloc. 2025-05-14 14:31:28 +03:00
4fd4a9b5bf loads analytics devices on sidebar selection. 2025-05-14 13:03:51 +03:00
14fa1b355e Added a uuid property to AnalyticsDevice. 2025-05-14 12:50:27 +03:00
78d4e58996 Added selected device state/event, and clear data event to AnalyticsDevicesBloc. 2025-05-14 12:50:16 +03:00
23b9cb5b78 Injected AnalyticsDevicesBloc into AnalyticsPage. 2025-05-14 12:42:51 +03:00
401d0a9788 Created AnalyticsDevicesBloc. 2025-05-14 12:41:44 +03:00
ac2b0d3fac Created an initial remote implementation of AnalyticsDevicesService. 2025-05-14 12:38:07 +03:00
3be7a377c0 Created AnalyticsDevicesService interface. 2025-05-14 12:37:52 +03:00
e4ee456384 Created empty AnalyticsDevice model. 2025-05-14 12:37:44 +03:00
f02c5d71ba Created GetAnalyticsDevicesParam. 2025-05-14 12:26:16 +03:00
d45ff262c7 Merge branch 'dev' into 1511-occupancy-heat-map-tooltip 2025-05-14 12:05:34 +03:00
ad227febc1 Merge pull request #185 from SyncrowIOT/SP-1512-FE-Apply-Responsive-Behavior-for-Dashboard-Layout-and-Sidebar-Collapse
Sp 1512 fe apply responsive behavior for dashboard layout and sidebar collapse
2025-05-14 12:04:41 +03:00
a9d6c6f4ee 1511-occupancy-heat-map-tooltip. 2025-05-14 12:03:47 +03:00
4d9e57c8b5 Created and connected a remote implementation that fetches the heat map occupancy per space from the API. 2025-05-14 10:51:37 +03:00
d1bb8da484 Updated OccupancyHeatMapModel model with what the api returns, and only used the necessary fields that the api returns for this feature to work. 2025-05-14 10:51:19 +03:00
300f9ae358 Matched the GetOccupancyHeatMapParam with what the API expects and removed the communityId since it is no longer necessary for the api, and renamed spaceId to spaceUuid for more clarity. 2025-05-14 10:49:32 +03:00
c1dab3400b removed a force unwrap from OccupancyHeatMap._maxValue to avoid any bugs. 2025-05-14 10:48:28 +03:00
46815585cb Fixed error in AnalyticsErrorWidget where it used to add the default error message to the errorMessage. 2025-05-14 10:47:54 +03:00
7f9d044f7e Merge pull request #184 from SyncrowIOT/SP-1530-FE-Add-card-for-the-water-heater-in-the-routine-web
add water heater operational values to routines
2025-05-14 09:20:07 +03:00
996a847a27 Refactor water heater value selector widget 2025-05-14 09:16:04 +03:00
5645fb7826 Merge pull request #182 from SyncrowIOT/SP-1519-FE-Handle-Loading-Skeletons-and-No-Data-Error-States
Sp 1519 fe handle loading skeletons and no data error states
2025-05-13 16:55:54 +03:00
e8f7c29652 Applies correct business logic of the sidebar. 2025-05-13 16:46:34 +03:00
36c5712c79 add water heater operational values to routines 2025-05-13 16:24:08 +03:00
c7fef11aec Fixed typo Tab to run to Tap to run. 2025-05-12 12:06:37 +03:00
ef29d78d70 Clears data when needed. 2025-05-12 10:02:56 +03:00
cd9941f544 Doesn't load occupancy data on initState in AnalyticsOccupancyView. 2025-05-12 10:02:08 +03:00
71aa64ba9e Merge pull request #181 from SyncrowIOT/bugfix/analytics_expansion_bugfix
bugfix/analytics_expansion_bugfix.
2025-05-12 09:22:12 +03:00
2262d3b2ba bugfix/analytics_expansion_bugfix. 2025-05-12 09:20:01 +03:00
b7ef9da35d Sp 1513 fe implement device dropdown and live status card presence vacancy (#179)
* Called the widget of presence sensor status widgets.

* Enahnced `PowerClampEnergyDataDeviceDropdown` design and made it a dropdown.

* connected the realtime feature to the occupancy side bar, but with a mock id.

* revert default tab to energyManagement.
2025-05-11 16:59:15 +03:00
59 changed files with 1825 additions and 434 deletions

View File

@ -0,0 +1,12 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7305_15779)">
<path d="M17.0872 11.5142C17.0872 13.2025 16.427 14.8021 15.2211 15.9954C14.0278 17.2014 12.4283 17.8615 10.7399 17.8615C9.05141 17.8615 7.45185 17.2014 6.25856 15.9954C5.05262 14.8021 4.39249 13.2025 4.39249 11.5142C4.39249 9.82574 5.05266 8.22618 6.25856 7.03289C7.45185 5.8269 9.05141 5.16681 10.7399 5.16681C11.8063 5.16681 12.8471 5.43337 13.7866 5.95388L11.2984 8.97523H21.0861L18.6486 0L16.2113 2.97053C14.5737 1.91691 12.6948 1.35835 10.7398 1.35835C8.02314 1.35835 5.47142 2.41197 3.55459 4.32888C1.63765 6.24578 0.583984 8.79747 0.583984 11.5142C0.583984 14.2309 1.63765 16.7825 3.55459 18.6994C5.47146 20.6163 8.0231 21.67 10.7398 21.67C13.4565 21.67 16.0082 20.6163 17.925 18.6994C19.8419 16.7825 20.8956 14.2309 20.8956 11.5142V10.8794H17.0872V11.5142Z" fill="#77DD00"/>
<path d="M17.0876 10.8799H20.8961V11.5146C20.8961 14.2313 19.8424 16.7829 17.9254 18.6998C16.0086 20.6168 13.4569 21.6704 10.7402 21.6704V17.862C12.4287 17.862 14.0282 17.2019 15.2215 15.9959C16.4275 14.8026 17.0876 13.203 17.0876 11.5147V10.8799H17.0876Z" fill="#66BB00"/>
<path d="M13.787 5.95388C12.8475 5.43333 11.8066 5.16681 10.7402 5.16681V1.35835C12.6952 1.35835 14.5741 1.91691 16.2117 2.97057L18.6491 0L21.0866 8.97523H11.2989L13.787 5.95388Z" fill="#66BB00"/>
</g>
<defs>
<clipPath id="clip0_7305_15779">
<rect width="21.67" height="21.67" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,13 @@
class AnalyticsDevice {
const AnalyticsDevice({required this.name, required this.uuid});
final String uuid;
final String name;
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
return AnalyticsDevice(
uuid: json['uuid'] as String? ?? '',
name: json['name'] as String? ?? '',
);
}
}

View File

@ -1,22 +1,28 @@
import 'package:equatable/equatable.dart';
class OccupancyHeatMapModel extends Equatable {
final DateTime date;
final String uuid;
final int occupancy;
final DateTime eventDate;
final int countTotalPresenceDetected;
const OccupancyHeatMapModel({
required this.date,
required this.occupancy,
required this.uuid,
required this.eventDate,
required this.countTotalPresenceDetected,
});
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
return OccupancyHeatMapModel(
date: DateTime.parse(json['date'] as String),
occupancy: json['occupancy'] as int,
uuid: json['uuid'] as String? ?? '',
eventDate: DateTime.parse(
json['event_date'] as String? ?? '${DateTime.now()}',
),
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
);
}
@override
List<Object?> get props => [date, occupancy];
List<Object?> get props => [uuid, eventDate, countTotalPresenceDetected];
}

View File

@ -1,27 +1,66 @@
import 'package:equatable/equatable.dart';
class PhasesEnergyConsumption extends Equatable {
final int month;
final double phaseA;
final double phaseB;
final double phaseC;
final String uuid;
final DateTime createdAt;
final DateTime updatedAt;
final String deviceUuid;
final DateTime date;
final double energyConsumedKw;
final double energyConsumedA;
final double energyConsumedB;
final double energyConsumedC;
const PhasesEnergyConsumption({
required this.month,
required this.phaseA,
required this.phaseB,
required this.phaseC,
required this.uuid,
required this.createdAt,
required this.updatedAt,
required this.deviceUuid,
required this.date,
required this.energyConsumedKw,
required this.energyConsumedA,
required this.energyConsumedB,
required this.energyConsumedC,
});
@override
List<Object?> get props => [month, phaseA, phaseB, phaseC];
List<Object?> get props => [
uuid,
createdAt,
updatedAt,
deviceUuid,
date,
energyConsumedKw,
energyConsumedA,
energyConsumedB,
energyConsumedC,
];
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
return PhasesEnergyConsumption(
month: json['month'] as int,
phaseA: (json['phaseA'] as num).toDouble(),
phaseB: (json['phaseB'] as num).toDouble(),
phaseC: (json['phaseC'] as num).toDouble(),
uuid: json['uuid'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
deviceUuid: json['deviceUuid'] as String,
date: DateTime.parse(json['date'] as String),
energyConsumedKw: double.parse(json['energyConsumedKw']),
energyConsumedA: double.parse(json['energyConsumedA']),
energyConsumedB: double.parse(json['energyConsumedB']),
energyConsumedC: double.parse(json['energyConsumedC']),
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'deviceUuid': deviceUuid,
'date': date.toIso8601String().split('T')[0],
'energyConsumedKw': energyConsumedKw.toString(),
'energyConsumedA': energyConsumedA.toString(),
'energyConsumedB': energyConsumedB.toString(),
'energyConsumedC': energyConsumedC.toString(),
};
}
}

View File

@ -0,0 +1,69 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
part 'analytics_devices_event.dart';
part 'analytics_devices_state.dart';
class AnalyticsDevicesBloc
extends Bloc<AnalyticsDevicesEvent, AnalyticsDevicesState> {
AnalyticsDevicesBloc(
this._analyticsDevicesService,
) : super(const AnalyticsDevicesState()) {
on<LoadAnalyticsDevicesEvent>(_onLoadAnalyticsDevices);
on<SelectAnalyticsDeviceEvent>(_onSelectAnalyticsDevice);
on<ClearAnalyticsDeviceEvent>(_onClearAnalyticsDevice);
}
final AnalyticsDevicesService _analyticsDevicesService;
Future<void> _onLoadAnalyticsDevices(
LoadAnalyticsDevicesEvent event,
Emitter<AnalyticsDevicesState> emit,
) async {
emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading));
try {
final devices = await _analyticsDevicesService.getDevices(event.param);
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.loaded,
devices: devices,
selectedDevice: devices.firstOrNull,
),
);
if (devices.isNotEmpty) {
event.onSuccess(devices.first);
}
} catch (e) {
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onSelectAnalyticsDevice(
SelectAnalyticsDeviceEvent event,
Emitter<AnalyticsDevicesState> emit,
) {
emit(
AnalyticsDevicesState(
selectedDevice: event.device,
devices: state.devices,
errorMessage: state.errorMessage,
status: state.status,
),
);
}
void _onClearAnalyticsDevice(
ClearAnalyticsDeviceEvent event,
Emitter<AnalyticsDevicesState> emit,
) {
emit(const AnalyticsDevicesState());
}
}

View File

@ -0,0 +1,31 @@
part of 'analytics_devices_bloc.dart';
sealed class AnalyticsDevicesEvent extends Equatable {
const AnalyticsDevicesEvent();
@override
List<Object> get props => [];
}
final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent {
const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess});
final GetAnalyticsDevicesParam param;
final void Function(AnalyticsDevice device) onSuccess;
@override
List<Object> get props => [param];
}
final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
const SelectAnalyticsDeviceEvent(this.device);
final AnalyticsDevice device;
@override
List<Object> get props => [device];
}
final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
const ClearAnalyticsDeviceEvent();
}

View File

@ -0,0 +1,20 @@
part of 'analytics_devices_bloc.dart';
enum AnalyticsDevicesStatus { initial, loading, loaded, failure }
final class AnalyticsDevicesState extends Equatable {
const AnalyticsDevicesState({
this.status = AnalyticsDevicesStatus.initial,
this.devices = const [],
this.errorMessage,
this.selectedDevice,
});
final AnalyticsDevicesStatus status;
final List<AnalyticsDevice> devices;
final AnalyticsDevice? selectedDevice;
final String? errorMessage;
@override
List<Object?> get props => [status, devices, errorMessage, selectedDevice];
}

View File

@ -14,13 +14,25 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
List<SpaceModel> spaces,
) {
// Add to space tree bloc first
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces,
),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(context);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
}
@override
@ -36,7 +48,19 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
space.children,
),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(context);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
spaceId: space.uuid ?? '',
);
}
@override
@ -45,14 +69,7 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
SpaceModel child,
) {
context.read<SpaceTreeBloc>().add(
OnSpaceSelected(
community,
child.uuid ?? '',
child.children,
),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(context);
// Do nothing else as per original implementation
}
@override

View File

@ -17,10 +17,20 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces,
spaces.isNotEmpty ? [spaces.first] : [],
),
);
FetchOccupancyDataHelper.loadOccupancyData(context);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
clearData(context);
return;
}
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
}
@override
@ -29,14 +39,32 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel space,
) {
context.read<SpaceTreeBloc>().add(
OnSpaceSelected(
community,
space.uuid ?? '',
space.children,
),
);
FetchOccupancyDataHelper.loadOccupancyData(context);
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces;
final isSpaceSelected = selectedSpacesIds.contains(space.uuid);
if (selectedSpacesIds.isEmpty) {
spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space]));
} else if (isSpaceSelected) {
spaceTreeBloc.add(const SpaceTreeClearSelectionEvent());
} else {
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
}
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
clearData(context);
return;
}
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
spaceId: space.uuid ?? '',
);
}
@override
@ -45,18 +73,12 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel child,
) {
context.read<SpaceTreeBloc>().add(
OnSpaceSelected(
community,
child.uuid ?? '',
child.children,
),
);
FetchOccupancyDataHelper.loadOccupancyData(context);
// Do nothing
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
FetchOccupancyDataHelper.clearAllData(context);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
@ -11,10 +12,13 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/fake_occupacy_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/fake_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
@ -23,9 +27,22 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class AnalyticsPage extends StatelessWidget {
class AnalyticsPage extends StatefulWidget {
const AnalyticsPage({super.key});
@override
State<AnalyticsPage> createState() => _AnalyticsPageState();
}
class _AnalyticsPageState extends State<AnalyticsPage> {
late final HTTPService _httpService;
@override
void initState() {
super.initState();
_httpService = HTTPService();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@ -35,22 +52,22 @@ class AnalyticsPage extends StatelessWidget {
),
BlocProvider(
create: (context) => TotalEnergyConsumptionBloc(
RemoteTotalEnergyConsumptionService(HTTPService()),
RemoteTotalEnergyConsumptionService(_httpService),
),
),
BlocProvider(
create: (context) => EnergyConsumptionByPhasesBloc(
FakeEnergyConsumptionByPhasesService(),
RemoteEnergyConsumptionByPhasesService(_httpService),
),
),
BlocProvider(
create: (context) => EnergyConsumptionPerDeviceBloc(
FakeEnergyConsumptionPerDeviceService(),
RemoteEnergyConsumptionPerDeviceService(_httpService),
),
),
BlocProvider(
create: (context) => PowerClampInfoBloc(
RemotePowerClampInfoService(HTTPService()),
RemotePowerClampInfoService(_httpService),
),
),
BlocProvider<RealtimeDeviceChangesBloc>(
@ -60,9 +77,19 @@ class AnalyticsPage extends StatelessWidget {
),
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()),
create: (context) => OccupancyHeatMapBloc(
RemoteOccupancyHeatMapService(_httpService),
),
),
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
BlocProvider(
create: (context) => AnalyticsDevicesBloc(
AnalyticsDevicesServiceDelegate(
RemoteOccupancyAnalyticsDevicesService(_httpService),
RemoteEnergyManagementAnalyticsDevicesService(_httpService),
),
),
),
],
child: const AnalyticsPageForm(),
);

View File

@ -9,29 +9,21 @@ class AnalyticsCommunitiesSidebar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
final selectedTab = context.read<AnalyticsTabBloc>().state;
final strategy =
AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
final selectedTab = context.watch<AnalyticsTabBloc>().state;
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
// Clear data when tab changes
strategy.clearData(context);
return Expanded(
child: AnalyticsSpaceTreeView(
onSelectCommunity: (community, spaces) {
strategy.onCommunitySelected(context, community, spaces);
},
onSelectSpace: (community, space) {
strategy.onSpaceSelected(context, community, space);
},
onSelectChildSpace: (community, child) {
strategy.onChildSpaceSelected(context, community, child);
},
),
);
},
return Expanded(
child: AnalyticsSpaceTreeView(
onSelectCommunity: (community, spaces) {
strategy.onCommunitySelected(context, community, spaces);
},
onSelectSpace: (community, space) {
strategy.onSpaceSelected(context, community, space);
},
onSelectChildSpace: (community, child) {
strategy.onChildSpaceSelected(context, community, child);
},
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_pa
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/style.dart';
class AnalyticsPageTabsAndChildren extends StatelessWidget {
@ -13,6 +14,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
@override
Widget build(BuildContext context) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
buildWhen: (previous, current) => previous != current,
builder: (context, selectedTab) => Column(
@ -53,32 +55,33 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
),
),
const Spacer(),
_buildAnimation(
child: Visibility(
key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement,
child: Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(
montlyDate: value),
);
FetchEnergyManagementDataHelper
.fetchEnergyManagementData(
context,
selectedDate: value,
);
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.monthlyDate,
),
Visibility(
key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement,
child: Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.monthlyDate,
),
),
),

View File

@ -1,20 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
abstract final class EnergyConsumptionByPhasesChartHelper {
const EnergyConsumptionByPhasesChartHelper._();
static const fakeData = <PhasesEnergyConsumption>[
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100),
];
}

View File

@ -1,75 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
abstract final class FetchEnergyManagementDataHelper {
const FetchEnergyManagementDataHelper._();
static void fetchEnergyManagementData(
// static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
}
static void loadEnergyManagementData(
BuildContext context, {
required String communityId,
required String spaceId,
DateTime? selectedDate,
}) {
final (selectedCommunities, selectedSpaces) =
getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
return;
}
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
loadTotalEnergyConsumption(context, selectedDate: datePickerState.monthlyDate);
loadEnergyConsumptionByPhases(
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
loadAnalyticsDevices(
context,
selectedDate: datePickerState.monthlyDate,
communityUuid: communityId,
spaceUuid: spaceId,
selectedDate: selectedDate0,
);
loadEnergyConsumptionPerDevice(context);
return;
}
static void loadEnergyManagementData(BuildContext context) {
final (selectedCommunities, selectedSpaces) =
FetchEnergyManagementDataHelper.getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) return;
FetchEnergyManagementDataHelper.fetchEnergyManagementData(context,
selectedDate: DateTime.now());
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(context);
context.read<PowerClampInfoBloc>().add(const ClearPowerClampInfoEvent());
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
} else {
FetchEnergyManagementDataHelper.loadPowerClampInfo(context);
loadTotalEnergyConsumption(
context,
selectedDate: selectedDate0,
communityId: communityId,
spaceId: spaceId,
);
final selectedDevice = getSelectedDevice(context);
if (selectedDevice case final AnalyticsDevice device) {
loadEnergyConsumptionByPhases(
context,
powerClampUuid: device.uuid,
selectedDate: selectedDate0,
);
}
}
static (List<String> selectedCommunities, List<String> selectedSpaces)
getSelectedCommunitiesAndSpaces(BuildContext context) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final selectedCommunities = spaceTreeState.selectedCommunities;
final selectedSpaces = spaceTreeState.selectedSpaces;
return (selectedCommunities, selectedSpaces);
loadEnergyConsumptionPerDevice(
context,
communityId: communityId,
spaceId: spaceId,
selectedDate: selectedDate0,
);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
}
static void loadEnergyConsumptionByPhases(
BuildContext context, {
required String powerClampUuid,
DateTime? selectedDate,
}) {
final param = GetEnergyConsumptionByPhasesParam(
startDate: selectedDate,
spaceId: '',
date: selectedDate,
powerClampUuid: powerClampUuid,
);
context.read<EnergyConsumptionByPhasesBloc>().add(
LoadEnergyConsumptionByPhasesEvent(param: param),
@ -79,13 +81,12 @@ abstract final class FetchEnergyManagementDataHelper {
static void loadTotalEnergyConsumption(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final (selectedCommunities, selectedSpaces) =
getSelectedCommunitiesAndSpaces(context);
final param = GetTotalEnergyConsumptionParam(
spaceId: selectedCommunities.firstOrNull,
communityId: selectedCommunities.firstOrNull,
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<TotalEnergyConsumptionBloc>().add(
@ -93,33 +94,81 @@ abstract final class FetchEnergyManagementDataHelper {
);
}
static void loadEnergyConsumptionPerDevice(BuildContext context) {
const param = GetEnergyConsumptionPerDeviceParam();
static void loadEnergyConsumptionPerDevice(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetEnergyConsumptionPerDeviceParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<EnergyConsumptionPerDeviceBloc>().add(
const LoadEnergyConsumptionPerDeviceEvent(param),
LoadEnergyConsumptionPerDeviceEvent(param),
);
}
static void loadPowerClampInfo(BuildContext context) {
context.read<PowerClampInfoBloc>().add(
const LoadPowerClampInfoEvent('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
final selectedDevice = getSelectedDevice(context);
if (selectedDevice case final AnalyticsDevice device) {
context.read<PowerClampInfoBloc>().add(
LoadPowerClampInfoEvent(device.uuid),
);
}
}
static void loadRealtimeDeviceChanges(
BuildContext context, {
String? deviceUuid,
}) {
final selectedDevice = getSelectedDevice(context);
context.read<RealtimeDeviceChangesBloc>().add(
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
);
}
static void loadRealtimeDeviceChanges(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
required DateTime selectedDate,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
onSuccess: (device) {
context.read<PowerClampInfoBloc>().add(
LoadPowerClampInfoEvent(device.uuid),
);
loadEnergyConsumptionByPhases(
context,
powerClampUuid: device.uuid,
selectedDate: selectedDate,
);
context.read<RealtimeDeviceChangesBloc>().add(
RealtimeDeviceChangesStarted(device.uuid),
);
},
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['PC'],
requestType: AnalyticsDeviceRequestType.energyManagement,
),
),
);
}
static void clearAllData(BuildContext context) {
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
context.read<EnergyConsumptionPerDeviceBloc>().add(
const ClearEnergyConsumptionPerDeviceEvent(),
);
@ -131,5 +180,6 @@ abstract final class FetchEnergyManagementDataHelper {
context.read<EnergyConsumptionByPhasesBloc>().add(
const ClearEnergyConsumptionByPhasesEvent(),
);
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
}
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class AnalyticsEnergyManagementView extends StatefulWidget {
const AnalyticsEnergyManagementView({super.key});
@ -16,7 +18,14 @@ class _AnalyticsEnergyManagementViewState
extends State<AnalyticsEnergyManagementView> {
@override
void initState() {
FetchEnergyManagementDataHelper.loadEnergyManagementData(context);
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final communityId = spaceTreeBloc.state.selectedCommunities.firstOrNull;
final spaceId = spaceTreeBloc.state.selectedSpaces.firstOrNull;
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: communityId ?? '',
spaceId: spaceId ?? '',
);
super.initState();
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsDeviceDropdown extends StatelessWidget {
const AnalyticsDeviceDropdown({required this.onChanged, super.key});
final ValueChanged<AnalyticsDevice> onChanged;
@override
Widget build(BuildContext context) {
return BlocBuilder<AnalyticsDevicesBloc, AnalyticsDevicesState>(
builder: (context, state) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: Visibility(
visible: state.devices.isNotEmpty,
replacement: _buildNoDevicesFound(context),
child: _buildDevicesDropdown(context, state),
),
);
},
);
}
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
);
Widget _buildNoDevicesFound(BuildContext context) {
return Padding(
padding: _defaultPadding,
child: Text(
'no devices found',
style: _getTextStyle(context),
),
);
}
Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) {
return DropdownButton<AnalyticsDevice?>(
value: state.selectedDevice,
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 16),
),
style: _getTextStyle(context),
padding: _defaultPadding,
items: state.devices.map((e) {
return DropdownMenuItem(
value: e,
child: Text(e.name),
);
}).toList(),
onChanged: (value) {
if (value case final AnalyticsDevice device) {
context.read<AnalyticsDevicesBloc>().add(
SelectAnalyticsDeviceEvent(device),
);
onChanged.call(device);
}
},
);
}
TextStyle? _getTextStyle(BuildContext context) {
return context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -31,21 +31,25 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
barRods: [
BarChartRodData(
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
toY: data.phaseA + data.phaseB + data.phaseC,
toY: data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
rodStackItems: [
BarChartRodStackItem(
0,
data.phaseA,
data.energyConsumedA,
ColorsManager.vividBlue.withValues(alpha: 0.8),
),
BarChartRodStackItem(
data.phaseA,
data.phaseA + data.phaseB,
data.energyConsumedA,
data.energyConsumedA + data.energyConsumedB,
ColorsManager.vividBlue.withValues(alpha: 0.4),
),
BarChartRodStackItem(
data.phaseA + data.phaseB,
data.phaseA + data.phaseB + data.phaseC,
data.energyConsumedA + data.energyConsumedB,
data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
ColorsManager.vividBlue.withValues(alpha: 0.15),
),
],
@ -91,18 +95,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
}) {
final data = energyData;
final month = data[group.x.toInt()].month.getMonthName;
final phaseA = data[group.x.toInt()].phaseA;
final phaseB = data[group.x.toInt()].phaseB;
final phaseC = data[group.x.toInt()].phaseC;
final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
final phaseA = data[group.x.toInt()].energyConsumedA;
final phaseB = data[group.x.toInt()].energyConsumedB;
final phaseC = data[group.x.toInt()].energyConsumedC;
final total = data[group.x.toInt()].energyConsumedKw;
return BarTooltipItem(
'$month\n',
'$date\n',
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 14,
),
textAlign: TextAlign.start,
children: [
TextSpan(
text: 'Total: $total\n',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
TextSpan(
text: 'Phase A: $phaseA\n',
style: context.textTheme.bodySmall?.copyWith(
@ -144,7 +157,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) {
final month = energyData[value.toInt()].month.getMonthName;
final month = DateFormat('dd/MM').format(energyData[value.toInt()].date);
return FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,

View File

@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
const PowerClampEnergyDataDeviceDropdown({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: DropdownButton<String>(
value: 'Device 1',
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 16),
),
style: context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
),
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
),
items: [
for (var i = 1; i < 10; i++)
DropdownMenuItem(
value: 'Device $i',
child: Text(
'Device $i',
style: context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
),
],
onChanged: (value) {},
),
);
}
}

View File

@ -1,14 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -50,7 +54,8 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
),
const SizedBox(height: 6),
SelectableText(
state.powerClampModel?.productUuid ?? 'N/A',
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
@ -107,7 +112,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 2,
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@ -122,11 +127,25 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
),
),
const Spacer(),
const Expanded(
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(),
child: AnalyticsDeviceDropdown(
onChanged: (value) {
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
context,
powerClampUuid: value.uuid,
selectedDate:
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
);
},
),
),
),
],

View File

@ -1,38 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
abstract final class FetchOccupancyDataHelper {
const FetchOccupancyDataHelper._();
static void loadOccupancyData(BuildContext context) {
final (selectedCommunities, selectedSpaces) =
FetchEnergyManagementDataHelper.getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
context.read<OccupancyBloc>().add(
const ClearOccupancyEvent(),
);
context.read<OccupancyHeatMapBloc>().add(
const ClearOccupancyHeatMapEvent(),
);
static void loadOccupancyData(
BuildContext context, {
required String communityId,
required String spaceId,
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
return;
}
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
loadAnalyticsDevices(context, communityUuid: communityId, spaceUuid: spaceId);
final selectedDevice = context.read<AnalyticsDevicesBloc>().state.selectedDevice;
context.read<OccupancyBloc>().add(
LoadOccupancyEvent(
GetOccupancyParam(
monthDate:
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}',
spaceUuid: selectedSpaces.firstOrNull,
communityUuid: selectedCommunities.first,
spaceUuid: spaceId,
communityUuid: communityId,
),
),
);
@ -40,18 +42,56 @@ abstract final class FetchOccupancyDataHelper {
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(
spaceId: selectedSpaces.isNotEmpty ? selectedSpaces.first : '',
communityId:
selectedCommunities.isNotEmpty ? selectedCommunities.first : '',
spaceUuid: spaceId,
year: datePickerState.yearlyDate,
),
),
);
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(
const RealtimeDeviceChangesStarted('14fe6e7e-47af-4a07-ae0a-7c4a26ef8135'),
);
if (selectedDevice case final AnalyticsDevice device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(
RealtimeDeviceChangesStarted(device.uuid),
);
}
}
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['WPS', 'CPS'],
requestType: AnalyticsDeviceRequestType.occupancy,
),
onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid));
},
),
);
}
static void clearAllData(BuildContext context) {
context.read<OccupancyBloc>().add(
const ClearOccupancyEvent(),
);
context.read<OccupancyHeatMapBloc>().add(
const ClearOccupancyHeatMapEvent(),
);
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<AnalyticsDevicesBloc>().add(
const ClearAnalyticsDeviceEvent(),
);
}
}

View File

@ -1,25 +1,13 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart';
class AnalyticsOccupancyView extends StatefulWidget {
class AnalyticsOccupancyView extends StatelessWidget {
const AnalyticsOccupancyView({super.key});
static const _padding = EdgeInsetsDirectional.all(32);
@override
State<AnalyticsOccupancyView> createState() => _AnalyticsOccupancyViewState();
}
class _AnalyticsOccupancyViewState extends State<AnalyticsOccupancyView> {
@override
void initState() {
FetchOccupancyDataHelper.loadOccupancyData(context);
super.initState();
}
@override
Widget build(BuildContext context) {
final height = MediaQuery.sizeOf(context).height;
@ -28,7 +16,7 @@ class _AnalyticsOccupancyViewState extends State<AnalyticsOccupancyView> {
final isMediumOrLess = constraints.maxWidth <= 900;
if (isMediumOrLess) {
return SingleChildScrollView(
padding: AnalyticsOccupancyView._padding,
padding: _padding,
child: Column(
spacing: 32,
children: [
@ -42,7 +30,7 @@ class _AnalyticsOccupancyViewState extends State<AnalyticsOccupancyView> {
return SingleChildScrollView(
child: Container(
padding: AnalyticsOccupancyView._padding,
padding: _padding,
height: height * 0.9,
child: const Row(
spacing: 32,

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class HeatMapTooltip extends StatelessWidget {
const HeatMapTooltip({
required this.date,
required this.value,
super.key,
});
final DateTime date;
final int value;
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.topStart,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.grey700,
borderRadius: BorderRadius.circular(3),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
DateFormat('MMM d, yyyy').format(date),
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
fontWeight: FontWeight.w700,
color: ColorsManager.whiteColors,
),
),
const Divider(height: 2, thickness: 1),
Text(
'$value Occupants',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w500,
color: ColorsManager.whiteColors,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
class InteractiveHeatMap extends StatefulWidget {
const InteractiveHeatMap({
required this.items,
required this.maxValue,
required this.cellSize,
super.key,
});
final List<OccupancyPaintItem> items;
final int maxValue;
final double cellSize;
@override
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
}
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
OccupancyPaintItem? _hoveredItem;
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
@override
void dispose() {
_removeOverlay();
_overlayEntry?.dispose();
super.dispose();
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
_removeOverlay();
final column = item.index ~/ 7;
final row = item.index % 7;
final x = column * widget.cellSize;
final y = row * widget.cellSize;
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
child: CompositedTransformFollower(
link: _layerLink,
offset: Offset(x + widget.cellSize, y),
child: Material(
color: Colors.transparent,
child: Transform.translate(
offset: Offset(-(widget.cellSize * 2.5), -50),
child: HeatMapTooltip(date: item.date, value: item.value),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: MouseRegion(
onHover: (event) {
final column = event.localPosition.dx ~/ widget.cellSize;
final row = event.localPosition.dy ~/ widget.cellSize;
final index = column * 7 + row;
if (index >= 0 && index < widget.items.length) {
final item = widget.items[index];
if (_hoveredItem != item) {
setState(() => _hoveredItem = item);
_showTooltip(item, event.localPosition);
}
} else {
_removeOverlay();
setState(() => _hoveredItem = null);
}
},
onExit: (_) {
_removeOverlay();
setState(() => _hoveredItem = null);
},
child: CustomPaint(
isComplex: true,
size: _painterSize,
painter: OccupancyPainter(
items: widget.items,
maxValue: widget.maxValue,
hoveredItem: _hoveredItem,
),
),
),
);
}
Size get _painterSize {
final height = 7 * widget.cellSize;
final width = widget.items.length ~/ 7 * widget.cellSize;
return Size(width, height);
}
}

View File

@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/oc
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyChartBox extends StatelessWidget {
@ -14,6 +15,7 @@ class OccupancyChartBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
return BlocBuilder<OccupancyBloc, OccupancyState>(
builder: (context, state) {
return Container(
@ -45,7 +47,11 @@ class OccupancyChartBox extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
FetchOccupancyDataHelper.loadOccupancyData(context);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: spaceTreeState.selectedCommunities.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
import 'package:uuid/uuid.dart';
class OccupancyEndSideBar extends StatelessWidget {
const OccupancyEndSideBar({super.key});
@ -37,7 +38,8 @@ class OccupancyEndSideBar extends StatelessWidget {
),
const SizedBox(height: 6),
SelectableText(
(const Uuid().v4()),
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
@ -105,7 +107,7 @@ class OccupancyEndSideBar extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 2,
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@ -120,11 +122,18 @@ class OccupancyEndSideBar extends StatelessWidget {
),
),
const Spacer(),
const Expanded(
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(),
child: AnalyticsDeviceDropdown(
onChanged: (value) =>
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
),
),
),
),
],

View File

@ -1,6 +1,7 @@
import 'dart:math' as math show max;
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
@ -15,7 +16,7 @@ class OccupancyHeatMap extends StatelessWidget {
static const _totalWeeks = 53;
int get _maxValue => heatMapData.isNotEmpty
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max)
? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max)
: 0;
DateTime _getStartingDate() {
@ -28,7 +29,7 @@ class OccupancyHeatMap extends StatelessWidget {
return List.generate(_totalWeeks * 7, (index) {
final date = startDate.add(Duration(days: index));
final value = heatMapData[date] ?? 0;
return OccupancyPaintItem(index: index, value: value);
return OccupancyPaintItem(index: index, value: value, date: date);
});
}
@ -58,15 +59,13 @@ class OccupancyHeatMap extends StatelessWidget {
child: Row(
children: [
const OccupancyHeatMapDays(cellSize: _cellSize),
CustomPaint(
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
child: CustomPaint(
isComplex: true,
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
painter: OccupancyPainter(
items: paintItems,
maxValue: _maxValue,
),
SizedBox(
width: _totalWeeks * _cellSize,
height: 7 * _cellSize,
child: InteractiveHeatMap(
items: paintItems,
maxValue: _maxValue,
cellSize: _cellSize,
),
),
],

View File

@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_he
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyHeatMapBox extends StatelessWidget {
@ -14,6 +15,7 @@ class OccupancyHeatMapBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
return BlocBuilder<OccupancyHeatMapBloc, OccupancyHeatMapState>(
builder: (context, state) {
return Container(
@ -45,7 +47,12 @@ class OccupancyHeatMapBox extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
);
FetchOccupancyDataHelper.loadOccupancyData(context);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
},
datePickerType: DatePickerType.year,
selectedDate: context
@ -61,7 +68,10 @@ class OccupancyHeatMapBox extends StatelessWidget {
Expanded(
child: OccupancyHeatMap(
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(value.date, value.occupancy),
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
),
),
),

View File

@ -4,18 +4,25 @@ import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyPaintItem {
final int index;
final int value;
final DateTime date;
const OccupancyPaintItem({required this.index, required this.value});
const OccupancyPaintItem({
required this.index,
required this.value,
required this.date,
});
}
class OccupancyPainter extends CustomPainter {
OccupancyPainter({
required this.items,
required this.maxValue,
this.hoveredItem,
});
final List<OccupancyPaintItem> items;
final int maxValue;
final OccupancyPaintItem? hoveredItem;
static const double cellSize = 16.0;
@ -25,6 +32,10 @@ class OccupancyPainter extends CustomPainter {
final Paint borderPaint = Paint()
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
..style = PaintingStyle.stroke;
final Paint hoveredBorderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
for (final item in items) {
final column = item.index ~/ 7;
@ -37,22 +48,27 @@ class OccupancyPainter extends CustomPainter {
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
canvas.drawRect(rect, fillPaint);
_drawDashedLine(
canvas,
Offset(x, y),
Offset(x + cellSize, y),
borderPaint,
);
_drawDashedLine(
canvas,
Offset(x, y + cellSize),
Offset(x + cellSize, y + cellSize),
borderPaint,
);
// Highlight the hovered item
if (hoveredItem != null && hoveredItem!.index == item.index) {
canvas.drawRect(rect, hoveredBorderPaint);
} else {
_drawDashedLine(
canvas,
Offset(x, y),
Offset(x + cellSize, y),
borderPaint,
);
_drawDashedLine(
canvas,
Offset(x, y + cellSize),
Offset(x + cellSize, y + cellSize),
borderPaint,
);
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
canvas.drawLine(
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
canvas.drawLine(Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize),
borderPaint);
}
}
}
@ -80,5 +96,6 @@ class OccupancyPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
bool shouldRepaint(covariant OccupancyPainter oldDelegate) =>
oldDelegate.hoveredItem != hoveredItem;
}

View File

@ -0,0 +1,22 @@
enum AnalyticsDeviceRequestType { energyManagement, occupancy }
class GetAnalyticsDevicesParam {
final String? spaceUuid;
final List<String> deviceTypes;
final String? communityUuid;
final AnalyticsDeviceRequestType requestType;
const GetAnalyticsDevicesParam({
required this.requestType,
required this.spaceUuid,
required this.deviceTypes,
required this.communityUuid,
});
Map<String, dynamic> toJson() {
return <String, dynamic>{
if (spaceUuid != null) 'spaceUuid': spaceUuid,
if (communityUuid != null) 'communityUuid': communityUuid,
};
}
}

View File

@ -1,24 +1,20 @@
import 'package:equatable/equatable.dart';
class GetEnergyConsumptionByPhasesParam extends Equatable {
final DateTime? startDate;
final DateTime? endDate;
final String? spaceId;
final String powerClampUuid;
final DateTime? date;
const GetEnergyConsumptionByPhasesParam({
this.startDate,
this.endDate,
this.spaceId,
required this.powerClampUuid,
this.date,
});
Map<String, dynamic> toJson() {
return {
'startDate': startDate?.toIso8601String(),
'endDate': endDate?.toIso8601String(),
'spaceId': spaceId,
'monthDate': '${date?.year}-${date?.month.toString().padLeft(2, '0')}',
};
}
@override
List<Object?> get props => [startDate, endDate, spaceId];
List<Object?> get props => [powerClampUuid, date];
}

View File

@ -1,3 +1,19 @@
class GetEnergyConsumptionPerDeviceParam {
const GetEnergyConsumptionPerDeviceParam();
const GetEnergyConsumptionPerDeviceParam({
this.monthDate,
this.spaceId,
this.communityId,
});
final DateTime? monthDate;
final String? spaceId;
final String? communityId;
Map<String, dynamic> toJson() => {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': true,
};
}

View File

@ -1,19 +1,13 @@
class GetOccupancyHeatMapParam {
final DateTime year;
final String communityId;
final String spaceId;
final String spaceUuid;
const GetOccupancyHeatMapParam({
required this.year,
required this.communityId,
required this.spaceId,
required this.spaceUuid,
});
Map<String, dynamic> toJson() {
return {
'year': year.toIso8601String(),
'communityId': communityId,
'spaceId': spaceId,
};
return {'year': year.year};
}
}

View File

@ -13,7 +13,7 @@ class GetTotalEnergyConsumptionParam {
return {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (communityId == null || communityId!.isEmpty) 'spaceUuid': spaceId,
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': false,
};

View File

@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
abstract interface class AnalyticsDevicesService {
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param);
}

View File

@ -0,0 +1,24 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
class AnalyticsDevicesServiceDelegate implements AnalyticsDevicesService {
const AnalyticsDevicesServiceDelegate(
this._occupancyService,
this._energyManagementService,
);
final AnalyticsDevicesService _occupancyService;
final AnalyticsDevicesService _energyManagementService;
@override
Future<List<AnalyticsDevice>> getDevices(
GetAnalyticsDevicesParam param,
) {
return switch (param.requestType) {
AnalyticsDeviceRequestType.occupancy => _occupancyService.getDevices(param),
AnalyticsDeviceRequestType.energyManagement =>
_energyManagementService.getDevices(param),
};
}
}

View File

@ -0,0 +1,36 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteEnergyManagementAnalyticsDevicesService
implements AnalyticsDevicesService {
const RemoteEnergyManagementAnalyticsDevicesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try {
final response = await _httpService.get(
path: '/devices-space-community/recursive-child',
queryParameters: param.toJson()
..addAll({'productType': param.deviceTypes.first}),
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
}

View File

@ -0,0 +1,62 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService {
const RemoteOccupancyAnalyticsDevicesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try {
final requests = await Future.wait<List<AnalyticsDevice>>(
param.deviceTypes.map((e) {
final mappedParam = GetAnalyticsDevicesParam(
requestType: AnalyticsDeviceRequestType.occupancy,
spaceUuid: param.spaceUuid,
deviceTypes: [e],
communityUuid: param.communityUuid,
);
return _makeRequest(mappedParam);
}).toList(),
);
final result = requests.map((e) => e.first).toList();
return result;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
Future<List<AnalyticsDevice>> _makeRequest(GetAnalyticsDevicesParam param) async {
try {
final projectUuid = await ProjectManager.getProjectUUID();
final response = await _httpService.get(
path:
'/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}/devices',
queryParameters: {
'requestType': param.requestType.name,
'communityUuid': param.communityUuid,
'spaceUuid': param.spaceUuid,
'productType': param.deviceTypes.first,
},
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
rethrow;
}
}
}

View File

@ -1,29 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart';
class FakeEnergyConsumptionByPhasesService
implements EnergyConsumptionByPhasesService {
@override
Future<List<PhasesEnergyConsumption>> load(
GetEnergyConsumptionByPhasesParam param,
) {
return Future.delayed(
const Duration(milliseconds: 500),
() => const [
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 100, phaseC: 100),
],
);
}
}

View File

@ -15,8 +15,9 @@ final class RemoteEnergyConsumptionByPhasesService
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
path: '/power-clamp/${param.powerClampUuid}/historical',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
@ -28,7 +29,7 @@ final class RemoteEnergyConsumptionByPhasesService
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per device: $e');
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -15,16 +17,10 @@ class RemoteEnergyConsumptionPerDeviceService
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
path: '/power-clamp/historical',
showServerMessage: true,
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return DeviceEnergyDataModel.fromJson(jsonData);
}).toList();
},
queryParameters: param.toJson(),
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
);
return response;
} catch (e) {
@ -32,3 +28,33 @@ class RemoteEnergyConsumptionPerDeviceService
}
}
}
abstract final class _EnergyConsumptionPerDeviceMapper {
const _EnergyConsumptionPerDeviceMapper._();
static List<DeviceEnergyDataModel> map(dynamic data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.where((e) {
final deviceData = (e as Map<String, dynamic>)['data'] as List<dynamic>? ?? [];
return deviceData.isNotEmpty;
}).map((e) {
final deviceData = e as Map<String, dynamic>;
final energyData = deviceData['data'] as List<dynamic>;
return DeviceEnergyDataModel(
deviceId: deviceData['deviceUuid'] as String,
deviceName: deviceData['deviceName'] as String,
color: Color((DateTime.now().microsecondsSinceEpoch +
deviceData['deviceUuid'].hashCode) |
0xFF000000),
energy: energyData.map((data) {
final energyJson = data as Map<String, dynamic>;
return EnergyDataModel(
date: DateTime.parse(energyJson['date'] as String),
value: double.parse(energyJson['total_energy_consumed_kw'] as String),
);
}).toList(),
);
}).toList();
}
}

View File

@ -1,25 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
class FakeOccupancyHeatMapService implements OccupancyHeatMapService {
@override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) {
return Future.delayed(const Duration(milliseconds: 200), () {
final now = DateTime.now();
final startOfYear = DateTime(now.year, 1, 1);
final endOfYear = DateTime(now.year, 12, 31);
final daysInYear = endOfYear.difference(startOfYear).inDays + 1;
final List<OccupancyHeatMapModel> data = List.generate(
daysInYear,
(index) => OccupancyHeatMapModel(
date: startOfYear.add(Duration(days: index)),
occupancy: ((index + 1) * 10) % 100,
),
);
return data;
});
}
}

View File

@ -0,0 +1,35 @@
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
const RemoteOccupancyHeatMapService(this._httpService);
final HTTPService _httpService;
@override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
try {
final response = await _httpService.get(
path: '/occupancy/heat-map/space/${param.spaceUuid}',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => OccupancyHeatMapModel.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption:');
}
}
}

View File

@ -12,7 +12,7 @@ class AnalyticsErrorWidget extends StatelessWidget {
return Visibility(
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
child: Text(
'$errorMessage ?? "Something went wrong"',
errorMessage ?? 'Something went wrong',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(

View File

@ -12,6 +12,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/
import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switch/three_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gateway.dart';
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart';
import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -358,7 +359,10 @@ SOS
case 'NCPS':
return [
FlushPresenceDelayFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF',),
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF',
),
FlushIlluminanceFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
@ -378,6 +382,17 @@ SOS
FlushTriggerLevelFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
];
case 'WH':
return [
WHRestartStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
WHSwitchFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
TimerConfirmTimeFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
BacklightFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
];
default:
return [];

View File

@ -536,7 +536,7 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// 'entityId': 'tab_to_run',
// 'uniqueCustomId': const Uuid().v4(),
// 'deviceId': 'tab_to_run',
// 'title': 'Tab to run',
// 'title': 'Tap to run',
// 'productType': 'tab_to_run',
// 'imagePath': Assets.tabToRun,
// }
@ -771,7 +771,7 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
'entityId': 'tab_to_run',
'uniqueCustomId': const Uuid().v4(),
'deviceId': 'tab_to_run',
'title': 'Tab to run',
'title': 'Tap to run',
'productType': 'tab_to_run',
'imagePath': Assets.tabToRun,
}

View File

@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_swit
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_presence_sensor.dart';
class DeviceDialogHelper {
static Future<Map<String, dynamic>?> showDeviceDialog({
@ -126,6 +127,15 @@ class DeviceDialogHelper {
dialogType: dialogType,
device: data['device'],
);
case 'WH':
return WaterHeaterDialogRoutines.showWHFunctionsDialog(
context: context,
functions: functions,
uniqueCustomId: data['uniqueCustomId'],
deviceSelectedFunctions: deviceSelectedFunctions,
dialogType: dialogType,
device: data['device'],
);
default:
return null;

View File

@ -162,7 +162,7 @@ class SaveRoutineHelper {
width: 24,
height: 24,
),
title: const Text('Tab to run'),
title: const Text('Tap to run'),
),
if (state.isAutomation)
...state.ifItems.map((item) {

View File

@ -0,0 +1,134 @@
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class WaterHeaterFunctions
extends DeviceFunction<WaterHeaterStatusModel> {
final String type;
WaterHeaterFunctions({
required super.deviceId,
required super.deviceName,
required super.code,
required super.operationName,
required super.icon,
required this.type,
});
List<WaterHeaterOperationalValue> getOperationalValues();
}
class WHRestartStatusFunction extends WaterHeaterFunctions {
final int min;
WHRestartStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
super(
code: 'relay_status',
operationName: 'Restart Status',
icon: Assets.refreshStatusIcon,
);
@override
List<WaterHeaterOperationalValue> getOperationalValues() {
return [
WaterHeaterOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: 'Power OFF',
value: "off",
),
WaterHeaterOperationalValue(
icon: Assets.assetsAcPower,
description: 'Power ON',
value: 'on',
),
WaterHeaterOperationalValue(
icon: Assets.refreshStatusIcon,
description: "Restart Memory",
value: 'memory',
),
];
}
}
class WHSwitchFunction extends WaterHeaterFunctions {
final int min;
WHSwitchFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
super(
code: 'switch_1',
operationName: 'Switch',
icon: Assets.assetsAcPower,
);
@override
List<WaterHeaterOperationalValue> getOperationalValues() {
return [
WaterHeaterOperationalValue(
icon: Assets.assetsAcPower,
description: 'ON',
value: true,
),
WaterHeaterOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: 'OFF',
value: false,
),
];
}
}
class TimerConfirmTimeFunction extends WaterHeaterFunctions {
TimerConfirmTimeFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'countdown_1',
operationName: 'Timer',
icon: Assets.targetConfirmTimeIcon,
);
@override
List<WaterHeaterOperationalValue> getOperationalValues() {
final values = <WaterHeaterOperationalValue>[];
return values;
}
}
class BacklightFunction extends WaterHeaterFunctions {
final int min;
BacklightFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
super(
code: 'switch_backlight',
operationName: 'Backlight',
icon: Assets.indicator,
);
@override
List<WaterHeaterOperationalValue> getOperationalValues() {
return [
WaterHeaterOperationalValue(
icon: Assets.assetsAcPower,
description: 'ON',
value: true,
),
WaterHeaterOperationalValue(
icon: Assets.assetsAcPowerOFF,
description: 'OFF',
value: false,
),
];
}
}

View File

@ -0,0 +1,11 @@
class WaterHeaterOperationalValue {
final String icon;
final String description;
final dynamic value;
WaterHeaterOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}

View File

@ -29,11 +29,11 @@ class ConditionsRoutinesDevicesView extends StatelessWidget {
children: [
DraggableCard(
imagePath: Assets.tabToRun,
title: 'Tab to run',
title: 'Tap to run',
deviceData: {
'deviceId': 'tab_to_run',
'type': 'trigger',
'name': 'Tab to run',
'name': 'Tap to run',
},
),
DraggableCard(

View File

@ -43,7 +43,7 @@ class IfContainer extends StatelessWidget {
children: [
DraggableCard(
imagePath: Assets.tabToRun,
title: 'Tab to run',
title: 'Tap to run',
deviceData: {},
),
],
@ -76,7 +76,8 @@ class IfContainer extends StatelessWidget {
'WPS',
'GW',
'CPS',
'NCPS'
'NCPS',
'WH',
].contains(state.ifItems[index]
['productType'])) {
@ -136,7 +137,7 @@ class IfContainer extends StatelessWidget {
context
.read<RoutineBloc>()
.add(AddToIfContainer(mutableData, false));
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', 'NCPS']
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', 'NCPS','WH']
.contains(mutableData['productType'])) {
context
.read<RoutineBloc>()

View File

@ -26,7 +26,7 @@ class FetchRoutineScenesAutomation extends StatelessWidget
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildListTitle(context, "Scenes (Tab to Run)"),
_buildListTitle(context, "Scenes (Tap to Run)"),
const SizedBox(height: 10),
Visibility(
visible: state.scenes.isNotEmpty,

View File

@ -25,7 +25,8 @@ class _RoutineDevicesState extends State<RoutineDevices> {
'WPS',
'GW',
'CPS',
'NCPS'
'NCPS',
'WH',
};
@override

View File

@ -1,11 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/flush/flush_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/time_wheel.dart';
class FlushOperationalValuesList extends StatelessWidget {
final List<FlushOperationalValue> values;

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_operational_value.dart';
class WaterHeaterOperationalValuesList extends StatelessWidget {
final List<WaterHeaterOperationalValue> values;
final dynamic selectedValue;
final AllDevicesModel? device;
final String operationName;
final String selectCode;
final ValueChanged<WaterHeaterOperationalValue> onSelect;
const WaterHeaterOperationalValuesList({
required this.values,
required this.selectedValue,
required this.device,
required this.operationName,
required this.selectCode,
required this.onSelect,
super.key,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: values.length,
itemBuilder: (context, index) => _buildValueItem(context, values[index]),
);
}
Widget _buildValueItem(
BuildContext context, WaterHeaterOperationalValue value) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
value.icon,
width: 24,
height: 24,
),
Expanded(child: _buildValueDescription(value)),
_buildValueRadio(context, value),
],
),
);
}
Widget _buildValueDescription(WaterHeaterOperationalValue value) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(value.description),
);
}
Widget _buildValueRadio(context, WaterHeaterOperationalValue value) {
return Radio<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (_) => onSelect(value));
}
}

View File

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_value_selector_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class WaterHeaterDialogRoutines extends StatefulWidget {
final List<DeviceFunction> functions;
final AllDevicesModel? device;
final List<DeviceFunctionData>? deviceSelectedFunctions;
final String? uniqueCustomId;
final String dialogType;
const WaterHeaterDialogRoutines({
super.key,
required this.functions,
this.device,
this.deviceSelectedFunctions,
this.uniqueCustomId,
required this.dialogType,
});
static Future<Map<String, dynamic>?> showWHFunctionsDialog({
required BuildContext context,
required List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String? uniqueCustomId,
required String dialogType,
}) async {
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) => WaterHeaterDialogRoutines(
functions: functions,
device: device,
deviceSelectedFunctions: deviceSelectedFunctions,
uniqueCustomId: uniqueCustomId,
dialogType: dialogType,
),
);
}
@override
State<WaterHeaterDialogRoutines> createState() =>
_WaterHeaterDialogRoutinesState();
}
class _WaterHeaterDialogRoutinesState extends State<WaterHeaterDialogRoutines> {
late final List<WaterHeaterFunctions> _waterHeaterFunctions;
@override
void initState() {
super.initState();
_waterHeaterFunctions =
widget.functions.whereType<WaterHeaterFunctions>().where((function) {
if (widget.dialogType == 'THEN') {
return function.type == 'THEN' || function.type == 'BOTH';
}
return function.type == 'IF' || function.type == 'BOTH';
}).toList();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()
..add(InitializeFunctions(widget.deviceSelectedFunctions ?? [])),
child: _buildDialogContent(),
);
}
Widget _buildDialogContent() {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('Water Heater Condition'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],
),
);
},
),
);
}
Widget _buildMainContent(BuildContext context, FunctionBlocState state) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildFunctionList(context),
if (state.selectedFunction != null) _buildValueSelector(context, state),
],
);
}
Widget _buildFunctionList(BuildContext context) {
return SizedBox(
width: 360,
child: ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _waterHeaterFunctions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(color: ColorsManager.dividerColor),
),
itemBuilder: (context, index) {
final function = _waterHeaterFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
placeholderBuilder: (context) => const SizedBox(
width: 24,
height: 24,
),
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () => context.read<FunctionBloc>().add(
SelectFunction(
functionCode: function.code,
operationName: function.operationName,
),
),
);
},
),
);
}
Widget _buildValueSelector(BuildContext context, FunctionBlocState state) {
final selectedFunction = state.selectedFunction ?? '';
final functionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction,
operationName: state.selectedOperationName ?? '',
value: null,
),
);
return Expanded(
child: WaterHeaterValueSelectorWidget(
selectedFunction: selectedFunction,
functionData: functionData,
whFunctions: _waterHeaterFunctions,
device: widget.device,
),
);
}
Widget _buildDialogFooter(BuildContext context, FunctionBlocState state) {
return DialogFooter(
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
widget.uniqueCustomId!,
),
);
Navigator.pop(
context,
{'deviceId': widget.functions.first.deviceId},
);
}
: null,
isConfirmEnabled: state.selectedFunction != null,
);
}
}

View File

@ -0,0 +1,196 @@
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/helper/duration_format_helper.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart';
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_operational_values_list.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class WaterHeaterValueSelectorWidget extends StatelessWidget {
final String selectedFunction;
final DeviceFunctionData functionData;
final List<WaterHeaterFunctions> whFunctions;
final AllDevicesModel? device;
const WaterHeaterValueSelectorWidget({
required this.selectedFunction,
required this.functionData,
required this.whFunctions,
required this.device,
super.key,
});
@override
Widget build(BuildContext context) {
final selectedFn = whFunctions.firstWhere(
(f) => f.code == selectedFunction,
orElse: () => WHSwitchFunction(
deviceId: '',
deviceName: '',
type: '',
),
);
if (selectedFunction == 'countdown_1') {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildConditionToggle(
context,
functionData.condition,
selectedFunction,
device,
selectedFn.operationName,
functionData,
),
_buildCountDownDisplay(
context,
functionData.value,
device,
selectedFn.operationName,
functionData,
selectedFunction,
),
_buildCountDownSlider(
context,
functionData.value,
device,
selectedFn.operationName,
functionData,
selectedFunction,
),
const SizedBox(height: 10),
],
);
}
return WaterHeaterOperationalValuesList(
values: selectedFn.getOperationalValues(),
selectedValue: functionData.value,
device: device,
operationName: selectedFn.operationName,
selectCode: selectedFunction,
onSelect: (selectedValue) async {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: selectedValue.value,
condition: functionData.condition,
),
),
);
},
);
}
static Widget _buildCountDownDisplay(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0),
style: context.textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
),
),
);
}
static Widget _buildCountDownSlider(
BuildContext context,
dynamic initialValue,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
String selectCode,
) {
const twelveHoursInSeconds = 43200.0;
final operationalValues = SwitchOperationalValue(
icon: '',
description: "sec",
value: 0.0,
minValue: 0,
maxValue: twelveHoursInSeconds,
stepValue: 1,
);
return Slider(
value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) -
(operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1))
.round(),
onChanged: (value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
);
}
static Widget _buildConditionToggle(
BuildContext context,
String? currentCondition,
String selectCode,
AllDevicesModel? device,
String operationName,
DeviceFunctionData? selectedFunctionData,
) {
final conditions = ["<", "==", ">"];
return ToggleButtons(
onPressed: (int index) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
condition: conditions[index],
value: selectedFunctionData?.value ?? 0,
valueDescription: selectedFunctionData?.valueDescription,
),
),
);
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
),
isSelected:
conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(),
);
}
}

View File

@ -116,7 +116,8 @@ class ThenContainer extends StatelessWidget {
'WPS',
'CPS',
"GW",
"NCPS"
"NCPS",
'WH',
].contains(state.thenItems[index]
['productType'])) {
context.read<RoutineBloc>().add(
@ -232,8 +233,17 @@ class ThenContainer extends StatelessWidget {
dialogType: "THEN");
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', "NCPS"]
.contains(mutableData['productType'])) {
} else if (![
'AC',
'1G',
'2G',
'3G',
'WPS',
'GW',
'CPS',
"NCPS",
"WH"
].contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
}
},

View File

@ -481,4 +481,5 @@ class Assets {
static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg';
static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg';
static const String blankCalendar = 'assets/icons/blank_calendar.svg';
static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg';
}