Compare commits

..

31 Commits

Author SHA1 Message Date
1567f10827 Revert "enhanced ci/cd by not running the deply jobs on the PR itself… (#237)
…, and now we only deploy when we merged a PR to `dev` or `main`, and
created a separate GitHub action that only builds and install
dependencies, which only runs on the PR itself."

This reverts commit f19120c754.

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

<!--- Describe your changes in detail -->

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [x] 🗑️ Chore
2025-06-11 09:53:19 +03:00
cdbd90b54c Revert "enhanced ci/cd by not running the deply jobs on the PR itself, and now we only deploy when we merged a PR to dev or main, and created a separate GitHub action that only builds and install dependencies, which only runs on the PR itself."
This reverts commit f19120c754.
2025-06-11 09:52:08 +03:00
03f5c869c6 chore/add-dependabot (#232)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

Added `dependabot` configuration, which notifies us of any updated
dependencies, and if there is security aspects that we'd need to take
care of.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] 🗑️ Chore
2025-06-04 16:41:42 +03:00
4f98891902 Created dependabot.yaml 2025-06-04 13:13:07 +03:00
7002bbfa04 CI/CD Enhancements (#230)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->
## Description

enhanced CI/CD by not running the deploy jobs on the PR itself, and now
we only deploy when we merged a PR to `dev` or `main`, and created a
separate GitHub action that only builds and install dependencies, which
only runs on the PR itself.


## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [x] 🗑️ Chore
2025-06-04 10:41:26 +03:00
f19120c754 enhanced ci/cd by not running the deply jobs on the PR itself, and now we only deploy when we merged a PR to dev or main, and created a separate GitHub action that only builds and install dependencies, which only runs on the PR itself. 2025-06-04 10:12:19 +03:00
6b3eca23af Update pull_request_template.md 2025-05-28 16:46:24 +03:00
4f4f11c330 Merge branch 'main' of https://github.com/SyncrowIOT/web 2025-05-28 14:26:36 +03:00
8a25fa798c Created pull_request_template.md. 2025-05-28 14:26:33 +03:00
6612e91430 Merge pull request #177 from SyncrowIOT/merge_sprint_19_bugfixes
merged DEV into staging.
2025-05-08 14:32:54 +03:00
56c613fb0c Disabled Syncrow Analytics feature for release purposes. 2025-05-08 14:32:08 +03:00
8d2d9dd0bb Merge branch 'dev' of https://github.com/SyncrowIOT/web 2025-04-29 10:39:54 +03:00
cfc68f1568 Merge pull request #116 from SyncrowIOT/dev
fix real time listenToChanges
2025-03-12 21:26:45 +03:00
02e08ad92f Merge pull request #115 from SyncrowIOT/dev
Dev
2025-03-12 14:21:13 +03:00
d7899a24f5 Merged with dev 2025-02-20 13:03:38 +03:00
800c0ba47f Merge pull request #101 from SyncrowIOT/bugfix/fix-endpoint
Main
2025-02-20 13:37:28 +04:00
fe4e775902 fixed endpoint 2025-02-20 13:35:59 +04:00
5247856cb4 Merge pull request #99 from SyncrowIOT:bugfix/add-tag-border
added back border of tag list
2025-02-20 11:50:07 +04:00
4a8b8a32ba added back border of tag list 2025-02-20 11:49:35 +04:00
2abce77eb5 Merge pull request #97 from SyncrowIOT/feat/fix-cursor-issue-in-main
Fixed cursor issue
2025-02-20 11:35:35 +04:00
7efd1c3c87 Fixed cursor issue 2025-02-20 11:34:24 +04:00
7a0d9aefb7 Merge pull request #95 from SyncrowIOT/bugfix/change-endpoint-prod
fix endpoints
2025-02-19 18:01:11 +04:00
21cc25cfc4 fix endpoints 2025-02-19 17:58:53 +04:00
e2ec4bbf31 Pulled main changes 2025-02-18 16:27:09 +03:00
51b46ae197 Merged with dev 2025-02-18 16:25:33 +03:00
36ee22603a fixes CommunityId and spaceUuid 2025-02-10 12:44:35 +03:00
b0abd42b0c projectId 2025-02-06 11:28:40 +03:00
ba4da78846 Merge branch 'dev' 2025-02-06 11:20:34 +03:00
dc20d69f20 Merge pull request #88 from SyncrowIOT/dev
Dev
2025-02-06 01:03:32 +03:00
cf6ec231dc Merged with dev 2025-02-06 00:57:29 +03:00
d0530f7fc3 Added staging space and community IDs 2024-12-04 09:57:29 +03:00
122 changed files with 2170 additions and 4833 deletions

10
.github/.github/dependabot.yaml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"

26
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,26 @@
<!--
Thanks for contributing!
Provide a description of your changes below and a general summary in the title
Please look at the following checklist to ensure that your PR can be accepted quickly:
-->
## Jira Ticket
[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
## Description
<!--- Describe your changes in detail -->
## Type of Change
<!--- Put an `x` in all the boxes that apply: -->
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

1
.gitignore vendored
View File

@ -30,7 +30,6 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
pubspec.lock
# Symbolication related
app.*.symbols

View File

@ -10,7 +10,6 @@
analyzer:
errors:
constant_identifier_names: ignore
overridden_fields: ignore
include: package:flutter_lints/flutter.yaml
linter:

View File

@ -1,12 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,56 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class DashedBorderPainter extends CustomPainter {
final double dashWidth;
final double dashSpace;
final Color color;
DashedBorderPainter({
this.dashWidth = 4.0,
this.dashSpace = 2.0,
this.color = Colors.black,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = 0.5
..style = PaintingStyle.stroke;
final Path topPath = Path()
..moveTo(0, 0)
..lineTo(size.width, 0);
final Path bottomPath = Path()
..moveTo(0, size.height)
..lineTo(size.width, size.height);
final dashedTopPath = _createDashedPath(topPath, dashWidth, dashSpace);
final dashedBottomPath = _createDashedPath(bottomPath, dashWidth, dashSpace);
canvas.drawPath(dashedTopPath, paint);
canvas.drawPath(dashedBottomPath, paint);
}
Path _createDashedPath(Path source, double dashWidth, double dashSpace) {
final Path dashedPath = Path();
for (PathMetric pathMetric in source.computeMetrics()) {
double distance = 0.0;
while (distance < pathMetric.length) {
final double nextDistance = distance + dashWidth;
dashedPath.addPath(
pathMetric.extractPath(distance, nextDistance),
Offset.zero,
);
distance = nextDistance + dashSpace;
}
}
return dashedPath;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -1,71 +0,0 @@
class AnalyticsDevice {
const AnalyticsDevice({
required this.uuid,
required this.name,
this.createdAt,
this.updatedAt,
this.deviceTuyaUuid,
this.isActive,
this.productDevice,
this.spaceUuid,
});
final String uuid;
final String name;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? deviceTuyaUuid;
final bool? isActive;
final ProductDevice? productDevice;
final String? spaceUuid;
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
return AnalyticsDevice(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
deviceTuyaUuid: json['deviceTuyaUuid'] as String?,
isActive: json['isActive'] as bool?,
productDevice: json['productDevice'] != null
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: (json['spaces'] as List<dynamic>?)
?.map((e) => e['uuid'])
.firstOrNull
?.toString(),
);
}
}
class ProductDevice {
const ProductDevice({
this.uuid,
this.createdAt,
this.updatedAt,
this.catName,
this.prodId,
this.name,
this.prodType,
});
final String? uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? catName;
final String? prodId;
final String? name;
final String? prodType;
factory ProductDevice.fromJson(Map<String, dynamic> json) {
return ProductDevice(
uuid: json['uuid'] as String?,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
catName: json['catName'] as String?,
prodId: json['prodId'] as String?,
name: json['name'] as String?,
prodType: json['prodType'] as String?,
);
}
}

View File

@ -1,32 +0,0 @@
import 'package:equatable/equatable.dart';
class Occupacy extends Equatable {
final DateTime date;
final String occupancy;
final String spaceUuid;
final int occupiedSeconds;
const Occupacy({
required this.date,
required this.occupancy,
required this.spaceUuid,
required this.occupiedSeconds,
});
factory Occupacy.fromJson(Map<String, dynamic> json) {
return Occupacy(
date: DateTime.parse(json['event_date'] as String? ?? '${DateTime.now()}'),
occupancy: (json['occupancy_percentage'] ?? 0).toString(),
spaceUuid: json['space_uuid'] as String? ?? '',
occupiedSeconds: json['occupied_seconds'] as int? ?? 0,
);
}
@override
List<Object?> get props => [
date,
occupancy,
spaceUuid,
occupiedSeconds,
];
}

View File

@ -1,28 +0,0 @@
import 'package:equatable/equatable.dart';
class OccupancyHeatMapModel extends Equatable {
final String uuid;
final DateTime eventDate;
final int countTotalPresenceDetected;
const OccupancyHeatMapModel({
required this.uuid,
required this.eventDate,
required this.countTotalPresenceDetected,
});
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
return OccupancyHeatMapModel(
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 => [uuid, eventDate, countTotalPresenceDetected];
}

View File

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

@ -2,23 +2,16 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'analytics_date_picker_event.dart';
part 'analytics_date_picker_state.dart';
class AnalyticsDatePickerBloc
extends Bloc<AnalyticsDatePickerEvent, AnalyticsDatePickerState> {
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
class AnalyticsDatePickerBloc extends Bloc<AnalyticsDatePickerEvent, DateTime> {
AnalyticsDatePickerBloc() : super(DateTime.now()) {
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
}
void _onUpdateAnalyticsDatePickerEvent(
UpdateAnalyticsDatePickerEvent event,
Emitter<AnalyticsDatePickerState> emit,
Emitter<DateTime> emit,
) {
emit(
state.copyWith(
monthlyDate: event.montlyDate ?? state.monthlyDate,
yearlyDate: event.yearlyDate ?? state.yearlyDate,
),
);
emit(event.date);
}
}

View File

@ -8,11 +8,10 @@ sealed class AnalyticsDatePickerEvent extends Equatable {
}
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
const UpdateAnalyticsDatePickerEvent(this.date);
final DateTime? montlyDate;
final DateTime? yearlyDate;
final DateTime date;
@override
List<Object?> get props => [montlyDate, yearlyDate];
List<Object?> get props => [date];
}

View File

@ -1,25 +0,0 @@
part of 'analytics_date_picker_bloc.dart';
final class AnalyticsDatePickerState extends Equatable {
AnalyticsDatePickerState({
DateTime? monthlyDate,
DateTime? yearlyDate,
}) : monthlyDate = monthlyDate ?? DateTime.now(),
yearlyDate = yearlyDate ?? DateTime.now();
final DateTime monthlyDate;
final DateTime yearlyDate;
AnalyticsDatePickerState copyWith({
DateTime? monthlyDate,
DateTime? yearlyDate,
}) {
return AnalyticsDatePickerState(
monthlyDate: monthlyDate ?? this.monthlyDate,
yearlyDate: yearlyDate ?? this.yearlyDate,
);
}
@override
List<Object?> get props => [monthlyDate, yearlyDate];
}

View File

@ -1,69 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
abstract class AnalyticsDataLoadingStrategy {
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
);
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
);
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
);
void clearData(BuildContext context);
}

View File

@ -1,14 +0,0 @@
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart';
abstract final class AnalyticsDataLoadingStrategyFactory {
const AnalyticsDataLoadingStrategyFactory._();
static AnalyticsDataLoadingStrategy getStrategy(AnalyticsPageTab tab) {
return switch (tab) {
AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(),
AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(),
};
}
}

View File

@ -1,80 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.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/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
@override
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
) {
// Add to space tree bloc first
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces,
),
);
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
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
) {
context.read<SpaceTreeBloc>().add(
OnSpaceSelected(
community,
space.uuid ?? '',
space.children,
),
);
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
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
// Do nothing else as per original implementation
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
FetchEnergyManagementDataHelper.clearAllData(context);
}
}

View File

@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
@override
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
) {
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces.isNotEmpty ? [spaces.first] : [],
),
);
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
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
) {
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
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
// Do nothing
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
FetchOccupancyDataHelper.clearAllData(context);
}
}

View File

@ -1,7 +1,5 @@
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';
@ -10,39 +8,19 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener
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/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/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/remote_occupancy_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/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/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';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class AnalyticsPage extends StatefulWidget {
class AnalyticsPage extends StatelessWidget {
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(
@ -52,22 +30,22 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
),
BlocProvider(
create: (context) => TotalEnergyConsumptionBloc(
RemoteTotalEnergyConsumptionService(_httpService),
FakeTotalEnergyConsumptionService(),
),
),
BlocProvider(
create: (context) => EnergyConsumptionByPhasesBloc(
RemoteEnergyConsumptionByPhasesService(_httpService),
FakeEnergyConsumptionByPhasesService(),
),
),
BlocProvider(
create: (context) => EnergyConsumptionPerDeviceBloc(
RemoteEnergyConsumptionPerDeviceService(_httpService),
FakeEnergyConsumptionPerDeviceService(),
),
),
BlocProvider(
create: (context) => PowerClampInfoBloc(
RemotePowerClampInfoService(_httpService),
RemotePowerClampInfoService(HTTPService()),
),
),
BlocProvider<RealtimeDeviceChangesBloc>(
@ -75,25 +53,6 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FirebaseRealtimeDeviceService(),
),
),
BlocProvider(
create: (context) => OccupancyBloc(
RemoteOccupancyService(_httpService),
),
),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(
RemoteOccupancyHeatMapService(_httpService),
),
),
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
BlocProvider(
create: (context) => AnalyticsDevicesBloc(
AnalyticsDevicesServiceDelegate(
RemoteOccupancyAnalyticsDevicesService(_httpService),
RemoteEnergyManagementAnalyticsDevicesService(_httpService),
),
),
),
],
child: const AnalyticsPageForm(),
);

View File

@ -1,29 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/strategies/analytics_data_loading_strategy_factory.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.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/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
class AnalyticsCommunitiesSidebar extends StatelessWidget {
const AnalyticsCommunitiesSidebar({super.key});
@override
Widget build(BuildContext context) {
final selectedTab = context.watch<AnalyticsTabBloc>().state;
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
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 Builder(
builder: (context) {
return Expanded(
child: SpaceTreeView(
title: const Text('Communities'),
shouldDisableDeselectingChildrenOfSelectedParent: true,
onSelect: () {
/// Necessary to wait for the state to update before fethcing the data.
Future.delayed(
const Duration(milliseconds: 100),
() {
if (context.mounted) {
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
context,
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
);
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
final (selectedCommunities, selectedSpaces) =
FetchEnergyManagementDataHelper
.getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
} else {
FetchEnergyManagementDataHelper.loadPowerClampInfo(
context,
);
}
}
},
);
},
isSide: false,
),
);
},
);
}
}

View File

@ -1,24 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.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/widgets/month_picker_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/year_picker_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
enum DatePickerType { month, year }
class AnalyticsDateFilterButton extends StatefulWidget {
const AnalyticsDateFilterButton({
required this.selectedDate,
required this.onDateSelected,
this.datePickerType = DatePickerType.month,
super.key,
});
final DateTime selectedDate;
final void Function(DateTime)? onDateSelected;
final DatePickerType datePickerType;
const AnalyticsDateFilterButton({super.key});
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@ -28,69 +19,79 @@ class AnalyticsDateFilterButton extends StatefulWidget {
}
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
late final AnalyticsDatePickerBloc _analyticsDatePickerBloc;
@override
void initState() {
_analyticsDatePickerBloc = AnalyticsDatePickerBloc();
super.initState();
}
@override
void dispose() {
_analyticsDatePickerBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: AnalyticsDateFilterButton._color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.greyColor,
width: 1,
return BlocProvider.value(
value: _analyticsDatePickerBloc,
child: Builder(builder: (context) {
final selectedDate = context.watch<AnalyticsDatePickerBloc>().state;
return TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: AnalyticsDateFilterButton._color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.greyColor,
width: 1,
),
),
backgroundColor: ColorsManager.transparentColor,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
),
backgroundColor: ColorsManager.transparentColor,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
icon: SvgPicture.asset(
Assets.blankCalendar,
height: 20,
width: 20,
colorFilter:
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
),
label: Text(
_formatDate(widget.selectedDate),
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
onPressed: () {
showDialog(
context: context,
builder: (_) {
return switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
DatePickerType.year => YearPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
};
icon: SvgPicture.asset(
Assets.blankCalendar,
height: 20,
width: 20,
colorFilter:
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
),
label: Text(
_formatDate(selectedDate),
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
onPressed: () {
showDialog(
context: context,
builder: (_) => MonthPickerWidget(
selectedDate: selectedDate,
onDateSelected: (value) {
_analyticsDatePickerBloc.add(
UpdateAnalyticsDatePickerEvent(value),
);
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
context,
selectedDate: value,
);
},
),
);
},
);
},
}),
);
}
String _formatDate(DateTime? date) {
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
DatePickerType.month => DateFormat('MMMM yyyy'),
DatePickerType.year => DateFormat('yyyy'),
};
final formattedDate = formatterBasedOnDatePickerType.format(
date ?? DateTime.now(),
);
final formatter = DateFormat('MMMM yyyy');
final formattedDate = formatter.format(date ?? DateTime.now());
return formattedDate;
}
}

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/enums/analytics_page_tab.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AnalyticsPageTabButton extends StatelessWidget {
@ -18,12 +17,9 @@ class AnalyticsPageTabButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context);
context.read<AnalyticsTabBloc>().add(
onPressed: () => context.read<AnalyticsTabBloc>().add(
UpdateAnalyticsTabEvent(tab),
);
},
),
child: Text(
tab.title,
textAlign: TextAlign.center,

View File

@ -1,12 +1,9 @@
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_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
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 {
@ -41,7 +38,9 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...AnalyticsPageTab.values.map(
(tab) => _buildAnimation(
(tab) => AnimatedSwitcher(
switchInCurve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
child: AnalyticsPageTabButton(
key: ValueKey(selectedTab),
tab: tab,
@ -54,42 +53,12 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
),
),
const Spacer(),
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),
);
final spaceTreeState =
context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
}
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.monthlyDate,
),
),
const Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(),
),
),
],
@ -98,18 +67,14 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
),
Expanded(
flex: 8,
child: _buildAnimation(child: selectedTab.child),
child: AnimatedSwitcher(
switchInCurve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
child: selectedTab.child,
),
),
],
),
);
}
Widget _buildAnimation({required Widget child}) {
return AnimatedSwitcher(
switchInCurve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
child: child,
);
}
}

View File

@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: ColorsManager.whiteColors,
backgroundColor: Theme.of(context).colorScheme.surface,
child: Container(
padding: const EdgeInsetsDirectional.all(20),
width: 320,
@ -121,7 +121,6 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
}
Row _buildYearSelector() {
final currentYear = DateTime.now().year;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -135,35 +134,17 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
),
const Spacer(),
IconButton(
onPressed: () {
setState(() {
_currentYear = _currentYear - 1;
});
},
onPressed: () => setState(() => _currentYear = _currentYear - 1),
icon: const Icon(
Icons.chevron_left,
color: ColorsManager.grey700,
),
),
IconButton(
onPressed: _currentYear < currentYear
? () {
setState(() {
_currentYear = _currentYear + 1;
// Clear selected month if it becomes invalid in the new year
if (_currentYear == currentYear &&
_selectedMonth != null &&
_selectedMonth! > DateTime.now().month - 1) {
_selectedMonth = null;
}
});
}
: null,
icon: Icon(
onPressed: () => setState(() => _currentYear = _currentYear + 1),
icon: const Icon(
Icons.chevron_right,
color: _currentYear < currentYear
? ColorsManager.grey700
: ColorsManager.grey700.withValues(alpha: 0.3),
color: ColorsManager.grey700,
),
),
],
@ -171,13 +152,11 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
}
Widget _buildMonthsGrid() {
final currentDate = DateTime.now();
final isCurrentYear = _currentYear == currentDate.year;
return GridView.builder(
shrinkWrap: true,
itemCount: 12,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
@ -186,43 +165,25 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
),
itemBuilder: (context, index) {
final isSelected = _selectedMonth == index;
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
return InkWell(
onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = index),
child: DecoratedBox(
onTap: () => setState(() => _selectedMonth = index),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFEDF2F7),
borderRadius: BorderRadius.only(
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
bottomRight:
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
),
color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
child: Text(
_monthNames[index],
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: isFutureMonth
? ColorsManager.grey700.withValues(alpha: 0.1)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
),
child: Text(
_monthNames[index],
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
color: isSelected
? ColorsManager.whiteColors
: isFutureMonth
? ColorsManager.blackColor.withValues(alpha: 0.3)
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
? ColorsManager.whiteColors
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
),

View File

@ -1,188 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/space_tree/view/custom_expansion.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsSpaceTreeView extends StatefulWidget {
const AnalyticsSpaceTreeView({
super.key,
this.onSelectCommunity,
this.onSelectSpace,
this.onSelectChildSpace,
});
final void Function(
CommunityModel community,
List<SpaceModel> spaces,
)? onSelectCommunity;
final void Function(
CommunityModel community,
SpaceModel space,
)? onSelectSpace;
final void Function(
CommunityModel community,
SpaceModel child,
)? onSelectChildSpace;
@override
State<AnalyticsSpaceTreeView> createState() => _AnalyticsSpaceTreeViewState();
}
class _AnalyticsSpaceTreeViewState extends State<AnalyticsSpaceTreeView> {
late final ScrollController _scrollController;
@override
void initState() {
_scrollController = ScrollController();
super.initState();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
return Container(
height: MediaQuery.sizeOf(context).height,
decoration: const BoxDecoration(color: ColorsManager.whiteColors),
child: state is SpaceTreeLoadingState
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.all(24),
child: DefaultTextStyle(
style: context.textTheme.titleMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 20,
),
child: const Text('Communities'),
),
),
CustomSearchBar(
onSearchChanged: (query) => context.read<SpaceTreeBloc>().add(
SearchQueryEvent(query),
),
),
const SizedBox(height: 16),
Expanded(
child: state.isSearching
? const Center(child: CircularProgressIndicator())
: SidebarCommunitiesList(
onScrollToEnd: () {
if (!state.paginationIsLoading) {
context.read<SpaceTreeBloc>().add(
PaginationEvent(
state.paginationModel,
state.communityList,
),
);
}
},
scrollController: _scrollController,
communities: communities,
itemBuilder: (context, index) {
return CustomExpansionTileSpaceTree(
title: communities[index].name,
isSelected: state.selectedCommunities
.contains(communities[index].uuid),
isSoldCheck: state.selectedCommunities
.contains(communities[index].uuid),
onExpansionChanged: () =>
context.read<SpaceTreeBloc>().add(
OnCommunityExpanded(
communities[index].uuid,
),
),
isExpanded: state.expandedCommunities.contains(
communities[index].uuid,
),
onItemSelected: () => widget.onSelectCommunity?.call(
communities[index],
communities[index].spaces,
),
children: communities[index].spaces.map(
(space) {
return CustomExpansionTileSpaceTree(
title: space.name,
isExpanded:
state.expandedSpaces.contains(space.uuid),
onItemSelected: () =>
widget.onSelectSpace?.call(
communities[index],
space,
),
onExpansionChanged: () =>
context.read<SpaceTreeBloc>().add(
OnSpaceExpanded(
communities[index].uuid,
space.uuid ?? '',
),
),
isSelected: state.selectedSpaces
.contains(space.uuid) ||
state.soldCheck.contains(space.uuid),
isSoldCheck:
state.soldCheck.contains(space.uuid),
children: _buildNestedSpaces(
context,
state,
space,
communities[index],
),
);
},
).toList(),
);
},
),
),
if (state.paginationIsLoading) const CircularProgressIndicator(),
],
),
);
});
}
List<Widget> _buildNestedSpaces(
BuildContext context,
SpaceTreeState state,
SpaceModel space,
CommunityModel community,
) {
return space.children.map((child) {
return CustomExpansionTileSpaceTree(
isSelected: state.selectedSpaces.contains(child.uuid) ||
state.soldCheck.contains(child.uuid),
isSoldCheck: state.soldCheck.contains(child.uuid),
title: child.name,
isExpanded: state.expandedSpaces.contains(child.uuid),
onItemSelected: () {
widget.onSelectChildSpace?.call(community, child);
},
onExpansionChanged: () {
context.read<SpaceTreeBloc>().add(
OnSpaceExpanded(community.uuid, child.uuid ?? ''),
);
},
children: _buildNestedSpaces(context, state, child, community),
);
}).toList();
}
}

View File

@ -1,158 +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 YearPickerWidget extends StatefulWidget {
const YearPickerWidget({
super.key,
required this.selectedDate,
required this.onDateSelected,
});
final DateTime selectedDate;
final ValueChanged<DateTime>? onDateSelected;
@override
State<YearPickerWidget> createState() => _YearPickerWidgetState();
}
class _YearPickerWidgetState extends State<YearPickerWidget> {
late int _currentYear;
static final years = List.generate(
DateTime.now().year - (DateTime.now().year - 5) + 1,
(index) => (2020 + index),
).where((year) => year <= DateTime.now().year).toList();
@override
void initState() {
super.initState();
_currentYear = widget.selectedDate.year;
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: ColorsManager.whiteColors,
child: Container(
padding: const EdgeInsetsDirectional.all(20),
width: 320,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildMonthsGrid(),
const SizedBox(height: 20),
Row(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: () => Navigator.pop(context),
style: FilledButton.styleFrom(
fixedSize: const Size(106, 40),
backgroundColor: const Color(0xFFEDF2F7),
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(
'Cancel',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ColorsManager.grey700,
),
),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
final date = DateTime(_currentYear);
widget.onDateSelected?.call(date);
},
style: FilledButton.styleFrom(
fixedSize: const Size(106, 40),
backgroundColor: ColorsManager.vividBlue.withValues(
alpha: 0.7,
),
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(
'Done',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ColorsManager.whiteColors,
),
),
),
],
),
],
),
),
);
}
Widget _buildMonthsGrid() {
return GridView.builder(
shrinkWrap: true,
itemCount: years.length,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
mainAxisSpacing: 8,
mainAxisExtent: 30,
),
itemBuilder: (context, index) {
final isSelected = _currentYear == years[index];
return InkWell(
onTap: () => setState(() => _currentYear = years[index]),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFFEDF2F7),
borderRadius: BorderRadius.only(
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
bottomRight:
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
),
),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
),
child: Text(
years[index].toString(),
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
),
),
);
},
);
}
}

View File

@ -0,0 +1,20 @@
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

@ -21,13 +21,12 @@ abstract final class EnergyManagementChartsHelper {
reservedSize: 32,
showTitles: true,
maxIncluded: true,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
value.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
color: ColorsManager.greyColor,
fontSize: 12,
),
),
@ -37,8 +36,7 @@ abstract final class EnergyManagementChartsHelper {
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: true,
maxIncluded: true,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
@ -93,38 +91,31 @@ abstract final class EnergyManagementChartsHelper {
);
}
static FlGridData gridData() {
return FlGridData(
static FlBorderData borderData() {
return FlBorderData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
horizontalInterval: 250,
getDrawingHorizontalLine: (value) {
return FlLine(
border: const Border.symmetric(
horizontal: BorderSide(
color: ColorsManager.greyColor,
strokeWidth: 1,
dashArray: value == 0 ? null : [5, 5],
);
},
style: BorderStyle.solid,
width: 1,
),
),
);
}
static FlBorderData borderData() {
return FlBorderData(
border: const Border(
bottom: BorderSide(
color: ColorsManager.greyColor,
style: BorderStyle.solid,
),
),
static FlGridData gridData() {
return const FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
);
}
static LineTouchData lineTouchData() {
return LineTouchData(
handleBuiltInTouches: true,
touchSpotThreshold: 16,
touchSpotThreshold: 2,
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
);
}

View File

@ -1,80 +1,52 @@
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 const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
}
static void loadEnergyManagementData(
static void fetchEnergyManagementData(
BuildContext context, {
required String communityId,
required String spaceId,
DateTime? selectedDate,
bool shouldFetchAnalyticsDevices = true,
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
final (selectedCommunities, selectedSpaces) =
getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
clearAllData(context);
return;
}
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
context,
communityUuid: communityId,
spaceUuid: spaceId,
selectedDate: selectedDate0,
);
loadRealtimeDeviceChanges(context);
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,
);
}
loadEnergyConsumptionPerDevice(
context,
communityId: communityId,
spaceId: spaceId,
selectedDate: selectedDate0,
);
loadTotalEnergyConsumption(context);
loadEnergyConsumptionByPhases(context);
loadEnergyConsumptionPerDevice(context);
return;
}
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);
}
static void loadEnergyConsumptionByPhases(
BuildContext context, {
required String powerClampUuid,
DateTime? selectedDate,
}) {
final param = GetEnergyConsumptionByPhasesParam(
date: selectedDate,
powerClampUuid: powerClampUuid,
startDate: selectedDate,
spaceId: '',
);
context.read<EnergyConsumptionByPhasesBloc>().add(
LoadEnergyConsumptionByPhasesEvent(param: param),
@ -84,94 +56,46 @@ 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: spaceId,
communityId: communityId,
monthDate: selectedDate,
spaceId: selectedCommunities.firstOrNull,
startDate: selectedDate,
);
context.read<TotalEnergyConsumptionBloc>().add(
TotalEnergyConsumptionLoadEvent(param: param),
);
}
static void loadEnergyConsumptionPerDevice(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetEnergyConsumptionPerDeviceParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
static void loadEnergyConsumptionPerDevice(BuildContext context) {
const param = GetEnergyConsumptionPerDeviceParam();
context.read<EnergyConsumptionPerDeviceBloc>().add(
LoadEnergyConsumptionPerDeviceEvent(param),
const LoadEnergyConsumptionPerDeviceEvent(param),
);
}
static void loadPowerClampInfo(BuildContext context) {
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 ?? ''),
context.read<PowerClampInfoBloc>().add(
const LoadPowerClampInfoEvent('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 loadRealtimeDeviceChanges(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
);
}
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(),
);
@ -183,6 +107,5 @@ abstract final class FetchEnergyManagementDataHelper {
context.read<EnergyConsumptionByPhasesBloc>().add(
const ClearEnergyConsumptionByPhasesEvent(),
);
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
}
}

View File

@ -1,35 +1,13 @@
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 {
class AnalyticsEnergyManagementView extends StatelessWidget {
const AnalyticsEnergyManagementView({super.key});
@override
State<AnalyticsEnergyManagementView> createState() =>
_AnalyticsEnergyManagementViewState();
}
class _AnalyticsEnergyManagementViewState
extends State<AnalyticsEnergyManagementView> {
@override
void initState() {
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();
}
static const _padding = EdgeInsetsDirectional.all(32);
@override
Widget build(BuildContext context) {
return LayoutBuilder(

View File

@ -1,108 +0,0 @@
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) {
final spaceUuid = state.selectedDevice?.spaceUuid;
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,
selectedItemBuilder: (context) {
return state.devices.map((e) => Text(e.name)).toList();
},
items: state.devices.map((e) {
return DropdownMenuItem(
value: e,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(e.name),
if (spaceUuid != null)
FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: Text(
spaceUuid,
style: _getTextStyle(context)?.copyWith(
fontSize: 10,
),
),
),
],
),
);
}).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:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.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';
@ -18,10 +18,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
Widget build(BuildContext context) {
return BarChart(
BarChartData(
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData(),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
@ -34,29 +31,25 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
barRods: [
BarChartRodData(
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
toY: data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
toY: data.phaseA + data.phaseB + data.phaseC,
rodStackItems: [
BarChartRodStackItem(
0,
data.energyConsumedA,
data.phaseA,
ColorsManager.vividBlue.withValues(alpha: 0.8),
),
BarChartRodStackItem(
data.energyConsumedA,
data.energyConsumedA + data.energyConsumedB,
data.phaseA,
data.phaseA + data.phaseB,
ColorsManager.vividBlue.withValues(alpha: 0.4),
),
BarChartRodStackItem(
data.energyConsumedA + data.energyConsumedB,
data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
data.phaseA + data.phaseB,
data.phaseA + data.phaseB + data.phaseC,
ColorsManager.vividBlue.withValues(alpha: 0.15),
),
],
width: 8,
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
@ -66,7 +59,6 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
);
}).toList(),
),
duration: Duration.zero,
);
}
@ -99,27 +91,18 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
}) {
final data = energyData;
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;
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;
return BarTooltipItem(
'$date\n',
'$month\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(
@ -161,9 +144,9 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) {
final month = DateFormat('d').format(energyData[value.toInt()].date);
final month = energyData[value.toInt()].month.getMonthName;
return FittedBox(
alignment: AlignmentDirectional.center,
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: RotatedBox(
quarterTurns: 3,
@ -177,7 +160,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
),
);
},
reservedSize: 18,
reservedSize: 36,
),
);

View File

@ -16,11 +16,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData(),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: chartData.map((e) {
@ -37,7 +33,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
);
}).toList(),
),
duration: Duration.zero,
duration: Durations.extralong1,
curve: Curves.easeIn,
);
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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_per_device/energy_consumption_per_device_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
@ -47,7 +46,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
flex: 2,
child: EnergyConsumptionPerDeviceDevicesList(
chartData: state.chartData,
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
),
),
],

View File

@ -1,16 +1,10 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
const EnergyConsumptionPerDeviceDevicesList({
required this.chartData,
required this.devices,
super.key,
});
const EnergyConsumptionPerDeviceDevicesList({required this.chartData, super.key});
final List<AnalyticsDevice> devices;
final List<DeviceEnergyDataModel> chartData;
@override
@ -22,60 +16,45 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: devices.map((e) => _buildDeviceCell(context, e)).toList(),
children: chartData.map((e) => _buildDeviceCell(context, e)).toList(),
),
);
}
Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) {
final deviceColor = chartData
.firstWhere(
(element) => element.deviceId == device.uuid,
orElse: () => const DeviceEnergyDataModel(
energy: [],
deviceName: '',
deviceId: '',
color: Colors.red,
),
)
.color;
return Tooltip(
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
child: Container(
height: MediaQuery.sizeOf(context).height * 0.0365,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,
Widget _buildDeviceCell(BuildContext context, DeviceEnergyDataModel device) {
return Container(
height: MediaQuery.sizeOf(context).height * 0.0365,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
borderRadius: BorderRadiusDirectional.circular(8),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
decoration: BoxDecoration(
borderRadius: BorderRadiusDirectional.circular(8),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: Row(
spacing: 6,
children: [
CircleAvatar(
radius: 4,
backgroundColor: deviceColor,
),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: Row(
spacing: 6,
children: [
CircleAvatar(
radius: 4,
backgroundColor: device.color,
),
Text(
device.deviceName,
textAlign: TextAlign.center,
style: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
Text(
device.name,
textAlign: TextAlign.center,
style: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
],
),
),
],
),
),
);

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
const PowerClampEnergyDataDeviceDropdown({super.key});
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override
Widget build(BuildContext context) {
return TextButton(
style: TextButton.styleFrom(
foregroundColor: _color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.greyColor,
width: 1,
),
),
backgroundColor: ColorsManager.transparentColor,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
child: const Text(
'Device 1',
style: TextStyle(
fontWeight: FontWeight.w700,
),
),
onPressed: () {},
);
}
}

View File

@ -1,13 +1,10 @@
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';
@ -53,8 +50,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
),
const SizedBox(height: 6),
SelectableText(
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
state.powerClampModel?.productUuid ?? 'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
@ -111,7 +107,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 3,
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@ -126,25 +122,11 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
),
),
const Spacer(),
Expanded(
flex: 2,
const Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDeviceDropdown(
onChanged: (value) {
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
context,
powerClampUuid: value.uuid,
selectedDate:
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
);
},
),
child: PowerClampEnergyDataDeviceDropdown(),
),
),
],

View File

@ -48,9 +48,6 @@ class PowerClampEnergyStatusWidget extends StatelessWidget {
fontWeight: FontWeight.w400,
fontSize: 16,
),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text.rich(
TextSpan(

View File

@ -4,6 +4,15 @@ import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
// energy_consumption_chart will return id, name and consumption
const phasesJson = {
"1": {
"phaseOne": 1000,
"phaseTwo": 2000,
"phaseThree": 3000,
}
};
class TotalEnergyConsumptionChart extends StatelessWidget {
const TotalEnergyConsumptionChart({required this.chartData, super.key});
@ -16,17 +25,14 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
LineChartData(
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
leftTitlesInterval: 5000,
),
gridData: EnergyManagementChartsHelper.gridData(),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: _lineBarsData,
),
duration: Duration.zero,
duration: Durations.extralong1,
curve: Curves.easeIn,
),
);
@ -43,7 +49,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
.entries
.map(
(entry) => FlSpot(
entry.value.date.day.toDouble(),
entry.key.toDouble(),
entry.value.value,
),
)

View File

@ -1,37 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
part 'occupancy_event.dart';
part 'occupancy_state.dart';
class OccupancyBloc extends Bloc<OccupancyEvent, OccupancyState> {
OccupancyBloc(this._occupacyService) : super(const OccupancyState()) {
on<LoadOccupancyEvent>(_onLoadOccupancyEvent);
on<ClearOccupancyEvent>(_onClearOccupancyEvent);
}
final OccupacyService _occupacyService;
Future<void> _onLoadOccupancyEvent(
LoadOccupancyEvent event,
Emitter<OccupancyState> emit,
) async {
emit(state.copyWith(status: OccupancyStatus.loading));
try {
final chartData = await _occupacyService.load(event.param);
emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded));
} catch (e) {
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e'));
}
}
void _onClearOccupancyEvent(
ClearOccupancyEvent event,
Emitter<OccupancyState> emit,
) {
emit(const OccupancyState());
}
}

View File

@ -1,21 +0,0 @@
part of 'occupancy_bloc.dart';
sealed class OccupancyEvent extends Equatable {
const OccupancyEvent();
@override
List<Object> get props => [];
}
final class LoadOccupancyEvent extends OccupancyEvent {
const LoadOccupancyEvent(this.param);
final GetOccupancyParam param;
@override
List<Object> get props => [param];
}
final class ClearOccupancyEvent extends OccupancyEvent {
const ClearOccupancyEvent();
}

View File

@ -1,30 +0,0 @@
part of 'occupancy_bloc.dart';
enum OccupancyStatus { initial, loading, loaded, failure }
final class OccupancyState extends Equatable {
const OccupancyState({
this.chartData = const [],
this.status = OccupancyStatus.initial,
this.errorMessage,
});
final List<Occupacy> chartData;
final OccupancyStatus status;
final String? errorMessage;
OccupancyState copyWith({
List<Occupacy>? chartData,
OccupancyStatus? status,
String? errorMessage,
}) {
return OccupancyState(
chartData: chartData ?? this.chartData,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [chartData, status, errorMessage];
}

View File

@ -1,49 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
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';
part 'occupancy_heat_map_event.dart';
part 'occupancy_heat_map_state.dart';
class OccupancyHeatMapBloc
extends Bloc<OccupancyHeatMapEvent, OccupancyHeatMapState> {
OccupancyHeatMapBloc(
this._occupancyHeatMapService,
) : super(const OccupancyHeatMapState()) {
on<LoadOccupancyHeatMapEvent>(_onLoadOccupancyHeatMapEvent);
on<ClearOccupancyHeatMapEvent>(_onClearOccupancyHeatMapEvent);
}
final OccupancyHeatMapService _occupancyHeatMapService;
Future<void> _onLoadOccupancyHeatMapEvent(
LoadOccupancyHeatMapEvent event,
Emitter<OccupancyHeatMapState> emit,
) async {
emit(state.copyWith(status: OccupancyHeatMapStatus.loading));
try {
final occupancyHeatMap = await _occupancyHeatMapService.load(event.param);
emit(
state.copyWith(
status: OccupancyHeatMapStatus.loaded,
heatMapData: occupancyHeatMap,
),
);
} catch (e) {
emit(
state.copyWith(
status: OccupancyHeatMapStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onClearOccupancyHeatMapEvent(
ClearOccupancyHeatMapEvent event,
Emitter<OccupancyHeatMapState> emit,
) {
emit(const OccupancyHeatMapState());
}
}

View File

@ -1,21 +0,0 @@
part of 'occupancy_heat_map_bloc.dart';
sealed class OccupancyHeatMapEvent extends Equatable {
const OccupancyHeatMapEvent();
@override
List<Object> get props => [];
}
final class LoadOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
const LoadOccupancyHeatMapEvent(this.param);
final GetOccupancyHeatMapParam param;
@override
List<Object> get props => [param];
}
final class ClearOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
const ClearOccupancyHeatMapEvent();
}

View File

@ -1,30 +0,0 @@
part of 'occupancy_heat_map_bloc.dart';
enum OccupancyHeatMapStatus { initial, loading, loaded, failure }
final class OccupancyHeatMapState extends Equatable {
const OccupancyHeatMapState({
this.status = OccupancyHeatMapStatus.initial,
this.heatMapData = const [],
this.errorMessage,
});
final OccupancyHeatMapStatus status;
final String? errorMessage;
final List<OccupancyHeatMapModel> heatMapData;
OccupancyHeatMapState copyWith({
OccupancyHeatMapStatus? status,
List<OccupancyHeatMapModel>? heatMapData,
String? errorMessage,
}) {
return OccupancyHeatMapState(
status: status ?? this.status,
heatMapData: heatMapData ?? this.heatMapData,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, errorMessage, heatMapData];
}

View File

@ -1,111 +0,0 @@
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/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, {
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;
loadOccupancyChartData(
context,
spaceUuid: spaceId,
date: datePickerState.monthlyDate,
);
loadHeatMapData(context, spaceUuid: spaceId, year: datePickerState.yearlyDate);
if (selectedDevice case final AnalyticsDevice device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(
RealtimeDeviceChangesStarted(device.uuid),
);
}
}
static void loadHeatMapData(
BuildContext context, {
required String spaceUuid,
required DateTime year,
}) {
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(spaceUuid: spaceUuid, year: year),
),
);
}
static void loadOccupancyChartData(
BuildContext context, {
required String spaceUuid,
required DateTime date,
}) {
context.read<OccupancyBloc>().add(
LoadOccupancyEvent(
GetOccupancyParam(
monthDate: '${date.year}-${date.month.toString().padLeft(2, '0')}',
spaceUuid: spaceUuid,
),
),
);
}
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,56 +1,12 @@
import 'package:flutter/material.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 StatelessWidget {
const AnalyticsOccupancyView({super.key});
static const _padding = EdgeInsetsDirectional.all(32);
@override
Widget build(BuildContext context) {
final height = MediaQuery.sizeOf(context).height;
return LayoutBuilder(
builder: (context, constraints) {
final isMediumOrLess = constraints.maxWidth <= 900;
if (isMediumOrLess) {
return SingleChildScrollView(
padding: _padding,
child: Column(
spacing: 32,
children: [
SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
],
),
);
}
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height * 0.9,
child: const Row(
spacing: 32,
children: [
Expanded(
flex: 5,
child: Column(
spacing: 20,
children: [
Expanded(child: OccupancyChartBox()),
Expanded(child: OccupancyHeatMapBox()),
],
),
),
Expanded(flex: 2, child: OccupancyEndSideBar()),
],
),
),
);
},
return const Center(
child: Text('AnalyticsOccupancyView is Working!'),
);
}
}

View File

@ -1,54 +0,0 @@
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

@ -1,108 +0,0 @@
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

@ -1,153 +0,0 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class OccupancyChart extends StatelessWidget {
const OccupancyChart({required this.chartData, super.key});
final List<Occupacy> chartData;
static const _chartWidth = 16.0;
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData(
maxY: 100.0,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 20,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context).copyWith(
leftTitles: _titlesData(context).leftTitles.copyWith(
sideTitles: _titlesData(context).leftTitles.sideTitles.copyWith(
maxIncluded: true,
minIncluded: true,
),
),
),
barGroups: List.generate(chartData.length, (index) {
final actual = chartData[index];
final occupancyValue = double.parse(actual.occupancy);
return BarChartGroupData(
x: index,
barsSpace: 0,
groupVertically: true,
barRods: [
BarChartRodData(
toY: 100.0,
fromY: occupancyValue == 0 ? occupancyValue : occupancyValue + 2.5,
color: ColorsManager.graysColor,
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
),
BarChartRodData(
toY: occupancyValue,
color: ColorsManager.vividBlue.withValues(alpha: 0.8),
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
),
],
);
}),
),
);
}
BarTouchData _barTouchData(BuildContext context) {
return BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) => getTooltipItem(
context: context,
group: group,
groupIndex: groupIndex,
rod: rod,
rodIndex: rodIndex,
),
),
);
}
BarTooltipItem? getTooltipItem({
required BuildContext context,
required BarChartGroupData group,
required int groupIndex,
required BarChartRodData rod,
required int rodIndex,
}) {
final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
return BarTooltipItem(
percentage,
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 14,
),
);
}
FlTitlesData _titlesData(BuildContext context) {
final titlesData = EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
);
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 20,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${(value).toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
),
),
),
),
),
);
final bottomTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: Text(
(value + 1).toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
fontSize: 8,
),
),
),
reservedSize: 36,
),
);
return titlesData.copyWith(
leftTitles: leftTitles,
bottomTitles: bottomTitles,
);
}
}

View File

@ -1,76 +0,0 @@
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/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_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 {
const OccupancyChartBox({super.key});
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
return BlocBuilder<OccupancyBloc, OccupancyState>(
builder: (context, state) {
return Container(
padding: const EdgeInsets.all(30),
decoration: containerWhiteDecoration,
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
Row(
children: [
const Expanded(
flex: 3,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Occupancy')),
),
),
const Spacer(),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadOccupancyChartData(
context,
spaceUuid:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
date: value,
);
}
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.monthlyDate,
),
),
),
],
),
const Divider(height: 0),
Expanded(child: OccupancyChart(chartData: state.chartData)),
],
),
);
},
);
}
}

View File

@ -1,142 +0,0 @@
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/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';
class OccupancyEndSideBar extends StatelessWidget {
const OccupancyEndSideBar({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
builder: (context, state) {
return Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsetsDirectional.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
Text(
'Device ID:',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const SizedBox(height: 6),
SelectableText(
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const SizedBox(height: 10),
const Divider(height: 1, color: ColorsManager.greyColor),
const SizedBox(height: 50),
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.presenceState,
title: 'Presence Status',
value: _valueFromCode(
'presence_state',
state.deviceStatusList,
),
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.presenceTimeIcon,
title: 'Presence Time',
value:
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.currentDistanceIcon,
title: 'Detection Distance',
value:
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
unit: '',
),
],
),
),
const SizedBox(height: 20),
],
),
);
},
);
}
String _valueFromCode(
String code,
List<Status> status, {
String? defaultValue,
}) {
final value = status
.firstWhere(
(e) => e.code == code,
orElse: () => Status(code: '--', value: '--'),
)
.value
.toString();
return value == 'null' ? defaultValue ?? '--' : value;
}
Widget _buildHeader(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: SelectableText(
'Presnce Sensor',
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
fontSize: 18,
),
),
),
),
const Spacer(),
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDeviceDropdown(
onChanged: (value) =>
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
),
),
),
),
],
);
}
}

View File

@ -1,83 +0,0 @@
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';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMap extends StatelessWidget {
const OccupancyHeatMap({required this.heatMapData, super.key});
final Map<DateTime, int> heatMapData;
static const _cellSize = 16.0;
static const _totalWeeks = 53;
int get _maxValue => heatMapData.isNotEmpty
? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max)
: 0;
DateTime _getStartingDate() {
final jan1 = DateTime(DateTime.now().year, 1, 1);
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
return startOfWeek;
}
List<OccupancyPaintItem> _generatePaintItems(DateTime startDate) {
return List.generate(_totalWeeks * 7, (index) {
final date = startDate.add(Duration(days: index));
final value = heatMapData[date] ?? 0;
return OccupancyPaintItem(index: index, value: value, date: date);
});
}
@override
Widget build(BuildContext context) {
final startDate = _getStartingDate();
final paintItems = _generatePaintItems(startDate);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OccupancyHeatMapMonths(startDate: startDate, cellSize: _cellSize),
Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: ColorsManager.grayBorder),
top: BorderSide(color: ColorsManager.grayBorder),
),
),
width: double.infinity,
child: Row(
children: [
Expanded(
child: FittedBox(
fit: BoxFit.fill,
child: Row(
children: [
const OccupancyHeatMapDays(cellSize: _cellSize),
SizedBox(
width: _totalWeeks * _cellSize,
height: 7 * _cellSize,
child: InteractiveHeatMap(
items: paintItems,
maxValue: _maxValue,
cellSize: _cellSize,
),
),
],
),
),
),
],
),
),
const SizedBox(height: 20),
OccupancyHeatMapGradient(maxValue: _maxValue),
],
);
}
}

View File

@ -1,86 +0,0 @@
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/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_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 {
const OccupancyHeatMapBox({super.key});
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
return BlocBuilder<OccupancyHeatMapBloc, OccupancyHeatMapState>(
builder: (context, state) {
return Container(
padding: const EdgeInsets.all(30),
decoration: containerWhiteDecoration,
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
Row(
children: [
const Expanded(
flex: 3,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Occupancy Heat Map')),
),
),
const Spacer(),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
);
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadHeatMapData(
context,
spaceUuid:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
year: value,
);
}
},
datePickerType: DatePickerType.year,
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.yearlyDate,
),
),
),
],
),
const Divider(height: 0),
Expanded(
child: OccupancyHeatMap(
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
),
),
),
],
),
);
},
);
}
}

View File

@ -1,51 +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 OccupancyHeatMapDays extends StatelessWidget {
const OccupancyHeatMapDays({
required this.cellSize,
this.textColor = ColorsManager.blackColor,
super.key,
});
final double cellSize;
final Color textColor;
static const _weekDayLabels = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(7, (i) {
final dayLabel = _weekDayLabels[i];
return Container(
height: cellSize,
alignment: AlignmentDirectional.centerStart,
margin: const EdgeInsetsDirectional.all(0.5).add(
const EdgeInsetsDirectional.only(end: 4),
),
padding: const EdgeInsets.only(right: 6),
child: Text(
dayLabel,
textAlign: TextAlign.start,
style: context.textTheme.bodySmall?.copyWith(
color: textColor,
fontSize: 8,
fontWeight: FontWeight.w500,
),
),
);
}),
);
}
}

View File

@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMapGradient extends StatelessWidget {
const OccupancyHeatMapGradient({super.key, required this.maxValue});
final int maxValue;
List<Color> _heatMapColors() {
if (maxValue == 0) {
return [
ColorsManager.vividBlue.withValues(alpha: 0),
ColorsManager.vividBlue.withValues(alpha: 0),
];
}
return List.generate(
maxValue + 1,
(index) => ColorsManager.vividBlue.withValues(alpha: index / maxValue),
);
}
@override
Widget build(BuildContext context) {
return Row(
children: [
const Spacer(),
Tooltip(
message: 'Min: 0 - Max: $maxValue',
child: Container(
width: 150,
height: 20,
decoration: BoxDecoration(
border: Border.all(
color: ColorsManager.grayBorder,
width: 1,
),
gradient: LinearGradient(
begin: AlignmentDirectional.centerEnd,
end: AlignmentDirectional.centerStart,
colors: _heatMapColors(),
),
),
),
),
],
);
}
}

View File

@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMapMonths extends StatelessWidget {
const OccupancyHeatMapMonths({
required this.startDate,
required this.cellSize,
super.key,
});
final DateTime startDate;
final double cellSize;
@override
Widget build(BuildContext context) {
return Container(
height: 48,
width: double.infinity,
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
OccupancyHeatMapDays(
cellSize: cellSize / 3,
textColor: Colors.transparent,
),
...List.generate(12, (monthIndex) {
final monthStartDate = DateTime(startDate.year, monthIndex + 1, 1);
final monthName = DateFormat.MMM().format(monthStartDate);
return Expanded(
child: RotatedBox(
quarterTurns: 3,
child: Container(
padding: EdgeInsetsDirectional.zero,
margin: EdgeInsetsDirectional.zero,
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: ColorsManager.borderColor),
),
),
width: cellSize * 4,
child: Padding(
padding: const EdgeInsets.only(left: 4, top: 2),
child: Text(
monthName,
style: const TextStyle(fontSize: 8),
),
),
),
),
);
}),
],
),
);
}
}

View File

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
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,
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;
@override
void paint(Canvas canvas, Size size) {
final Paint fillPaint = Paint();
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;
final row = item.index % 7;
final x = column * cellSize;
final y = row * cellSize;
fillPaint.color = _getColor(item.value);
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
canvas.drawRect(rect, fillPaint);
// 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);
}
}
}
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
const double dashWidth = 2.0;
const double dashSpace = 4.0;
final double totalLength = (end - start).distance;
final Offset direction = (end - start) / (end - start).distance;
double currentLength = 0.0;
while (currentLength < totalLength) {
final Offset dashStart = start + direction * currentLength;
final double nextLength = currentLength + dashWidth;
final Offset dashEnd =
start + direction * (nextLength < totalLength ? nextLength : totalLength);
canvas.drawLine(dashStart, dashEnd, paint);
currentLength = nextLength + dashSpace;
}
}
Color _getColor(int value) {
if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0);
final opacity = value.clamp(0, maxValue) / maxValue;
return ColorsManager.vividBlue.withValues(alpha: opacity);
}
@override
bool shouldRepaint(covariant OccupancyPainter oldDelegate) =>
oldDelegate.hoveredItem != hoveredItem;
}

View File

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

View File

@ -1,19 +1,3 @@
class 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,
};
const GetEnergyConsumptionPerDeviceParam();
}

View File

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

View File

@ -1,11 +0,0 @@
class GetOccupancyParam {
final String monthDate;
final String? spaceUuid;
GetOccupancyParam({
required this.monthDate,
required this.spaceUuid,
});
Map<String, dynamic> toJson() => {'monthDate': monthDate};
}

View File

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

View File

@ -1,6 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,61 +0,0 @@
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: {
'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

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

View File

@ -0,0 +1,39 @@
import 'dart:math' as math show Random;
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';
class FakeEnergyConsumptionPerDeviceService
implements EnergyConsumptionPerDeviceService {
@override
Future<List<DeviceEnergyDataModel>> load(
GetEnergyConsumptionPerDeviceParam param,
) {
final random = math.Random();
return Future.delayed(const Duration(milliseconds: 500), () {
return [
(Colors.redAccent, 1),
(Colors.lightBlueAccent, 2),
(Colors.purpleAccent, 3),
].map((e) {
final (color, index) = e;
return DeviceEnergyDataModel(
color: color,
energy: List.generate(30, (i) => i)
.map(
(index) => EnergyDataModel(
date: DateTime(2025, 1, index + 1),
value: random.nextInt(100) + (index * 100),
),
)
.toList(),
deviceName: 'Device $index',
deviceId: 'device_$index',
);
}).toList();
});
}
}

View File

@ -1,6 +1,4 @@
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';
@ -17,10 +15,16 @@ class RemoteEnergyConsumptionPerDeviceService
) async {
try {
final response = await _httpService.get(
path: '/power-clamp/historical',
path: 'endpoint',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
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();
},
);
return response;
} catch (e) {
@ -28,30 +32,3 @@ 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.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,6 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
abstract interface class OccupacyService {
Future<List<Occupacy>> load(GetOccupancyParam param);
}

View File

@ -1,32 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyService implements OccupacyService {
const RemoteOccupancyService(this._httpService);
final HTTPService _httpService;
@override
Future<List<Occupacy>> load(GetOccupancyParam param) async {
try {
final response = await _httpService.get(
path: '/occupancy/duration/space/${param.spaceUuid}',
showServerMessage: true,
queryParameters: param.toJson(),
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 Occupacy.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -1,6 +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';
abstract interface class OccupancyHeatMapService {
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param);
}

View File

@ -1,35 +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';
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

@ -23,7 +23,7 @@ class FirebaseRealtimeDeviceService implements RealtimeDeviceService {
return Status(
code: status['code']?.toString() ?? '',
value: status['value']?.toString() ?? '',
value: num.tryParse(status['value']?.toString() ?? '0'),
);
}).toList();
});

View File

@ -0,0 +1,19 @@
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
class FakeTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
@override
Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param,
) {
return Future.value(
List.generate(30, (index) {
return EnergyDataModel(
date: DateTime(2025, 1, index + 1),
value: 20000 + (index * 1000) % 5000,
);
}),
);
}
}

View File

@ -14,37 +14,20 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi
) async {
try {
final response = await _httpService.get(
path: '/power-clamp/historical',
path: 'endpoint',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: _TotalEnergyConsumptionResponseMapper.map,
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 EnergyDataModel.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
}
abstract final class _TotalEnergyConsumptionResponseMapper {
const _TotalEnergyConsumptionResponseMapper._();
static List<EnergyDataModel> map(dynamic data) {
final json = data as Map<String, dynamic>? ?? {};
final dailyData = json['data'] as List<dynamic>? ?? [];
return dailyData.map((dayData) {
final date = dayData['date'] as String;
final energyValue = double.tryParse(
dayData['total_energy_consumed_kw'] as String? ?? '0',
) ??
0.0;
return EnergyDataModel(
date: DateTime.parse(date),
value: energyValue,
);
}).toList();
}
}

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

@ -5,7 +5,6 @@ class ChartsLoadingWidget extends StatelessWidget {
required this.isLoading,
super.key,
});
final bool isLoading;
@override

View File

@ -12,7 +12,6 @@ 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';
@ -359,10 +358,7 @@ SOS
case 'NCPS':
return [
FlushPresenceDelayFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF',
),
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF',),
FlushIlluminanceFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
@ -382,17 +378,6 @@ 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

@ -143,19 +143,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
color: ColorsManager.primaryColor,
),
HomeItemModel(
title: 'Syncrow Analytics',
icon: Assets.devicesIcon,
active: true,
onPress: (context) {
context.read<SpaceTreeBloc>().add(ClearCachedData());
BlocProvider.of<RoutineBloc>(context)
.add(const TriggerSwitchTabsEvent(isRoutineTab: false));
context.go(RoutesConst.analytics);
},
color: ColorsManager.primaryColor,
),
// HomeItemModel(
// title: 'Move in',
// icon: Assets.moveinIcon,

View File

@ -19,7 +19,6 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class UsersPage extends StatelessWidget {
UsersPage({super.key});

View File

@ -26,10 +26,8 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
functionCode: event.functionData.functionCode,
operationName: event.functionData.operationName,
value: event.functionData.value ?? existingData.value,
valueDescription: event.functionData.valueDescription ??
existingData.valueDescription,
valueDescription: event.functionData.valueDescription ?? existingData.valueDescription,
condition: event.functionData.condition ?? existingData.condition,
step: event.functionData.step ?? existingData.step,
);
} else {
functions.clear();
@ -61,10 +59,8 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
);
}
FutureOr<void> _onSelectFunction(
SelectFunction event, Emitter<FunctionBlocState> emit) {
FutureOr<void> _onSelectFunction(SelectFunction event, Emitter<FunctionBlocState> emit) {
emit(state.copyWith(
selectedFunction: event.functionCode,
selectedOperationName: event.operationName));
selectedFunction: event.functionCode, selectedOperationName: event.operationName));
}
}

View File

@ -1,10 +1,9 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
@ -90,8 +89,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
final updatedIfItems = List<Map<String, dynamic>>.from(state.ifItems);
// Find the index of the item in teh current itemsList
int index =
updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = updatedIfItems
.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
updatedIfItems[index] = event.item;
@ -100,9 +99,11 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
if (event.isTabToRun) {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
} else {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
}
}
@ -110,8 +111,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
final currentItems = List<Map<String, dynamic>>.from(state.thenItems);
// Find the index of the item in teh current itemsList
int index =
currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = currentItems
.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
currentItems[index] = event.item;
@ -122,7 +123,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
emit(state.copyWith(thenItems: currentItems));
}
void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter<RoutineState> emit) {
void _onAddFunctionsToRoutine(
AddFunctionToRoutine event, Emitter<RoutineState> emit) {
try {
if (event.functions.isEmpty) return;
@ -172,17 +174,20 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid));
scenes
.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectUuid));
scenes.addAll(await SceneApi.getScenes(createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId, projectUuid));
}
emit(state.copyWith(
@ -199,7 +204,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadAutomation(LoadAutomation event, Emitter<RoutineState> emit) async {
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> automations = [];
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -207,17 +213,22 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
try {
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
automations.addAll(await SceneApi.getAutomation(spaceId, communityId, projectId));
automations.addAll(
await SceneApi.getAutomation(spaceId, communityId, projectId));
}
}
} else {
automations.addAll(await SceneApi.getAutomation(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectId));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectId));
}
emit(state.copyWith(
automations: automations,
@ -233,14 +244,16 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onSearchRoutines(SearchRoutines event, Emitter<RoutineState> emit) async {
FutureOr<void> _onSearchRoutines(
SearchRoutines event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(isLoading: false, errorMessage: null));
emit(state.copyWith(searchText: event.query));
}
FutureOr<void> _onAddSelectedIcon(AddSelectedIcon event, Emitter<RoutineState> emit) {
FutureOr<void> _onAddSelectedIcon(
AddSelectedIcon event, Emitter<RoutineState> emit) {
emit(state.copyWith(selectedIcon: event.icon));
}
@ -254,7 +267,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
return actions.last['deviceId'] == 'delay';
}
Future<void> _onCreateScene(CreateSceneEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateScene(
CreateSceneEvent event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -343,7 +357,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onCreateAutomation(CreateAutomationEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateAutomation(
CreateAutomationEvent event, Emitter<RoutineState> emit) async {
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (state.routineName == null || state.routineName!.isEmpty) {
@ -456,7 +471,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.createAutomation(createAutomationModel, projectUuid);
final result =
await SceneApi.createAutomation(createAutomationModel, projectUuid);
if (result['success']) {
add(ResetRoutineState());
add(const LoadAutomation());
@ -477,17 +493,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onRemoveDragCard(RemoveDragCard event, Emitter<RoutineState> emit) {
FutureOr<void> _onRemoveDragCard(
RemoveDragCard event, Emitter<RoutineState> emit) {
if (event.isFromThen) {
final thenItems = List<Map<String, dynamic>>.from(state.thenItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
thenItems.removeAt(event.index);
selectedFunctions.remove(event.key);
emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
thenItems: thenItems, selectedFunctions: selectedFunctions));
} else {
final ifItems = List<Map<String, dynamic>>.from(state.ifItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
ifItems.removeAt(event.index);
selectedFunctions.remove(event.key);
@ -510,11 +530,13 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
}
FutureOr<void> _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
FutureOr<void> _onEffectiveTimeEvent(
EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
emit(state.copyWith(effectiveTime: event.effectiveTime));
}
FutureOr<void> _onSetRoutineName(SetRoutineName event, Emitter<RoutineState> emit) {
FutureOr<void> _onSetRoutineName(
SetRoutineName event, Emitter<RoutineState> emit) {
emit(state.copyWith(
routineName: event.name,
));
@ -536,7 +558,7 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// 'entityId': 'tab_to_run',
// 'uniqueCustomId': const Uuid().v4(),
// 'deviceId': 'tab_to_run',
// 'title': 'Tap to run',
// 'title': 'Tab to run',
// 'productType': 'tab_to_run',
// 'imagePath': Assets.tabToRun,
// }
@ -641,7 +663,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// return (thenItems, ifItems, currentFunctions);
// }
Future<void> _onGetSceneDetails(GetSceneDetails event, Emitter<RoutineState> emit) async {
Future<void> _onGetSceneDetails(
GetSceneDetails event, Emitter<RoutineState> emit) async {
try {
emit(state.copyWith(
isLoading: true,
@ -690,9 +713,10 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
deviceCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay'
? action.entityId
: const Uuid().v4(),
'uniqueCustomId':
action.type == 'automation' || action.actionExecutor == 'delay'
? action.entityId
: const Uuid().v4(),
'title': action.actionExecutor == 'delay'
? 'Delay'
: action.type == 'automation'
@ -732,7 +756,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
),
);
// emit(state.copyWith(automationActionExecutor: action.actionExecutor));
} else if (action.executorProperty != null && action.actionExecutor != 'delay') {
} else if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice?.functions ?? [];
final functionCode = action.executorProperty?.functionCode;
for (DeviceFunction function in functions) {
@ -771,7 +796,7 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
'entityId': 'tab_to_run',
'uniqueCustomId': const Uuid().v4(),
'deviceId': 'tab_to_run',
'title': 'Tap to run',
'title': 'Tab to run',
'productType': 'tab_to_run',
'imagePath': Assets.tabToRun,
}
@ -798,7 +823,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onResetRoutineState(ResetRoutineState event, Emitter<RoutineState> emit) {
FutureOr<void> _onResetRoutineState(
ResetRoutineState event, Emitter<RoutineState> emit) {
emit(state.copyWith(
ifItems: [],
thenItems: [],
@ -831,7 +857,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var spaceBloc = context.read<SpaceTreeBloc>();
if (state.isTabToRun) {
await SceneApi.deleteScene(
unitUuid: spaceBloc.state.selectedSpaces[0], sceneId: state.sceneId ?? '');
unitUuid: spaceBloc.state.selectedSpaces[0],
sceneId: state.sceneId ?? '');
} else {
await SceneApi.deleteAutomation(
unitUuid: spaceBloc.state.selectedSpaces[0],
@ -876,7 +903,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// }
// }
FutureOr<void> _fetchDevices(FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
FutureOr<void> _fetchDevices(
FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true));
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@ -885,17 +913,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var createRoutineBloc = context.read<CreateRoutineBloc>();
var spaceBloc = context.read<SpaceTreeBloc>();
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
devices.addAll(
await DevicesManagementApi().fetchDevices(communityId, spaceId, projectUuid));
devices.addAll(await DevicesManagementApi()
.fetchDevices(communityId, spaceId, projectUuid));
}
}
} else {
devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId, createRoutineBloc.selectedSpaceId, projectUuid));
createRoutineBloc.selectedCommunityId,
createRoutineBloc.selectedSpaceId,
projectUuid));
}
emit(state.copyWith(isLoading: false, devices: devices));
@ -904,7 +936,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateScene(UpdateScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateScene(
UpdateScene event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -971,7 +1004,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
final result =
await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
if (result['success']) {
add(ResetRoutineState());
add(const LoadScenes());
@ -990,7 +1024,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateAutomation(UpdateAutomation event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateAutomation(
UpdateAutomation event, Emitter<RoutineState> emit) async {
try {
if (state.routineName == null || state.routineName!.isEmpty) {
emit(state.copyWith(
@ -1106,8 +1141,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (result['success']) {
add(ResetRoutineState());
add(LoadAutomation());
add(LoadScenes());
add(const LoadAutomation());
add(const LoadScenes());
} else {
emit(state.copyWith(
isLoading: false,
@ -1291,10 +1326,13 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
final ifItems = deviceIfCards.values.where((card) => card['type'] == 'condition').toList();
final ifItems =
deviceIfCards.values.where((card) => card['type'] == 'condition').toList();
final thenItems = deviceThenCards.values
.where((card) =>
card['type'] == 'action' || card['type'] == 'automation' || card['type'] == 'scene')
card['type'] == 'action' ||
card['type'] == 'automation' ||
card['type'] == 'scene')
.toList();
emit(state.copyWith(
@ -1316,7 +1354,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onSceneTrigger(SceneTrigger event, Emitter<RoutineState> emit) async {
Future<void> _onSceneTrigger(
SceneTrigger event, Emitter<RoutineState> emit) async {
emit(state.copyWith(loadingSceneId: event.sceneId));
try {
@ -1361,21 +1400,24 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
event.automationStatusUpdate.spaceUuid, event.communityId, projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
));
} else {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update failed',
));
}
} catch (e) {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update error: ${e.toString()}',

View File

@ -10,7 +10,6 @@ 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({
@ -127,15 +126,6 @@ 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('Tap to run'),
title: const Text('Tab to run'),
),
if (state.isAutomation)
...state.ifItems.map((item) {

View File

@ -14,10 +14,6 @@ abstract class ACFunction extends DeviceFunction<AcStatusModel> {
required super.operationName,
required super.icon,
required this.type,
super.step,
super.unit,
super.max,
super.min,
});
List<ACOperationalValue> getOperationalValues();
@ -79,24 +75,26 @@ class ModeFunction extends ACFunction {
}
class TempSetFunction extends ACFunction {
TempSetFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
final int min;
final int max;
final int step;
TempSetFunction(
{required super.deviceId, required super.deviceName, required type})
: min = 160,
max = 300,
step = 1,
super(
code: 'temp_set',
operationName: 'Set Temperature',
icon: Assets.assetsTempreture,
min: 200,
max: 300,
step: 1,
unit: "°C",
type: type,
);
@override
List<ACOperationalValue> getOperationalValues() {
List<ACOperationalValue> values = [];
for (int temp = min!.toInt(); temp <= max!; temp += step!.toInt()) {
for (int temp = min; temp <= max; temp += step) {
values.add(ACOperationalValue(
icon: Assets.assetsTempreture,
description: "${temp / 10}°C",
@ -106,6 +104,7 @@ class TempSetFunction extends ACFunction {
return values;
}
}
class LevelFunction extends ACFunction {
LevelFunction(
{required super.deviceId, required super.deviceName, required type})
@ -167,10 +166,9 @@ class ChildLockFunction extends ACFunction {
}
class CurrentTempFunction extends ACFunction {
final double min;
final double max;
final double step;
final String unit = "°C";
final int min;
final int max;
final int step;
CurrentTempFunction(
{required super.deviceId, required super.deviceName, required type})
@ -187,7 +185,7 @@ class CurrentTempFunction extends ACFunction {
@override
List<ACOperationalValue> getOperationalValues() {
List<ACOperationalValue> values = [];
for (int temp = min.toInt(); temp <= max; temp += step.toInt()) {
for (int temp = min; temp <= max; temp += step) {
values.add(ACOperationalValue(
icon: Assets.currentTemp,
description: "${temp / 10}°C",

View File

@ -6,12 +6,10 @@ class CpsOperationalValue {
final String description;
final dynamic value;
CpsOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}
@ -96,9 +94,9 @@ final class CpsSensitivityFunction extends CpsFunctions {
icon: Assets.sensitivity,
);
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
static const _images = <String>[
Assets.sensitivityFeature1,
@ -117,10 +115,10 @@ final class CpsSensitivityFunction extends CpsFunctions {
@override
List<CpsOperationalValue> getOperationalValues() {
final values = <CpsOperationalValue>[];
for (var value = min; value <= max; value += step.toInt()) {
for (var value = min; value <= max; value += step) {
values.add(
CpsOperationalValue(
icon: _images[value.toInt()],
icon: _images[value],
description: '$value',
value: value,
),
@ -144,9 +142,9 @@ final class CpsMovingSpeedFunction extends CpsFunctions {
icon: Assets.speedoMeter,
);
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
@override
List<CpsOperationalValue> getOperationalValues() {
@ -175,9 +173,9 @@ final class CpsSpatialStaticValueFunction extends CpsFunctions {
icon: Assets.spatialStaticValue,
);
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
@override
List<CpsOperationalValue> getOperationalValues() {
@ -206,9 +204,9 @@ final class CpsSpatialMotionValueFunction extends CpsFunctions {
icon: Assets.spatialMotionValue,
);
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
@override
List<CpsOperationalValue> getOperationalValues() {
@ -377,9 +375,9 @@ final class CpsPresenceJudgementThrsholdFunction extends CpsFunctions {
icon: Assets.presenceJudgementThrshold,
);
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
@override
List<CpsOperationalValue> getOperationalValues() {
@ -408,9 +406,9 @@ final class CpsMotionAmplitudeTriggerThresholdFunction extends CpsFunctions {
icon: Assets.presenceJudgementThrshold,
);
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
@override
List<CpsOperationalValue> getOperationalValues() {

View File

@ -4,11 +4,6 @@ abstract class DeviceFunction<T> {
final String code;
final String operationName;
final String icon;
final double? step;
final String? unit;
final double? max;
final double? min;
DeviceFunction({
required this.deviceId,
@ -16,10 +11,6 @@ abstract class DeviceFunction<T> {
required this.code,
required this.operationName,
required this.icon,
this.step,
this.unit,
this.max,
this.min,
});
}
@ -31,10 +22,6 @@ class DeviceFunctionData {
final dynamic value;
final String? condition;
final String? valueDescription;
final double? step;
final String? unit;
final double? max;
final double? min;
DeviceFunctionData({
required this.entityId,
@ -44,10 +31,6 @@ class DeviceFunctionData {
required this.value,
this.condition,
this.valueDescription,
this.step,
this.unit,
this.max,
this.min,
});
Map<String, dynamic> toJson() {
@ -59,10 +42,6 @@ class DeviceFunctionData {
'value': value,
if (condition != null) 'condition': condition,
if (valueDescription != null) 'valueDescription': valueDescription,
if (step != null) 'step': step,
if (unit != null) 'unit': unit,
if (max != null) 'max': max,
if (min != null) 'min': min,
};
}
@ -75,10 +54,6 @@ class DeviceFunctionData {
value: json['value'],
condition: json['condition'],
valueDescription: json['valueDescription'],
step: json['step']?.toDouble(),
unit: json['unit'],
max: json['max']?.toDouble(),
min: json['min']?.toDouble(),
);
}
@ -93,11 +68,7 @@ class DeviceFunctionData {
other.operationName == operationName &&
other.value == value &&
other.condition == condition &&
other.valueDescription == valueDescription &&
other.step == step &&
other.unit == unit &&
other.max == max &&
other.min == min;
other.valueDescription == valueDescription;
}
@override
@ -108,10 +79,6 @@ class DeviceFunctionData {
operationName.hashCode ^
value.hashCode ^
condition.hashCode ^
valueDescription.hashCode ^
step.hashCode ^
unit.hashCode ^
max.hashCode ^
min.hashCode;
valueDescription.hashCode;
}
}

View File

@ -20,11 +20,12 @@ abstract class FlushFunctions
}
class FlushPresenceDelayFunction extends FlushFunctions {
final int min;
FlushPresenceDelayFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) :
}) : min = 0,
super(
code: FlushMountedPresenceSensorModel.codePresenceState,
operationName: 'Presence State',
@ -49,9 +50,9 @@ class FlushPresenceDelayFunction extends FlushFunctions {
}
class FlushSensiReduceFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
FlushSensiReduceFunction({
required super.deviceId,
@ -79,8 +80,8 @@ class FlushSensiReduceFunction extends FlushFunctions {
}
class FlushNoneDelayFunction extends FlushFunctions {
final double min;
final double max;
final int min;
final int max;
final String unit;
FlushNoneDelayFunction({
@ -109,9 +110,9 @@ class FlushNoneDelayFunction extends FlushFunctions {
}
class FlushIlluminanceFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
FlushIlluminanceFunction({
required super.deviceId,
@ -129,7 +130,7 @@ class FlushIlluminanceFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
List<FlushOperationalValue> values = [];
for (int lux = min.toInt(); lux <= max; lux += step.toInt()) {
for (int lux = min; lux <= max; lux += step) {
values.add(FlushOperationalValue(
icon: Assets.IlluminanceIcon,
description: "$lux Lux",
@ -141,9 +142,9 @@ class FlushIlluminanceFunction extends FlushFunctions {
}
class FlushOccurDistReduceFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
FlushOccurDistReduceFunction({
required super.deviceId,
@ -172,9 +173,9 @@ class FlushOccurDistReduceFunction extends FlushFunctions {
// ==== then functions ====
class FlushSensitivityFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
FlushSensitivityFunction({
required super.deviceId,
@ -202,9 +203,9 @@ class FlushSensitivityFunction extends FlushFunctions {
}
class FlushNearDetectionFunction extends FlushFunctions {
final double min;
final int min;
final double max;
final double step;
final int step;
final String unit;
FlushNearDetectionFunction({
@ -224,7 +225,7 @@ class FlushNearDetectionFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min.toDouble(); value <= max; value += step) {
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
@ -236,9 +237,9 @@ class FlushNearDetectionFunction extends FlushFunctions {
}
class FlushMaxDetectDistFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
final String unit;
FlushMaxDetectDistFunction({
@ -258,7 +259,7 @@ class FlushMaxDetectDistFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step.toInt()) {
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
@ -270,9 +271,9 @@ class FlushMaxDetectDistFunction extends FlushFunctions {
}
class FlushTargetConfirmTimeFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
final String unit;
FlushTargetConfirmTimeFunction({
@ -292,7 +293,7 @@ class FlushTargetConfirmTimeFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min.toDouble(); value <= max; value += step) {
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
@ -304,9 +305,9 @@ class FlushTargetConfirmTimeFunction extends FlushFunctions {
}
class FlushDisappeDelayFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
final String unit;
FlushDisappeDelayFunction({
@ -326,7 +327,7 @@ class FlushDisappeDelayFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min.toDouble(); value <= max; value += step) {
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
@ -338,9 +339,9 @@ class FlushDisappeDelayFunction extends FlushFunctions {
}
class FlushIndentLevelFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
final String unit;
FlushIndentLevelFunction({
@ -360,7 +361,7 @@ class FlushIndentLevelFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min.toDouble(); value <= max; value += step) {
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
@ -372,9 +373,9 @@ class FlushIndentLevelFunction extends FlushFunctions {
}
class FlushTriggerLevelFunction extends FlushFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
final String unit;
FlushTriggerLevelFunction({
@ -394,7 +395,7 @@ class FlushTriggerLevelFunction extends FlushFunctions {
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min.toDouble(); value <= max; value += step) {
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',

View File

@ -1,130 +0,0 @@
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 {
WHRestartStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : 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 {
WHSwitchFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : 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 {
BacklightFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) :
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

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

View File

@ -4,7 +4,7 @@ import 'package:syncrow_web/pages/routines/models/wps/wps_operational_value.dart
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class WpsFunctions extends DeviceFunction<WallSensorModel> {
final String type;
final String type;
WpsFunctions({
required super.deviceId,
@ -13,10 +13,6 @@ abstract class WpsFunctions extends DeviceFunction<WallSensorModel> {
required super.operationName,
required super.icon,
required this.type,
super.step,
super.unit,
super.max,
super.min,
});
List<WpsOperationalValue> getOperationalValues();
@ -24,13 +20,9 @@ abstract class WpsFunctions extends DeviceFunction<WallSensorModel> {
// For far_detection (75-600cm in 75cm steps)
class FarDetectionFunction extends WpsFunctions {
final double min;
@override
final double max;
@override
final double step;
@override
final int min;
final int max;
final int step;
final String unit;
FarDetectionFunction(
@ -49,7 +41,7 @@ class FarDetectionFunction extends WpsFunctions {
@override
List<WpsOperationalValue> getOperationalValues() {
final values = <WpsOperationalValue>[];
for (var value = min; value <= max; value += step.toInt()) {
for (var value = min; value <= max; value += step) {
values.add(WpsOperationalValue(
icon: Assets.currentDistanceIcon,
description: '$value $unit',
@ -62,9 +54,9 @@ class FarDetectionFunction extends WpsFunctions {
// For presence_time (0-65535 minutes)
class PresenceTimeFunction extends WpsFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
final String unit;
PresenceTimeFunction(
@ -94,9 +86,9 @@ class PresenceTimeFunction extends WpsFunctions {
// For motion_sensitivity_value (1-5 levels)
class MotionSensitivityFunction extends WpsFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
MotionSensitivityFunction(
{required super.deviceId, required super.deviceName, required type})
@ -124,9 +116,9 @@ class MotionSensitivityFunction extends WpsFunctions {
}
class MotionLessSensitivityFunction extends WpsFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
MotionLessSensitivityFunction(
{required super.deviceId, required super.deviceName, required type})
@ -179,8 +171,8 @@ class IndicatorFunction extends WpsFunctions {
}
class NoOneTimeFunction extends WpsFunctions {
final double min;
final double max;
final int min;
final int max;
final String unit;
NoOneTimeFunction(
@ -233,9 +225,9 @@ class PresenceStateFunction extends WpsFunctions {
}
class CurrentDistanceFunction extends WpsFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
CurrentDistanceFunction(
{required super.deviceId, required super.deviceName, required type})
@ -252,10 +244,11 @@ class CurrentDistanceFunction extends WpsFunctions {
@override
List<WpsOperationalValue> getOperationalValues() {
List<WpsOperationalValue> values = [];
for (int cm = min.toInt(); cm <= max; cm += step.toInt()) {
for (int cm = min; cm <= max; cm += step) {
values.add(WpsOperationalValue(
icon: Assets.assetsTempreture,
description: "${cm}CM",
value: cm,
));
}
@ -264,9 +257,9 @@ class CurrentDistanceFunction extends WpsFunctions {
}
class IlluminanceValueFunction extends WpsFunctions {
final double min;
final double max;
final double step;
final int min;
final int max;
final int step;
IlluminanceValueFunction({
required super.deviceId,
@ -284,7 +277,7 @@ class IlluminanceValueFunction extends WpsFunctions {
@override
List<WpsOperationalValue> getOperationalValues() {
List<WpsOperationalValue> values = [];
for (int lux = min.toInt(); lux <= max; lux += step.toInt()) {
for (int lux = min; lux <= max; lux += step) {
values.add(WpsOperationalValue(
icon: Assets.IlluminanceIcon,
description: "$lux Lux",

View File

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

View File

@ -1,297 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncrow_web/pages/routines/widgets/condition_toggle.dart';
import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CustomRoutinesTextbox extends StatefulWidget {
final String? currentCondition;
final String dialogType;
final (double, double) sliderRange;
final dynamic displayedValue;
final dynamic initialValue;
final void Function(String condition) onConditionChanged;
final void Function(double value) onTextChanged;
final String unit;
final double dividendOfRange;
final double stepIncreaseAmount;
final bool withSpecialChar;
const CustomRoutinesTextbox({
required this.dialogType,
required this.sliderRange,
required this.displayedValue,
required this.initialValue,
required this.onConditionChanged,
required this.onTextChanged,
required this.currentCondition,
required this.unit,
required this.dividendOfRange,
required this.stepIncreaseAmount,
required this.withSpecialChar,
super.key,
});
@override
State<CustomRoutinesTextbox> createState() => _CustomRoutinesTextboxState();
}
class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
late final TextEditingController _controller;
bool hasError = false;
String? errorMessage;
int getDecimalPlaces(double step) {
String stepStr = step.toString();
if (stepStr.contains('.')) {
List<String> parts = stepStr.split('.');
String decimalPart = parts[1];
decimalPart = decimalPart.replaceAll(RegExp(r'0+$'), '');
return decimalPart.isEmpty ? 0 : decimalPart.length;
} else {
return 0;
}
}
@override
void initState() {
super.initState();
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
double initialValue;
if (widget.initialValue != null &&
widget.initialValue is num &&
(widget.initialValue as num) == 0) {
initialValue = 0.0;
} else {
initialValue = double.tryParse(widget.displayedValue) ?? 0.0;
}
_controller = TextEditingController(
text: initialValue.toStringAsFixed(decimalPlaces),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _validateInput(String value) {
final doubleValue = double.tryParse(value);
if (doubleValue == null) {
setState(() {
errorMessage = "Invalid number";
hasError = true;
});
return;
}
final min = widget.sliderRange.$1;
final max = widget.sliderRange.$2;
if (doubleValue < min) {
setState(() {
errorMessage = "Value must be at least $min";
hasError = true;
});
} else if (doubleValue > max) {
setState(() {
errorMessage = "Value must be at most $max";
hasError = true;
});
} else {
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
int factor = pow(10, decimalPlaces).toInt();
int scaledStep = (widget.stepIncreaseAmount * factor).round();
int scaledValue = (doubleValue * factor).round();
if (scaledValue % scaledStep != 0) {
setState(() {
errorMessage = "must be a multiple of ${widget.stepIncreaseAmount}";
hasError = true;
});
} else {
setState(() {
errorMessage = null;
hasError = false;
});
}
}
}
@override
void didUpdateWidget(CustomRoutinesTextbox oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialValue != oldWidget.initialValue) {
if (widget.initialValue != null &&
widget.initialValue is num &&
(widget.initialValue as num) == 0) {
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
_controller.text = 0.0.toStringAsFixed(decimalPlaces);
}
}
}
void _correctAndUpdateValue(String value) {
final doubleValue = double.tryParse(value) ?? 0.0;
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
double rounded = (doubleValue / widget.stepIncreaseAmount).round() *
widget.stepIncreaseAmount;
rounded = rounded.clamp(widget.sliderRange.$1, widget.sliderRange.$2);
rounded = double.parse(rounded.toStringAsFixed(decimalPlaces));
setState(() {
hasError = false;
errorMessage = null;
});
_controller.text = rounded.toStringAsFixed(decimalPlaces);
_controller.selection = TextSelection.fromPosition(
TextPosition(offset: _controller.text.length),
);
widget.onTextChanged(rounded);
}
@override
Widget build(BuildContext context) {
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
List<TextInputFormatter> formatters = [];
if (decimalPlaces == 0) {
formatters.add(FilteringTextInputFormatter.digitsOnly);
} else {
formatters.add(FilteringTextInputFormatter.allow(
RegExp(r'^\d*\.?\d{0,' + decimalPlaces.toString() + r'}$'),
));
}
formatters.add(RangeInputFormatter(
min: widget.sliderRange.$1,
max: widget.sliderRange.$2,
));
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.dialogType == 'IF')
ConditionToggle(
currentCondition: widget.currentCondition,
onChanged: widget.onConditionChanged,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'Step: ${widget.stepIncreaseAmount}',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.grayColor,
fontSize: 10,
fontWeight: FontWeight.w400,
),
),
],
),
),
Center(
child: Container(
width: 170,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(20),
border: hasError
? Border.all(color: Colors.red, width: 1)
: Border.all(
color: ColorsManager.lightGrayBorderColor, width: 1),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
style: context.textTheme.bodyLarge?.copyWith(
fontSize: 20,
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
keyboardType: TextInputType.number,
inputFormatters: widget.withSpecialChar == true
? [FilteringTextInputFormatter.digitsOnly]
: null,
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
onChanged: _validateInput,
onFieldSubmitted: _correctAndUpdateValue,
onTapOutside: (_) =>
_correctAndUpdateValue(_controller.text),
),
),
const SizedBox(width: 12),
Text(
widget.unit,
style: context.textTheme.bodyMedium?.copyWith(
fontSize: 20,
fontWeight: FontWeight.bold,
color: ColorsManager.vividBlue,
),
),
],
),
),
),
if (errorMessage != null)
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
errorMessage!,
style: context.textTheme.bodySmall?.copyWith(
color: Colors.red,
fontSize: 10,
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Min. ${widget.sliderRange.$1.toInt()}${widget.unit}',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.grayColor,
fontSize: 10,
fontWeight: FontWeight.w400,
),
),
Text(
'Max. ${widget.sliderRange.$2.toInt()}${widget.unit}',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.grayColor,
fontSize: 10,
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(height: 16),
],
);
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More