Compare commits
303 Commits
SP-1464-FE
...
SP-1603-FE
Author | SHA1 | Date | |
---|---|---|---|
fccf395c38 | |||
7c65b874eb | |||
25db6ec687 | |||
7f5d2ca6ea | |||
5a5173c19b | |||
83363b4c50 | |||
6ebdc59966 | |||
5f3a0c74ac | |||
03009ed276 | |||
a1142eb38c | |||
1aa7bf2162 | |||
043820f84f | |||
d90d3d4026 | |||
3ac5254abf | |||
f5d926f5a2 | |||
c1d6db8bba | |||
50fc5f9562 | |||
1b0d8d446c | |||
8a5173f429 | |||
bee8652d03 | |||
9546d7bdd1 | |||
cb4956f915 | |||
ec7b0aa078 | |||
296b03e1aa | |||
177c7f1030 | |||
3746c36a71 | |||
0b4337fb6c | |||
171dc52e28 | |||
642d8e9591 | |||
5a8ef578c3 | |||
63ca98895f | |||
7e54cfdccd | |||
fb4d44450f | |||
12e4285b14 | |||
82adbcf4df | |||
7305d511bc | |||
61acaa17c5 | |||
4af81bcc10 | |||
d4dd7a19ba | |||
9ab906d24c | |||
5c57143ea5 | |||
4a3085e1b4 | |||
eb8ba1806c | |||
902419f9c4 | |||
926bcd9a5d | |||
33f9add78a | |||
563a3e1cf5 | |||
791b71276a | |||
24e3eb2311 | |||
82006e9aaf | |||
cedef666f6 | |||
a10d998ec6 | |||
ed50ac03d3 | |||
cd2eb46f49 | |||
39351a710d | |||
c8fe4e3baa | |||
12deceb7d3 | |||
9d27ed2dc5 | |||
a878b9328a | |||
6606491458 | |||
92abcdc4f9 | |||
7aa9e7e5dc | |||
e9abac7933 | |||
0f9227a6f5 | |||
5b13962d41 | |||
8c53d5322a | |||
af4d37939b | |||
d43c1847ff | |||
4c5b390887 | |||
5eeac01666 | |||
717d698378 | |||
9adbbb9a2d | |||
e792dbd72f | |||
9eaa367d32 | |||
d2eea33714 | |||
24372a0618 | |||
8988947694 | |||
ef875ef7dc | |||
5a61647fe4 | |||
568b6be354 | |||
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 | |||
53222bee81 | |||
bfb9158652 | |||
7f03222c12 | |||
5e6c14efeb | |||
9bbf3e75fa | |||
303b0236f1 | |||
4e3e63723e | |||
38ff20f86a | |||
d539e6266e | |||
7467f8d0ea | |||
a11e20147e | |||
55a6974bdc | |||
f8f58a24b8 | |||
682e69e65f | |||
59a59231ec | |||
ad41a2a87e | |||
974aa8f2a4 | |||
428cd34492 | |||
1a6121c452 | |||
e8f9ae944c | |||
7e37aed026 | |||
d89e227599 | |||
5a68b22f0c | |||
38184ca8b2 | |||
4d5de7bc05 | |||
1a3006fa43 | |||
490ca2057e | |||
06637a16bb | |||
696978a78d | |||
818e4e4d51 | |||
af877d7839 | |||
a33b1e3f49 | |||
c3cce334ab | |||
947e9e404c | |||
cd8264b6ce | |||
7467be6980 | |||
0353c73dac | |||
a050792f32 | |||
464f7b7347 | |||
cd54574279 | |||
18acae3e85 | |||
f081a7fc2d | |||
5996ff3928 | |||
a0d1cb988a | |||
c3ec9000d4 | |||
3d6a60b406 | |||
69c9240641 | |||
098013e5c8 | |||
11fb9e4894 | |||
390da9213d | |||
cae8b029fe | |||
6b883c8bb3 | |||
08c99bcbcb | |||
f6448d3eff | |||
a657a9a25e | |||
f55fa25bdf | |||
7242218b2f | |||
e43de3f64c | |||
9c250986b2 | |||
d8faafd1c0 | |||
24c30ddcb5 | |||
bafd2b4d13 | |||
56f9b1fc9a | |||
a9cc92ff86 | |||
3c7edae88a | |||
56c2d11535 | |||
3aa5bff758 | |||
28d1e5a5a7 | |||
fe036a8190 | |||
82e145de9d | |||
ebeb514a5b | |||
6b7e02ee53 | |||
b01136b6e9 | |||
97f8c6c8c9 | |||
6e527503c1 | |||
d6ef06c1b3 | |||
c9aaf2580f | |||
d9cd5d0438 | |||
3eb87dfde1 | |||
f29ff2551f | |||
67dd59ee9c | |||
bb3c3906d1 | |||
3873deca90 | |||
9431dd4500 | |||
63718185e7 | |||
1f4e82d567 | |||
9f68d171ff | |||
6eba640037 | |||
7a088074e3 | |||
d8f40badc0 | |||
fdd5d0feed | |||
fb1f79c7bb | |||
1923ac7014 | |||
c114161357 | |||
fe1dbb66ac | |||
ea88f54d20 | |||
ccce7bb671 | |||
b4de07de2f | |||
acefe6b433 | |||
63bc7a56de | |||
7b3635deae | |||
58755eafe1 | |||
ce225818fb | |||
8762a7aaa8 | |||
8dc833b2c3 | |||
13cef151aa | |||
ab23be9828 | |||
687b68ab22 | |||
25614c3dd0 | |||
7cbe20ae88 | |||
349fe6c555 | |||
9779f3783c | |||
fe3db663b6 | |||
888d444752 | |||
bab5e06968 | |||
d7b6174dee | |||
6ef0b2f9d1 | |||
3ceb03826e | |||
52608b1f35 | |||
ac2996629e | |||
51c52c66cb | |||
0f56273d99 | |||
34d4d892d9 | |||
3193fd26fe | |||
43802a9fad | |||
6e0b1775f0 | |||
233fb2ee2c | |||
b26928b3d5 | |||
6fc35a7b9a | |||
756457927c | |||
f30d7c0117 | |||
976d6e385a | |||
ff07e7509d | |||
17a582ab99 | |||
09fb604acc | |||
2068df173d | |||
bfc2a381d2 | |||
c03b8f290c | |||
2c684a9495 |
30
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
<!--
|
||||
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
@ -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:
|
||||
@ -26,6 +27,7 @@ linter:
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
prefer_const_constructors: true
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
3
assets/icons/blank_calendar.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 2.1875H14.6875V1.875C14.6875 1.62636 14.5887 1.3879 14.4129 1.21209C14.2371 1.03627 13.9986 0.9375 13.75 0.9375C13.5014 0.9375 13.2629 1.03627 13.0871 1.21209C12.9113 1.3879 12.8125 1.62636 12.8125 1.875V2.1875H7.1875V1.875C7.1875 1.62636 7.08873 1.3879 6.91291 1.21209C6.7371 1.03627 6.49864 0.9375 6.25 0.9375C6.00136 0.9375 5.7629 1.03627 5.58709 1.21209C5.41127 1.3879 5.3125 1.62636 5.3125 1.875V2.1875H3.75C3.3356 2.1875 2.93817 2.35212 2.64515 2.64515C2.35212 2.93817 2.1875 3.3356 2.1875 3.75V16.25C2.1875 16.6644 2.35212 17.0618 2.64515 17.3549C2.93817 17.6479 3.3356 17.8125 3.75 17.8125H16.25C16.6644 17.8125 17.0618 17.6479 17.3549 17.3549C17.6479 17.0618 17.8125 16.6644 17.8125 16.25V3.75C17.8125 3.3356 17.6479 2.93817 17.3549 2.64515C17.0618 2.35212 16.6644 2.1875 16.25 2.1875ZM5.3125 4.0625C5.3125 4.31114 5.41127 4.5496 5.58709 4.72541C5.7629 4.90123 6.00136 5 6.25 5C6.49864 5 6.7371 4.90123 6.91291 4.72541C7.08873 4.5496 7.1875 4.31114 7.1875 4.0625H12.8125C12.8125 4.31114 12.9113 4.5496 13.0871 4.72541C13.2629 4.90123 13.5014 5 13.75 5C13.9986 5 14.2371 4.90123 14.4129 4.72541C14.5887 4.5496 14.6875 4.31114 14.6875 4.0625H15.9375V5.9375H4.0625V4.0625H5.3125ZM4.0625 15.9375V7.8125H15.9375V15.9375H4.0625Z" fill="#475569"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
17
assets/icons/disappe_delay_icon.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8194_10048)">
|
||||
<path d="M11.4732 16.7316C8.31096 16.7316 5.73828 14.1589 5.73828 10.9967C5.73828 7.8344 8.31096 5.26172 11.4732 5.26172C14.6355 5.26172 17.2081 7.8344 17.2081 10.9967C17.2081 14.1589 14.6355 16.7316 11.4732 16.7316Z" fill="#EBF6FF"/>
|
||||
<path d="M17.2076 10.9967C17.2076 7.8344 14.6349 5.26172 11.4727 5.26172V16.7316C14.6349 16.7316 17.2076 14.1589 17.2076 10.9967Z" fill="#D7E7F8"/>
|
||||
<path d="M11.4721 17.3699C7.95827 17.3699 5.09961 14.5112 5.09961 10.9974C5.09961 7.48362 7.95827 4.625 11.472 4.625C14.9858 4.625 17.8445 7.48366 17.8445 10.9974C17.8445 14.5112 14.9858 17.3699 11.4721 17.3699ZM11.4721 5.89473C8.6584 5.89473 6.36934 8.18379 6.36934 10.9974C6.36934 13.811 8.6584 16.1001 11.472 16.1001C14.2857 16.1001 16.5748 13.8111 16.5748 10.9974C16.5748 8.18375 14.2857 5.89473 11.4721 5.89473Z" fill="#9FE066"/>
|
||||
<path d="M11.4728 11.6318H8.28525C7.93464 11.6318 7.65039 11.3475 7.65039 10.9969C7.65039 10.6463 7.93464 10.3621 8.28525 10.3621H10.8379V8.95127C10.8379 8.60066 11.1222 8.31641 11.4728 8.31641C11.8234 8.31641 12.1076 8.60066 12.1076 8.95127V10.9969C12.1077 11.3475 11.8234 11.6318 11.4728 11.6318Z" fill="#394949"/>
|
||||
<path d="M12.1075 10.9969V8.95127C12.1075 8.60066 11.8233 8.31641 11.4727 8.31641V11.6318C11.8233 11.6318 12.1075 11.3475 12.1075 10.9969Z" fill="#151F1F"/>
|
||||
<path d="M16.5753 10.9974C16.5753 13.8111 14.2863 16.1001 11.4727 16.1001V17.3698C14.9865 17.3698 17.8451 14.5112 17.8451 10.9974C17.8451 7.48366 14.9864 4.625 11.4727 4.625V5.89473C14.2863 5.89473 16.5753 8.18379 16.5753 10.9974Z" fill="#4ACA7B"/>
|
||||
<path d="M18.6832 3.78752C16.7572 1.86147 14.1963 0.800781 11.4725 0.800781C8.74869 0.800781 6.18782 1.86147 4.26177 3.78752C2.67775 5.37158 1.67966 7.38516 1.37492 9.56545L1.08378 9.2743C0.835882 9.02641 0.433887 9.02641 0.185951 9.2743C-0.0619838 9.52219 -0.0619838 9.92419 0.185951 10.1721L1.46097 11.4471C1.58494 11.5711 1.74742 11.6331 1.90986 11.6331C2.0723 11.6331 2.23482 11.5711 2.35875 11.4471L3.63377 10.1721C3.8817 9.92423 3.8817 9.52224 3.63377 9.2743C3.38587 9.02641 2.98388 9.02641 2.73594 9.2743L2.70678 9.30346C3.50083 5.18845 7.12907 2.07051 11.4725 2.07051C16.3953 2.07051 20.4003 6.07548 20.4003 10.9983C20.4003 15.921 16.3953 19.926 11.4725 19.926C7.46868 19.926 3.9294 17.234 2.86558 13.3794C2.77234 13.0413 2.42266 12.8429 2.08474 12.9363C1.74674 13.0295 1.54837 13.3792 1.64165 13.7171C2.22378 15.8264 3.50315 17.7286 5.24408 19.0732C7.04184 20.4618 9.19555 21.1957 11.4725 21.1957C14.1963 21.1957 16.7571 20.135 18.6832 18.209C20.6093 16.2829 21.67 13.7221 21.67 10.9983C21.67 8.27439 20.6093 5.71361 18.6832 3.78752Z" fill="#FF4D5B"/>
|
||||
<path d="M20.4004 10.9983C20.4004 15.9211 16.3955 19.926 11.4727 19.926V21.1958C14.1965 21.1958 16.7573 20.135 18.6834 18.209C20.6095 16.2829 21.6702 13.7221 21.6702 10.9983C21.6702 8.27439 20.6094 5.71361 18.6834 3.78752C16.7573 1.86147 14.1965 0.800781 11.4727 0.800781V2.07051C16.3955 2.07051 20.4004 6.07548 20.4004 10.9983Z" fill="#DE0062"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8194_10048">
|
||||
<rect width="21.67" height="21.67" fill="white" transform="translate(0 0.164062)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
10
assets/icons/energy_consumed_icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6.90237L13.6328 13.1837L6.60156 6.1524L2.32422 10.418L1.50391 9.59769L6.60156 4.48828L13.6328 11.5195L19.1797 6.08206L20 6.90237Z" fill="#64E1DC"/>
|
||||
<path d="M20 13.1133L19.1797 13.9336L13.6328 8.49615L7.77344 14.3555L5.42969 12.0118L2.32422 15.1055L1.50391 14.2852L5.42969 10.3477L7.77344 12.6914L13.6328 6.83203L20 13.1133Z" fill="#FDBF00"/>
|
||||
<path d="M20 6.90234L13.6328 13.1836L10.1172 9.668V8.00388L13.6328 11.5195L19.1797 6.08203L20 6.90234Z" fill="#00C8C8"/>
|
||||
<path d="M20 13.1133L19.1797 13.9336L13.6328 8.49615L10.1172 12.0118V10.3477L13.6328 6.83203L20 13.1133Z" fill="#FF9100"/>
|
||||
<path d="M19.1714 17.625V18.7813L17.7184 18.7821L10.1172 18.7851L1.32812 18.7891V0.75H2.5V17.625H19.1714Z" fill="#676E74"/>
|
||||
<path d="M3.0127 2.37976L1.91406 1.50024L0.732422 2.37976L0 1.46423L1.91406 0L3.74512 1.46423L3.0127 2.37976Z" fill="#676E74"/>
|
||||
<path d="M19.1714 17.625V18.7813L17.7176 18.7824L17.7184 18.7821L10.1172 18.7851V17.625H19.1714Z" fill="#474F54"/>
|
||||
<path d="M19.9998 18.2108L18.1205 19.9999L17.292 19.1714L17.7174 18.7823L17.7182 18.782L18.3427 18.2108L17.7565 17.6249L17.292 17.1604L18.1205 16.332L19.9998 18.2108Z" fill="#474F54"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
13
assets/icons/indent_level_icon.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8194_10065)">
|
||||
<path d="M17.6069 21.8341H4.06313C1.81893 21.8341 0 20.0151 0 17.7709V4.22719C0 1.98299 1.81893 0.164062 4.06313 0.164062H17.6069C19.8511 0.164062 21.67 1.98299 21.67 4.22719V17.7709C21.67 20.0151 19.8511 21.8341 17.6069 21.8341Z" fill="#E3F8FA"/>
|
||||
<path d="M15.1312 7.18375L10.955 5.60387C10.8771 5.57475 10.7925 5.57475 10.7153 5.60387L6.53904 7.18375C6.40767 7.23386 6.32031 7.35982 6.32031 7.50067V9.30605C6.32031 9.41711 6.37516 9.5214 6.46591 9.58438C6.55868 9.64736 6.67516 9.6609 6.77877 9.6223L10.8351 8.08779L14.8915 9.62298C14.9301 9.6372 14.9707 9.64465 15.0113 9.64465C15.0791 9.64465 15.1461 9.62433 15.2043 9.58438C15.2958 9.5214 15.3499 9.41711 15.3499 9.30605V7.5C15.3499 7.35914 15.2626 7.23318 15.1312 7.18375Z" fill="#8CE1EB"/>
|
||||
<path d="M15.1312 13.9572L10.955 12.3773C10.8771 12.3482 10.7925 12.3482 10.7153 12.3773L6.53904 13.9572C6.40767 14.0073 6.32031 14.1333 6.32031 14.2741V16.0795C6.32031 16.1906 6.37516 16.2948 6.46591 16.3578C6.55868 16.4215 6.67516 16.435 6.77877 16.3957L10.8351 14.8612L14.8915 16.3964C14.9301 16.4106 14.9707 16.4181 15.0113 16.4181C15.0791 16.4181 15.1461 16.3978 15.2043 16.3578C15.2958 16.2948 15.3499 16.1906 15.3499 16.0795V14.2734C15.3499 14.1326 15.2626 14.0066 15.1312 13.9572Z" fill="#26C6DA"/>
|
||||
<path d="M15.1312 10.5705L10.955 8.99059C10.8771 8.96147 10.7925 8.96147 10.7153 8.99059L6.53904 10.5705C6.40767 10.6206 6.32031 10.7465 6.32031 10.8874V12.6928C6.32031 12.8038 6.37516 12.9081 6.46591 12.9711C6.55868 13.0348 6.67516 13.0483 6.77877 13.009L10.8351 11.4745L14.8915 13.0097C14.9301 13.0239 14.9707 13.0314 15.0113 13.0314C15.0791 13.0314 15.1461 13.0111 15.2043 12.9711C15.2958 12.9081 15.3499 12.8038 15.3499 12.6928V10.8867C15.3499 10.7459 15.2626 10.6199 15.1312 10.5705Z" fill="#26C6DA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8194_10065">
|
||||
<rect width="21.67" height="21.67" fill="white" transform="translate(0 0.164062)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
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
@ -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 |
21
assets/icons/target_confirm_time_icon.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.7802 9.0399L20.0667 9.18046C19.7057 9.25157 19.4663 9.5894 19.5103 9.95466C19.6254 10.9106 19.5871 11.9137 19.3549 12.9433C18.5334 16.5849 15.4143 19.3394 11.6982 19.6957C6.22309 20.2206 1.65927 15.6884 2.13085 10.223C2.48684 6.09727 5.78343 2.73681 9.90242 2.3098C12.864 2.00274 15.5631 3.17762 17.3531 5.18175L18.396 4.25096C16.4588 2.08197 13.6036 0.750365 10.4397 0.87082C5.29437 1.06665 1.04625 5.19944 0.720226 10.3382C0.34608 16.2355 5.01846 21.1352 10.8352 21.1352C16.4331 21.1352 20.9712 16.5971 20.9712 10.9992C20.9712 10.3288 20.9044 9.674 20.7802 9.0399Z" fill="#FFF0D2"/>
|
||||
<path d="M10.8355 12.7451C11.8006 12.7451 12.583 11.9627 12.583 10.9976C12.583 10.0324 11.8006 9.25 10.8355 9.25C9.8703 9.25 9.08789 10.0324 9.08789 10.9976C9.08789 11.9627 9.8703 12.7451 10.8355 12.7451Z" fill="#FFF0D2"/>
|
||||
<path d="M10.8358 16.5911C10.6426 16.5911 10.4863 16.4346 10.4863 16.2416V10.9989C10.4863 10.9062 10.5232 10.8173 10.5887 10.7517L14.4334 6.90709C14.5699 6.77055 14.7911 6.77055 14.9276 6.90709C15.0642 7.04363 15.0642 7.26482 14.9276 7.40131L11.1854 11.1436V16.2416C11.1854 16.4347 11.029 16.5911 10.8358 16.5911Z" fill="#7A4646"/>
|
||||
<path d="M10.8356 12.0463C11.4147 12.0463 11.8842 11.5769 11.8842 10.9978C11.8842 10.4187 11.4147 9.94922 10.8356 9.94922C10.2566 9.94922 9.78711 10.4187 9.78711 10.9978C9.78711 11.5769 10.2566 12.0463 10.8356 12.0463Z" fill="#AA6E4D"/>
|
||||
<path d="M10.8358 4.35899C10.6426 4.35899 10.4863 4.20248 10.4863 4.00948V3.31045C10.4863 3.11745 10.6427 2.96094 10.8358 2.96094C11.0291 2.96094 11.1854 3.11745 11.1854 3.31045V4.00948C11.1854 4.20248 11.029 4.35899 10.8358 4.35899Z" fill="#DE966C"/>
|
||||
<path d="M10.8358 19.0387C10.6426 19.0387 10.4863 18.8822 10.4863 18.6892V17.9901C10.4863 17.7971 10.6427 17.6406 10.8358 17.6406C11.0291 17.6406 11.1854 17.7971 11.1854 17.9901V18.6892C11.1854 18.8822 11.029 19.0387 10.8358 19.0387Z" fill="#DE966C"/>
|
||||
<path d="M18.5251 11.3475H17.8261C17.6329 11.3475 17.4766 11.1909 17.4766 10.998C17.4766 10.805 17.6329 10.6484 17.8261 10.6484H18.5251C18.7183 10.6484 18.8746 10.805 18.8746 10.998C18.8746 11.1909 18.7183 11.3475 18.5251 11.3475Z" fill="#DE966C"/>
|
||||
<path d="M3.84546 11.3475H3.14639C2.95318 11.3475 2.79688 11.1909 2.79688 10.998C2.79688 10.805 2.95322 10.6484 3.14639 10.6484H3.84541C4.03863 10.6484 4.19493 10.805 4.19493 10.998C4.19493 11.1909 4.03863 11.3475 3.84546 11.3475Z" fill="#DE966C"/>
|
||||
<path d="M4.78099 7.85338C4.72161 7.85338 4.66151 7.83836 4.60657 7.80661L4.00108 7.4571C3.83382 7.36051 3.77651 7.14682 3.87309 6.9796C3.96968 6.81233 4.18371 6.75502 4.35059 6.85161L4.95608 7.20112C5.12335 7.29771 5.18066 7.5114 5.08407 7.67862C5.01923 7.79074 4.90178 7.85338 4.78099 7.85338Z" fill="#F7B97E"/>
|
||||
<path d="M17.4939 15.1933C17.4345 15.1933 17.3744 15.1782 17.3195 15.1465L16.714 14.797C16.5467 14.7004 16.4894 14.4867 16.586 14.3195C16.6826 14.152 16.8966 14.0949 17.0635 14.1915L17.669 14.541C17.8362 14.6376 17.8935 14.8513 17.797 15.0185C17.7321 15.1306 17.6147 15.1933 17.4939 15.1933Z" fill="#F7B97E"/>
|
||||
<path d="M14.3298 5.29299C14.2704 5.29299 14.2103 5.27796 14.1554 5.24622C13.9881 5.14964 13.9308 4.93594 14.0274 4.76872L14.3769 4.16323C14.4735 3.99579 14.6875 3.93866 14.8544 4.03524C15.0217 4.13182 15.079 4.34552 14.9824 4.51274L14.6329 5.11823C14.568 5.23035 14.4506 5.29299 14.3298 5.29299Z" fill="#F7B97E"/>
|
||||
<path d="M6.98995 18.0077C6.93057 18.0077 6.87047 17.9927 6.81553 17.961C6.64827 17.8644 6.59096 17.6507 6.68755 17.4834L7.03706 16.878C7.13331 16.7107 7.34768 16.6536 7.51456 16.75C7.68183 16.8466 7.73913 17.0602 7.64255 17.2275L7.29304 17.833C7.2282 17.9451 7.11079 18.0077 6.98995 18.0077Z" fill="#F7B97E"/>
|
||||
<path d="M16.8891 7.85337C16.7682 7.85337 16.6508 7.79073 16.586 7.67861C16.4894 7.51135 16.5467 7.2977 16.714 7.20111L17.3195 6.8516C17.486 6.75502 17.7004 6.81236 17.797 6.97959C17.8935 7.14685 17.8362 7.36051 17.669 7.45709L17.0635 7.8066C17.0085 7.83835 16.9485 7.85337 16.8891 7.85337Z" fill="#F7B97E"/>
|
||||
<path d="M4.17617 15.1933C4.05533 15.1933 3.93792 15.1306 3.87308 15.0185C3.7765 14.8512 3.83385 14.6376 4.00107 14.541L4.60656 14.1915C4.77277 14.0949 4.98714 14.1521 5.08406 14.3195C5.18065 14.4867 5.1233 14.7004 4.95608 14.797L4.35059 15.1465C4.29565 15.1782 4.23555 15.1933 4.17617 15.1933Z" fill="#F7B97E"/>
|
||||
<path d="M7.34013 5.29298C7.2193 5.29298 7.10189 5.23034 7.03705 5.11822L6.68754 4.51273C6.59095 4.34547 6.6483 4.13181 6.81553 4.03523C6.98207 3.93865 7.1961 3.99583 7.29303 4.16322L7.64254 4.76871C7.73912 4.93597 7.68178 5.14963 7.51455 5.24621C7.45962 5.27795 7.39956 5.29298 7.34013 5.29298Z" fill="#F7B97E"/>
|
||||
<path d="M14.68 18.0077C14.5591 18.0077 14.4417 17.9451 14.3769 17.833L14.0274 17.2275C13.9308 17.0602 13.9881 16.8465 14.1554 16.75C14.3219 16.6535 14.5363 16.7107 14.6329 16.8779L14.9824 17.4834C15.079 17.6507 15.0216 17.8644 14.8544 17.9609C14.7994 17.9927 14.7394 18.0077 14.68 18.0077Z" fill="#F7B97E"/>
|
||||
<path d="M10.8358 11.3475C11.0289 11.3475 11.1854 11.191 11.1854 10.998C11.1854 10.8049 11.0289 10.6484 10.8358 10.6484C10.6428 10.6484 10.4863 10.8049 10.4863 10.998C10.4863 11.191 10.6428 11.3475 10.8358 11.3475Z" fill="#7A4646"/>
|
||||
<path d="M10.8356 1.56212C5.42443 1.56212 1.07697 6.11655 1.41743 11.6012C1.70913 16.3007 5.53397 20.1256 10.2335 20.4173C15.7181 20.7578 20.2726 16.4103 20.2726 10.9991C20.2726 10.2541 20.1854 9.5297 20.0217 8.83474C19.9829 8.66988 20.0623 8.5019 20.2172 8.43346C20.4105 8.34805 20.6773 8.24402 20.8922 8.16267C21.0866 8.08907 21.3059 8.19949 21.3557 8.40125C21.6326 9.52411 21.7356 10.7154 21.6299 11.9463C21.4062 14.551 20.2433 16.8986 18.4888 18.652C18.4888 18.652 14.9619 21.4157 11.4687 21.4157C4.94621 21.4157 0.410156 16.7271 0.410156 10.1916C0.410156 7.40625 3.18409 3.34816 3.18409 3.34816C5.04237 1.49153 7.56422 0.300058 10.3375 0.175287C13.4409 0.035659 16.2657 1.20796 18.3212 3.17781L18.9202 2.71389C19.1388 2.5446 19.4582 2.68418 19.4824 2.95967L19.7199 5.66482C19.7422 5.91826 19.4949 6.10982 19.2551 6.02483L16.6954 5.11791C16.4348 5.02556 16.3795 4.68142 16.5982 4.51212L17.2071 4.04055C15.5283 2.50215 13.2923 1.56212 10.8356 1.56212Z" fill="#FB5F7A"/>
|
||||
<path d="M18.485 18.6485C18.2212 18.3848 17.7996 18.3723 17.5193 18.6183C15.5456 20.3511 12.893 21.3292 10.0123 21.102C5.09917 20.7145 1.11879 16.7339 0.731479 11.8208C0.50441 8.94025 1.48256 6.28788 3.21519 4.3143C3.46126 4.03403 3.44709 3.61138 3.18336 3.34766C1.37642 5.15274 0.197009 7.58634 0.0232257 10.2799C-0.397562 16.8019 4.95607 22.1858 11.468 21.8155C14.1882 21.6609 16.6595 20.4788 18.4881 18.6515L18.485 18.6485Z" fill="#F74455"/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
23
assets/icons/trigger_level_icon.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8194_10077)">
|
||||
<path d="M17.6 18.673C17.3521 18.4251 17.3521 18.0231 17.6 17.7752C19.4054 15.9698 20.3996 13.5678 20.3996 11.0115C20.3996 8.45525 19.4054 6.05318 17.6 4.24784C17.3521 3.99994 17.3521 3.59795 17.6 3.35001C17.8479 3.10208 18.2499 3.10208 18.4978 3.35001C20.543 5.39516 21.6694 8.11606 21.6694 11.0115C21.6694 13.9069 20.543 16.6278 18.4978 18.673C18.2503 18.9205 17.8484 18.9213 17.6 18.673Z" fill="#7AFFE4"/>
|
||||
<path d="M15.6781 16.7502C15.4302 16.5023 15.4302 16.1003 15.6781 15.8524C16.9704 14.5602 17.6821 12.8408 17.6821 11.011C17.6821 9.18128 16.9705 7.46194 15.6781 6.1697C15.4302 5.92176 15.4302 5.51981 15.6781 5.27187C15.926 5.02398 16.328 5.02394 16.576 5.27187C18.1081 6.80393 18.9519 8.84213 18.9519 11.0111C18.9519 13.18 18.1081 15.2182 16.576 16.7503C16.3279 16.9982 15.926 16.9981 15.6781 16.7502Z" fill="#00DDC1"/>
|
||||
<path d="M13.7543 14.8285C13.5064 14.5805 13.5064 14.1785 13.7543 13.9306C14.5331 13.152 14.9619 12.1158 14.9619 11.0131C14.9619 9.91034 14.5331 8.87416 13.7543 8.09548C13.5064 7.84758 13.5064 7.44559 13.7543 7.19765C14.0022 6.94976 14.4042 6.94972 14.6521 7.19765C15.6707 8.21619 16.2317 9.5712 16.2317 11.0131C16.2317 12.455 15.6707 13.81 14.6521 14.8285C14.4042 15.0764 14.0022 15.0763 13.7543 14.8285Z" fill="#7AFFE4"/>
|
||||
<path d="M17.6 18.6732C17.8484 18.9216 18.2503 18.9207 18.4978 18.6732C20.543 16.6281 21.6694 13.9072 21.6694 11.0117H20.3996C20.3996 13.568 19.4054 15.97 17.6 17.7754C17.3521 18.0233 17.3521 18.4253 17.6 18.6732Z" fill="#00DDC1"/>
|
||||
<path d="M15.6781 16.7509C15.926 16.9988 16.3279 16.9989 16.5759 16.7509C18.1081 15.2189 18.9518 13.1807 18.9518 11.0117H17.6821C17.6821 12.8415 16.9704 14.5608 15.6781 15.8531C15.4302 16.101 15.4302 16.503 15.6781 16.7509Z" fill="#00B4BC"/>
|
||||
<path d="M13.7543 14.8272C14.0022 15.0751 14.4041 15.0751 14.6521 14.8272C15.6707 13.8086 16.2316 12.4536 16.2316 11.0117H14.9619C14.9619 12.1144 14.533 13.1506 13.7543 13.9293C13.5064 14.1772 13.5064 14.5792 13.7543 14.8272Z" fill="#00DDC1"/>
|
||||
<path d="M3.17152 18.673C1.12633 16.6278 0 13.9069 0 11.0115C0 8.11604 1.12633 5.39515 3.17152 3.35C3.41946 3.10206 3.82141 3.10211 4.06935 3.35C4.31728 3.59793 4.31728 3.99989 4.06935 4.24782C2.26396 6.05316 1.26973 8.45523 1.26973 11.0115C1.26973 13.5677 2.26396 15.9698 4.06935 17.7751C4.31728 18.023 4.31728 18.425 4.06935 18.673C3.82141 18.921 3.41937 18.9209 3.17152 18.673Z" fill="#7AFFE4"/>
|
||||
<path d="M5.09462 16.7503C3.56253 15.2182 2.71875 13.18 2.71875 11.0111C2.71875 8.84212 3.56253 6.80391 5.09466 5.27186C5.3426 5.02396 5.74459 5.02396 5.99249 5.27186C6.24038 5.51979 6.24038 5.92175 5.99249 6.16968C4.70016 7.46197 3.98848 9.1813 3.98848 11.0111C3.98848 12.8408 4.70016 14.5602 5.99249 15.8524C6.24042 16.1003 6.24042 16.5023 5.99249 16.7502C5.74489 16.9978 5.34294 16.9985 5.09462 16.7503Z" fill="#00DDC1"/>
|
||||
<path d="M7.01704 14.8285C5.99847 13.81 5.4375 12.455 5.4375 11.0131C5.4375 9.5712 5.99847 8.21619 7.01704 7.19765C7.26498 6.94972 7.66697 6.94976 7.91486 7.19765C8.1628 7.44559 8.16276 7.84758 7.91486 8.09548C7.1361 8.87416 6.70723 9.91034 6.70723 11.0131C6.70723 12.1158 7.1361 13.152 7.91486 13.9306C8.1628 14.1785 8.1628 14.5805 7.91486 14.8285C7.66727 15.0761 7.26531 15.0768 7.01704 14.8285Z" fill="#7AFFE4"/>
|
||||
<path d="M4.06935 18.6732C4.31728 18.4253 4.31728 18.0233 4.06935 17.7754C2.26396 15.97 1.26973 13.568 1.26973 11.0117H0C0 13.9072 1.12633 16.6281 3.17152 18.6732C3.41937 18.9211 3.82141 18.9212 4.06935 18.6732Z" fill="#00DDC1"/>
|
||||
<path d="M5.99249 16.7509C6.24038 16.5029 6.24038 16.101 5.99249 15.8531C4.70016 14.5609 3.98848 12.8415 3.98848 11.0117H2.71875C2.71875 13.1807 3.56253 15.2189 5.09466 16.7509C5.34294 16.9992 5.74489 16.9985 5.99249 16.7509Z" fill="#00B4BC"/>
|
||||
<path d="M7.91486 14.8271C8.1628 14.5792 8.16276 14.1772 7.91486 13.9293C7.1361 13.1506 6.70723 12.1144 6.70723 11.0117H5.4375C5.4375 12.4536 5.99846 13.8086 7.01704 14.8272C7.26527 15.0754 7.66722 15.0747 7.91486 14.8271Z" fill="#00DDC1"/>
|
||||
<path d="M10.835 13.6857C12.3123 13.6857 13.5099 12.4881 13.5099 11.0108C13.5099 9.53353 12.3123 8.33594 10.835 8.33594C9.35775 8.33594 8.16016 9.53353 8.16016 11.0108C8.16016 12.4881 9.35775 13.6857 10.835 13.6857Z" fill="#00DDC1"/>
|
||||
<path d="M10.835 13.6866C12.31 13.6866 13.5099 12.4866 13.5099 11.0117H8.16016C8.16016 12.4867 9.36009 13.6866 10.835 13.6866Z" fill="#00B4BC"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8194_10077">
|
||||
<rect width="21.67" height="21.67" fill="white" transform="translate(0 0.164062)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/images/web_Background.png
Normal file
After Width: | Height: | Size: 40 KiB |
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;
|
||||
}
|
6
lib/pages/analytics/helpers/format_number_to_kwh.dart
Normal file
@ -0,0 +1,6 @@
|
||||
extension FormatNumberToKwh on num {
|
||||
String get formatNumberToKwh {
|
||||
final regExp = RegExp(r'(\d)(?=(\d{3})+$)');
|
||||
return '${toStringAsFixed(0).replaceAllMapped(regExp, (match) => '${match[1]},')} kWh';
|
||||
}
|
||||
}
|
19
lib/pages/analytics/helpers/get_month_name_from_int.dart
Normal file
@ -0,0 +1,19 @@
|
||||
extension GetMonthNameFromNumber on num {
|
||||
String get getMonthName {
|
||||
return switch (this) {
|
||||
1 => 'JAN',
|
||||
2 => 'FEB',
|
||||
3 => 'MAR',
|
||||
4 => 'APR',
|
||||
5 => 'MAY',
|
||||
6 => 'JUN',
|
||||
7 => 'JUL',
|
||||
8 => 'AUG',
|
||||
9 => 'SEP',
|
||||
10 => 'OCT',
|
||||
11 => 'NOV',
|
||||
12 => 'DEC',
|
||||
_ => 'N/A'
|
||||
};
|
||||
}
|
||||
}
|
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/device_energy_data_model.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||
|
||||
class DeviceEnergyDataModel extends Equatable {
|
||||
const DeviceEnergyDataModel({
|
||||
required this.energy,
|
||||
required this.deviceName,
|
||||
required this.deviceId,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final List<EnergyDataModel> energy;
|
||||
final String deviceName;
|
||||
final String deviceId;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [energy, deviceName, deviceId];
|
||||
|
||||
factory DeviceEnergyDataModel.fromJson(Map<String, dynamic> json) {
|
||||
final energy = (json['energy'] as List<dynamic>? ?? [])
|
||||
.map((e) => EnergyDataModel.fromJson(e))
|
||||
.toList();
|
||||
return DeviceEnergyDataModel(
|
||||
energy: energy,
|
||||
deviceName: json['device_name'] as String? ?? '',
|
||||
deviceId: json['device_id'] as String? ?? '',
|
||||
color: Color(int.parse(json['color'] as String? ?? '0xFF000000')),
|
||||
);
|
||||
}
|
||||
}
|
21
lib/pages/analytics/models/energy_data_model.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class EnergyDataModel extends Equatable {
|
||||
const EnergyDataModel({
|
||||
required this.date,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final double value;
|
||||
|
||||
factory EnergyDataModel.fromJson(Map<String, dynamic> json) {
|
||||
return EnergyDataModel(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
value: (json['value'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date, value];
|
||||
}
|
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
@ -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];
|
||||
}
|
66
lib/pages/analytics/models/phases_energy_consumption.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PhasesEnergyConsumption extends Equatable {
|
||||
final String uuid;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String deviceUuid;
|
||||
final DateTime date;
|
||||
final double energyConsumedKw;
|
||||
final double energyConsumedA;
|
||||
final double energyConsumedB;
|
||||
final double energyConsumedC;
|
||||
|
||||
const PhasesEnergyConsumption({
|
||||
required this.uuid,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deviceUuid,
|
||||
required this.date,
|
||||
required this.energyConsumedKw,
|
||||
required this.energyConsumedA,
|
||||
required this.energyConsumedB,
|
||||
required this.energyConsumedC,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
uuid,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deviceUuid,
|
||||
date,
|
||||
energyConsumedKw,
|
||||
energyConsumedA,
|
||||
energyConsumedB,
|
||||
energyConsumedC,
|
||||
];
|
||||
|
||||
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
|
||||
return PhasesEnergyConsumption(
|
||||
uuid: json['uuid'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
deviceUuid: json['deviceUuid'] as String,
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
energyConsumedKw: double.parse(json['energyConsumedKw']),
|
||||
energyConsumedA: double.parse(json['energyConsumedA']),
|
||||
energyConsumedB: double.parse(json['energyConsumedB']),
|
||||
energyConsumedC: double.parse(json['energyConsumedC']),
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
13
lib/pages/analytics/models/power_clamp_energy_status.dart
Normal file
@ -0,0 +1,13 @@
|
||||
class PowerClampEnergyStatus {
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String value;
|
||||
final String unit;
|
||||
|
||||
const PowerClampEnergyStatus({
|
||||
required this.iconPath,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
});
|
||||
}
|
18
lib/pages/analytics/models/range_of_aqi.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class RangeOfAqi extends Equatable {
|
||||
final double min;
|
||||
final double avg;
|
||||
final double max;
|
||||
final DateTime date;
|
||||
|
||||
const RangeOfAqi({
|
||||
required this.min,
|
||||
required this.avg,
|
||||
required this.max,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [min, avg, max, date];
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
|
||||
|
||||
part 'range_of_aqi_event.dart';
|
||||
part 'range_of_aqi_state.dart';
|
||||
|
||||
class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
|
||||
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
|
||||
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
|
||||
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
|
||||
}
|
||||
|
||||
final RangeOfAqiService _rangeOfAqiService;
|
||||
|
||||
Future<void> _onLoadRangeOfAqiEvent(
|
||||
LoadRangeOfAqiEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) async {
|
||||
emit(
|
||||
RangeOfAqiState(
|
||||
status: RangeOfAqiStatus.loading,
|
||||
rangeOfAqi: state.rangeOfAqi,
|
||||
),
|
||||
);
|
||||
try {
|
||||
final rangeOfAqi = await _rangeOfAqiService.load(event.param);
|
||||
emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi));
|
||||
} catch (e) {
|
||||
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e'));
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearRangeOfAqiEvent(
|
||||
ClearRangeOfAqiEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) {
|
||||
emit(const RangeOfAqiState());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
part of 'range_of_aqi_bloc.dart';
|
||||
|
||||
sealed class RangeOfAqiEvent extends Equatable {
|
||||
const RangeOfAqiEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
|
||||
const LoadRangeOfAqiEvent(this.param);
|
||||
|
||||
final GetRangeOfAqiParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
|
||||
const ClearRangeOfAqiEvent();
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
part of 'range_of_aqi_bloc.dart';
|
||||
|
||||
enum RangeOfAqiStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class RangeOfAqiState extends Equatable {
|
||||
const RangeOfAqiState({
|
||||
this.rangeOfAqi = const [],
|
||||
this.status = RangeOfAqiStatus.initial,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final RangeOfAqiStatus status;
|
||||
final List<RangeOfAqi> rangeOfAqi;
|
||||
final String? errorMessage;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, rangeOfAqi, errorMessage];
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.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/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
|
||||
|
||||
abstract final class FetchAirQualityDataHelper {
|
||||
const FetchAirQualityDataHelper._();
|
||||
|
||||
static void loadAirQualityData(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
);
|
||||
loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
aqiType: AqiType.aqi,
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
const ClearAnalyticsDeviceEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
|
||||
}
|
||||
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['AQI'],
|
||||
requestType: AnalyticsDeviceRequestType.energyManagement,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(RealtimeDeviceChangesStarted(device.uuid));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadRangeOfAqi(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
required AqiType aqiType,
|
||||
}) {
|
||||
context.read<RangeOfAqiBloc>().add(
|
||||
LoadRangeOfAqiEvent(
|
||||
GetRangeOfAqiParam(
|
||||
date: date,
|
||||
spaceUuid: spaceUuid,
|
||||
aqiType: aqiType,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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';
|
||||
|
||||
abstract final class RangeOfAqiChartsHelper {
|
||||
const RangeOfAqiChartsHelper._();
|
||||
|
||||
static const gradientData = <(Color color, String label)>[
|
||||
(ColorsManager.goodGreen, 'Good'),
|
||||
(ColorsManager.moderateYellow, 'Moderate'),
|
||||
(ColorsManager.poorOrange, 'Poor'),
|
||||
(ColorsManager.unhealthyRed, 'Unhealthy'),
|
||||
(ColorsManager.severePink, 'Severe'),
|
||||
(ColorsManager.hazardousPurple, 'Hazardous'),
|
||||
];
|
||||
|
||||
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(context);
|
||||
return titlesData.copyWith(
|
||||
bottomTitles: titlesData.bottomTitles.copyWith(
|
||||
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
data.isNotEmpty ? data[value.toInt()].date.day.toString() : '',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
leftTitles: titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 50,
|
||||
maxIncluded: false,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final text = value >= 300 ? '301+' : value.toInt().toString();
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
text,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<LineTooltipItem?> getTooltipItems(
|
||||
List<LineBarSpot> touchedSpots,
|
||||
List<RangeOfAqi> chartData,
|
||||
) {
|
||||
return touchedSpots.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final spot = entry.value;
|
||||
|
||||
final label = switch (spot.barIndex) {
|
||||
0 => 'Max',
|
||||
1 => 'Avg',
|
||||
2 => 'Min',
|
||||
_ => '',
|
||||
};
|
||||
|
||||
final date = DateFormat('dd/MM').format(chartData[spot.x.toInt()].date);
|
||||
|
||||
return LineTooltipItem(
|
||||
index == 0 ? '$date\n' : '',
|
||||
const TextStyle(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: '$label: ${spot.y.toStringAsFixed(0)}'),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static LineTouchData lineTouchData(
|
||||
List<RangeOfAqi> chartData,
|
||||
) {
|
||||
return LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
showOnTopOfTheChartBoxArea: false,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItems: (touchedSpots) => RangeOfAqiChartsHelper.getTooltipItems(
|
||||
touchedSpots,
|
||||
chartData,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
|
||||
|
||||
class AirQualityView extends StatelessWidget {
|
||||
const AirQualityView({super.key});
|
||||
|
||||
static const _padding = EdgeInsetsDirectional.all(32);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMediumOrLess = constraints.maxWidth <= 900;
|
||||
final height = MediaQuery.sizeOf(context).height;
|
||||
if (isMediumOrLess) {
|
||||
return SingleChildScrollView(
|
||||
padding: _padding,
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: height * 1.2,
|
||||
child: const AirQualityEndSideWidget(),
|
||||
),
|
||||
SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()),
|
||||
SizedBox(height: height * 0.5, child: const Placeholder()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: RangeOfAqiChartBox()),
|
||||
Expanded(child: Placeholder()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: AirQualityEndSideWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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_devices/analytics_devices_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/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AirQualityEndSideWidget extends StatelessWidget {
|
||||
const AirQualityEndSideWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectableText(
|
||||
'AQI Sensor',
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
SelectAnalyticsDeviceEvent(value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
enum AqiType {
|
||||
aqi('AQI'),
|
||||
pm25('PM2.5'),
|
||||
pm10('PM10'),
|
||||
hcho('HCHO'),
|
||||
tvoc('TVOC'),
|
||||
co2('CO2'),
|
||||
c6h6('C6H6');
|
||||
|
||||
final String value;
|
||||
const AqiType(this.value);
|
||||
}
|
||||
|
||||
class AqiTypeDropdown extends StatefulWidget {
|
||||
const AqiTypeDropdown({super.key, required this.onChanged});
|
||||
|
||||
final ValueChanged<AqiType?> onChanged;
|
||||
|
||||
@override
|
||||
State<AqiTypeDropdown> createState() => _AqiTypeDropdownState();
|
||||
}
|
||||
|
||||
class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
|
||||
AqiType? _selectedItem = AqiType.aqi;
|
||||
|
||||
void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: DropdownButton<AqiType?>(
|
||||
value: _selectedItem,
|
||||
isDense: true,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
dropdownColor: ColorsManager.whiteColors,
|
||||
underline: const SizedBox.shrink(),
|
||||
icon: const RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Icon(Icons.chevron_right, size: 24),
|
||||
),
|
||||
style: _getTextStyle(context),
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 2,
|
||||
),
|
||||
items: AqiType.values
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
_updateSelectedItem(value);
|
||||
widget.onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? _getTextStyle(BuildContext context) {
|
||||
return context.textTheme.labelSmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RangeOfAqiChart extends StatelessWidget {
|
||||
final List<RangeOfAqi> chartData;
|
||||
|
||||
const RangeOfAqiChart({
|
||||
super.key,
|
||||
required this.chartData,
|
||||
});
|
||||
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines => [
|
||||
(
|
||||
chartData.map((e) => e.max).toList(),
|
||||
ColorsManager.maxPurple,
|
||||
ColorsManager.maxPurpleDot,
|
||||
),
|
||||
(
|
||||
chartData.map((e) => e.avg).toList(),
|
||||
Colors.white,
|
||||
null,
|
||||
),
|
||||
(
|
||||
chartData.map((e) => e.min).toList(),
|
||||
ColorsManager.minBlue,
|
||||
ColorsManager.minBlueDot,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: 301,
|
||||
clipData: const FlClipData.vertical(),
|
||||
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
|
||||
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
|
||||
betweenBarsData: [
|
||||
BetweenBarsData(
|
||||
fromIndex: 0,
|
||||
toIndex: 2,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
|
||||
colors: RangeOfAqiChartsHelper.gradientData.map((e) {
|
||||
final (color, _) = e;
|
||||
return color.withValues(alpha: 0.6);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
lineBarsData: _lines.map((e) {
|
||||
final (values, color, dotColor) = e;
|
||||
return _buildLine(values: values, color: color, dotColor: dotColor);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
FlDotData _buildDotData(Color color) {
|
||||
return FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (_, __, ___, ____) => FlDotCirclePainter(
|
||||
radius: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
strokeWidth: 2,
|
||||
strokeColor: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LineChartBarData _buildLine({
|
||||
required List<double> values,
|
||||
required Color color,
|
||||
Color? dotColor,
|
||||
}) {
|
||||
const invisibleDot = FlDotData(show: false);
|
||||
return LineChartBarData(
|
||||
spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])),
|
||||
isCurved: true,
|
||||
color: color,
|
||||
barWidth: 4,
|
||||
isStrokeCapRound: true,
|
||||
dotData: dotColor != null ? _buildDotData(dotColor) : invisibleDot,
|
||||
belowBarData: BarAreaData(show: false),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const RangeOfAqiChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RangeOfAqiBloc, RangeOfAqiState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(30),
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.errorMessage != null) ...[
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
RangeOfAqiChartTitle(
|
||||
isLoading: state.status == RangeOfAqiStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.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/chart_informative_cell.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class RangeOfAqiChartTitle extends StatelessWidget {
|
||||
const RangeOfAqiChartTitle({required this.isLoading, super.key});
|
||||
final bool isLoading;
|
||||
|
||||
static const List<(Color color, String title, bool hasBorder)> _colors = [
|
||||
(Color(0xFF962DFF), 'Max', false),
|
||||
(Color(0xFF93AAFD), 'Min', false),
|
||||
(Colors.transparent, 'Avg', true),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
ChartsLoadingWidget(isLoading: isLoading),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ChartTitle(title: Text('Range of AQI')),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 3),
|
||||
..._colors.map(
|
||||
(e) {
|
||||
final (color, title, hasBorder) = e;
|
||||
return Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: ChartInformativeCell(
|
||||
title: Text(title),
|
||||
color: color,
|
||||
hasBorder: hasBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull;
|
||||
|
||||
if (spaceUuid == null) return;
|
||||
|
||||
FetchAirQualityDataHelper.loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
aqiType: value ?? AqiType.aqi,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'analytics_date_picker_event.dart';
|
||||
part 'analytics_date_picker_state.dart';
|
||||
|
||||
class AnalyticsDatePickerBloc
|
||||
extends Bloc<AnalyticsDatePickerEvent, AnalyticsDatePickerState> {
|
||||
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
|
||||
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
|
||||
}
|
||||
|
||||
void _onUpdateAnalyticsDatePickerEvent(
|
||||
UpdateAnalyticsDatePickerEvent event,
|
||||
Emitter<AnalyticsDatePickerState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
monthlyDate: event.montlyDate ?? state.monthlyDate,
|
||||
yearlyDate: event.yearlyDate ?? state.yearlyDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
part of 'analytics_date_picker_bloc.dart';
|
||||
|
||||
sealed class AnalyticsDatePickerEvent extends Equatable {
|
||||
const AnalyticsDatePickerEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
|
||||
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
|
||||
|
||||
final DateTime? montlyDate;
|
||||
final DateTime? yearlyDate;
|
||||
|
||||
@override
|
||||
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,18 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||
|
||||
part 'analytics_tab_event.dart';
|
||||
|
||||
class AnalyticsTabBloc extends Bloc<AnalyticsTabEvent, AnalyticsPageTab> {
|
||||
AnalyticsTabBloc() : super(AnalyticsPageTab.energyManagement) {
|
||||
on<UpdateAnalyticsTabEvent>(_onUpdateAnalyticsTabEvent);
|
||||
}
|
||||
|
||||
void _onUpdateAnalyticsTabEvent(
|
||||
UpdateAnalyticsTabEvent event,
|
||||
Emitter<AnalyticsPageTab> emit,
|
||||
) {
|
||||
emit(event.analyticsTab);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
part of 'analytics_tab_bloc.dart';
|
||||
|
||||
sealed class AnalyticsTabEvent extends Equatable {
|
||||
const AnalyticsTabEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class UpdateAnalyticsTabEvent extends AnalyticsTabEvent {
|
||||
const UpdateAnalyticsTabEvent(this.analyticsTab);
|
||||
|
||||
final AnalyticsPageTab analyticsTab;
|
||||
|
||||
@override
|
||||
List<Object> get props => [analyticsTab];
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/views/air_quality_view.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart';
|
||||
|
||||
enum AnalyticsPageTab {
|
||||
energyManagement(
|
||||
title: 'Energy Management',
|
||||
child: AnalyticsEnergyManagementView(),
|
||||
),
|
||||
occupancy(
|
||||
title: 'Occupancy',
|
||||
child: AnalyticsOccupancyView(),
|
||||
),
|
||||
airQuality(
|
||||
title: 'Air Quality',
|
||||
child: AirQualityView(),
|
||||
);
|
||||
|
||||
const AnalyticsPageTab({
|
||||
required this.title,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final String title;
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.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';
|
||||
|
||||
final class AirQualityDataLoadingStrategy 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 ?? '', []));
|
||||
|
||||
FetchAirQualityDataHelper.loadAirQualityData(
|
||||
context,
|
||||
communityUuid: community.uuid,
|
||||
spaceUuid: space.uuid ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onChildSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel child,
|
||||
) {
|
||||
return onSpaceSelected(context, community, child);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||
FetchAirQualityDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
@ -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,16 @@
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.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(),
|
||||
AnalyticsPageTab.airQuality => AirQualityDataLoadingStrategy(),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
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,
|
||||
) {
|
||||
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,
|
||||
) {
|
||||
return onSpaceSelected(context, community, child);
|
||||
}
|
||||
|
||||
@override
|
||||
void clearData(BuildContext context) {
|
||||
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
|
||||
FetchOccupancyDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
130
lib/pages/analytics/modules/analytics/views/analytics_page.dart
Normal file
@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_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';
|
||||
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/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/range_of_aqi/fake_range_of_aqi_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class AnalyticsPage extends StatefulWidget {
|
||||
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(
|
||||
providers: [
|
||||
BlocProvider<AnalyticsTabBloc>(
|
||||
create: (context) => AnalyticsTabBloc(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => TotalEnergyConsumptionBloc(
|
||||
RemoteTotalEnergyConsumptionService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionByPhasesBloc(
|
||||
RemoteEnergyConsumptionByPhasesService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => EnergyConsumptionPerDeviceBloc(
|
||||
RemoteEnergyConsumptionPerDeviceService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PowerClampInfoBloc(
|
||||
RemotePowerClampInfoService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider<RealtimeDeviceChangesBloc>(
|
||||
create: (context) => RealtimeDeviceChangesBloc(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => RangeOfAqiBloc(
|
||||
FakeRangeOfAqiService(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const AnalyticsPageForm(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsPageForm extends StatelessWidget {
|
||||
const AnalyticsPageForm({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebScaffold(
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
appBarTitle: Text(
|
||||
'Syncrow Analytics',
|
||||
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||
),
|
||||
enableMenuSidebar: false,
|
||||
scaffoldBody: const Row(
|
||||
children: [
|
||||
AnalyticsCommunitiesSidebar(),
|
||||
Expanded(flex: 5, child: AnalyticsPageTabsAndChildren()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart';
|
||||
|
||||
class AnalyticsCommunitiesSidebar extends StatelessWidget {
|
||||
const AnalyticsCommunitiesSidebar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedTab = context.watch<AnalyticsTabBloc>().state;
|
||||
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
|
||||
|
||||
return Expanded(
|
||||
child: AnalyticsSpaceTreeView(
|
||||
onSelectCommunity: (community, spaces) {
|
||||
strategy.onCommunitySelected(context, community, spaces);
|
||||
},
|
||||
onSelectSpace: (community, space) {
|
||||
strategy.onSpaceSelected(context, community, space);
|
||||
},
|
||||
onSelectChildSpace: (community, child) {
|
||||
strategy.onChildSpaceSelected(context, community, child);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/month_picker_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/year_picker_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
enum DatePickerType { month, year }
|
||||
|
||||
class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
const AnalyticsDateFilterButton({
|
||||
required this.selectedDate,
|
||||
required this.onDateSelected,
|
||||
this.datePickerType = DatePickerType.month,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DateTime selectedDate;
|
||||
final void Function(DateTime)? onDateSelected;
|
||||
final DatePickerType datePickerType;
|
||||
|
||||
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
|
||||
@override
|
||||
State<AnalyticsDateFilterButton> createState() =>
|
||||
_AnalyticsDateFilterButtonState();
|
||||
}
|
||||
|
||||
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AnalyticsDateFilterButton._color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.transparentColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
icon: SvgPicture.asset(
|
||||
Assets.blankCalendar,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter:
|
||||
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
|
||||
),
|
||||
label: Text(
|
||||
_formatDate(widget.selectedDate),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return switch (widget.datePickerType) {
|
||||
DatePickerType.month => MonthPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
DatePickerType.year => YearPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime? date) {
|
||||
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
|
||||
DatePickerType.month => DateFormat('MMMM yyyy'),
|
||||
DatePickerType.year => DateFormat('yyyy'),
|
||||
};
|
||||
final formattedDate = formatterBasedOnDatePickerType.format(
|
||||
date ?? DateTime.now(),
|
||||
);
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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 {
|
||||
const AnalyticsPageTabButton({
|
||||
super.key,
|
||||
required this.tab,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final AnalyticsPageTab tab;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context);
|
||||
context.read<AnalyticsTabBloc>().add(
|
||||
UpdateAnalyticsTabEvent(tab),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
tab.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400,
|
||||
fontSize: 16,
|
||||
color:
|
||||
isSelected ? ColorsManager.slidingBlueColor : ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
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 {
|
||||
const AnalyticsPageTabsAndChildren({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, selectedTab) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: subSectionContainerDecoration,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
spacing: 32,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...AnalyticsPageTab.values.map(
|
||||
(tab) => _buildAnimation(
|
||||
child: AnalyticsPageTabButton(
|
||||
key: ValueKey(selectedTab),
|
||||
tab: tab,
|
||||
isSelected: tab == selectedTab,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Visibility(
|
||||
key: ValueKey(selectedTab),
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement,
|
||||
child: Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDateFilterButton(
|
||||
onDateSelected: (DateTime value) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: value),
|
||||
);
|
||||
|
||||
final spaceTreeState =
|
||||
context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
FetchEnergyManagementDataHelper
|
||||
.loadEnergyManagementData(
|
||||
context,
|
||||
shouldFetchAnalyticsDevices: false,
|
||||
selectedDate: value,
|
||||
communityId:
|
||||
spaceTreeState.selectedCommunities.firstOrNull ??
|
||||
'',
|
||||
spaceId:
|
||||
spaceTreeState.selectedSpaces.firstOrNull ?? '',
|
||||
);
|
||||
}
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
.state
|
||||
.monthlyDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: _buildAnimation(child: selectedTab.child),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimation({required Widget child}) {
|
||||
return AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ChartInformativeCell extends StatelessWidget {
|
||||
const ChartInformativeCell({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.color,
|
||||
this.hasBorder = false,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Color color;
|
||||
final bool hasBorder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.sizeOf(context).height * 0.0385,
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadiusDirectional.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.greyColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(color: ColorsManager.grayBorder),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
child: title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class MonthPickerWidget extends StatefulWidget {
|
||||
const MonthPickerWidget({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onDateSelected,
|
||||
});
|
||||
|
||||
final DateTime selectedDate;
|
||||
final ValueChanged<DateTime>? onDateSelected;
|
||||
|
||||
@override
|
||||
State<MonthPickerWidget> createState() => _MonthPickerWidgetState();
|
||||
}
|
||||
|
||||
class _MonthPickerWidgetState extends State<MonthPickerWidget> {
|
||||
late int _currentYear;
|
||||
int? _selectedMonth;
|
||||
|
||||
static const _monthNames = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentYear = widget.selectedDate.year;
|
||||
_selectedMonth = widget.selectedDate.month - 1;
|
||||
}
|
||||
|
||||
@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: [
|
||||
_buildYearSelector(),
|
||||
_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,
|
||||
_selectedMonth! + 1,
|
||||
);
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildYearSelector() {
|
||||
final currentYear = DateTime.now().year;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'$_currentYear',
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorsManager.grey700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentYear = _currentYear - 1;
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.chevron_left,
|
||||
color: ColorsManager.grey700,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _currentYear < currentYear
|
||||
? () {
|
||||
setState(() {
|
||||
_currentYear = _currentYear + 1;
|
||||
// Clear selected month if it becomes invalid in the new year
|
||||
if (_currentYear == currentYear &&
|
||||
_selectedMonth != null &&
|
||||
_selectedMonth! > DateTime.now().month - 1) {
|
||||
_selectedMonth = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.chevron_right,
|
||||
color: _currentYear < currentYear
|
||||
? ColorsManager.grey700
|
||||
: ColorsManager.grey700.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthsGrid() {
|
||||
final currentDate = DateTime.now();
|
||||
final isCurrentYear = _currentYear == currentDate.year;
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 12,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 2.5,
|
||||
mainAxisSpacing: 8,
|
||||
mainAxisExtent: 30,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _selectedMonth == index;
|
||||
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
|
||||
|
||||
return InkWell(
|
||||
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,
|
||||
),
|
||||
child: Text(
|
||||
_monthNames[index],
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? ColorsManager.whiteColors
|
||||
: isFutureMonth
|
||||
? ColorsManager.blackColor.withValues(alpha: 0.3)
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
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';
|
||||
|
||||
part 'energy_consumption_by_phases_event.dart';
|
||||
part 'energy_consumption_by_phases_state.dart';
|
||||
|
||||
class EnergyConsumptionByPhasesBloc
|
||||
extends Bloc<EnergyConsumptionByPhasesEvent, EnergyConsumptionByPhasesState> {
|
||||
EnergyConsumptionByPhasesBloc(
|
||||
this._energyConsumptionByPhasesService,
|
||||
) : super(const EnergyConsumptionByPhasesState()) {
|
||||
on<LoadEnergyConsumptionByPhasesEvent>(_onLoadEnergyConsumptionByPhasesEvent);
|
||||
on<ClearEnergyConsumptionByPhasesEvent>(_onClearEnergyConsumptionByPhasesEvent);
|
||||
}
|
||||
|
||||
final EnergyConsumptionByPhasesService _energyConsumptionByPhasesService;
|
||||
|
||||
Future<void> _onLoadEnergyConsumptionByPhasesEvent(
|
||||
LoadEnergyConsumptionByPhasesEvent event,
|
||||
Emitter<EnergyConsumptionByPhasesState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: EnergyConsumptionByPhasesStatus.loading));
|
||||
try {
|
||||
final chartData = await _energyConsumptionByPhasesService.load(event.param);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionByPhasesStatus.loaded,
|
||||
chartData: chartData,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionByPhasesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearEnergyConsumptionByPhasesEvent(
|
||||
ClearEnergyConsumptionByPhasesEvent event,
|
||||
Emitter<EnergyConsumptionByPhasesState> emit,
|
||||
) async {
|
||||
emit(const EnergyConsumptionByPhasesState());
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
part of 'energy_consumption_by_phases_bloc.dart';
|
||||
|
||||
sealed class EnergyConsumptionByPhasesEvent extends Equatable {
|
||||
const EnergyConsumptionByPhasesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent {
|
||||
const LoadEnergyConsumptionByPhasesEvent({
|
||||
required this.param,
|
||||
});
|
||||
|
||||
final GetEnergyConsumptionByPhasesParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent {
|
||||
const ClearEnergyConsumptionByPhasesEvent();
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
part of 'energy_consumption_by_phases_bloc.dart';
|
||||
|
||||
enum EnergyConsumptionByPhasesStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
final class EnergyConsumptionByPhasesState extends Equatable {
|
||||
const EnergyConsumptionByPhasesState({
|
||||
this.status = EnergyConsumptionByPhasesStatus.initial,
|
||||
this.chartData = const <PhasesEnergyConsumption>[],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final List<PhasesEnergyConsumption> chartData;
|
||||
final EnergyConsumptionByPhasesStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
EnergyConsumptionByPhasesState copyWith({
|
||||
List<PhasesEnergyConsumption>? chartData,
|
||||
EnergyConsumptionByPhasesStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return EnergyConsumptionByPhasesState(
|
||||
chartData: chartData ?? this.chartData,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [chartData, status, errorMessage];
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_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';
|
||||
|
||||
part 'energy_consumption_per_device_event.dart';
|
||||
part 'energy_consumption_per_device_state.dart';
|
||||
|
||||
class EnergyConsumptionPerDeviceBloc
|
||||
extends Bloc<EnergyConsumptionPerDeviceEvent, EnergyConsumptionPerDeviceState> {
|
||||
EnergyConsumptionPerDeviceBloc(
|
||||
this._energyConsumptionPerDeviceService,
|
||||
) : super(const EnergyConsumptionPerDeviceState()) {
|
||||
on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent);
|
||||
on<ClearEnergyConsumptionPerDeviceEvent>(_onClearEnergyConsumptionPerDeviceEvent);
|
||||
}
|
||||
|
||||
final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService;
|
||||
|
||||
Future<void> _onLoadEnergyConsumptionPerDeviceEvent(
|
||||
LoadEnergyConsumptionPerDeviceEvent event,
|
||||
Emitter<EnergyConsumptionPerDeviceState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: EnergyConsumptionPerDeviceStatus.loading));
|
||||
try {
|
||||
final chartData = await _energyConsumptionPerDeviceService.load(event.param);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionPerDeviceStatus.loaded,
|
||||
chartData: chartData,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionPerDeviceStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearEnergyConsumptionPerDeviceEvent(
|
||||
ClearEnergyConsumptionPerDeviceEvent event,
|
||||
Emitter<EnergyConsumptionPerDeviceState> emit,
|
||||
) async {
|
||||
emit(const EnergyConsumptionPerDeviceState());
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
part of 'energy_consumption_per_device_bloc.dart';
|
||||
|
||||
sealed class EnergyConsumptionPerDeviceEvent extends Equatable {
|
||||
const EnergyConsumptionPerDeviceEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadEnergyConsumptionPerDeviceEvent
|
||||
extends EnergyConsumptionPerDeviceEvent {
|
||||
const LoadEnergyConsumptionPerDeviceEvent(this.param);
|
||||
|
||||
final GetEnergyConsumptionPerDeviceParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearEnergyConsumptionPerDeviceEvent
|
||||
extends EnergyConsumptionPerDeviceEvent {
|
||||
const ClearEnergyConsumptionPerDeviceEvent();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'energy_consumption_per_device_bloc.dart';
|
||||
|
||||
enum EnergyConsumptionPerDeviceStatus { initial, loading, loaded, failure }
|
||||
|
||||
final class EnergyConsumptionPerDeviceState extends Equatable {
|
||||
const EnergyConsumptionPerDeviceState({
|
||||
this.status = EnergyConsumptionPerDeviceStatus.initial,
|
||||
this.chartData = const <DeviceEnergyDataModel>[],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final List<DeviceEnergyDataModel> chartData;
|
||||
final EnergyConsumptionPerDeviceStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
EnergyConsumptionPerDeviceState copyWith({
|
||||
List<DeviceEnergyDataModel>? chartData,
|
||||
EnergyConsumptionPerDeviceStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return EnergyConsumptionPerDeviceState(
|
||||
chartData: chartData ?? this.chartData,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [chartData, status, errorMessage];
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
|
||||
|
||||
part 'power_clamp_info_event.dart';
|
||||
part 'power_clamp_info_state.dart';
|
||||
|
||||
class PowerClampInfoBloc extends Bloc<PowerClampInfoEvent, PowerClampInfoState> {
|
||||
PowerClampInfoBloc(
|
||||
this._powerClampInfoService,
|
||||
) : super(const PowerClampInfoState()) {
|
||||
on<LoadPowerClampInfoEvent>(_onLoadPowerClampInfoEvent);
|
||||
on<UpdatePowerClampStatusEvent>(_onUpdatePowerClampStatusEvent);
|
||||
on<ClearPowerClampInfoEvent>(_onClearPowerClampInfoEvent);
|
||||
}
|
||||
|
||||
final PowerClampInfoService _powerClampInfoService;
|
||||
|
||||
Future<void> _onLoadPowerClampInfoEvent(
|
||||
LoadPowerClampInfoEvent event,
|
||||
Emitter<PowerClampInfoState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: PowerClampInfoStatus.loading));
|
||||
try {
|
||||
final powerClampModel = await _powerClampInfoService.getInfo(event.deviceId);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: PowerClampInfoStatus.loaded,
|
||||
powerClampModel: powerClampModel,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: PowerClampInfoStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdatePowerClampStatusEvent(
|
||||
UpdatePowerClampStatusEvent event,
|
||||
Emitter<PowerClampInfoState> emit,
|
||||
) async {
|
||||
final currentModel = state.powerClampModel;
|
||||
if (currentModel == null) return;
|
||||
|
||||
final updatedStatus = PowerStatus.fromStatusList(event.statusList);
|
||||
final updatedModel = currentModel.copyWith(statusPower: updatedStatus);
|
||||
|
||||
emit(state.copyWith(powerClampModel: updatedModel));
|
||||
}
|
||||
|
||||
void _onClearPowerClampInfoEvent(
|
||||
ClearPowerClampInfoEvent event,
|
||||
Emitter<PowerClampInfoState> emit,
|
||||
) {
|
||||
emit(const PowerClampInfoState());
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'power_clamp_info_bloc.dart';
|
||||
|
||||
sealed class PowerClampInfoEvent extends Equatable {
|
||||
const PowerClampInfoEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadPowerClampInfoEvent extends PowerClampInfoEvent {
|
||||
const LoadPowerClampInfoEvent(this.deviceId);
|
||||
|
||||
final String deviceId;
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
|
||||
final class UpdatePowerClampStatusEvent extends PowerClampInfoEvent {
|
||||
const UpdatePowerClampStatusEvent(this.statusList);
|
||||
|
||||
final List<Status> statusList;
|
||||
|
||||
@override
|
||||
List<Object> get props => [statusList];
|
||||
}
|
||||
|
||||
final class ClearPowerClampInfoEvent extends PowerClampInfoEvent {
|
||||
const ClearPowerClampInfoEvent();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'power_clamp_info_bloc.dart';
|
||||
|
||||
enum PowerClampInfoStatus { initial, loading, loaded, error }
|
||||
|
||||
final class PowerClampInfoState extends Equatable {
|
||||
const PowerClampInfoState({
|
||||
this.status = PowerClampInfoStatus.initial,
|
||||
this.powerClampModel,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final PowerClampInfoStatus status;
|
||||
final PowerClampModel? powerClampModel;
|
||||
final String? errorMessage;
|
||||
|
||||
PowerClampInfoState copyWith({
|
||||
PowerClampInfoStatus? status,
|
||||
PowerClampModel? powerClampModel,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return PowerClampInfoState(
|
||||
status: status ?? this.status,
|
||||
powerClampModel: powerClampModel ?? this.powerClampModel,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, powerClampModel, errorMessage];
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/realtime_device_service.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
|
||||
part 'realtime_device_changes_event.dart';
|
||||
part 'realtime_device_changes_state.dart';
|
||||
|
||||
class RealtimeDeviceChangesBloc
|
||||
extends Bloc<RealtimeDeviceChangesEvent, RealtimeDeviceChangesState> {
|
||||
RealtimeDeviceChangesBloc(
|
||||
this._realtimeDeviceService,
|
||||
) : super(const RealtimeDeviceChangesState()) {
|
||||
on<RealtimeDeviceChangesStarted>(_onRealtimeDeviceChangesStarted);
|
||||
on<RealtimeDeviceChangesClosed>(_onRealtimeDeviceChangesClosed);
|
||||
on<_RealtimeDeviceChangesUpdated>(_onRealtimeDeviceChangesUpdated);
|
||||
}
|
||||
|
||||
final RealtimeDeviceService _realtimeDeviceService;
|
||||
StreamSubscription<List<Status>>? _subscription;
|
||||
|
||||
Future<void> _onRealtimeDeviceChangesStarted(
|
||||
RealtimeDeviceChangesStarted event,
|
||||
Emitter<RealtimeDeviceChangesState> emit,
|
||||
) async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = _realtimeDeviceService.subscribe(event.deviceId).listen(
|
||||
(data) {
|
||||
add(_RealtimeDeviceChangesUpdated(data));
|
||||
},
|
||||
onError: (error) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: RealtimeDeviceChangesStatus.failure,
|
||||
errorMessage: '$error',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onRealtimeDeviceChangesClosed(
|
||||
RealtimeDeviceChangesClosed event,
|
||||
Emitter<RealtimeDeviceChangesState> emit,
|
||||
) async {
|
||||
add(const _RealtimeDeviceChangesUpdated([]));
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
emit(const RealtimeDeviceChangesState());
|
||||
}
|
||||
|
||||
void _onRealtimeDeviceChangesUpdated(
|
||||
_RealtimeDeviceChangesUpdated event,
|
||||
Emitter<RealtimeDeviceChangesState> emit,
|
||||
) {
|
||||
final currentState = state;
|
||||
final updatedList = [
|
||||
...currentState.deviceStatusList.where(
|
||||
(device) => !event.deviceStatusList
|
||||
.any((newDevice) => newDevice.code == device.code),
|
||||
),
|
||||
...event.deviceStatusList,
|
||||
];
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: RealtimeDeviceChangesStatus.loaded,
|
||||
deviceStatusList: updatedList,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _subscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
part of 'realtime_device_changes_bloc.dart';
|
||||
|
||||
sealed class RealtimeDeviceChangesEvent extends Equatable {
|
||||
const RealtimeDeviceChangesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class RealtimeDeviceChangesStarted extends RealtimeDeviceChangesEvent {
|
||||
const RealtimeDeviceChangesStarted(this.deviceId);
|
||||
|
||||
final String deviceId;
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
final class RealtimeDeviceChangesClosed extends RealtimeDeviceChangesEvent {
|
||||
const RealtimeDeviceChangesClosed();
|
||||
}
|
||||
|
||||
class _RealtimeDeviceChangesUpdated extends RealtimeDeviceChangesEvent {
|
||||
final List<Status> deviceStatusList;
|
||||
|
||||
const _RealtimeDeviceChangesUpdated(this.deviceStatusList);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'realtime_device_changes_bloc.dart';
|
||||
|
||||
enum RealtimeDeviceChangesStatus { initial, loaded, failure }
|
||||
|
||||
final class RealtimeDeviceChangesState extends Equatable {
|
||||
const RealtimeDeviceChangesState({
|
||||
this.status = RealtimeDeviceChangesStatus.initial,
|
||||
this.deviceStatusList = const <Status>[],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final RealtimeDeviceChangesStatus status;
|
||||
final List<Status> deviceStatusList;
|
||||
final String? errorMessage;
|
||||
|
||||
RealtimeDeviceChangesState copyWith({
|
||||
RealtimeDeviceChangesStatus? status,
|
||||
List<Status>? deviceStatusList,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return RealtimeDeviceChangesState(
|
||||
status: status ?? this.status,
|
||||
deviceStatusList: deviceStatusList ?? this.deviceStatusList,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, deviceStatusList, errorMessage];
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
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';
|
||||
|
||||
part 'total_energy_consumption_event.dart';
|
||||
part 'total_energy_consumption_state.dart';
|
||||
|
||||
class TotalEnergyConsumptionBloc
|
||||
extends Bloc<TotalEnergyConsumptionEvent, TotalEnergyConsumptionState> {
|
||||
TotalEnergyConsumptionBloc(
|
||||
this._totalEnergyConsumptionService,
|
||||
) : super(const TotalEnergyConsumptionState()) {
|
||||
on<TotalEnergyConsumptionLoadEvent>(_onTotalEnergyConsumptionLoadEvent);
|
||||
on<ClearTotalEnergyConsumptionEvent>(_onClearTotalEnergyConsumptionEvent);
|
||||
}
|
||||
|
||||
final TotalEnergyConsumptionService _totalEnergyConsumptionService;
|
||||
|
||||
Future<void> _onTotalEnergyConsumptionLoadEvent(
|
||||
TotalEnergyConsumptionLoadEvent event,
|
||||
Emitter<TotalEnergyConsumptionState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(state.copyWith(status: TotalEnergyConsumptionStatus.loading));
|
||||
final chartData = await _totalEnergyConsumptionService.load(event.param);
|
||||
emit(
|
||||
state.copyWith(
|
||||
chartData: chartData,
|
||||
status: TotalEnergyConsumptionStatus.loaded,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: e.toString(),
|
||||
status: TotalEnergyConsumptionStatus.failure,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearTotalEnergyConsumptionEvent(
|
||||
ClearTotalEnergyConsumptionEvent event,
|
||||
Emitter<TotalEnergyConsumptionState> emit,
|
||||
) async {
|
||||
emit(const TotalEnergyConsumptionState());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
part of 'total_energy_consumption_bloc.dart';
|
||||
|
||||
sealed class TotalEnergyConsumptionEvent extends Equatable {
|
||||
const TotalEnergyConsumptionEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
final class TotalEnergyConsumptionLoadEvent extends TotalEnergyConsumptionEvent {
|
||||
const TotalEnergyConsumptionLoadEvent({required this.param});
|
||||
|
||||
final GetTotalEnergyConsumptionParam param;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearTotalEnergyConsumptionEvent extends TotalEnergyConsumptionEvent {
|
||||
const ClearTotalEnergyConsumptionEvent();
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
part of 'total_energy_consumption_bloc.dart';
|
||||
|
||||
enum TotalEnergyConsumptionStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
final class TotalEnergyConsumptionState extends Equatable {
|
||||
const TotalEnergyConsumptionState({
|
||||
this.status = TotalEnergyConsumptionStatus.initial,
|
||||
this.chartData = const <EnergyDataModel>[],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final List<EnergyDataModel> chartData;
|
||||
final TotalEnergyConsumptionStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
TotalEnergyConsumptionState copyWith({
|
||||
List<EnergyDataModel>? chartData,
|
||||
TotalEnergyConsumptionStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return TotalEnergyConsumptionState(
|
||||
chartData: chartData ?? this.chartData,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [chartData, status, errorMessage];
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
abstract final class EnergyManagementChartsHelper {
|
||||
const EnergyManagementChartsHelper._();
|
||||
|
||||
static FlTitlesData titlesData(
|
||||
BuildContext context, {
|
||||
double? leftTitlesInterval,
|
||||
}) {
|
||||
const emptyTitle = AxisTitles(sideTitles: SideTitles(showTitles: false));
|
||||
return FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
drawBelowEverything: true,
|
||||
sideTitles: SideTitles(
|
||||
interval: 1,
|
||||
reservedSize: 32,
|
||||
showTitles: true,
|
||||
maxIncluded: true,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
value.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: false,
|
||||
interval: leftTitlesInterval,
|
||||
reservedSize: 110,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value.formatNumberToKwh,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
rightTitles: emptyTitle,
|
||||
topTitles: emptyTitle,
|
||||
);
|
||||
}
|
||||
|
||||
static String getToolTipLabel(num month, double value) {
|
||||
final monthLabel = month.toString();
|
||||
final valueLabel = value.formatNumberToKwh;
|
||||
final labels = [monthLabel, valueLabel];
|
||||
return labels.where((element) => element.isNotEmpty).join(', ');
|
||||
}
|
||||
|
||||
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
return LineTooltipItem(
|
||||
getToolTipLabel(spot.x, spot.y),
|
||||
const TextStyle(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static LineTouchTooltipData lineTouchTooltipData() {
|
||||
return LineTouchTooltipData(
|
||||
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(color: ColorsManager.semiTransparentBlack),
|
||||
tooltipRoundedRadius: 16,
|
||||
showOnTopOfTheChartBoxArea: false,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItems: getTooltipItems,
|
||||
);
|
||||
}
|
||||
|
||||
static FlGridData gridData({
|
||||
double horizontalInterval = 250,
|
||||
}) {
|
||||
return FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
drawHorizontalLine: true,
|
||||
horizontalInterval: horizontalInterval,
|
||||
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: 16,
|
||||
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
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';
|
||||
|
||||
abstract final class FetchEnergyManagementDataHelper {
|
||||
const FetchEnergyManagementDataHelper._();
|
||||
|
||||
// static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
|
||||
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
|
||||
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
|
||||
}
|
||||
|
||||
static void loadEnergyManagementData(
|
||||
BuildContext context, {
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
DateTime? selectedDate,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
if (communityId.isEmpty && spaceId.isEmpty) {
|
||||
clearAllData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
|
||||
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
|
||||
if (shouldFetchAnalyticsDevices) {
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityId,
|
||||
spaceUuid: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
loadRealtimeDeviceChanges(context);
|
||||
loadPowerClampInfo(context);
|
||||
}
|
||||
loadTotalEnergyConsumption(
|
||||
context,
|
||||
selectedDate: selectedDate0,
|
||||
communityId: communityId,
|
||||
spaceId: spaceId,
|
||||
);
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
}
|
||||
loadEnergyConsumptionPerDevice(
|
||||
context,
|
||||
communityId: communityId,
|
||||
spaceId: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionByPhases(
|
||||
BuildContext context, {
|
||||
required String powerClampUuid,
|
||||
DateTime? selectedDate,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionByPhasesParam(
|
||||
date: selectedDate,
|
||||
powerClampUuid: powerClampUuid,
|
||||
);
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
LoadEnergyConsumptionByPhasesEvent(param: param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadTotalEnergyConsumption(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final param = GetTotalEnergyConsumptionParam(
|
||||
spaceId: spaceId,
|
||||
communityId: communityId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<TotalEnergyConsumptionBloc>().add(
|
||||
TotalEnergyConsumptionLoadEvent(param: param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadEnergyConsumptionPerDevice(
|
||||
BuildContext context, {
|
||||
DateTime? selectedDate,
|
||||
required String communityId,
|
||||
required String spaceId,
|
||||
}) {
|
||||
final param = GetEnergyConsumptionPerDeviceParam(
|
||||
spaceId: spaceId,
|
||||
communityId: communityId,
|
||||
monthDate: selectedDate,
|
||||
);
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
LoadEnergyConsumptionPerDeviceEvent(param),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadPowerClampInfo(BuildContext context) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
if (selectedDevice case final AnalyticsDevice device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadRealtimeDeviceChanges(
|
||||
BuildContext context, {
|
||||
String? deviceUuid,
|
||||
}) {
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadAnalyticsDevices(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
required DateTime selectedDate,
|
||||
}) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
LoadAnalyticsDevicesEvent(
|
||||
onSuccess: (device) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
LoadPowerClampInfoEvent(device.uuid),
|
||||
);
|
||||
loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate: selectedDate,
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
RealtimeDeviceChangesStarted(device.uuid),
|
||||
);
|
||||
},
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['PC'],
|
||||
requestType: AnalyticsDeviceRequestType.energyManagement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void clearAllData(BuildContext context) {
|
||||
context.read<PowerClampInfoBloc>().add(
|
||||
const ClearPowerClampInfoEvent(),
|
||||
);
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<EnergyConsumptionPerDeviceBloc>().add(
|
||||
const ClearEnergyConsumptionPerDeviceEvent(),
|
||||
);
|
||||
|
||||
context.read<TotalEnergyConsumptionBloc>().add(
|
||||
const ClearTotalEnergyConsumptionEvent(),
|
||||
);
|
||||
|
||||
context.read<EnergyConsumptionByPhasesBloc>().add(
|
||||
const ClearEnergyConsumptionByPhasesEvent(),
|
||||
);
|
||||
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class AnalyticsEnergyManagementView extends StatefulWidget {
|
||||
const AnalyticsEnergyManagementView({super.key});
|
||||
|
||||
@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(
|
||||
builder: (context, constraints) {
|
||||
final isMediumOrLess = constraints.maxWidth <= 900;
|
||||
if (isMediumOrLess) {
|
||||
return SingleChildScrollView(
|
||||
padding: _padding,
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 1.2,
|
||||
child: const PowerClampEnergyDataWidget(),
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.5,
|
||||
child: const TotalEnergyConsumptionChartBox(),
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.5,
|
||||
child: const EnergyConsumptionPerDeviceChartBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: MediaQuery.sizeOf(context).height * 1,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: TotalEnergyConsumptionChartBox()),
|
||||
Expanded(child: EnergyConsumptionPerDeviceChartBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: PowerClampEnergyDataWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ChartTitle extends StatelessWidget {
|
||||
const ChartTitle({super.key, required this.title});
|
||||
|
||||
final Widget title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTextStyle(
|
||||
style: context.textTheme.titleLarge!.copyWith(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
child: title,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.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';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class EnergyConsumptionByPhasesChart extends StatelessWidget {
|
||||
const EnergyConsumptionByPhasesChart({
|
||||
super.key,
|
||||
required this.energyData,
|
||||
});
|
||||
|
||||
final List<PhasesEnergyConsumption> energyData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
barGroups: energyData.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final data = entry.value;
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||
toY: data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
rodStackItems: [
|
||||
BarChartRodStackItem(
|
||||
0,
|
||||
data.energyConsumedA,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.8),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.energyConsumedA,
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.4),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
data.energyConsumedA + data.energyConsumedB,
|
||||
data.energyConsumedA +
|
||||
data.energyConsumedB +
|
||||
data.energyConsumedC,
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.15),
|
||||
),
|
||||
],
|
||||
width: 8,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
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 = energyData;
|
||||
|
||||
final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
|
||||
final phaseA = data[group.x.toInt()].energyConsumedA;
|
||||
final phaseB = data[group.x.toInt()].energyConsumedB;
|
||||
final phaseC = data[group.x.toInt()].energyConsumedC;
|
||||
final total = data[group.x.toInt()].energyConsumedKw;
|
||||
|
||||
return BarTooltipItem(
|
||||
'$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(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Phase B: $phaseB\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Phase C: $phaseC',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
FlTitlesData _titlesData(BuildContext context) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
);
|
||||
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
),
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) {
|
||||
final month = DateFormat('d').format(energyData[value.toInt()].date);
|
||||
return FittedBox(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Text(
|
||||
month,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 18,
|
||||
),
|
||||
);
|
||||
|
||||
return titlesData.copyWith(
|
||||
leftTitles: leftTitles,
|
||||
bottomTitles: bottomTitles,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/widgets/energy_consumption_by_phases_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class EnergyConsumptionByPhasesChartBox extends StatelessWidget {
|
||||
const EnergyConsumptionByPhasesChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<EnergyConsumptionByPhasesBloc,
|
||||
EnergyConsumptionByPhasesState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
decoration: secondarySection,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
EnergyConsumptionByPhasesTitle(
|
||||
isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: EnergyConsumptionByPhasesChart(
|
||||
energyData: state.chartData,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class EnergyConsumptionByPhasesTitle extends StatelessWidget {
|
||||
const EnergyConsumptionByPhasesTitle({super.key, required this.isLoading});
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ChartsLoadingWidget(isLoading: isLoading),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(
|
||||
title: Text(
|
||||
'Energy Consumption by Phases',
|
||||
style: context.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
...<(String title, double opacity)>[
|
||||
('A', 0.8),
|
||||
('B', 0.4),
|
||||
('C', 0.15),
|
||||
].map((phase) => _buildPhaseCell(context, phase)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhaseCell(
|
||||
BuildContext context,
|
||||
(String title, double colorOpacity) phase,
|
||||
) {
|
||||
final (title, colorOpacity) = phase;
|
||||
|
||||
return Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: ColorsManager.vividBlue.withValues(
|
||||
alpha: colorOpacity,
|
||||
),
|
||||
radius: 4,
|
||||
),
|
||||
Text(
|
||||
'Phase $title',
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
|
||||
class EnergyConsumptionPerDeviceChart extends StatelessWidget {
|
||||
const EnergyConsumptionPerDeviceChart({super.key, required this.chartData});
|
||||
|
||||
final List<DeviceEnergyDataModel> chartData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
clipData: const FlClipData.vertical(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: chartData.map((e) {
|
||||
return _buildChartBar(
|
||||
color: e.color,
|
||||
spots: e.energy
|
||||
.map(
|
||||
(energy) => FlSpot(
|
||||
energy.date.day.toDouble(),
|
||||
energy.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
||||
LineChartBarData _buildChartBar({
|
||||
required Color color,
|
||||
required List<FlSpot> spots,
|
||||
}) {
|
||||
return LineChartBarData(
|
||||
spots: spots,
|
||||
dashArray: [12, 18],
|
||||
isCurved: true,
|
||||
color: color,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
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';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
const EnergyConsumptionPerDeviceChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<EnergyConsumptionPerDeviceBloc,
|
||||
EnergyConsumptionPerDeviceState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
if (state.status == EnergyConsumptionPerDeviceStatus.loading)
|
||||
const ChartsLoadingWidget(isLoading: true),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ChartTitle(
|
||||
title: Text('Energy Consumption per Device'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: EnergyConsumptionPerDeviceDevicesList(
|
||||
chartData: state.chartData,
|
||||
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Expanded(
|
||||
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
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/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
|
||||
|
||||
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
const EnergyConsumptionPerDeviceDevicesList({
|
||||
required this.chartData,
|
||||
required this.devices,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<AnalyticsDevice> devices;
|
||||
final List<DeviceEnergyDataModel> chartData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 16,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: devices.map((e) => _buildDeviceCell(context, e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) {
|
||||
final deviceColor = chartData
|
||||
.firstWhere(
|
||||
(element) => element.deviceId == device.uuid,
|
||||
orElse: () => const DeviceEnergyDataModel(
|
||||
energy: [],
|
||||
deviceName: '',
|
||||
deviceId: '',
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
.color;
|
||||
|
||||
return Tooltip(
|
||||
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
|
||||
child: ChartInformativeCell(title: Text(device.name), color: deviceColor),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
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_status_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
|
||||
import 'package:syncrow_web/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 PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
const PowerClampEnergyDataWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.deviceStatusList != current.deviceStatusList ||
|
||||
previous.status != current.status,
|
||||
listener: (context, state) => context.read<PowerClampInfoBloc>().add(
|
||||
UpdatePowerClampStatusEvent(state.deviceStatusList),
|
||||
),
|
||||
child: BlocBuilder<PowerClampInfoBloc, PowerClampInfoState>(
|
||||
builder: (context, state) {
|
||||
final generalDataPoints =
|
||||
state.powerClampModel?.status.general.dataPoints ?? [];
|
||||
return Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
_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 Divider(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
status: [
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.powerActiveIcon,
|
||||
title: 'Active',
|
||||
value: _valueFromCode('ActivePower', generalDataPoints),
|
||||
unit: 'W',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.voltMeterIcon,
|
||||
title: 'Current',
|
||||
value: _valueFromCode('Current', generalDataPoints)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
)
|
||||
.replaceAll('.0', ''),
|
||||
unit: 'A',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.frequencyIcon,
|
||||
title: 'Frequency',
|
||||
value: _valueFromCode('Frequency', generalDataPoints),
|
||||
unit: 'Hz',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: PowerClampPhasesDataWidget(
|
||||
phaseA: state.powerClampModel?.status.phaseA,
|
||||
phaseB: state.powerClampModel?.status.phaseB,
|
||||
phaseC: state.powerClampModel?.status.phaseC,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Expanded(flex: 3, child: EnergyConsumptionByPhasesChartBox()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectableText(
|
||||
'Smart Power Clamp',
|
||||
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.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: value.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _valueFromCode(String code, List<DataPoint> points) {
|
||||
return points
|
||||
.firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--'))
|
||||
.value
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class PowerClampEnergyStatusWidget extends StatelessWidget {
|
||||
const PowerClampEnergyStatusWidget({
|
||||
super.key,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final List<PowerClampEnergyStatus> status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
status.length * 2 - 1,
|
||||
(index) => index.isEven
|
||||
? Expanded(child: _buildItem(context, status[index ~/ 2]))
|
||||
: _buildDivider(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, PowerClampEnergyStatus item) {
|
||||
return Center(
|
||||
child: ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: SvgPicture.asset(
|
||||
item.iconPath,
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
height: 18,
|
||||
width: 18,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
title: Text(
|
||||
item.title,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text.rich(
|
||||
TextSpan(
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: '${item.value} '),
|
||||
TextSpan(
|
||||
text: item.unit,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return Container(
|
||||
height: 1,
|
||||
decoration: const BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(20, 0, 0, 0),
|
||||
offset: Offset(0, 1),
|
||||
blurRadius: 1,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(30, 0, 0, 0),
|
||||
offset: Offset(0, -2),
|
||||
blurRadius: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class PowerClampPhase extends StatelessWidget {
|
||||
const PowerClampPhase({
|
||||
super.key,
|
||||
required this.iconPath,
|
||||
required this.title,
|
||||
required this.value,
|
||||
this.unit,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String value;
|
||||
final String? unit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: containerWhiteDecoration.copyWith(boxShadow: const []),
|
||||
padding: const EdgeInsetsDirectional.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
_buildIcon(),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
children: [
|
||||
_buildTitle(context),
|
||||
_buildValue(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildValue(BuildContext context) {
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor.withValues(
|
||||
alpha: 0.83,
|
||||
),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 15,
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: textStyle,
|
||||
children: [
|
||||
TextSpan(text: '$value '),
|
||||
if (unit != null)
|
||||
TextSpan(
|
||||
text: unit,
|
||||
style: textStyle?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor.withValues(
|
||||
alpha: 0.83,
|
||||
),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context) {
|
||||
return Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
return Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: SvgPicture.asset(iconPath),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phase.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.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 PowerClampPhasesDataWidget extends StatelessWidget {
|
||||
const PowerClampPhasesDataWidget({
|
||||
required this.phaseA,
|
||||
required this.phaseB,
|
||||
required this.phaseC,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Phase? phaseA;
|
||||
final Phase? phaseB;
|
||||
final Phase? phaseC;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final phases = [phaseA, phaseB, phaseC];
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||
child: Row(
|
||||
children: List.generate(5, (index) {
|
||||
if (index.isOdd) return _buildSeparator();
|
||||
final phaseIndex = index ~/ 2;
|
||||
final phase = phases[phaseIndex];
|
||||
final phaseSuffix = ['A', 'B', 'C'][phaseIndex];
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(horizontal: 14),
|
||||
child: Column(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Text(
|
||||
'Phase ${phaseIndex + 1}',
|
||||
style: context.textTheme.titleLarge?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
PowerClampPhase(
|
||||
iconPath: Assets.powerActiveIcon,
|
||||
title: 'Active Power',
|
||||
value: _valueFromCode(
|
||||
code: 'ActivePower$phaseSuffix',
|
||||
points: phase?.dataPoints,
|
||||
),
|
||||
unit: 'W',
|
||||
),
|
||||
PowerClampPhase(
|
||||
iconPath: Assets.voltageIcon,
|
||||
title: 'Voltage',
|
||||
value: _valueFromCode(
|
||||
code: 'Voltage$phaseSuffix',
|
||||
points: phase?.dataPoints,
|
||||
),
|
||||
unit: 'V',
|
||||
),
|
||||
PowerClampPhase(
|
||||
iconPath: Assets.voltMeterIcon,
|
||||
title: 'Current',
|
||||
value: _valueFromCode(
|
||||
code: 'Current$phaseSuffix',
|
||||
points: phase?.dataPoints,
|
||||
),
|
||||
unit: 'A',
|
||||
),
|
||||
PowerClampPhase(
|
||||
iconPath: Assets.speedoMeter,
|
||||
title: 'Power Factor',
|
||||
value: _valueFromCode(
|
||||
code: 'PowerFactor$phaseSuffix',
|
||||
points: phase?.dataPoints,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSeparator() {
|
||||
return Container(
|
||||
height: double.infinity,
|
||||
width: 1,
|
||||
decoration: const BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(20, 0, 0, 0),
|
||||
offset: Offset(1, 0),
|
||||
blurRadius: 1,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(30, 0, 0, 0),
|
||||
offset: Offset(-2, 0),
|
||||
blurRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _valueFromCode({
|
||||
required String code,
|
||||
required List<DataPoint>? points,
|
||||
}) {
|
||||
final element = points?.firstWhere(
|
||||
(e) => e.code == code,
|
||||
orElse: () => DataPoint(value: '--'),
|
||||
);
|
||||
final value = element?.value;
|
||||
if (code.contains('Current')) {
|
||||
return _formatCurrentValue(value?.toString());
|
||||
}
|
||||
if (code.contains('PowerFactor')) {
|
||||
return _formatPowerFactor(value?.toString());
|
||||
}
|
||||
if (code.contains('Voltage')) {
|
||||
return _formatVoltage(value?.toString());
|
||||
}
|
||||
return value?.toString() ?? '--';
|
||||
}
|
||||
|
||||
String _formatCurrentValue(String? value) {
|
||||
if (value == null) return '--';
|
||||
String str = value;
|
||||
if (str.isEmpty || str == '--') return '--';
|
||||
str = str.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (str.isEmpty) return '--';
|
||||
if (str.length == 1) return '${str[0]}.0';
|
||||
return '${str[0]}.${str.substring(1)}';
|
||||
}
|
||||
|
||||
String _formatPowerFactor(String? value) {
|
||||
if (value == null) return '--';
|
||||
String str = value;
|
||||
if (str.isEmpty || str == '--') return '--';
|
||||
str = str.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (str.isEmpty) return '--';
|
||||
final intValue = int.tryParse(str);
|
||||
if (intValue == null) return '--';
|
||||
final doubleValue = intValue / 100;
|
||||
return doubleValue.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String _formatVoltage(String? value) {
|
||||
if (value == null) return '--';
|
||||
String str = value;
|
||||
if (str.isEmpty || str == '--') return '--';
|
||||
str = str.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (str.isEmpty) return '--';
|
||||
if (str.length == 1) return '0.${str[0]}';
|
||||
return '${str.substring(0, str.length - 1)}.${str.substring(str.length - 1)}';
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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';
|
||||
|
||||
class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
const TotalEnergyConsumptionChart({required this.chartData, super.key});
|
||||
|
||||
final List<EnergyDataModel> chartData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
clipData: const FlClipData.vertical(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
lineBarsData: _lineBarsData,
|
||||
),
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<LineChartBarData> get _lineBarsData {
|
||||
return [
|
||||
LineChartBarData(
|
||||
spots: chartData
|
||||
.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => FlSpot(
|
||||
entry.value.date.day.toDouble(),
|
||||
entry.value.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
color: ColorsManager.blueColor.withValues(alpha: 0.6),
|
||||
shadow: const Shadow(color: Colors.black12),
|
||||
show: true,
|
||||
isCurved: true,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.3),
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.2),
|
||||
ColorsManager.vividBlue.withValues(alpha: 0.1),
|
||||
Colors.transparent,
|
||||
],
|
||||
begin: Alignment.center,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
dotData: const FlDotData(show: false),
|
||||
isStrokeCapRound: true,
|
||||
barWidth: 3,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
const TotalEnergyConsumptionChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TotalEnergyConsumptionBloc, TotalEnergyConsumptionState>(
|
||||
builder: (context, state) => Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
Row(
|
||||
children: [
|
||||
ChartsLoadingWidget(
|
||||
isLoading: state.status == TotalEnergyConsumptionStatus.loading,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(title: Text('Total Energy Consumption')),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
TotalEnergyConsumptionChart(chartData: state.chartData),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +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) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|