mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-14 17:25:50 +00:00
Compare commits
92 Commits
pr-templat
...
SP-1492-la
Author | SHA1 | Date | |
---|---|---|---|
6606491458 | |||
92abcdc4f9 | |||
7aa9e7e5dc | |||
e9abac7933 | |||
9eaa367d32 | |||
24372a0618 | |||
8988947694 | |||
ef875ef7dc | |||
94e4fbd5db | |||
302ef36b17 | |||
c508d016c2 | |||
e0ad7855d3 | |||
ecf588cfcb | |||
c9d15d102b | |||
64a29681de | |||
02b07cfdb6 | |||
0a94557eee | |||
4f8d1c4ffd | |||
06b320a75d | |||
000fe70663 | |||
4257f7f0f3 | |||
b2bf3866a9 | |||
a15b5439f0 | |||
fd2a09cada | |||
4c2802acfc | |||
15343be258 | |||
c21842cc6d | |||
4326559e14 | |||
4ded7d5202 | |||
0d45a155e3 | |||
625f737791 | |||
494ae1c941 | |||
f67d0e2912 | |||
17aad13b2a | |||
a849c1dafb | |||
3e3e17019a | |||
b1bae3cb15 | |||
051bf657ed | |||
5191c1e456 | |||
7a073f10aa | |||
900d47faae | |||
e35a7fdc70 | |||
d80f5e1f3a | |||
baaf5111b1 | |||
745205063e | |||
c07b53107e | |||
39d125ac7e | |||
ad15d0e138 | |||
e6d272a60d | |||
8dfe8d10d4 | |||
5279020d08 | |||
da481536c4 | |||
f21366268a | |||
c3aef736fd | |||
887ac58f40 | |||
c709477500 | |||
63e7b3faa2 | |||
0e61e52bf8 | |||
7515b347ce | |||
3dfbcb5935 | |||
4fd4a9b5bf | |||
14fa1b355e | |||
78d4e58996 | |||
23b9cb5b78 | |||
401d0a9788 | |||
ac2b0d3fac | |||
3be7a377c0 | |||
e4ee456384 | |||
f02c5d71ba | |||
d45ff262c7 | |||
ad227febc1 | |||
a9d6c6f4ee | |||
4d9e57c8b5 | |||
d1bb8da484 | |||
300f9ae358 | |||
c1dab3400b | |||
46815585cb | |||
7f9d044f7e | |||
996a847a27 | |||
5645fb7826 | |||
e8f7c29652 | |||
36c5712c79 | |||
c7fef11aec | |||
ef29d78d70 | |||
cd9941f544 | |||
71aa64ba9e | |||
2262d3b2ba | |||
b7ef9da35d | |||
49e93329c8 | |||
d6f0b53b59 | |||
7154693379 | |||
2e2bc99501 |
30
.github/pull_request_template.md
vendored
30
.github/pull_request_template.md
vendored
@ -1,30 +0,0 @@
|
||||
<!--
|
||||
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
|
||||
<!-- Add your Jira ticket number as a link (e.g., [PROJ-123](https://jira.company.com/browse/PROJ-123)) -->
|
||||
|
||||
## Status
|
||||
|
||||
**READY/IN DEVELOPMENT/HOLD**
|
||||
|
||||
## 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
1
.gitignore
vendored
@ -30,6 +30,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
pubspec.lock
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
@ -10,6 +10,7 @@
|
||||
analyzer:
|
||||
errors:
|
||||
constant_identifier_names: ignore
|
||||
overridden_fields: ignore
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
|
19
assets/icons/landing_analytics.svg
Normal file
19
assets/icons/landing_analytics.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<svg width="101" height="101" viewBox="0 0 101 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_9554_2115)">
|
||||
<path d="M93.8334 86.1048C97.5937 86.1048 100.653 83.0454 100.653 79.2849V9.83487C100.653 4.4501 96.2722 0.0692444 90.8875 0.0692444H10.4187C5.03394 0.0692444 0.653076 4.4501 0.653076 9.83487V79.2849C0.653076 83.0454 3.71245 86.1048 7.4728 86.1048H9.63745V96.163H2.6062C1.52749 96.163 0.653076 97.0376 0.653076 98.1161C0.653076 99.1946 1.52749 100.069 2.6062 100.069H98.7C99.7787 100.069 100.653 99.1946 100.653 98.1161C100.653 97.0376 99.7787 96.163 98.7 96.163H91.6687V86.1048H93.8334ZM4.55933 9.83487C4.55933 6.60401 7.18784 3.97549 10.4187 3.97549H90.8875C94.1183 3.97549 96.7468 6.60401 96.7468 9.83487V14.1317H4.75542C4.68921 14.1317 4.62378 14.1353 4.55933 14.1417V9.83487ZM22.1375 96.163H13.5437V78.5849H22.1375V96.163ZM44.0125 96.163H35.4187V56.9896H44.0125V96.163ZM65.8875 96.163H57.2937V73.5067H65.8875V96.163ZM89.7156 50.1669H77.2156C76.1369 50.1669 75.2625 51.0415 75.2625 52.12V61.9831C75.2625 63.0616 76.1369 63.9362 77.2156 63.9362C78.2943 63.9362 79.1687 63.0616 79.1687 61.9831V54.0732H87.7625V96.163H79.1687V75.1181C79.1687 74.0396 78.2943 73.1649 77.2156 73.1649C76.1369 73.1649 75.2625 74.0396 75.2625 75.1181V96.163H69.7937V71.5536C69.7937 70.4751 68.9193 69.6005 67.8406 69.6005H55.3406C54.2619 69.6005 53.3875 70.4751 53.3875 71.5536V96.163H47.9187V55.0364C47.9187 53.9579 47.0443 53.0833 45.9656 53.0833H33.4656C32.3869 53.0833 31.5125 53.9579 31.5125 55.0364V96.163H26.0437V76.6317C26.0437 75.5532 25.1693 74.6786 24.0906 74.6786H11.5906C10.5119 74.6786 9.63745 75.5532 9.63745 76.6317V82.1985H7.4728C5.86616 82.1985 4.55933 80.8915 4.55933 79.2849V18.028C4.62378 18.0345 4.68921 18.038 4.75542 18.038H96.7468V79.2849C96.7468 80.8915 95.4398 82.1985 93.8334 82.1985H91.6687V52.12C91.6687 51.0415 90.7943 50.1669 89.7156 50.1669Z" fill="white"/>
|
||||
<path d="M88.1531 7.10049H44.0251C42.9464 7.10049 42.072 7.9751 42.072 9.05362C42.072 10.1321 42.9464 11.0067 44.0251 11.0067H88.1531C89.2318 11.0067 90.1062 10.1321 90.1062 9.05362C90.1062 7.9751 89.2318 7.10049 88.1531 7.10049Z" fill="white"/>
|
||||
<path d="M22.3464 7.67276C21.9832 7.30948 21.4792 7.10049 20.9656 7.10049C20.4519 7.10049 19.948 7.30928 19.5847 7.67276C19.2214 8.03604 19.0125 8.53995 19.0125 9.05362C19.0125 9.56729 19.2214 10.0712 19.5847 10.4343C19.948 10.7978 20.4519 11.0067 20.9656 11.0067C21.4792 11.0067 21.9832 10.7978 22.3464 10.4343C22.7097 10.0712 22.9187 9.56729 22.9187 9.05362C22.9187 8.53995 22.7097 8.03604 22.3464 7.67276Z" fill="white"/>
|
||||
<path d="M14.5339 7.67276C14.1707 7.30948 13.6667 7.10049 13.1531 7.10049C12.6394 7.10049 12.1355 7.30928 11.7722 7.67276C11.4089 8.03604 11.2 8.53995 11.2 9.05362C11.2 9.56729 11.4089 10.0712 11.7722 10.4343C12.1355 10.7978 12.6394 11.0067 13.1531 11.0067C13.6667 11.0067 14.1707 10.7978 14.5339 10.4343C14.8972 10.0712 15.1062 9.56729 15.1062 9.05362C15.1062 8.53995 14.8972 8.03604 14.5339 7.67276Z" fill="white"/>
|
||||
<path d="M30.1589 7.67276C29.7957 7.30948 29.2937 7.10049 28.7781 7.10049C28.2644 7.10049 27.7605 7.30928 27.3972 7.67276C27.0339 8.03604 26.825 8.53995 26.825 9.05362C26.825 9.56729 27.0339 10.0712 27.3972 10.4343C27.7605 10.7978 28.2644 11.0067 28.7781 11.0067C29.2917 11.0067 29.7957 10.7978 30.1589 10.4343C30.5222 10.0712 30.7312 9.56729 30.7312 9.05362C30.7312 8.53995 30.5222 8.03604 30.1589 7.67276Z" fill="white"/>
|
||||
<path d="M78.5964 67.5634C78.2332 67.1981 77.7292 66.9911 77.2156 66.9911C76.7019 66.9911 76.198 67.1983 75.8347 67.5634C75.4714 67.9267 75.2625 68.4306 75.2625 68.9442C75.2625 69.4579 75.4714 69.9618 75.8347 70.3251C76.198 70.6882 76.7019 70.8974 77.2156 70.8974C77.7292 70.8974 78.2332 70.6884 78.5964 70.3251C78.9597 69.9618 79.1687 69.4579 79.1687 68.9442C79.1687 68.4286 78.9597 67.9267 78.5964 67.5634Z" fill="white"/>
|
||||
<path d="M83.4656 26.6198C79.5885 26.6198 76.4344 29.7739 76.4344 33.6511C76.4344 35.1911 76.9334 36.6161 77.7764 37.7757L64.8027 50.7493C63.7959 50.1921 62.6393 49.8737 61.4092 49.8737C60.3008 49.8737 59.252 50.1325 58.3186 50.5913L44.7719 37.0446C45.3291 36.0378 45.6475 34.8812 45.6475 33.6511C45.6475 29.7739 42.4934 26.6198 38.6162 26.6198C34.7391 26.6198 31.585 29.7739 31.585 33.6511C31.585 35.0062 31.9713 36.2722 32.6381 37.3468L21.1043 48.8804C20.0975 48.3232 18.9408 48.0048 17.7109 48.0048C13.8338 48.0048 10.6797 51.1589 10.6797 55.0361C10.6797 58.9132 13.8338 62.0673 17.7109 62.0673C21.5881 62.0673 24.7422 58.9132 24.7422 55.0361C24.7422 53.806 24.4238 52.6493 23.8666 51.6425L35.5381 39.971C36.4684 40.4259 37.5129 40.6821 38.6162 40.6821C39.8461 40.6821 41.0027 40.3638 42.0096 39.8066L55.4236 53.2204C54.7613 54.2927 54.3779 55.5544 54.3779 56.9046C54.3779 60.7818 57.532 63.9359 61.4092 63.9359C65.2863 63.9359 68.4404 60.7818 68.4404 56.9046C68.4404 55.6745 68.1221 54.5179 67.5648 53.5111L80.8861 40.1898C81.6855 40.5066 82.5553 40.6823 83.4656 40.6823C87.3428 40.6823 90.4969 37.5282 90.4969 33.6511C90.4969 29.7739 87.3428 26.6198 83.4656 26.6198ZM17.7111 58.1614C15.9881 58.1614 14.5861 56.7595 14.5861 55.0364C14.5861 53.3134 15.9881 51.9114 17.7111 51.9114C19.4342 51.9114 20.8361 53.3134 20.8361 55.0364C20.8361 56.7595 19.4342 58.1614 17.7111 58.1614ZM38.6164 36.7761C36.8934 36.7761 35.4914 35.3741 35.4914 33.6511C35.4914 31.928 36.8934 30.5261 38.6164 30.5261C40.3395 30.5261 41.7414 31.928 41.7414 33.6511C41.7414 35.3741 40.3395 36.7761 38.6164 36.7761ZM61.4094 60.03C59.6863 60.03 58.2844 58.628 58.2844 56.905C58.2844 55.182 59.6863 53.78 61.4094 53.78C63.1324 53.78 64.5344 55.182 64.5344 56.905C64.5344 58.628 63.1324 60.03 61.4094 60.03ZM83.4656 36.7761C81.7426 36.7761 80.3406 35.3741 80.3406 33.6511C80.3406 31.928 81.7426 30.5261 83.4656 30.5261C85.1887 30.5261 86.5906 31.928 86.5906 33.6511C86.5906 35.3741 85.1887 36.7761 83.4656 36.7761Z" fill="white"/>
|
||||
<path d="M29.6238 25.255C29.2585 24.8917 28.7566 24.6825 28.2429 24.6825C27.7273 24.6825 27.2234 24.8915 26.8601 25.255C26.4968 25.6181 26.2898 26.122 26.2898 26.6357C26.2898 27.1493 26.4968 27.6532 26.8601 28.0165C27.2253 28.3798 27.7273 28.5888 28.2429 28.5888C28.7566 28.5888 29.2585 28.38 29.6238 28.0165C29.9871 27.6532 30.196 27.1493 30.196 26.6357C30.196 26.122 29.9871 25.6181 29.6238 25.255Z" fill="white"/>
|
||||
<path d="M20.9656 24.6827H11.5906C10.5119 24.6827 9.63745 25.5573 9.63745 26.6358C9.63745 27.7143 10.5119 28.589 11.5906 28.589H20.9656C22.0443 28.589 22.9187 27.7143 22.9187 26.6358C22.9187 25.5573 22.0443 24.6827 20.9656 24.6827Z" fill="white"/>
|
||||
<path d="M20.9656 34.0577H11.5906C10.5119 34.0577 9.63745 34.9323 9.63745 36.0108C9.63745 37.0893 10.5119 37.964 11.5906 37.964H20.9656C22.0443 37.964 22.9187 37.0893 22.9187 36.0108C22.9187 34.9323 22.0443 34.0577 20.9656 34.0577Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9554_2115">
|
||||
<rect width="100" height="100" fill="white" transform="translate(0.653076 0.0692444)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.8 KiB |
12
assets/icons/refresh_status_icon.svg
Normal file
12
assets/icons/refresh_status_icon.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7305_15779)">
|
||||
<path d="M17.0872 11.5142C17.0872 13.2025 16.427 14.8021 15.2211 15.9954C14.0278 17.2014 12.4283 17.8615 10.7399 17.8615C9.05141 17.8615 7.45185 17.2014 6.25856 15.9954C5.05262 14.8021 4.39249 13.2025 4.39249 11.5142C4.39249 9.82574 5.05266 8.22618 6.25856 7.03289C7.45185 5.8269 9.05141 5.16681 10.7399 5.16681C11.8063 5.16681 12.8471 5.43337 13.7866 5.95388L11.2984 8.97523H21.0861L18.6486 0L16.2113 2.97053C14.5737 1.91691 12.6948 1.35835 10.7398 1.35835C8.02314 1.35835 5.47142 2.41197 3.55459 4.32888C1.63765 6.24578 0.583984 8.79747 0.583984 11.5142C0.583984 14.2309 1.63765 16.7825 3.55459 18.6994C5.47146 20.6163 8.0231 21.67 10.7398 21.67C13.4565 21.67 16.0082 20.6163 17.925 18.6994C19.8419 16.7825 20.8956 14.2309 20.8956 11.5142V10.8794H17.0872V11.5142Z" fill="#77DD00"/>
|
||||
<path d="M17.0876 10.8799H20.8961V11.5146C20.8961 14.2313 19.8424 16.7829 17.9254 18.6998C16.0086 20.6168 13.4569 21.6704 10.7402 21.6704V17.862C12.4287 17.862 14.0282 17.2019 15.2215 15.9959C16.4275 14.8026 17.0876 13.203 17.0876 11.5147V10.8799H17.0876Z" fill="#66BB00"/>
|
||||
<path d="M13.787 5.95388C12.8475 5.43333 11.8066 5.16681 10.7402 5.16681V1.35835C12.6952 1.35835 14.5741 1.91691 16.2117 2.97057L18.6491 0L21.0866 8.97523H11.2989L13.787 5.95388Z" fill="#66BB00"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7305_15779">
|
||||
<rect width="21.67" height="21.67" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/web_Background.png
Normal file
BIN
assets/images/web_Background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
56
lib/pages/analytics/helpers/dashed_border_painter.dart
Normal file
56
lib/pages/analytics/helpers/dashed_border_painter.dart
Normal file
@ -0,0 +1,56 @@
|
||||
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;
|
||||
}
|
71
lib/pages/analytics/models/analytics_device.dart
Normal file
71
lib/pages/analytics/models/analytics_device.dart
Normal file
@ -0,0 +1,71 @@
|
||||
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?,
|
||||
);
|
||||
}
|
||||
}
|
32
lib/pages/analytics/models/occupacy.dart
Normal file
32
lib/pages/analytics/models/occupacy.dart
Normal file
@ -0,0 +1,32 @@
|
||||
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,
|
||||
];
|
||||
}
|
28
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
28
lib/pages/analytics/models/occupancy_heat_map_model.dart
Normal file
@ -0,0 +1,28 @@
|
||||
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];
|
||||
}
|
@ -1,27 +1,66 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PhasesEnergyConsumption extends Equatable {
|
||||
final int month;
|
||||
final double phaseA;
|
||||
final double phaseB;
|
||||
final double phaseC;
|
||||
final String uuid;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String deviceUuid;
|
||||
final DateTime date;
|
||||
final double energyConsumedKw;
|
||||
final double energyConsumedA;
|
||||
final double energyConsumedB;
|
||||
final double energyConsumedC;
|
||||
|
||||
const PhasesEnergyConsumption({
|
||||
required this.month,
|
||||
required this.phaseA,
|
||||
required this.phaseB,
|
||||
required this.phaseC,
|
||||
required this.uuid,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deviceUuid,
|
||||
required this.date,
|
||||
required this.energyConsumedKw,
|
||||
required this.energyConsumedA,
|
||||
required this.energyConsumedB,
|
||||
required this.energyConsumedC,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [month, phaseA, phaseB, phaseC];
|
||||
List<Object?> get props => [
|
||||
uuid,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deviceUuid,
|
||||
date,
|
||||
energyConsumedKw,
|
||||
energyConsumedA,
|
||||
energyConsumedB,
|
||||
energyConsumedC,
|
||||
];
|
||||
|
||||
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
|
||||
return PhasesEnergyConsumption(
|
||||
month: json['month'] as int,
|
||||
phaseA: (json['phaseA'] as num).toDouble(),
|
||||
phaseB: (json['phaseB'] as num).toDouble(),
|
||||
phaseC: (json['phaseC'] as num).toDouble(),
|
||||
uuid: json['uuid'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
deviceUuid: json['deviceUuid'] as String,
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
energyConsumedKw: double.parse(json['energyConsumedKw']),
|
||||
energyConsumedA: double.parse(json['energyConsumedA']),
|
||||
energyConsumedB: double.parse(json['energyConsumedB']),
|
||||
energyConsumedC: double.parse(json['energyConsumedC']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'deviceUuid': deviceUuid,
|
||||
'date': date.toIso8601String().split('T')[0],
|
||||
'energyConsumedKw': energyConsumedKw.toString(),
|
||||
'energyConsumedA': energyConsumedA.toString(),
|
||||
'energyConsumedB': energyConsumedB.toString(),
|
||||
'energyConsumedC': energyConsumedC.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,23 @@ 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, DateTime> {
|
||||
AnalyticsDatePickerBloc() : super(DateTime.now()) {
|
||||
class AnalyticsDatePickerBloc
|
||||
extends Bloc<AnalyticsDatePickerEvent, AnalyticsDatePickerState> {
|
||||
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
|
||||
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
|
||||
}
|
||||
|
||||
void _onUpdateAnalyticsDatePickerEvent(
|
||||
UpdateAnalyticsDatePickerEvent event,
|
||||
Emitter<DateTime> emit,
|
||||
Emitter<AnalyticsDatePickerState> emit,
|
||||
) {
|
||||
emit(event.date);
|
||||
emit(
|
||||
state.copyWith(
|
||||
monthlyDate: event.montlyDate ?? state.monthlyDate,
|
||||
yearlyDate: event.yearlyDate ?? state.yearlyDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ sealed class AnalyticsDatePickerEvent extends Equatable {
|
||||
}
|
||||
|
||||
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
|
||||
const UpdateAnalyticsDatePickerEvent(this.date);
|
||||
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
|
||||
|
||||
final DateTime date;
|
||||
final DateTime? montlyDate;
|
||||
final DateTime? yearlyDate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date];
|
||||
List<Object?> get props => [montlyDate, yearlyDate];
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
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];
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
|
||||
part 'analytics_devices_event.dart';
|
||||
part 'analytics_devices_state.dart';
|
||||
|
||||
class AnalyticsDevicesBloc
|
||||
extends Bloc<AnalyticsDevicesEvent, AnalyticsDevicesState> {
|
||||
AnalyticsDevicesBloc(
|
||||
this._analyticsDevicesService,
|
||||
) : super(const AnalyticsDevicesState()) {
|
||||
on<LoadAnalyticsDevicesEvent>(_onLoadAnalyticsDevices);
|
||||
on<SelectAnalyticsDeviceEvent>(_onSelectAnalyticsDevice);
|
||||
on<ClearAnalyticsDeviceEvent>(_onClearAnalyticsDevice);
|
||||
}
|
||||
final AnalyticsDevicesService _analyticsDevicesService;
|
||||
|
||||
Future<void> _onLoadAnalyticsDevices(
|
||||
LoadAnalyticsDevicesEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) async {
|
||||
emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading));
|
||||
|
||||
try {
|
||||
final devices = await _analyticsDevicesService.getDevices(event.param);
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.loaded,
|
||||
devices: devices,
|
||||
selectedDevice: devices.firstOrNull,
|
||||
),
|
||||
);
|
||||
if (devices.isNotEmpty) {
|
||||
event.onSuccess(devices.first);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectAnalyticsDevice(
|
||||
SelectAnalyticsDeviceEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
selectedDevice: event.device,
|
||||
devices: state.devices,
|
||||
errorMessage: state.errorMessage,
|
||||
status: state.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onClearAnalyticsDevice(
|
||||
ClearAnalyticsDeviceEvent event,
|
||||
Emitter<AnalyticsDevicesState> emit,
|
||||
) {
|
||||
emit(const AnalyticsDevicesState());
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'analytics_devices_bloc.dart';
|
||||
|
||||
sealed class AnalyticsDevicesEvent extends Equatable {
|
||||
const AnalyticsDevicesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent {
|
||||
const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess});
|
||||
|
||||
final GetAnalyticsDevicesParam param;
|
||||
final void Function(AnalyticsDevice device) onSuccess;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
|
||||
const SelectAnalyticsDeviceEvent(this.device);
|
||||
|
||||
final AnalyticsDevice device;
|
||||
|
||||
@override
|
||||
List<Object> get props => [device];
|
||||
}
|
||||
|
||||
final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
|
||||
const ClearAnalyticsDeviceEvent();
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
part of 'analytics_devices_bloc.dart';
|
||||
|
||||
enum AnalyticsDevicesStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class AnalyticsDevicesState extends Equatable {
|
||||
const AnalyticsDevicesState({
|
||||
this.status = AnalyticsDevicesStatus.initial,
|
||||
this.devices = const [],
|
||||
this.errorMessage,
|
||||
this.selectedDevice,
|
||||
});
|
||||
|
||||
final AnalyticsDevicesStatus status;
|
||||
final List<AnalyticsDevice> devices;
|
||||
final AnalyticsDevice? selectedDevice;
|
||||
final String? errorMessage;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, devices, errorMessage, selectedDevice];
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
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,
|
||||
) {
|
||||
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,
|
||||
) {
|
||||
if (child.children.isNotEmpty) {
|
||||
return onSpaceSelected(context, community, child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||
FetchEnergyManagementDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
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,
|
||||
) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@override
|
||||
void onSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
) {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
|
||||
if (isSpaceSelected) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
spaceTreeBloc
|
||||
..add(const SpaceTreeClearSelectionEvent())
|
||||
..add(OnSpaceSelected(community, space.uuid ?? '', []));
|
||||
|
||||
FetchOccupancyDataHelper.loadOccupancyData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
spaceId: space.uuid ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onChildSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel child,
|
||||
) {
|
||||
onSpaceSelected(context, community, child);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||
FetchOccupancyDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
|
||||
@ -8,19 +10,39 @@ 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/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/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/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/fake_total_energy_consumption_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_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 StatelessWidget {
|
||||
class AnalyticsPage extends StatefulWidget {
|
||||
const AnalyticsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsPage> createState() => _AnalyticsPageState();
|
||||
}
|
||||
|
||||
class _AnalyticsPageState extends State<AnalyticsPage> {
|
||||
late final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_httpService = HTTPService();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -30,22 +52,22 @@ class AnalyticsPage extends StatelessWidget {
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => TotalEnergyConsumptionBloc(
|
||||
FakeTotalEnergyConsumptionService(),
|
||||
RemoteTotalEnergyConsumptionService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionByPhasesBloc(
|
||||
FakeEnergyConsumptionByPhasesService(),
|
||||
RemoteEnergyConsumptionByPhasesService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionPerDeviceBloc(
|
||||
FakeEnergyConsumptionPerDeviceService(),
|
||||
RemoteEnergyConsumptionPerDeviceService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PowerClampInfoBloc(
|
||||
RemotePowerClampInfoService(HTTPService()),
|
||||
RemotePowerClampInfoService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider<RealtimeDeviceChangesBloc>(
|
||||
@ -53,6 +75,25 @@ class AnalyticsPage extends StatelessWidget {
|
||||
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(),
|
||||
);
|
||||
|
@ -1,55 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.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';
|
||||
|
||||
class AnalyticsCommunitiesSidebar extends StatelessWidget {
|
||||
const AnalyticsCommunitiesSidebar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
final selectedTab = context.watch<AnalyticsTabBloc>().state;
|
||||
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
},
|
||||
isSide: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
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/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/year_picker_widget.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({super.key});
|
||||
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;
|
||||
|
||||
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
|
||||
@ -19,25 +28,8 @@ 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 BlocProvider.value(
|
||||
value: _analyticsDatePickerBloc,
|
||||
child: Builder(builder: (context) {
|
||||
final selectedDate = context.watch<AnalyticsDatePickerBloc>().state;
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AnalyticsDateFilterButton._color,
|
||||
@ -62,7 +54,7 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
|
||||
),
|
||||
label: Text(
|
||||
_formatDate(selectedDate),
|
||||
_formatDate(widget.selectedDate),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
@ -70,28 +62,35 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => MonthPickerWidget(
|
||||
selectedDate: selectedDate,
|
||||
builder: (_) {
|
||||
return switch (widget.datePickerType) {
|
||||
DatePickerType.month => MonthPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
_analyticsDatePickerBloc.add(
|
||||
UpdateAnalyticsDatePickerEvent(value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
|
||||
context,
|
||||
selectedDate: value,
|
||||
);
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
DatePickerType.year => YearPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime? date) {
|
||||
final formatter = DateFormat('MMMM yyyy');
|
||||
final formattedDate = formatter.format(date ?? DateTime.now());
|
||||
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
|
||||
DatePickerType.month => DateFormat('MMMM yyyy'),
|
||||
DatePickerType.year => DateFormat('yyyy'),
|
||||
};
|
||||
final formattedDate = formatterBasedOnDatePickerType.format(
|
||||
date ?? DateTime.now(),
|
||||
);
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ 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 {
|
||||
@ -17,9 +18,12 @@ class AnalyticsPageTabButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () => context.read<AnalyticsTabBloc>().add(
|
||||
onPressed: () {
|
||||
AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context);
|
||||
context.read<AnalyticsTabBloc>().add(
|
||||
UpdateAnalyticsTabEvent(tab),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
tab.title,
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -1,9 +1,12 @@
|
||||
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 {
|
||||
@ -38,9 +41,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...AnalyticsPageTab.values.map(
|
||||
(tab) => AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
(tab) => _buildAnimation(
|
||||
child: AnalyticsPageTabButton(
|
||||
key: ValueKey(selectedTab),
|
||||
tab: tab,
|
||||
@ -53,12 +54,42 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Expanded(
|
||||
Visibility(
|
||||
key: ValueKey(selectedTab),
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement,
|
||||
child: Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -67,14 +98,18 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: selectedTab.child,
|
||||
),
|
||||
child: _buildAnimation(child: selectedTab.child),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimation({required Widget child}) {
|
||||
return AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
width: 320,
|
||||
@ -121,6 +121,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
}
|
||||
|
||||
Row _buildYearSelector() {
|
||||
final currentYear = DateTime.now().year;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -134,17 +135,35 @@ 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: () => setState(() => _currentYear = _currentYear + 1),
|
||||
icon: const Icon(
|
||||
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(
|
||||
Icons.chevron_right,
|
||||
color: ColorsManager.grey700,
|
||||
color: _currentYear < currentYear
|
||||
? ColorsManager.grey700
|
||||
: ColorsManager.grey700.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -152,11 +171,13 @@ 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,
|
||||
@ -165,13 +186,28 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _selectedMonth == index;
|
||||
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _selectedMonth = index),
|
||||
onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = 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)
|
||||
: isFutureMonth
|
||||
? ColorsManager.grey700.withValues(alpha: 0.1)
|
||||
: const Color(0xFFEDF2F7),
|
||||
borderRadius:
|
||||
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
|
||||
@ -182,11 +218,14 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: isFutureMonth
|
||||
? ColorsManager.blackColor.withValues(alpha: 0.3)
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -0,0 +1,188 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
|
||||
abstract final class EnergyConsumptionByPhasesChartHelper {
|
||||
const EnergyConsumptionByPhasesChartHelper._();
|
||||
|
||||
static const fakeData = <PhasesEnergyConsumption>[
|
||||
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
|
||||
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
|
||||
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
|
||||
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
|
||||
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100),
|
||||
];
|
||||
}
|
@ -21,12 +21,13 @@ 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.greyColor,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@ -36,7 +37,8 @@ abstract final class EnergyManagementChartsHelper {
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
maxIncluded: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
interval: leftTitlesInterval,
|
||||
reservedSize: 110,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
@ -91,31 +93,38 @@ abstract final class EnergyManagementChartsHelper {
|
||||
);
|
||||
}
|
||||
|
||||
static FlBorderData borderData() {
|
||||
return FlBorderData(
|
||||
show: true,
|
||||
border: const Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
style: BorderStyle.solid,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static FlGridData gridData() {
|
||||
return const FlGridData(
|
||||
return FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
drawHorizontalLine: true,
|
||||
horizontalInterval: 250,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: ColorsManager.greyColor,
|
||||
strokeWidth: 1,
|
||||
dashArray: value == 0 ? null : [5, 5],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static FlBorderData borderData() {
|
||||
return FlBorderData(
|
||||
border: const Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
show: true,
|
||||
);
|
||||
}
|
||||
|
||||
static LineTouchData lineTouchData() {
|
||||
return LineTouchData(
|
||||
handleBuiltInTouches: true,
|
||||
touchSpotThreshold: 2,
|
||||
touchSpotThreshold: 16,
|
||||
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
|
||||
);
|
||||
}
|
||||
|
@ -1,52 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
abstract final class FetchEnergyManagementDataHelper {
|
||||
const FetchEnergyManagementDataHelper._();
|
||||
|
||||
static void fetchEnergyManagementData(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
}) {
|
||||
final (selectedCommunities, selectedSpaces) =
|
||||
getSelectedCommunitiesAndSpaces(context);
|
||||
// static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
|
||||
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
|
||||
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
|
||||
}
|
||||
|
||||
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
|
||||
static void loadEnergyManagementData(
|
||||
BuildContext context, {
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
DateTime? selectedDate,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
if (communityId.isEmpty && spaceId.isEmpty) {
|
||||
clearAllData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
loadTotalEnergyConsumption(context);
|
||||
loadEnergyConsumptionByPhases(context);
|
||||
loadEnergyConsumptionPerDevice(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);
|
||||
}
|
||||
|
||||
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);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionByPhases(
|
||||
BuildContext context, {
|
||||
required String powerClampUuid,
|
||||
DateTime? selectedDate,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionByPhasesParam(
|
||||
startDate: selectedDate,
|
||||
spaceId: '',
|
||||
date: selectedDate,
|
||||
powerClampUuid: powerClampUuid,
|
||||
);
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
LoadEnergyConsumptionByPhasesEvent(param: param),
|
||||
@ -56,46 +84,94 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
static void loadTotalEnergyConsumption(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final (selectedCommunities, selectedSpaces) =
|
||||
getSelectedCommunitiesAndSpaces(context);
|
||||
|
||||
final param = GetTotalEnergyConsumptionParam(
|
||||
spaceId: selectedCommunities.firstOrNull,
|
||||
startDate: selectedDate,
|
||||
spaceId: spaceId,
|
||||
communityId: communityId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<TotalEnergyConsumptionBloc>().add(
|
||||
TotalEnergyConsumptionLoadEvent(param: param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionPerDevice(BuildContext context) {
|
||||
const param = GetEnergyConsumptionPerDeviceParam();
|
||||
static void loadEnergyConsumptionPerDevice(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionPerDeviceParam(
|
||||
spaceId: spaceId,
|
||||
communityId: communityId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadPowerClampInfo(BuildContext context) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const LoadPowerClampInfoEvent('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(
|
||||
BuildContext context, {
|
||||
String? deviceUuid,
|
||||
}) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(BuildContext context) {
|
||||
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(
|
||||
const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
|
||||
RealtimeDeviceChangesStarted(device.uuid),
|
||||
);
|
||||
},
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['PC'],
|
||||
requestType: AnalyticsDeviceRequestType.energyManagement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const ClearEnergyConsumptionPerDeviceEvent(),
|
||||
);
|
||||
@ -107,5 +183,6 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
const ClearEnergyConsumptionByPhasesEvent(),
|
||||
);
|
||||
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,35 @@
|
||||
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 StatelessWidget {
|
||||
class AnalyticsEnergyManagementView extends StatefulWidget {
|
||||
const AnalyticsEnergyManagementView({super.key});
|
||||
|
||||
static const _padding = EdgeInsetsDirectional.all(32);
|
||||
@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(
|
||||
|
@ -0,0 +1,108 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
@ -18,7 +18,10 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
@ -31,25 +34,29 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||
toY: data.phaseA + data.phaseB + data.phaseC,
|
||||
toY: data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
rodStackItems: [
|
||||
BarChartRodStackItem(
|
||||
0,
|
||||
data.phaseA,
|
||||
data.energyConsumedA,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.8),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.phaseA,
|
||||
data.phaseA + data.phaseB,
|
||||
data.energyConsumedA,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.4),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.phaseA + data.phaseB,
|
||||
data.phaseA + data.phaseB + data.phaseC,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.15),
|
||||
),
|
||||
],
|
||||
width: 16,
|
||||
width: 8,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
@ -59,6 +66,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
@ -91,18 +99,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
}) {
|
||||
final data = energyData;
|
||||
|
||||
final month = data[group.x.toInt()].month.getMonthName;
|
||||
final phaseA = data[group.x.toInt()].phaseA;
|
||||
final phaseB = data[group.x.toInt()].phaseB;
|
||||
final phaseC = data[group.x.toInt()].phaseC;
|
||||
final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
|
||||
final phaseA = data[group.x.toInt()].energyConsumedA;
|
||||
final phaseB = data[group.x.toInt()].energyConsumedB;
|
||||
final phaseC = data[group.x.toInt()].energyConsumedC;
|
||||
final total = data[group.x.toInt()].energyConsumedKw;
|
||||
|
||||
return BarTooltipItem(
|
||||
'$month\n',
|
||||
'$date\n',
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Total: $total\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Phase A: $phaseA\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
@ -144,9 +161,9 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) {
|
||||
final month = energyData[value.toInt()].month.getMonthName;
|
||||
final month = DateFormat('d').format(energyData[value.toInt()].date);
|
||||
return FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
@ -160,7 +177,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 36,
|
||||
reservedSize: 18,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -16,7 +16,11 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: chartData.map((e) {
|
||||
@ -33,7 +37,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Durations.extralong1,
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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';
|
||||
@ -46,6 +47,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: EnergyConsumptionPerDeviceDevicesList(
|
||||
chartData: state.chartData,
|
||||
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,10 +1,16 @@
|
||||
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, super.key});
|
||||
const EnergyConsumptionPerDeviceDevicesList({
|
||||
required this.chartData,
|
||||
required this.devices,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<AnalyticsDevice> devices;
|
||||
final List<DeviceEnergyDataModel> chartData;
|
||||
|
||||
@override
|
||||
@ -16,13 +22,27 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: chartData.map((e) => _buildDeviceCell(context, e)).toList(),
|
||||
children: devices.map((e) => _buildDeviceCell(context, e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceCell(BuildContext context, DeviceEnergyDataModel device) {
|
||||
return Container(
|
||||
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,
|
||||
@ -43,10 +63,10 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 4,
|
||||
backgroundColor: device.color,
|
||||
backgroundColor: deviceColor,
|
||||
),
|
||||
Text(
|
||||
device.deviceName,
|
||||
device.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
@ -57,6 +77,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
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: () {},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
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';
|
||||
@ -50,7 +53,8 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
state.powerClampModel?.productUuid ?? 'N/A',
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
@ -107,7 +111,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@ -122,11 +126,25 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: PowerClampEnergyDataDeviceDropdown(),
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) {
|
||||
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: value.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -48,6 +48,9 @@ class PowerClampEnergyStatusWidget extends StatelessWidget {
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text.rich(
|
||||
TextSpan(
|
||||
|
@ -4,15 +4,6 @@ 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});
|
||||
|
||||
@ -25,14 +16,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
LineChartData(
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 5000,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData(),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: _lineBarsData,
|
||||
),
|
||||
duration: Durations.extralong1,
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
);
|
||||
@ -49,7 +43,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
.entries
|
||||
.map(
|
||||
(entry) => FlSpot(
|
||||
entry.key.toDouble(),
|
||||
entry.value.date.day.toDouble(),
|
||||
entry.value.value,
|
||||
),
|
||||
)
|
||||
|
@ -0,0 +1,37 @@
|
||||
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());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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];
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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];
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +1,56 @@
|
||||
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) {
|
||||
return const Center(
|
||||
child: Text('AnalyticsOccupancyView is Working!'),
|
||||
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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class HeatMapTooltip extends StatelessWidget {
|
||||
const HeatMapTooltip({
|
||||
required this.date,
|
||||
required this.value,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.grey700,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('MMM d, yyyy').format(date),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
const Divider(height: 2, thickness: 1),
|
||||
Text(
|
||||
'$value Occupants',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorsManager.whiteColors,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
|
||||
|
||||
class InteractiveHeatMap extends StatefulWidget {
|
||||
const InteractiveHeatMap({
|
||||
required this.items,
|
||||
required this.maxValue,
|
||||
required this.cellSize,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<OccupancyPaintItem> items;
|
||||
final int maxValue;
|
||||
final double cellSize;
|
||||
|
||||
@override
|
||||
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
|
||||
}
|
||||
|
||||
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
|
||||
OccupancyPaintItem? _hoveredItem;
|
||||
OverlayEntry? _overlayEntry;
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeOverlay();
|
||||
_overlayEntry?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _removeOverlay() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
|
||||
_removeOverlay();
|
||||
|
||||
final column = item.index ~/ 7;
|
||||
final row = item.index % 7;
|
||||
final x = column * widget.cellSize;
|
||||
final y = row * widget.cellSize;
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
child: CompositedTransformFollower(
|
||||
link: _layerLink,
|
||||
offset: Offset(x + widget.cellSize, y),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-(widget.cellSize * 2.5), -50),
|
||||
child: HeatMapTooltip(date: item.date, value: item.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: MouseRegion(
|
||||
onHover: (event) {
|
||||
final column = event.localPosition.dx ~/ widget.cellSize;
|
||||
final row = event.localPosition.dy ~/ widget.cellSize;
|
||||
final index = column * 7 + row;
|
||||
|
||||
if (index >= 0 && index < widget.items.length) {
|
||||
final item = widget.items[index];
|
||||
if (_hoveredItem != item) {
|
||||
setState(() => _hoveredItem = item);
|
||||
_showTooltip(item, event.localPosition);
|
||||
}
|
||||
} else {
|
||||
_removeOverlay();
|
||||
setState(() => _hoveredItem = null);
|
||||
}
|
||||
},
|
||||
onExit: (_) {
|
||||
_removeOverlay();
|
||||
setState(() => _hoveredItem = null);
|
||||
},
|
||||
child: CustomPaint(
|
||||
isComplex: true,
|
||||
size: _painterSize,
|
||||
painter: OccupancyPainter(
|
||||
items: widget.items,
|
||||
maxValue: widget.maxValue,
|
||||
hoveredItem: _hoveredItem,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Size get _painterSize {
|
||||
final height = 7 * widget.cellSize;
|
||||
final width = widget.items.length ~/ 7 * widget.cellSize;
|
||||
return Size(width, height);
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
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;
|
||||
}
|
22
lib/pages/analytics/params/get_analytics_devices_param.dart
Normal file
22
lib/pages/analytics/params/get_analytics_devices_param.dart
Normal file
@ -0,0 +1,22 @@
|
||||
enum AnalyticsDeviceRequestType { energyManagement, occupancy }
|
||||
|
||||
class GetAnalyticsDevicesParam {
|
||||
final String? spaceUuid;
|
||||
final List<String> deviceTypes;
|
||||
final String? communityUuid;
|
||||
final AnalyticsDeviceRequestType requestType;
|
||||
|
||||
const GetAnalyticsDevicesParam({
|
||||
required this.requestType,
|
||||
required this.spaceUuid,
|
||||
required this.deviceTypes,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
if (spaceUuid != null) 'spaceUuid': spaceUuid,
|
||||
if (communityUuid != null) 'communityUuid': communityUuid,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,24 +1,20 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class GetEnergyConsumptionByPhasesParam extends Equatable {
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final String? spaceId;
|
||||
final String powerClampUuid;
|
||||
final DateTime? date;
|
||||
|
||||
const GetEnergyConsumptionByPhasesParam({
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.spaceId,
|
||||
required this.powerClampUuid,
|
||||
this.date,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'startDate': startDate?.toIso8601String(),
|
||||
'endDate': endDate?.toIso8601String(),
|
||||
'spaceId': spaceId,
|
||||
'monthDate': '${date?.year}-${date?.month.toString().padLeft(2, '0')}',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [startDate, endDate, spaceId];
|
||||
List<Object?> get props => [powerClampUuid, date];
|
||||
}
|
||||
|
@ -1,3 +1,19 @@
|
||||
class GetEnergyConsumptionPerDeviceParam {
|
||||
const GetEnergyConsumptionPerDeviceParam();
|
||||
const GetEnergyConsumptionPerDeviceParam({
|
||||
this.monthDate,
|
||||
this.spaceId,
|
||||
this.communityId,
|
||||
});
|
||||
|
||||
final DateTime? monthDate;
|
||||
final String? spaceId;
|
||||
final String? communityId;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'monthDate':
|
||||
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
|
||||
'communityUuid': communityId,
|
||||
'groupByDevice': true,
|
||||
};
|
||||
}
|
||||
|
13
lib/pages/analytics/params/get_occupancy_heat_map_param.dart
Normal file
13
lib/pages/analytics/params/get_occupancy_heat_map_param.dart
Normal file
@ -0,0 +1,13 @@
|
||||
class GetOccupancyHeatMapParam {
|
||||
final DateTime year;
|
||||
final String spaceUuid;
|
||||
|
||||
const GetOccupancyHeatMapParam({
|
||||
required this.year,
|
||||
required this.spaceUuid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'year': year.year};
|
||||
}
|
||||
}
|
11
lib/pages/analytics/params/get_occupancy_param.dart
Normal file
11
lib/pages/analytics/params/get_occupancy_param.dart
Normal file
@ -0,0 +1,11 @@
|
||||
class GetOccupancyParam {
|
||||
final String monthDate;
|
||||
final String? spaceUuid;
|
||||
|
||||
GetOccupancyParam({
|
||||
required this.monthDate,
|
||||
required this.spaceUuid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {'monthDate': monthDate};
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
class GetTotalEnergyConsumptionParam {
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final DateTime? monthDate;
|
||||
final String? spaceId;
|
||||
final String? communityId;
|
||||
|
||||
const GetTotalEnergyConsumptionParam({
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.monthDate,
|
||||
this.spaceId,
|
||||
this.communityId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'startDate': startDate?.toIso8601String(),
|
||||
'endDate': endDate?.toIso8601String(),
|
||||
'spaceId': spaceId,
|
||||
'monthDate':
|
||||
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
|
||||
'communityUuid': communityId,
|
||||
'groupByDevice': false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
|
||||
abstract interface class AnalyticsDevicesService {
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
|
||||
class AnalyticsDevicesServiceDelegate implements AnalyticsDevicesService {
|
||||
const AnalyticsDevicesServiceDelegate(
|
||||
this._occupancyService,
|
||||
this._energyManagementService,
|
||||
);
|
||||
|
||||
final AnalyticsDevicesService _occupancyService;
|
||||
final AnalyticsDevicesService _energyManagementService;
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(
|
||||
GetAnalyticsDevicesParam param,
|
||||
) {
|
||||
return switch (param.requestType) {
|
||||
AnalyticsDeviceRequestType.occupancy => _occupancyService.getDevices(param),
|
||||
AnalyticsDeviceRequestType.energyManagement =>
|
||||
_energyManagementService.getDevices(param),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteEnergyManagementAnalyticsDevicesService
|
||||
implements AnalyticsDevicesService {
|
||||
const RemoteEnergyManagementAnalyticsDevicesService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: '/devices-space-community/recursive-child',
|
||||
queryParameters: param.toJson()
|
||||
..addAll({'productType': param.deviceTypes.first}),
|
||||
expectedResponseModel: (response) {
|
||||
final json = response as Map<String, dynamic>;
|
||||
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
|
||||
|
||||
final result = dailyData.map(
|
||||
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption: $e');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart';
|
||||
|
||||
class FakeEnergyConsumptionByPhasesService
|
||||
implements EnergyConsumptionByPhasesService {
|
||||
@override
|
||||
Future<List<PhasesEnergyConsumption>> load(
|
||||
GetEnergyConsumptionByPhasesParam param,
|
||||
) {
|
||||
return Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => const [
|
||||
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
|
||||
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
|
||||
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
|
||||
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
|
||||
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
|
||||
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
|
||||
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
|
||||
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 100, phaseC: 100),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,8 +15,9 @@ final class RemoteEnergyConsumptionByPhasesService
|
||||
) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
path: '/power-clamp/${param.powerClampUuid}/historical',
|
||||
showServerMessage: true,
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: (data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
@ -28,7 +29,7 @@ final class RemoteEnergyConsumptionByPhasesService
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per device: $e');
|
||||
throw Exception('Failed to load energy consumption per phase: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
@ -15,16 +17,10 @@ class RemoteEnergyConsumptionPerDeviceService
|
||||
) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
path: '/power-clamp/historical',
|
||||
showServerMessage: true,
|
||||
expectedResponseModel: (data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
return mappedData.map((e) {
|
||||
final jsonData = e as Map<String, dynamic>;
|
||||
return DeviceEnergyDataModel.fromJson(jsonData);
|
||||
}).toList();
|
||||
},
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
@ -32,3 +28,30 @@ 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();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
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);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
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);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||
const RemoteOccupancyHeatMapService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: '/occupancy/heat-map/space/${param.spaceUuid}',
|
||||
showServerMessage: true,
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: (response) {
|
||||
final json = response as Map<String, dynamic>;
|
||||
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
|
||||
|
||||
final result = dailyData.map(
|
||||
(json) => OccupancyHeatMapModel.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption:');
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class FirebaseRealtimeDeviceService implements RealtimeDeviceService {
|
||||
|
||||
return Status(
|
||||
code: status['code']?.toString() ?? '',
|
||||
value: num.tryParse(status['value']?.toString() ?? '0'),
|
||||
value: status['value']?.toString() ?? '',
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
|
@ -1,19 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -14,20 +14,37 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi
|
||||
) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
path: '/power-clamp/historical',
|
||||
showServerMessage: true,
|
||||
expectedResponseModel: (data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
return mappedData.map((e) {
|
||||
final jsonData = e as Map<String, dynamic>;
|
||||
return EnergyDataModel.fromJson(jsonData);
|
||||
}).toList();
|
||||
},
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: _TotalEnergyConsumptionResponseMapper.map,
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -5,6 +5,7 @@ class ChartsLoadingWidget extends StatelessWidget {
|
||||
required this.isLoading,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
|
@ -63,7 +63,8 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _compareListOfLists(List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||
bool _compareListOfLists(
|
||||
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||
// Check if the old and new lists are the same
|
||||
if (oldList.length != newList.length) return false;
|
||||
|
||||
@ -111,8 +112,8 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
trackVisibility: true,
|
||||
child: Scrollbar(
|
||||
controller: _horizontalScrollController,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
notificationPredicate: (notif) => notif.depth == 1,
|
||||
child: SingleChildScrollView(
|
||||
controller: _verticalScrollController,
|
||||
@ -132,14 +133,18 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
children: [
|
||||
if (widget.withCheckBox) _buildSelectAllCheckbox(),
|
||||
...List.generate(widget.headers.length, (index) {
|
||||
return _buildTableHeaderCell(widget.headers[index], index);
|
||||
return _buildTableHeaderCell(
|
||||
widget.headers[index], index);
|
||||
})
|
||||
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
|
||||
],
|
||||
),
|
||||
),
|
||||
widget.isEmpty
|
||||
? Column(
|
||||
? SizedBox(
|
||||
height: widget.size.height * 0.5,
|
||||
width: widget.size.width,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
@ -153,25 +158,35 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
height: 15,
|
||||
),
|
||||
Text(
|
||||
widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices',
|
||||
widget.tableName == 'AccessManagement'
|
||||
? 'No Password '
|
||||
: 'No Devices',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: ColorsManager.grayColor),
|
||||
.copyWith(
|
||||
color:
|
||||
ColorsManager.grayColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: List.generate(widget.data.length, (index) {
|
||||
children:
|
||||
List.generate(widget.data.length, (index) {
|
||||
final row = widget.data[index];
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08),
|
||||
...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)),
|
||||
if (widget.withCheckBox)
|
||||
_buildRowCheckbox(
|
||||
index, widget.size.height * 0.08),
|
||||
...row.map((cell) => _buildTableCell(
|
||||
cell.toString(),
|
||||
widget.size.height * 0.08)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
@ -196,7 +211,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
),
|
||||
child: Checkbox(
|
||||
value: _selectAll,
|
||||
onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null,
|
||||
onChanged: widget.withSelectAll && widget.data.isNotEmpty
|
||||
? _toggleSelectAll
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -238,7 +255,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
constraints: const BoxConstraints.expand(height: 40),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
|
||||
vertical: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall!.copyWith(
|
||||
|
@ -12,6 +12,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/
|
||||
import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switch/three_gang_switch.dart';
|
||||
import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart';
|
||||
import 'package:syncrow_web/pages/routines/models/gateway.dart';
|
||||
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart';
|
||||
import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart';
|
||||
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
@ -358,7 +359,10 @@ SOS
|
||||
case 'NCPS':
|
||||
return [
|
||||
FlushPresenceDelayFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF',),
|
||||
deviceId: uuid ?? '',
|
||||
deviceName: name ?? '',
|
||||
type: 'IF',
|
||||
),
|
||||
|
||||
FlushIlluminanceFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
|
||||
@ -378,6 +382,17 @@ SOS
|
||||
FlushTriggerLevelFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
|
||||
];
|
||||
case 'WH':
|
||||
return [
|
||||
WHRestartStatusFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
|
||||
WHSwitchFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
|
||||
TimerConfirmTimeFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
|
||||
BacklightFunction(
|
||||
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
|
||||
];
|
||||
|
||||
default:
|
||||
return [];
|
||||
|
@ -97,7 +97,8 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
children: [
|
||||
_buildInfoRow('Space Name:',
|
||||
device.spaces?.firstOrNull?.spaceName ?? 'N/A'),
|
||||
_buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'),
|
||||
_buildInfoRow(
|
||||
'Sub space:', device.subspace?.subspaceName ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
|
@ -11,16 +11,11 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/services/home_api.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/constants/routes_const.dart';
|
||||
import 'package:syncrow_web/utils/navigation_service.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
// final Graph graph = Graph()..isTree = true;
|
||||
// final BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();
|
||||
// List<Node> sourcesList = [];
|
||||
// List<Node> destinationsList = [];
|
||||
UserModel? user;
|
||||
String terms = '';
|
||||
String policy = '';
|
||||
@ -33,22 +28,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
on<ConfirmUserAgreementEvent>(_confirmUserAgreement);
|
||||
}
|
||||
|
||||
// void _createNode(CreateNewNode event, Emitter<HomeState> emit) async {
|
||||
// emit(HomeInitial());
|
||||
// sourcesList.add(event.sourceNode);
|
||||
// destinationsList.add(event.destinationNode);
|
||||
// for (int i = 0; i < sourcesList.length; i++) {
|
||||
// graph.addEdge(sourcesList[i], destinationsList[i]);
|
||||
// }
|
||||
|
||||
// builder
|
||||
// ..siblingSeparation = (100)
|
||||
// ..levelSeparation = (150)
|
||||
// ..subtreeSeparation = (150)
|
||||
// ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);
|
||||
// emit(HomeUpdateTree(graph: graph, builder: builder));
|
||||
// }
|
||||
|
||||
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
|
||||
try {
|
||||
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
|
||||
@ -99,16 +78,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
}
|
||||
}
|
||||
|
||||
// static Future fetchUserInfo() async {
|
||||
// try {
|
||||
// var uuid =
|
||||
// await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
|
||||
// user = await HomeApi().fetchUserInfo(uuid);
|
||||
// } catch (e) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
List<HomeItemModel> homeItems = [
|
||||
HomeItemModel(
|
||||
title: 'Access Management',
|
||||
@ -118,7 +87,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
context.read<SpaceTreeBloc>().add(ClearCachedData());
|
||||
context.go(RoutesConst.accessManagementPage);
|
||||
},
|
||||
color: null,
|
||||
color: const Color(0xFF0036E6),
|
||||
),
|
||||
HomeItemModel(
|
||||
title: 'Space Management',
|
||||
@ -128,7 +97,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
context.read<SpaceTreeBloc>().add(ClearCachedData());
|
||||
context.go(RoutesConst.spacesManagementPage);
|
||||
},
|
||||
color: ColorsManager.primaryColor,
|
||||
color: const Color(0xFF0026A2),
|
||||
),
|
||||
HomeItemModel(
|
||||
title: 'Devices Management',
|
||||
@ -140,43 +109,19 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
.add(const TriggerSwitchTabsEvent(isRoutineTab: false));
|
||||
context.go(RoutesConst.deviceManagementPage);
|
||||
},
|
||||
color: ColorsManager.primaryColor,
|
||||
color: const Color(0xFF00165E),
|
||||
),
|
||||
HomeItemModel(
|
||||
title: 'Syncrow Analytics',
|
||||
icon: Assets.analyticsIcon,
|
||||
active: true,
|
||||
onPress: (context) {
|
||||
context.read<SpaceTreeBloc>().add(ClearCachedData());
|
||||
BlocProvider.of<RoutineBloc>(context)
|
||||
.add(const TriggerSwitchTabsEvent(isRoutineTab: false));
|
||||
context.go(RoutesConst.analytics);
|
||||
},
|
||||
color: const Color(0xFF023DFE),
|
||||
),
|
||||
|
||||
// HomeItemModel(
|
||||
// title: 'Move in',
|
||||
// icon: Assets.moveinIcon,
|
||||
// active: false,
|
||||
// onPress: (context) {},
|
||||
// color: ColorsManager.primaryColor,
|
||||
// ),
|
||||
// HomeItemModel(
|
||||
// title: 'Construction',
|
||||
// icon: Assets.constructionIcon,
|
||||
// active: false,
|
||||
// onPress: (context) {},
|
||||
// color: ColorsManager.primaryColor,
|
||||
// ),
|
||||
// HomeItemModel(
|
||||
// title: 'Energy',
|
||||
// icon: Assets.energyIcon,
|
||||
// active: false,
|
||||
// onPress: (context) {},
|
||||
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
|
||||
// ),
|
||||
// HomeItemModel(
|
||||
// title: 'Integrations',
|
||||
// icon: Assets.integrationsIcon,
|
||||
// active: false,
|
||||
// onPress: (context) {},
|
||||
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
|
||||
// ),
|
||||
// HomeItemModel(
|
||||
// title: 'Asset',
|
||||
// icon: Assets.assetIcon,
|
||||
// active: false,
|
||||
// onPress: (context) {},
|
||||
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
|
||||
// ),
|
||||
];
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class HomeCard extends StatelessWidget {
|
||||
final bool active;
|
||||
@ -8,6 +7,7 @@ class HomeCard extends StatelessWidget {
|
||||
final int index;
|
||||
final String name;
|
||||
final Function()? onTap;
|
||||
final Color? color;
|
||||
const HomeCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
@ -15,28 +15,16 @@ class HomeCard extends StatelessWidget {
|
||||
this.active = false,
|
||||
required this.img,
|
||||
required this.onTap,
|
||||
required this.color,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// bool evenNumbers = index % 2 == 0;
|
||||
return InkWell(
|
||||
onTap: active ? onTap : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: index == 0 && active
|
||||
? ColorsManager.blue1.withOpacity(0.9)
|
||||
: index == 1 && active
|
||||
? ColorsManager.blue2.withOpacity(0.9)
|
||||
: index == 2 && active
|
||||
? ColorsManager.blue3
|
||||
: index == 4 && active == false
|
||||
? ColorsManager.blue4.withOpacity(0.2)
|
||||
: index == 7 && active == false
|
||||
? ColorsManager.blue4.withOpacity(0.2)
|
||||
: ColorsManager.blueColor.withOpacity(0.2),
|
||||
// (active ?ColorsManager.blueColor
|
||||
// : ColorsManager.blueColor.withOpacity(0.2)),
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
@ -64,15 +52,9 @@ class HomeCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: SvgPicture.asset(
|
||||
img,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: SvgPicture.asset(img),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -50,7 +50,7 @@ class HomeMobilePage extends StatelessWidget {
|
||||
height: size.height * 0.6,
|
||||
width: size.width * 0.68,
|
||||
child: GridView.builder(
|
||||
itemCount: homeItems.length,
|
||||
itemCount: homeBloc.homeItems.length,
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
@ -61,7 +61,8 @@ class HomeMobilePage extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
return HomeCard(
|
||||
index: index,
|
||||
active: homeBloc.homeItems[index].active!,
|
||||
active: true,
|
||||
color: homeBloc.homeItems[index].color,
|
||||
name: homeBloc.homeItems[index].title!,
|
||||
img: homeBloc.homeItems[index].icon!,
|
||||
onTap: () =>
|
||||
@ -78,56 +79,4 @@ class HomeMobilePage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final dynamic homeItems = [
|
||||
{
|
||||
'title': 'Access',
|
||||
'icon': Assets.accessIcon,
|
||||
'active': true,
|
||||
},
|
||||
{
|
||||
'title': 'Space\nManagement',
|
||||
'icon': Assets.spaseManagementIcon,
|
||||
'color': ColorsManager.primaryColor,
|
||||
'active': true,
|
||||
},
|
||||
{
|
||||
'title': 'Devices',
|
||||
'icon': Assets.devicesIcon,
|
||||
'active': true,
|
||||
},
|
||||
{
|
||||
'title': 'Syncrow Analytics',
|
||||
'icon': Assets.iconEdit,
|
||||
'active': true,
|
||||
},
|
||||
// {
|
||||
// 'title': 'Move in',
|
||||
// 'icon': Assets.moveinIcon,
|
||||
// 'active': false,
|
||||
// },
|
||||
// {
|
||||
// 'title': 'Construction',
|
||||
// 'icon': Assets.constructionIcon,
|
||||
// 'active': false,
|
||||
// },
|
||||
// {
|
||||
// 'title': 'Energy',
|
||||
// 'icon': Assets.energyIcon,
|
||||
// 'color': ColorsManager.slidingBlueColor.withOpacity(0.2),
|
||||
// 'active': false,
|
||||
// },
|
||||
// {
|
||||
// 'title': 'Integrations',
|
||||
// 'icon': Assets.integrationsIcon,
|
||||
// 'color': ColorsManager.slidingBlueColor.withOpacity(0.2),
|
||||
// 'active': false,
|
||||
// },
|
||||
// {
|
||||
// 'title': 'Asset',
|
||||
// 'icon': Assets.assetIcon,
|
||||
// 'color': ColorsManager.slidingBlueColor.withOpacity(0.2),
|
||||
// 'active': false,
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return HomeCard(
|
||||
color: homeBloc.homeItems[index].color,
|
||||
index: index,
|
||||
active: homeBloc.homeItems[index].active!,
|
||||
name: homeBloc.homeItems[index].title!,
|
||||
|
@ -19,6 +19,7 @@ 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});
|
||||
|
||||
|
@ -26,8 +26,10 @@ 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();
|
||||
@ -59,8 +61,10 @@ 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));
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
@ -89,8 +90,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;
|
||||
@ -99,11 +100,9 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,8 +110,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;
|
||||
@ -123,8 +122,7 @@ 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;
|
||||
|
||||
@ -174,20 +172,17 @@ 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(
|
||||
@ -204,8 +199,7 @@ 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() ?? '';
|
||||
@ -213,22 +207,17 @@ 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,
|
||||
@ -244,16 +233,14 @@ 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));
|
||||
}
|
||||
|
||||
@ -267,8 +254,7 @@ 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)) {
|
||||
@ -357,8 +343,7 @@ 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) {
|
||||
@ -471,8 +456,7 @@ 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());
|
||||
@ -493,21 +477,17 @@ 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);
|
||||
@ -530,13 +510,11 @@ 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,
|
||||
));
|
||||
@ -558,7 +536,7 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
|
||||
// 'entityId': 'tab_to_run',
|
||||
// 'uniqueCustomId': const Uuid().v4(),
|
||||
// 'deviceId': 'tab_to_run',
|
||||
// 'title': 'Tab to run',
|
||||
// 'title': 'Tap to run',
|
||||
// 'productType': 'tab_to_run',
|
||||
// 'imagePath': Assets.tabToRun,
|
||||
// }
|
||||
@ -663,8 +641,7 @@ 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,
|
||||
@ -713,8 +690,7 @@ 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'
|
||||
'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay'
|
||||
? action.entityId
|
||||
: const Uuid().v4(),
|
||||
'title': action.actionExecutor == 'delay'
|
||||
@ -756,8 +732,7 @@ 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) {
|
||||
@ -796,7 +771,7 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
|
||||
'entityId': 'tab_to_run',
|
||||
'uniqueCustomId': const Uuid().v4(),
|
||||
'deviceId': 'tab_to_run',
|
||||
'title': 'Tab to run',
|
||||
'title': 'Tap to run',
|
||||
'productType': 'tab_to_run',
|
||||
'imagePath': Assets.tabToRun,
|
||||
}
|
||||
@ -823,8 +798,7 @@ 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: [],
|
||||
@ -857,8 +831,7 @@ 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],
|
||||
@ -903,8 +876,7 @@ 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() ?? '';
|
||||
@ -913,21 +885,17 @@ 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));
|
||||
@ -936,8 +904,7 @@ 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)) {
|
||||
@ -1004,8 +971,7 @@ 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());
|
||||
@ -1024,8 +990,7 @@ 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(
|
||||
@ -1141,8 +1106,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
|
||||
|
||||
if (result['success']) {
|
||||
add(ResetRoutineState());
|
||||
add(const LoadAutomation());
|
||||
add(const LoadScenes());
|
||||
add(LoadAutomation());
|
||||
add(LoadScenes());
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
@ -1326,13 +1291,10 @@ 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(
|
||||
@ -1354,8 +1316,7 @@ 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 {
|
||||
@ -1400,24 +1361,21 @@ 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()}',
|
||||
|
@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_swit
|
||||
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart';
|
||||
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart';
|
||||
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart';
|
||||
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_presence_sensor.dart';
|
||||
|
||||
class DeviceDialogHelper {
|
||||
static Future<Map<String, dynamic>?> showDeviceDialog({
|
||||
@ -126,6 +127,15 @@ class DeviceDialogHelper {
|
||||
dialogType: dialogType,
|
||||
device: data['device'],
|
||||
);
|
||||
case 'WH':
|
||||
return WaterHeaterDialogRoutines.showWHFunctionsDialog(
|
||||
context: context,
|
||||
functions: functions,
|
||||
uniqueCustomId: data['uniqueCustomId'],
|
||||
deviceSelectedFunctions: deviceSelectedFunctions,
|
||||
dialogType: dialogType,
|
||||
device: data['device'],
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
|
@ -162,7 +162,7 @@ class SaveRoutineHelper {
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
title: const Text('Tab to run'),
|
||||
title: const Text('Tap to run'),
|
||||
),
|
||||
if (state.isAutomation)
|
||||
...state.ifItems.map((item) {
|
||||
|
@ -14,6 +14,10 @@ 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();
|
||||
@ -75,26 +79,24 @@ class ModeFunction extends ACFunction {
|
||||
}
|
||||
|
||||
class TempSetFunction extends ACFunction {
|
||||
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(
|
||||
TempSetFunction({
|
||||
required super.deviceId,
|
||||
required super.deviceName,
|
||||
required super.type,
|
||||
}) : super(
|
||||
code: 'temp_set',
|
||||
operationName: 'Set Temperature',
|
||||
icon: Assets.assetsTempreture,
|
||||
type: type,
|
||||
min: 200,
|
||||
max: 300,
|
||||
step: 1,
|
||||
unit: "°C",
|
||||
);
|
||||
|
||||
@override
|
||||
List<ACOperationalValue> getOperationalValues() {
|
||||
List<ACOperationalValue> values = [];
|
||||
for (int temp = min; temp <= max; temp += step) {
|
||||
for (int temp = min!.toInt(); temp <= max!; temp += step!.toInt()) {
|
||||
values.add(ACOperationalValue(
|
||||
icon: Assets.assetsTempreture,
|
||||
description: "${temp / 10}°C",
|
||||
@ -104,7 +106,6 @@ class TempSetFunction extends ACFunction {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
class LevelFunction extends ACFunction {
|
||||
LevelFunction(
|
||||
{required super.deviceId, required super.deviceName, required type})
|
||||
@ -166,9 +167,10 @@ class ChildLockFunction extends ACFunction {
|
||||
}
|
||||
|
||||
class CurrentTempFunction extends ACFunction {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
final String unit = "°C";
|
||||
|
||||
CurrentTempFunction(
|
||||
{required super.deviceId, required super.deviceName, required type})
|
||||
@ -185,7 +187,7 @@ class CurrentTempFunction extends ACFunction {
|
||||
@override
|
||||
List<ACOperationalValue> getOperationalValues() {
|
||||
List<ACOperationalValue> values = [];
|
||||
for (int temp = min; temp <= max; temp += step) {
|
||||
for (int temp = min.toInt(); temp <= max; temp += step.toInt()) {
|
||||
values.add(ACOperationalValue(
|
||||
icon: Assets.currentTemp,
|
||||
description: "${temp / 10}°C",
|
||||
|
@ -6,10 +6,12 @@ class CpsOperationalValue {
|
||||
final String description;
|
||||
final dynamic value;
|
||||
|
||||
|
||||
CpsOperationalValue({
|
||||
required this.icon,
|
||||
required this.description,
|
||||
required this.value,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -94,9 +96,9 @@ final class CpsSensitivityFunction extends CpsFunctions {
|
||||
icon: Assets.sensitivity,
|
||||
);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
static const _images = <String>[
|
||||
Assets.sensitivityFeature1,
|
||||
@ -115,10 +117,10 @@ final class CpsSensitivityFunction extends CpsFunctions {
|
||||
@override
|
||||
List<CpsOperationalValue> getOperationalValues() {
|
||||
final values = <CpsOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min; value <= max; value += step.toInt()) {
|
||||
values.add(
|
||||
CpsOperationalValue(
|
||||
icon: _images[value],
|
||||
icon: _images[value.toInt()],
|
||||
description: '$value',
|
||||
value: value,
|
||||
),
|
||||
@ -142,9 +144,9 @@ final class CpsMovingSpeedFunction extends CpsFunctions {
|
||||
icon: Assets.speedoMeter,
|
||||
);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
List<CpsOperationalValue> getOperationalValues() {
|
||||
@ -173,9 +175,9 @@ final class CpsSpatialStaticValueFunction extends CpsFunctions {
|
||||
icon: Assets.spatialStaticValue,
|
||||
);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
List<CpsOperationalValue> getOperationalValues() {
|
||||
@ -204,9 +206,9 @@ final class CpsSpatialMotionValueFunction extends CpsFunctions {
|
||||
icon: Assets.spatialMotionValue,
|
||||
);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
List<CpsOperationalValue> getOperationalValues() {
|
||||
@ -375,9 +377,9 @@ final class CpsPresenceJudgementThrsholdFunction extends CpsFunctions {
|
||||
icon: Assets.presenceJudgementThrshold,
|
||||
);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
List<CpsOperationalValue> getOperationalValues() {
|
||||
@ -406,9 +408,9 @@ final class CpsMotionAmplitudeTriggerThresholdFunction extends CpsFunctions {
|
||||
icon: Assets.presenceJudgementThrshold,
|
||||
);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
List<CpsOperationalValue> getOperationalValues() {
|
||||
|
@ -4,6 +4,11 @@ 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,
|
||||
@ -11,6 +16,10 @@ abstract class DeviceFunction<T> {
|
||||
required this.code,
|
||||
required this.operationName,
|
||||
required this.icon,
|
||||
this.step,
|
||||
this.unit,
|
||||
this.max,
|
||||
this.min,
|
||||
});
|
||||
}
|
||||
|
||||
@ -22,6 +31,10 @@ 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,
|
||||
@ -31,6 +44,10 @@ class DeviceFunctionData {
|
||||
required this.value,
|
||||
this.condition,
|
||||
this.valueDescription,
|
||||
this.step,
|
||||
this.unit,
|
||||
this.max,
|
||||
this.min,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@ -42,6 +59,10 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,6 +75,10 @@ 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(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -68,7 +93,11 @@ class DeviceFunctionData {
|
||||
other.operationName == operationName &&
|
||||
other.value == value &&
|
||||
other.condition == condition &&
|
||||
other.valueDescription == valueDescription;
|
||||
other.valueDescription == valueDescription &&
|
||||
other.step == step &&
|
||||
other.unit == unit &&
|
||||
other.max == max &&
|
||||
other.min == min;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -79,6 +108,10 @@ class DeviceFunctionData {
|
||||
operationName.hashCode ^
|
||||
value.hashCode ^
|
||||
condition.hashCode ^
|
||||
valueDescription.hashCode;
|
||||
valueDescription.hashCode ^
|
||||
step.hashCode ^
|
||||
unit.hashCode ^
|
||||
max.hashCode ^
|
||||
min.hashCode;
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,11 @@ 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',
|
||||
@ -50,9 +49,9 @@ class FlushPresenceDelayFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushSensiReduceFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
FlushSensiReduceFunction({
|
||||
required super.deviceId,
|
||||
@ -80,8 +79,8 @@ class FlushSensiReduceFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushNoneDelayFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final double min;
|
||||
final double max;
|
||||
final String unit;
|
||||
|
||||
FlushNoneDelayFunction({
|
||||
@ -110,9 +109,9 @@ class FlushNoneDelayFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushIlluminanceFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
FlushIlluminanceFunction({
|
||||
required super.deviceId,
|
||||
@ -130,7 +129,7 @@ class FlushIlluminanceFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
List<FlushOperationalValue> values = [];
|
||||
for (int lux = min; lux <= max; lux += step) {
|
||||
for (int lux = min.toInt(); lux <= max; lux += step.toInt()) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.IlluminanceIcon,
|
||||
description: "$lux Lux",
|
||||
@ -142,9 +141,9 @@ class FlushIlluminanceFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushOccurDistReduceFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
FlushOccurDistReduceFunction({
|
||||
required super.deviceId,
|
||||
@ -173,9 +172,9 @@ class FlushOccurDistReduceFunction extends FlushFunctions {
|
||||
|
||||
// ==== then functions ====
|
||||
class FlushSensitivityFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
|
||||
FlushSensitivityFunction({
|
||||
required super.deviceId,
|
||||
@ -203,9 +202,9 @@ class FlushSensitivityFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushNearDetectionFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final double min;
|
||||
final double max;
|
||||
final int step;
|
||||
final double step;
|
||||
final String unit;
|
||||
|
||||
FlushNearDetectionFunction({
|
||||
@ -225,7 +224,7 @@ class FlushNearDetectionFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
final values = <FlushOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min.toDouble(); value <= max; value += step) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.nobodyTime,
|
||||
description: '$value $unit',
|
||||
@ -237,9 +236,9 @@ class FlushNearDetectionFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushMaxDetectDistFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
final String unit;
|
||||
|
||||
FlushMaxDetectDistFunction({
|
||||
@ -259,7 +258,7 @@ class FlushMaxDetectDistFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
final values = <FlushOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min; value <= max; value += step.toInt()) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.nobodyTime,
|
||||
description: '$value $unit',
|
||||
@ -271,9 +270,9 @@ class FlushMaxDetectDistFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushTargetConfirmTimeFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
final String unit;
|
||||
|
||||
FlushTargetConfirmTimeFunction({
|
||||
@ -293,7 +292,7 @@ class FlushTargetConfirmTimeFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
final values = <FlushOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min.toDouble(); value <= max; value += step) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.nobodyTime,
|
||||
description: '$value $unit',
|
||||
@ -305,9 +304,9 @@ class FlushTargetConfirmTimeFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushDisappeDelayFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
final String unit;
|
||||
|
||||
FlushDisappeDelayFunction({
|
||||
@ -327,7 +326,7 @@ class FlushDisappeDelayFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
final values = <FlushOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min.toDouble(); value <= max; value += step) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.nobodyTime,
|
||||
description: '$value $unit',
|
||||
@ -339,9 +338,9 @@ class FlushDisappeDelayFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushIndentLevelFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
final String unit;
|
||||
|
||||
FlushIndentLevelFunction({
|
||||
@ -361,7 +360,7 @@ class FlushIndentLevelFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
final values = <FlushOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min.toDouble(); value <= max; value += step) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.nobodyTime,
|
||||
description: '$value $unit',
|
||||
@ -373,9 +372,9 @@ class FlushIndentLevelFunction extends FlushFunctions {
|
||||
}
|
||||
|
||||
class FlushTriggerLevelFunction extends FlushFunctions {
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final double min;
|
||||
final double max;
|
||||
final double step;
|
||||
final String unit;
|
||||
|
||||
FlushTriggerLevelFunction({
|
||||
@ -395,7 +394,7 @@ class FlushTriggerLevelFunction extends FlushFunctions {
|
||||
@override
|
||||
List<FlushOperationalValue> getOperationalValues() {
|
||||
final values = <FlushOperationalValue>[];
|
||||
for (var value = min; value <= max; value += step) {
|
||||
for (var value = min.toDouble(); value <= max; value += step) {
|
||||
values.add(FlushOperationalValue(
|
||||
icon: Assets.nobodyTime,
|
||||
description: '$value $unit',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user