Compare commits
184 Commits
SP-1607-FE
...
space_mana
Author | SHA1 | Date | |
---|---|---|---|
f415aa1676 | |||
08f8c3c79a | |||
329a4ef027 | |||
940b179686 | |||
5ddfb47977 | |||
2a5d602e94 | |||
5d6747056e | |||
8a274af7be | |||
316c3bd8a7 | |||
aa3b79bdaf | |||
0e31a3ea96 | |||
fd192894cd | |||
692c9e7792 | |||
08a9a5c71f | |||
7eb1d5b0b0 | |||
0d5734a236 | |||
e22bab00d9 | |||
d2a2d391e0 | |||
1d30c753f5 | |||
ca02de2093 | |||
8f7bfa984b | |||
8e9278c93c | |||
15d3a05553 | |||
c6b55cb28b | |||
bfd8e964f7 | |||
08725201d5 | |||
7fe34c61b2 | |||
0c6e4fed80 | |||
69c23525ba | |||
3e32968209 | |||
beb5239c4f | |||
3a98f71ff3 | |||
24a7f3ac2a | |||
ad8e06ac40 | |||
5f8eb9de06 | |||
79b974ee6c | |||
651ac6785e | |||
9fa59ce78b | |||
e2c44ba85f | |||
1edeb664aa | |||
25a55ad820 | |||
e48fc8b82c | |||
8e8fdf0fc6 | |||
8d999f118c | |||
bcb6e49a01 | |||
0d0d51463d | |||
2797dce637 | |||
8827f571f4 | |||
7472aff704 | |||
575ba2aed2 | |||
eb708edc83 | |||
906c2d0430 | |||
e86c25c74a | |||
c2c58e6a7a | |||
0135b6711e | |||
46feb0ea28 | |||
74ae9d7ce1 | |||
710f316f8d | |||
7cc46d464f | |||
0c82a19a1d | |||
d1df33b31e | |||
6a36405530 | |||
3c98365338 | |||
88a7607395 | |||
f58ddf76da | |||
a71a66034c | |||
b06a23cc60 | |||
5595bb7f25 | |||
8e11749ed7 | |||
7bc9079212 | |||
97801872e0 | |||
fa9210f387 | |||
57b6f01177 | |||
cabd37a08a | |||
98ad4246e1 | |||
ba08fcf71f | |||
cf5e05a888 | |||
77d39bfc53 | |||
3bd2bd114b | |||
f98636a2e5 | |||
19548e99ab | |||
b60c674496 | |||
6f3dfb607e | |||
62dabf1ce2 | |||
f07dbad1ea | |||
87df8e4091 | |||
2d68fc23a3 | |||
15ea1b4c5a | |||
17f6985dbf | |||
d1ddf75a42 | |||
393a5361f0 | |||
a56e93d0d7 | |||
94847fa936 | |||
b0ed844893 | |||
066f967cd1 | |||
e28f3c3c03 | |||
2be15e648a | |||
2e12d73151 | |||
c50ed693ae | |||
8dc7d2b3d0 | |||
accafb150e | |||
736e0c3d9c | |||
455d9c1f01 | |||
4479ed04b7 | |||
286dea3f51 | |||
44c4648941 | |||
ca1feb9600 | |||
7b31914e1c | |||
10f35d3747 | |||
1998a629b6 | |||
5940e52826 | |||
7c55e8bbf9 | |||
fdabfe5d95 | |||
8916000696 | |||
305d695358 | |||
3d183528c5 | |||
2c4da63266 | |||
4ebe65f820 | |||
5654d66b60 | |||
b6879035f0 | |||
8ad048e18d | |||
d92b699a2b | |||
6ffb677c33 | |||
e7476a084d | |||
511acc186f | |||
cde79fc168 | |||
283a0dd536 | |||
5636fbe6c9 | |||
3d4c17214c | |||
b95f4063d9 | |||
bc289a0ddf | |||
d9448d9709 | |||
a1d7457065 | |||
a44d4231f1 | |||
7bd0c061d4 | |||
36ddebb5ae | |||
43cb985e74 | |||
7bfd08238e | |||
94b4aa7c46 | |||
3d133581ff | |||
a75e6a89a9 | |||
cc5f107ccb | |||
79c5fe1651 | |||
fd186a00fd | |||
5b91ceb639 | |||
5d3ef95cb7 | |||
a87b11d084 | |||
7c69c7ddbd | |||
16dc066440 | |||
9a41e0c4f5 | |||
c99b32fb81 | |||
a23370471c | |||
595966d306 | |||
321df401fd | |||
ee244fa5ed | |||
fc330d6e17 | |||
1db069e9a5 | |||
cf9bafef4d | |||
2c73dd6c31 | |||
6ec20e2d72 | |||
4feae9ad87 | |||
52046909d5 | |||
fc81555be3 | |||
42319cc4f9 | |||
aded80fb9a | |||
077c6e99d6 | |||
1f444ccfcb | |||
fe716baba7 | |||
34279cfdae | |||
0bf34c66aa | |||
7726ceecb8 | |||
ae2078d28c | |||
95eca869c9 | |||
8967852ca8 | |||
a87e79878b | |||
056e7372e0 | |||
d69d867120 | |||
644fe56478 | |||
766a39f161 | |||
c97dd40b05 | |||
0b65c58947 | |||
e0951aa13d | |||
9e8ebf3768 | |||
b593e75c67 |
8
.github/pull_request_template.md
vendored
@ -7,11 +7,7 @@
|
||||
-->
|
||||
|
||||
## 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**
|
||||
[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
|
||||
|
||||
## Description
|
||||
|
||||
@ -27,4 +23,4 @@
|
||||
- [ ] 🧹 Code refactor
|
||||
- [ ] ✅ Build configuration change
|
||||
- [ ] 📝 Documentation
|
||||
- [ ] 🗑️ Chore
|
||||
- [ ] 🗑️ Chore
|
||||
|
@ -4,10 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
|
@ -4,18 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
steps:
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
29
.github/workflows/pr-check.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: Pull Request Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
|
||||
jobs:
|
||||
setup_flutter:
|
||||
runs-on: ubuntu-latest
|
||||
name: Setup Flutter and Dependencies
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
lfs: false
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.27.3'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run Flutter Build
|
||||
run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart
|
3
.vscode/launch.json
vendored
@ -16,6 +16,7 @@
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_dev.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
@ -35,6 +36,7 @@
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_staging.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
@ -54,6 +56,7 @@
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
5
assets/icons/aqi_air_quality.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="70" height="75" viewBox="0 0 70 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M49.5 5.59985e-05C44.6887 -0.000931248 40.0384 1.7325 36.4016 4.8825C32.7649 8.03249 30.3856 12.3879 29.7 17.1501C29.6496 17.5065 29.6767 17.8696 29.7796 18.2145C29.8825 18.5595 30.0586 18.8782 30.296 19.1488C30.5333 19.4194 30.8264 19.6355 31.155 19.7825C31.4836 19.9294 31.8401 20.0036 32.2 20.0001C32.8029 20.0069 33.388 19.7956 33.8474 19.4051C34.3068 19.0146 34.6096 18.4712 34.7 17.8751C35.2437 13.9233 37.3349 10.3494 40.5137 7.9396C43.6924 5.52975 47.6983 4.48136 51.65 5.02506C55.6017 5.56875 59.1756 7.65999 61.5855 10.8387C63.9953 14.0175 65.0437 18.0233 64.5 21.9751C63.9311 25.6593 62.043 29.0113 59.1871 31.4073C56.3312 33.8033 52.702 35.0801 48.975 35.0001H-0.5C-1.16304 35.0001 -1.79893 35.2634 -2.26777 35.7323C-2.73661 36.2011 -3 36.837 -3 37.5001C-3 38.1631 -2.73661 38.799 -2.26777 39.2678C-1.79893 39.7367 -1.16304 40.0001 -0.5 40.0001H48.8C53.9647 40.0838 58.9698 38.2099 62.8097 34.7549C66.6495 31.3 69.0397 26.5199 69.5 21.3751C69.6888 18.6353 69.3114 15.886 68.3912 13.2985C67.4709 10.711 66.0277 8.34072 64.1514 6.33539C62.275 4.33006 60.0058 2.73264 57.4851 1.64268C54.9644 0.552712 52.2463 -0.00644241 49.5 5.59985e-05Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M38.975 45H-0.5C-1.16304 45 -1.79893 45.2634 -2.26777 45.7322C-2.73661 46.2011 -3 46.837 -3 47.5C-3 48.163 -2.73661 48.7989 -2.26777 49.2678C-1.79893 49.7366 -1.16304 50 -0.5 50H38.975C40.4886 49.9992 41.9825 50.3421 43.3443 51.0026C44.7061 51.6632 45.9002 52.6242 46.8366 53.8134C47.773 55.0025 48.4272 56.3887 48.75 57.8674C49.0728 59.3461 49.0557 60.8788 48.7 62.35C48.1662 64.553 46.8997 66.5092 45.1083 67.8981C43.3169 69.2869 41.1068 70.0259 38.8403 69.9939C36.5738 69.962 34.3854 69.1609 32.6338 67.7221C30.8823 66.2833 29.6715 64.2922 29.2 62.075C29.0988 61.488 28.7914 60.9565 28.333 60.576C27.8747 60.1956 27.2956 59.9913 26.7 60C26.3376 59.9959 25.9787 60.0706 25.648 60.219C25.3174 60.3674 25.023 60.5859 24.7852 60.8594C24.5474 61.1329 24.3719 61.4548 24.2708 61.8028C24.1698 62.1508 24.1456 62.5167 24.2 62.875C24.7446 65.6637 26.0701 68.2403 28.0221 70.305C29.9742 72.3697 32.4725 73.8375 35.2263 74.5375C37.9801 75.2376 40.8761 75.1411 43.5772 74.2592C46.2782 73.3774 48.6733 71.7465 50.4835 69.5565C52.2938 67.3664 53.4448 64.7072 53.8025 61.8884C54.1603 59.0697 53.71 56.2073 52.5043 53.6344C51.2985 51.0616 49.3867 48.8841 46.9916 47.3555C44.5964 45.8269 41.8164 45.0101 38.975 45Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M13.475 24.625H-0.300049C-0.96309 24.625 -1.59897 24.8884 -2.06782 25.3572C-2.53666 25.826 -2.80005 26.4619 -2.80005 27.125C-2.80005 27.788 -2.53666 28.4239 -2.06782 28.8927C-1.59897 29.3616 -0.96309 29.625 -0.300049 29.625H13.35C16.2081 29.672 18.983 28.662 21.1422 26.7888C23.3015 24.9156 24.693 22.3111 25.05 19.475C25.3248 16.6914 24.5527 13.9052 22.8845 11.66C21.2162 9.41477 18.7714 7.87157 16.0269 7.33142C13.2824 6.79126 10.4351 7.29289 8.04041 8.73845C5.64574 10.184 3.87548 12.4698 3.07495 15.15C2.95867 15.5323 2.93577 15.937 3.00815 16.3301C3.08053 16.7231 3.24609 17.0931 3.49096 17.409C3.73582 17.7249 4.05284 17.9774 4.41544 18.1455C4.77805 18.3136 5.17566 18.3923 5.57495 18.375C6.14604 18.3916 6.7056 18.2121 7.16047 17.8664C7.61534 17.5207 7.93807 17.0297 8.07495 16.475C8.54422 15.0056 9.54026 13.7617 10.8714 12.9826C12.2026 12.2034 13.7749 11.944 15.2858 12.2542C16.7967 12.5645 18.1396 13.4225 19.056 14.6632C19.9724 15.9039 20.3977 17.4396 20.25 18.975C20.0327 20.5938 19.2163 22.0723 17.9619 23.1184C16.7076 24.1645 15.1065 24.7021 13.475 24.625Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
7
assets/icons/aqi_humidity.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="65" height="75" viewBox="0 0 65 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32.4384 30.1807C31.6907 29.8668 30.8306 30.2187 30.5171 30.9662L20.3571 55.1748C20.0433 55.9222 20.395 56.7825 21.1424 57.0961C21.328 57.174 21.5205 57.2108 21.7099 57.2108C22.2834 57.2108 22.828 56.8726 23.0638 56.3106L33.2237 32.1019C33.5373 31.3545 33.1856 30.4944 32.4384 30.1807Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M19.7629 28.9307C16.4783 28.9307 13.8062 31.9292 13.8062 35.615C13.8062 39.3009 16.4785 42.2994 19.7629 42.2994C23.0477 42.2994 25.7199 39.3009 25.7199 35.615C25.7199 31.9292 23.0477 28.9307 19.7629 28.9307ZM19.7629 39.3639C18.0968 39.3639 16.7416 37.6821 16.7416 35.615C16.7416 33.548 18.0968 31.8662 19.7629 31.8662C21.4291 31.8662 22.7845 33.548 22.7845 35.615C22.7845 37.6821 21.4291 39.3639 19.7629 39.3639Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M27.5299 52.1678C27.5299 55.8535 30.2022 58.8521 33.4867 58.8521C36.7712 58.8521 39.4435 55.8536 39.4435 52.1678C39.4435 48.4819 36.7712 45.4834 33.4867 45.4834C30.2022 45.4834 27.5299 48.4821 27.5299 52.1678ZM33.4867 48.4188C35.1528 48.4188 36.5081 50.1006 36.5081 52.1678C36.5081 54.235 35.1528 55.9166 33.4867 55.9166C31.8206 55.9166 30.4653 54.2348 30.4653 52.1678C30.4653 50.1006 31.8206 48.4188 33.4867 48.4188Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M62.2349 28.6673L48.9685 5.68931C48.2842 4.50308 47.0581 3.79482 45.6887 3.79482C44.3193 3.79482 43.0932 4.50308 42.4091 5.68901L37.5363 14.1299L30.756 2.38608C29.8933 0.891943 28.3485 0 26.6232 0C24.8981 0 23.3532 0.891943 22.4907 2.38594L3.59513 35.1141C-1.112 43.2671 -1.20179 53.0487 3.35504 61.2801C3.74762 61.9893 4.64088 62.2459 5.35002 61.8533C6.05915 61.4606 6.31579 60.5676 5.92321 59.8583C1.86969 52.5363 1.94982 43.8349 6.13737 36.5817L25.0328 3.85371C25.5112 3.0249 26.3045 2.9354 26.6232 2.9354C26.9421 2.9354 27.7353 3.0249 28.2137 3.85371L47.1093 36.5818C51.3852 43.9879 51.3852 52.8309 47.1093 60.2369C42.8335 67.643 35.175 72.0646 26.6232 72.0646C22.2829 72.0646 18.0757 70.9146 14.4567 68.7388C13.762 68.3209 12.8603 68.5458 12.4425 69.2405C12.0249 69.9352 12.2494 70.8369 12.9442 71.2547C17.02 73.7049 21.75 75 26.6234 75C36.2364 75 44.845 70.0298 49.6514 61.7046C50.6635 59.9517 51.4596 58.1266 52.0456 56.2607C55.7953 54.952 59.0005 52.5164 61.316 49.2136C63.6089 45.9432 64.8209 42.1323 64.8209 38.1927C64.8211 34.8907 63.9268 31.5968 62.2349 28.6673ZM58.9126 47.5285C57.3392 49.7726 55.2829 51.5414 52.8959 52.7241C53.8949 46.7682 52.814 40.5916 49.6516 35.1139L39.231 17.065L44.9515 7.15591C45.1733 6.77153 45.5408 6.72993 45.6887 6.72993C45.8365 6.72993 46.2042 6.77139 46.4261 7.1562L59.6928 30.1348C61.1478 32.6543 61.8857 35.3651 61.8857 38.1924C61.8857 41.4785 60.8298 44.794 58.9126 47.5285Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M8.53564 67.3704C9.34627 67.3704 10.0034 66.7132 10.0034 65.9026C10.0034 65.092 9.34627 64.4348 8.53564 64.4348C7.72502 64.4348 7.06787 65.092 7.06787 65.9026C7.06787 66.7132 7.72502 67.3704 8.53564 67.3704Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
8
assets/icons/aqi_temperature.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="45" height="75" viewBox="0 0 45 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.9116 48.6345V19.6655C19.9116 18.4519 18.928 17.4683 17.7143 17.4683C16.5012 17.4683 15.517 18.4519 15.517 19.6655V48.6345C11.6547 49.6164 8.78906 53.1229 8.78906 57.2874C8.78906 62.2095 12.7928 66.2138 17.7143 66.2138C22.6358 66.2138 26.6401 62.2095 26.6401 57.2874C26.6401 53.1229 23.774 49.6164 19.9116 48.6345ZM17.7143 61.8193C15.2161 61.8193 13.1836 59.7862 13.1836 57.2874C13.1836 54.788 15.2161 52.755 17.7143 52.755C20.2131 52.755 22.2456 54.788 22.2456 57.2874C22.2456 59.7862 20.2131 61.8193 17.7143 61.8193Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M28.6194 43.3193V10.9062C28.6194 4.89235 23.7276 0 17.7143 0C11.7016 0 6.80981 4.89235 6.80981 10.9062V43.3193C2.52514 46.6684 0 51.804 0 57.2823C0 67.0521 7.94678 75 17.7143 75C27.4824 75 35.4292 67.0521 35.4292 57.2823C35.4292 51.804 32.9041 46.6684 28.6194 43.3193ZM17.7143 70.6055C10.3695 70.6055 4.39453 64.6288 4.39453 57.2823C4.39453 52.8631 6.58035 48.7398 10.2419 46.2524C10.8438 45.8433 11.2043 45.1624 11.2043 44.4345V10.9062C11.2043 7.31564 14.1249 4.39453 17.7143 4.39453C21.3043 4.39453 24.2249 7.31564 24.2249 10.9062V44.4345C24.2249 45.1624 24.5853 45.8433 25.1873 46.2524C28.8488 48.7398 31.0347 52.8631 31.0347 57.2823C31.0347 64.6288 25.0591 70.6055 17.7143 70.6055Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M42.021 35.0811H35.2112C33.9975 35.0811 33.0139 36.0647 33.0139 37.2783C33.0139 38.492 33.9975 39.4756 35.2112 39.4756H42.021C43.2341 39.4756 44.2183 38.492 44.2183 37.2783C44.2183 36.0647 43.2341 35.0811 42.021 35.0811Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M42.021 26.2939H35.2112C33.9975 26.2939 33.0139 27.2776 33.0139 28.4912C33.0139 29.7049 33.9975 30.6885 35.2112 30.6885H42.021C43.2341 30.6885 44.2183 29.7049 44.2183 28.4912C44.2183 27.2776 43.2341 26.2939 42.021 26.2939Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M42.021 17.5049H35.2112C33.9975 17.5049 33.0139 18.4885 33.0139 19.7021C33.0139 20.9158 33.9975 21.8994 35.2112 21.8994H42.021C43.2341 21.8994 44.2183 20.9158 44.2183 19.7021C44.2183 18.4885 43.2341 17.5049 42.021 17.5049Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
<path d="M35.2112 13.1104H42.021C43.2341 13.1104 44.2183 12.1267 44.2183 10.9131C44.2183 9.70001 43.2341 8.71582 42.021 8.71582H35.2112C33.9975 8.71582 33.0139 9.70001 33.0139 10.9131C33.0139 12.1267 33.9975 13.1104 35.2112 13.1104Z" fill="#5D5D5D" fill-opacity="0.1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
3
assets/icons/close_settings_icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 24C8.79469 24 5.78123 22.7518 3.51469 20.4853C1.24823 18.2188 0 15.2053 0 12C0 8.79469 1.24823 5.78123 3.51469 3.51469C5.78123 1.24823 8.79469 0 12 0C15.2053 0 18.2188 1.24823 20.4853 3.51469C22.7518 5.78123 24 8.79469 24 12C24 15.2053 22.7518 18.2188 20.4853 20.4853C18.2188 22.7518 15.2053 24 12 24ZM12 1.875C9.2955 1.875 6.75291 2.92819 4.84055 4.84055C2.92819 6.75291 1.875 9.2955 1.875 12C1.875 14.7045 2.92819 17.2471 4.84055 19.1595C6.75291 21.0718 9.2955 22.125 12 22.125C14.7045 22.125 17.2471 21.0718 19.1595 19.1595C21.0718 17.2471 22.125 14.7045 22.125 12C22.125 9.2955 21.0718 6.75291 19.1595 4.84055C17.2471 2.92819 14.7045 1.875 12 1.875ZM15.9775 17.3033L12 13.3258L8.02252 17.3033L6.6967 15.9775L10.6742 12L6.6967 8.02252L8.02252 6.6967L12 10.6742L15.9775 6.6967L17.3033 8.02252L13.3258 12L17.3033 15.9775L15.9775 17.3033Z" fill="#999999" fill-opacity="0.7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 992 B |
3
assets/icons/edit_name_icon_settings.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4854 3.1332L9.8668 0.515228C9.64704 0.295536 9.34903 0.172119 9.03829 0.172119C8.72755 0.172119 8.42953 0.295536 8.20977 0.515228L0.983989 7.74042C0.874797 7.84895 0.788225 7.97806 0.729285 8.12027C0.670346 8.26249 0.640212 8.41499 0.640629 8.56894V11.1875C0.640629 11.4983 0.764094 11.7964 0.983864 12.0161C1.20363 12.2359 1.5017 12.3594 1.8125 12.3594H11.6563C11.8427 12.3594 12.0216 12.2853 12.1534 12.1534C12.2853 12.0216 12.3594 11.8427 12.3594 11.6562C12.3594 11.4698 12.2853 11.2909 12.1534 11.1591C12.0216 11.0272 11.8427 10.9531 11.6563 10.9531H6.32422L12.4854 4.79081C12.5942 4.68199 12.6806 4.55278 12.7395 4.41057C12.7984 4.26836 12.8288 4.11594 12.8288 3.96201C12.8288 3.80807 12.7984 3.65565 12.7395 3.51344C12.6806 3.37123 12.5942 3.24202 12.4854 3.1332ZM4.33204 10.9531H2.04688V8.66796L6.96875 3.74609L9.25391 6.03124L4.33204 10.9531ZM10.25 5.03515L7.96485 2.74999L9.03946 1.67538L11.3246 3.96054L10.25 5.03515Z" fill="#D5D5D5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
7
assets/icons/humidity.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.00332 3.62169C4.9136 3.58402 4.81039 3.62624 4.77277 3.71594L3.55357 6.62098C3.51592 6.71066 3.55812 6.8139 3.6478 6.85153C3.67008 6.86088 3.69317 6.86529 3.7159 6.86529C3.78472 6.86529 3.85008 6.82471 3.87838 6.75728L5.09756 3.85223C5.13519 3.76254 5.09299 3.65932 5.00332 3.62169Z" fill="#5D5D5D"/>
|
||||
<path d="M3.48227 3.47168C3.08812 3.47168 2.76746 3.8315 2.76746 4.2738C2.76746 4.71611 3.08813 5.07593 3.48227 5.07593C3.87644 5.07593 4.1971 4.71611 4.1971 4.2738C4.1971 3.8315 3.87644 3.47168 3.48227 3.47168ZM3.48227 4.72366C3.28234 4.72366 3.1197 4.52185 3.1197 4.2738C3.1197 4.02576 3.28234 3.82395 3.48227 3.82395C3.6822 3.82395 3.84485 4.02576 3.84485 4.2738C3.84485 4.52185 3.6822 4.72366 3.48227 4.72366Z" fill="#5D5D5D"/>
|
||||
<path d="M4.41431 6.26013C4.41431 6.70242 4.73498 7.06226 5.12912 7.06226C5.52326 7.06226 5.84394 6.70243 5.84394 6.26013C5.84394 5.81783 5.52326 5.45801 5.12912 5.45801C4.73498 5.45801 4.41431 5.81785 4.41431 6.26013ZM5.12912 5.81026C5.32905 5.81026 5.49169 6.01207 5.49169 6.26013C5.49169 6.5082 5.32905 6.70999 5.12912 6.70999C4.92919 6.70999 4.76655 6.50818 4.76655 6.26013C4.76655 6.01207 4.92919 5.81026 5.12912 5.81026Z" fill="#5D5D5D"/>
|
||||
<path d="M8.5789 3.44007L6.98694 0.682717C6.90482 0.540369 6.75769 0.455379 6.59337 0.455379C6.42903 0.455379 6.2819 0.540369 6.19981 0.682682L5.61507 1.69559L4.80143 0.28633C4.69792 0.107033 4.51254 0 4.3055 0C4.09849 0 3.91311 0.107033 3.80961 0.286312L1.54213 4.21369C0.977278 5.19205 0.966503 6.36585 1.51332 7.35361C1.56043 7.43871 1.66762 7.46951 1.75272 7.4224C1.83782 7.37527 1.86861 7.26812 1.8215 7.183C1.33508 6.30436 1.3447 5.26018 1.8472 4.3898L4.11466 0.462445C4.17207 0.362988 4.26725 0.352248 4.3055 0.352248C4.34377 0.352248 4.43896 0.362988 4.49637 0.462445L6.76384 4.38982C7.27694 5.27855 7.27694 6.33971 6.76384 7.22842C6.25073 8.11716 5.33171 8.64775 4.3055 8.64775C3.78466 8.64775 3.2798 8.50975 2.84552 8.24866C2.76216 8.19851 2.65395 8.22549 2.60382 8.30886C2.5537 8.39222 2.58065 8.50043 2.66402 8.55056C3.15312 8.84459 3.72071 9 4.30552 9C5.45908 9 6.49212 8.40357 7.06889 7.40456C7.19034 7.1942 7.28587 6.97519 7.35619 6.75128C7.80615 6.59424 8.19078 6.30197 8.46864 5.90563C8.74379 5.51319 8.88923 5.05587 8.88923 4.58313C8.88925 4.18688 8.78193 3.79162 8.5789 3.44007ZM8.18023 5.70342C7.99143 5.97271 7.74466 6.18497 7.45823 6.32689C7.57811 5.61219 7.4484 4.87099 7.06891 4.21367L5.81844 2.0478L6.5049 0.858709C6.53151 0.812584 6.57561 0.807592 6.59337 0.807592C6.6111 0.807592 6.65522 0.812566 6.68185 0.858744L8.27385 3.61617C8.44846 3.91852 8.537 4.24382 8.537 4.58309C8.537 4.97742 8.41029 5.37529 8.18023 5.70342Z" fill="#5D5D5D"/>
|
||||
<path d="M2.135 8.08444C2.23227 8.08444 2.31113 8.00559 2.31113 7.90831C2.31113 7.81104 2.23227 7.73218 2.135 7.73218C2.03772 7.73218 1.95886 7.81104 1.95886 7.90831C1.95886 8.00559 2.03772 8.08444 2.135 8.08444Z" fill="#5D5D5D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
11
assets/icons/location_pin.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_9576_4022)">
|
||||
<path d="M8.00334 7.92855C9.28029 5.92481 9.11977 6.1748 9.15656 6.12255C9.62147 5.46682 9.86719 4.69505 9.86719 3.89062C9.86719 1.75734 8.13607 0 6 0C3.87089 0 2.13281 1.75388 2.13281 3.89062C2.13281 4.69453 2.38369 5.48651 2.86383 6.15108L3.99661 7.92858C2.78548 8.1147 0.726562 8.66934 0.726562 9.89062C0.726562 10.3358 1.01714 10.9703 2.40145 11.4647C3.36806 11.8099 4.64604 12 6 12C8.53184 12 11.2734 11.2858 11.2734 9.89062C11.2734 8.66913 9.21694 8.11507 8.00334 7.92855ZM3.45115 5.76434C3.44728 5.75829 3.44325 5.75238 3.43903 5.74657C3.03949 5.19691 2.83594 4.54549 2.83594 3.89062C2.83594 2.13239 4.2517 0.703125 6 0.703125C7.74466 0.703125 9.16406 2.13302 9.16406 3.89062C9.16406 4.54655 8.96435 5.17587 8.58642 5.71104C8.55255 5.75571 8.72925 5.48121 6 9.7638L3.45115 5.76434ZM6 11.2969C3.23452 11.2969 1.42969 10.484 1.42969 9.89062C1.42969 9.49181 2.35706 8.83605 4.41206 8.58045L5.70352 10.6069C5.76806 10.7082 5.87986 10.7695 5.99998 10.7695C6.12009 10.7695 6.23191 10.7082 6.29644 10.6069L7.58787 8.58045C9.64291 8.83605 10.5703 9.49181 10.5703 9.89062C10.5703 10.479 8.78173 11.2969 6 11.2969Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M6 2.13281C5.03074 2.13281 4.24219 2.92137 4.24219 3.89062C4.24219 4.85988 5.03074 5.64844 6 5.64844C6.96926 5.64844 7.75781 4.85988 7.75781 3.89062C7.75781 2.92137 6.96926 2.13281 6 2.13281ZM6 4.94531C5.41845 4.94531 4.94531 4.47218 4.94531 3.89062C4.94531 3.30907 5.41845 2.83594 6 2.83594C6.58155 2.83594 7.05469 3.30907 7.05469 3.89062C7.05469 4.47218 6.58155 4.94531 6 4.94531Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9576_4022">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
24
assets/icons/settings_button.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<svg width="40" height="26" viewBox="0 0 40 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_9292_1537)">
|
||||
<rect x="3" y="3" width="34" height="20" rx="10" fill="#F5F6F7"/>
|
||||
<g clip-path="url(#clip0_9292_1537)">
|
||||
<path d="M20.4393 20H19.5607C18.85 20 18.2718 19.4218 18.2718 18.7112V18.414C17.9697 18.3174 17.6762 18.1956 17.3942 18.0497L17.1835 18.2603C16.6733 18.7711 15.8561 18.7562 15.3607 18.2601L14.7397 17.6391C14.2434 17.1434 14.2291 16.3264 14.7398 15.8163L14.9503 15.6058C14.8044 15.3238 14.6826 15.0303 14.586 14.7281H14.2888C13.5782 14.7281 13 14.15 13 13.4393V12.5607C13 11.85 13.5782 11.2719 14.2888 11.2719H14.586C14.6826 10.9697 14.8044 10.6762 14.9503 10.3942L14.7397 10.1836C14.2293 9.67374 14.2434 8.85666 14.7399 8.36072L15.3609 7.73969C15.8574 7.24247 16.6745 7.23006 17.1838 7.73986L17.3942 7.95032C17.6762 7.80441 17.9698 7.68257 18.2719 7.58602V7.28879C18.2719 6.57816 18.85 6 19.5607 6H20.4393C21.15 6 21.7281 6.57816 21.7281 7.28879V7.58605C22.0302 7.68257 22.3238 7.80441 22.6058 7.95035L22.8164 7.73969C23.3266 7.22886 24.1439 7.24384 24.6393 7.73988L25.2603 8.36086C25.7566 8.85657 25.7708 9.67358 25.2601 10.1837L25.0497 10.3942C25.1956 10.6762 25.3174 10.9697 25.414 11.2719H25.7112C26.4218 11.2719 27 11.85 27 12.5607V13.4393C27 14.15 26.4218 14.7281 25.7112 14.7281H25.414C25.3174 15.0303 25.1956 15.3238 25.0497 15.6058L25.2603 15.8164C25.7707 16.3263 25.7566 17.1434 25.2601 17.6393L24.6391 18.2603C24.1426 18.7576 23.3255 18.77 22.8162 18.2602L22.6058 18.0497C22.3238 18.1956 22.0302 18.3175 21.7281 18.414V18.7113C21.7281 19.4218 21.15 20 20.4393 20ZM17.5313 17.1882C17.9231 17.4199 18.3447 17.595 18.7845 17.7085C18.9656 17.7552 19.0922 17.9185 19.0922 18.1056V18.7112C19.0922 18.9695 19.3024 19.1797 19.5607 19.1797H20.4393C20.6976 19.1797 20.9078 18.9695 20.9078 18.7112V18.1056C20.9078 17.9185 21.0344 17.7552 21.2155 17.7085C21.6553 17.595 22.0769 17.4199 22.4687 17.1882C22.6299 17.0929 22.8351 17.1188 22.9675 17.2513L23.3965 17.6803C23.5815 17.8654 23.8785 17.8611 24.0589 17.6805L24.6803 17.059C24.8603 16.8793 24.8663 16.5822 24.6805 16.3966L24.2513 15.9675C24.1189 15.8351 24.093 15.6298 24.1883 15.4687C24.42 15.0769 24.595 14.6553 24.7085 14.2155C24.7552 14.0344 24.9186 13.9078 25.1056 13.9078H25.7112C25.9695 13.9078 26.1797 13.6977 26.1797 13.4394V12.5607C26.1797 12.3024 25.9695 12.0922 25.7112 12.0922H25.1056C24.9186 12.0922 24.7552 11.9657 24.7085 11.7846C24.595 11.3447 24.42 10.9231 24.1883 10.5314C24.093 10.3702 24.1189 10.165 24.2513 10.0326L24.6803 9.60358C24.8658 9.41835 24.8609 9.1214 24.6805 8.94118L24.0591 8.31979C23.879 8.13943 23.5819 8.13415 23.3967 8.31962L22.9676 8.74879C22.8352 8.88121 22.6299 8.90713 22.4687 8.81181C22.077 8.58013 21.6553 8.4051 21.2155 8.2916C21.0344 8.24487 20.9079 8.08152 20.9079 7.89446V7.28879C20.9079 7.03048 20.6977 6.82031 20.4394 6.82031H19.5607C19.3024 6.82031 19.0922 7.03048 19.0922 7.28879V7.8944C19.0922 8.08146 18.9657 8.24481 18.7845 8.29154C18.3447 8.40505 17.9231 8.58007 17.5314 8.81176C17.3701 8.90705 17.1649 8.88113 17.0325 8.74873L16.6036 8.31973C16.4186 8.13456 16.1216 8.13886 15.9412 8.31954L15.3197 8.94096C15.1398 9.12071 15.1337 9.41775 15.3196 9.60336L15.7487 10.0325C15.8811 10.1649 15.9071 10.3702 15.8118 10.5313C15.5801 10.9231 15.4051 11.3447 15.2916 11.7845C15.2448 11.9656 15.0815 12.0922 14.8944 12.0922H14.2888C14.0305 12.0922 13.8203 12.3024 13.8203 12.5607V13.4393C13.8203 13.6976 14.0305 13.9078 14.2888 13.9078H14.8944C15.0815 13.9078 15.2448 14.0344 15.2915 14.2155C15.405 14.6553 15.5801 15.0769 15.8117 15.4686C15.907 15.6298 15.8811 15.8351 15.7487 15.9675L15.3197 16.3965C15.1343 16.5817 15.1391 16.8786 15.3195 17.0589L15.9409 17.6802C16.121 17.8606 16.4181 17.8659 16.6033 17.6804L17.0325 17.2512C17.13 17.1537 17.333 17.0709 17.5313 17.1882Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M19.9992 16.0461C18.3196 16.0461 16.9531 14.6796 16.9531 13C16.9531 11.3204 18.3196 9.95391 19.9992 9.95391C21.6789 9.95391 23.0453 11.3204 23.0453 13C23.0453 14.6796 21.6789 16.0461 19.9992 16.0461ZM19.9992 10.7742C18.7719 10.7742 17.7734 11.7727 17.7734 13C17.7734 14.2273 18.7719 15.2258 19.9992 15.2258C21.2265 15.2258 22.225 14.2273 22.225 13C22.225 11.7727 21.2265 10.7742 19.9992 10.7742Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_9292_1537" x="0" y="0" width="40" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_9292_1537"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_9292_1537" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_9292_1537">
|
||||
<rect width="14" height="14" fill="white" transform="translate(13 6)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
8
assets/icons/thermometer.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.73509 5.83614V2.35986C4.73509 2.21423 4.61706 2.09619 4.47142 2.09619C4.32585 2.09619 4.20775 2.21423 4.20775 2.35986V5.83614C3.74426 5.95397 3.40039 6.37475 3.40039 6.87449C3.40039 7.46514 3.88084 7.94566 4.47142 7.94566C5.062 7.94566 5.54252 7.46514 5.54252 6.87449C5.54252 6.37475 5.19858 5.95397 4.73509 5.83614ZM4.47142 7.41831C4.17163 7.41831 3.92773 7.17435 3.92773 6.87449C3.92773 6.57456 4.17163 6.3306 4.47142 6.3306C4.77128 6.3306 5.01517 6.57456 5.01517 6.87449C5.01517 7.17435 4.77128 7.41831 4.47142 7.41831Z" fill="#5D5D5D"/>
|
||||
<path d="M5.78003 5.19832V1.30875C5.78003 0.587082 5.19302 0 4.47142 0C3.74989 0 3.16288 0.587082 3.16288 1.30875V5.19832C2.64872 5.60021 2.3457 6.21648 2.3457 6.87387C2.3457 8.04625 3.29932 9 4.47142 9C5.64359 9 6.59721 8.04625 6.59721 6.87387C6.59721 6.21648 6.29419 5.60021 5.78003 5.19832ZM4.47142 8.47266C3.59004 8.47266 2.87305 7.75546 2.87305 6.87387C2.87305 6.34357 3.13535 5.84878 3.57473 5.55029C3.64697 5.5012 3.69022 5.41949 3.69022 5.33215V1.30875C3.69022 0.877876 4.04069 0.527344 4.47142 0.527344C4.90222 0.527344 5.25269 0.877876 5.25269 1.30875V5.33215C5.25269 5.41949 5.29594 5.5012 5.36818 5.55029C5.80756 5.84878 6.06986 6.34357 6.06986 6.87387C6.06986 7.75546 5.3528 8.47266 4.47142 8.47266Z" fill="#5D5D5D"/>
|
||||
<path d="M7.38822 4.20972H6.57104C6.42541 4.20972 6.30737 4.32775 6.30737 4.47339C6.30737 4.61903 6.42541 4.73706 6.57104 4.73706H7.38822C7.53379 4.73706 7.65189 4.61903 7.65189 4.47339C7.65189 4.32775 7.53379 4.20972 7.38822 4.20972Z" fill="#5D5D5D"/>
|
||||
<path d="M7.38822 3.15527H6.57104C6.42541 3.15527 6.30737 3.27331 6.30737 3.41895C6.30737 3.56458 6.42541 3.68262 6.57104 3.68262H7.38822C7.53379 3.68262 7.65189 3.56458 7.65189 3.41895C7.65189 3.27331 7.53379 3.15527 7.38822 3.15527Z" fill="#5D5D5D"/>
|
||||
<path d="M7.38822 2.10059H6.57104C6.42541 2.10059 6.30737 2.21862 6.30737 2.36426C6.30737 2.5099 6.42541 2.62793 6.57104 2.62793H7.38822C7.53379 2.62793 7.65189 2.5099 7.65189 2.36426C7.65189 2.21862 7.53379 2.10059 7.38822 2.10059Z" fill="#5D5D5D"/>
|
||||
<path d="M6.57104 1.57324H7.38822C7.53379 1.57324 7.65189 1.45521 7.65189 1.30957C7.65189 1.164 7.53379 1.0459 7.38822 1.0459H6.57104C6.42541 1.0459 6.30737 1.164 6.30737 1.30957C6.30737 1.45521 6.42541 1.57324 6.57104 1.57324Z" fill="#5D5D5D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -17,7 +17,8 @@ class TagDialogTextfieldDropdown extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState();
|
||||
_DialogTextfieldDropdownState createState() =>
|
||||
_DialogTextfieldDropdownState();
|
||||
}
|
||||
|
||||
class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
@ -36,6 +37,12 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
|
||||
_focusNode.addListener(() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
// Call onSelected when focus is lost
|
||||
final selectedTag = _filteredItems.firstWhere(
|
||||
(tag) => tag.tag == _controller.text,
|
||||
orElse: () => Tag(tag: _controller.text),
|
||||
);
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
}
|
||||
});
|
||||
@ -43,7 +50,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
|
||||
void _filterItems() {
|
||||
setState(() {
|
||||
_filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList();
|
||||
_filteredItems = widget.items;
|
||||
// .where((tag) => tag.product?.uuid == widget.product)
|
||||
// .toList();
|
||||
});
|
||||
}
|
||||
|
||||
@ -112,7 +121,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: ColorsManager.textPrimaryColor)),
|
||||
?.copyWith(
|
||||
color: ColorsManager
|
||||
.textPrimaryColor)),
|
||||
onTap: () {
|
||||
_controller.text = tag.tag ?? '';
|
||||
widget.onSelected(tag);
|
||||
@ -156,13 +167,15 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onFieldSubmitted: (value) {
|
||||
final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value,
|
||||
final selectedTag = _filteredItems.firstWhere(
|
||||
(tag) => tag.tag == value,
|
||||
orElse: () => Tag(tag: value));
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
},
|
||||
onTapOutside: (event) {
|
||||
widget.onSelected(_filteredItems.firstWhere((tag) => tag.tag == _controller.text,
|
||||
widget.onSelected(_filteredItems.firstWhere(
|
||||
(tag) => tag.tag == _controller.text,
|
||||
orElse: () => Tag(tag: _controller.text)));
|
||||
_closeDropdown();
|
||||
},
|
||||
|
57
lib/pages/analytics/models/air_quality_data_model.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AirQualityDataModel extends Equatable {
|
||||
const AirQualityDataModel({
|
||||
required this.date,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final List<AirQualityPercentageData> data;
|
||||
|
||||
factory AirQualityDataModel.fromJson(Map<String, dynamic> json) {
|
||||
return AirQualityDataModel(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
static final Map<String, Color> metricColors = {
|
||||
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
|
||||
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
|
||||
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7),
|
||||
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
|
||||
'severe': ColorsManager.severePink.withValues(alpha: 0.7),
|
||||
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date, data];
|
||||
}
|
||||
|
||||
class AirQualityPercentageData extends Equatable {
|
||||
const AirQualityPercentageData({
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.percentage,
|
||||
});
|
||||
|
||||
final String type;
|
||||
final String name;
|
||||
final double percentage;
|
||||
|
||||
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
|
||||
return AirQualityPercentageData(
|
||||
type: json['type'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, name, percentage];
|
||||
}
|
@ -8,6 +8,8 @@ class AnalyticsDevice {
|
||||
this.isActive,
|
||||
this.productDevice,
|
||||
this.spaceUuid,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
});
|
||||
|
||||
final String uuid;
|
||||
@ -18,22 +20,27 @@ class AnalyticsDevice {
|
||||
final bool? isActive;
|
||||
final ProductDevice? productDevice;
|
||||
final String? spaceUuid;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
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,
|
||||
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(),
|
||||
productDevice: json['productDevice'] != null
|
||||
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
|
||||
: null,
|
||||
spaceUuid: json['spaceUuid'] as String?,
|
||||
latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null,
|
||||
longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -60,8 +67,12 @@ class ProductDevice {
|
||||
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,
|
||||
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?,
|
||||
|
55
lib/pages/analytics/models/device_location_info.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class DeviceLocationInfo extends Equatable {
|
||||
const DeviceLocationInfo({
|
||||
this.airQuality,
|
||||
this.humidity,
|
||||
this.city,
|
||||
this.country,
|
||||
this.address,
|
||||
this.temperature,
|
||||
});
|
||||
|
||||
final double? airQuality;
|
||||
final double? humidity;
|
||||
final String? city;
|
||||
final String? country;
|
||||
final String? address;
|
||||
final double? temperature;
|
||||
|
||||
factory DeviceLocationInfo.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceLocationInfo(
|
||||
airQuality: json['aqi'] as double?,
|
||||
humidity: json['humidity'] as double?,
|
||||
temperature: json['temperature'] as double?,
|
||||
);
|
||||
}
|
||||
|
||||
DeviceLocationInfo copyWith({
|
||||
double? airQuality,
|
||||
double? humidity,
|
||||
String? city,
|
||||
String? country,
|
||||
String? address,
|
||||
double? temperature,
|
||||
}) {
|
||||
return DeviceLocationInfo(
|
||||
airQuality: airQuality ?? this.airQuality,
|
||||
humidity: humidity ?? this.humidity,
|
||||
city: city ?? this.city,
|
||||
country: country ?? this.country,
|
||||
address: address ?? this.address,
|
||||
temperature: temperature ?? this.temperature,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
airQuality,
|
||||
humidity,
|
||||
city,
|
||||
country,
|
||||
address,
|
||||
temperature,
|
||||
];
|
||||
}
|
@ -1,18 +1,49 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class RangeOfAqi extends Equatable {
|
||||
final double min;
|
||||
final double avg;
|
||||
final double max;
|
||||
final DateTime date;
|
||||
final List<RangeOfAqiValue> data;
|
||||
|
||||
const RangeOfAqi({
|
||||
required this.min,
|
||||
required this.avg,
|
||||
required this.max,
|
||||
required this.data,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
factory RangeOfAqi.fromJson(Map<String, dynamic> json) {
|
||||
return RangeOfAqi(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => RangeOfAqiValue.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [min, avg, max, date];
|
||||
List<Object?> get props => [data, date];
|
||||
}
|
||||
|
||||
class RangeOfAqiValue extends Equatable {
|
||||
final String type;
|
||||
final double min;
|
||||
final double average;
|
||||
final double max;
|
||||
|
||||
const RangeOfAqiValue({
|
||||
required this.type,
|
||||
required this.min,
|
||||
required this.average,
|
||||
required this.max,
|
||||
});
|
||||
|
||||
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
|
||||
return RangeOfAqiValue(
|
||||
type: json['type'] as String,
|
||||
min: (json['min'] as num).toDouble(),
|
||||
average: (json['average'] as num).toDouble(),
|
||||
max: (json['max'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, min, average, max];
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
|
||||
|
||||
part 'air_quality_distribution_event.dart';
|
||||
part 'air_quality_distribution_state.dart';
|
||||
|
||||
class AirQualityDistributionBloc
|
||||
extends Bloc<AirQualityDistributionEvent, AirQualityDistributionState> {
|
||||
final AirQualityDistributionService _aqiDistributionService;
|
||||
|
||||
AirQualityDistributionBloc(
|
||||
this._aqiDistributionService,
|
||||
) : super(const AirQualityDistributionState()) {
|
||||
on<LoadAirQualityDistribution>(_onLoadAirQualityDistribution);
|
||||
on<ClearAirQualityDistribution>(_onClearAirQualityDistribution);
|
||||
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
|
||||
}
|
||||
|
||||
Future<void> _onLoadAirQualityDistribution(
|
||||
LoadAirQualityDistribution event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(state.copyWith(status: AirQualityDistributionStatus.loading));
|
||||
final result = await _aqiDistributionService.getAirQualityDistribution(
|
||||
event.param,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: AirQualityDistributionStatus.success,
|
||||
chartData: result,
|
||||
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
AirQualityDistributionState(
|
||||
status: AirQualityDistributionStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
selectedAqiType: state.selectedAqiType,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onClearAirQualityDistribution(
|
||||
ClearAirQualityDistribution event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) async {
|
||||
emit(const AirQualityDistributionState());
|
||||
}
|
||||
|
||||
void _onUpdateAqiTypeEvent(
|
||||
UpdateAqiTypeEvent event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedAqiType: event.aqiType,
|
||||
filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<AirQualityDataModel> _arrangeChartDataByType(
|
||||
List<AirQualityDataModel> data,
|
||||
AqiType aqiType,
|
||||
) {
|
||||
final filteredData = data.map(
|
||||
(data) => AirQualityDataModel(
|
||||
date: data.date,
|
||||
data: data.data.where((value) => value.type == aqiType.code).toList(),
|
||||
),
|
||||
);
|
||||
return filteredData.toList();
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'air_quality_distribution_bloc.dart';
|
||||
|
||||
sealed class AirQualityDistributionEvent extends Equatable {
|
||||
const AirQualityDistributionEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class LoadAirQualityDistribution extends AirQualityDistributionEvent {
|
||||
final GetAirQualityDistributionParam param;
|
||||
|
||||
const LoadAirQualityDistribution(this.param);
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class UpdateAqiTypeEvent extends AirQualityDistributionEvent {
|
||||
const UpdateAqiTypeEvent(this.aqiType);
|
||||
|
||||
final AqiType aqiType;
|
||||
|
||||
@override
|
||||
List<Object> get props => [aqiType];
|
||||
}
|
||||
|
||||
final class ClearAirQualityDistribution extends AirQualityDistributionEvent {
|
||||
const ClearAirQualityDistribution();
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
part of 'air_quality_distribution_bloc.dart';
|
||||
|
||||
enum AirQualityDistributionStatus {
|
||||
initial,
|
||||
loading,
|
||||
success,
|
||||
failure,
|
||||
}
|
||||
|
||||
class AirQualityDistributionState extends Equatable {
|
||||
const AirQualityDistributionState({
|
||||
this.status = AirQualityDistributionStatus.initial,
|
||||
this.chartData = const [],
|
||||
this.filteredChartData = const [],
|
||||
this.errorMessage,
|
||||
this.selectedAqiType = AqiType.aqi,
|
||||
});
|
||||
|
||||
final AirQualityDistributionStatus status;
|
||||
final List<AirQualityDataModel> chartData;
|
||||
final List<AirQualityDataModel> filteredChartData;
|
||||
final String? errorMessage;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
AirQualityDistributionState copyWith({
|
||||
AirQualityDistributionStatus? status,
|
||||
List<AirQualityDataModel>? chartData,
|
||||
List<AirQualityDataModel>? filteredChartData,
|
||||
String? errorMessage,
|
||||
AqiType? selectedAqiType,
|
||||
}) {
|
||||
return AirQualityDistributionState(
|
||||
status: status ?? this.status,
|
||||
chartData: chartData ?? this.chartData,
|
||||
filteredChartData: filteredChartData ?? this.filteredChartData,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, chartData, errorMessage, selectedAqiType];
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_location_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart';
|
||||
|
||||
part 'device_location_event.dart';
|
||||
part 'device_location_state.dart';
|
||||
|
||||
class DeviceLocationBloc extends Bloc<DeviceLocationEvent, DeviceLocationState> {
|
||||
DeviceLocationBloc(
|
||||
this._deviceLocationService,
|
||||
) : super(const DeviceLocationState()) {
|
||||
on<LoadDeviceLocationEvent>(_onLoadDeviceLocation);
|
||||
on<ClearDeviceLocationEvent>(_onClearDeviceLocation);
|
||||
}
|
||||
|
||||
final DeviceLocationService _deviceLocationService;
|
||||
|
||||
Future<void> _onLoadDeviceLocation(
|
||||
LoadDeviceLocationEvent event,
|
||||
Emitter<DeviceLocationState> emit,
|
||||
) async {
|
||||
emit(const DeviceLocationState(status: DeviceLocationStatus.loading));
|
||||
|
||||
try {
|
||||
final locationInfo = await _deviceLocationService.get(event.param);
|
||||
emit(
|
||||
DeviceLocationState(
|
||||
status: DeviceLocationStatus.success,
|
||||
locationInfo: locationInfo,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
DeviceLocationState(
|
||||
status: DeviceLocationStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearDeviceLocation(
|
||||
ClearDeviceLocationEvent event,
|
||||
Emitter<DeviceLocationState> emit,
|
||||
) {
|
||||
emit(const DeviceLocationState());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
part of 'device_location_bloc.dart';
|
||||
|
||||
sealed class DeviceLocationEvent extends Equatable {
|
||||
const DeviceLocationEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
final class LoadDeviceLocationEvent extends DeviceLocationEvent {
|
||||
const LoadDeviceLocationEvent(this.param);
|
||||
|
||||
final GetDeviceLocationDataParam param;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearDeviceLocationEvent extends DeviceLocationEvent {
|
||||
const ClearDeviceLocationEvent();
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
part of 'device_location_bloc.dart';
|
||||
|
||||
enum DeviceLocationStatus { initial, loading, success, failure }
|
||||
|
||||
final class DeviceLocationState extends Equatable {
|
||||
const DeviceLocationState({
|
||||
this.status = DeviceLocationStatus.initial,
|
||||
this.locationInfo,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final DeviceLocationStatus status;
|
||||
final DeviceLocationInfo? locationInfo;
|
||||
final String? errorMessage;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, locationInfo, errorMessage];
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
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/modules/air_quality/widgets/aqi_type_dropdown.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';
|
||||
|
||||
@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
|
||||
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
|
||||
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
|
||||
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
|
||||
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
|
||||
}
|
||||
|
||||
final RangeOfAqiService _rangeOfAqiService;
|
||||
@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) async {
|
||||
emit(
|
||||
RangeOfAqiState(
|
||||
status: RangeOfAqiStatus.loading,
|
||||
rangeOfAqi: state.rangeOfAqi,
|
||||
),
|
||||
state.copyWith(status: RangeOfAqiStatus.loading),
|
||||
);
|
||||
try {
|
||||
final rangeOfAqi = await _rangeOfAqiService.load(event.param);
|
||||
emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi));
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: RangeOfAqiStatus.loaded,
|
||||
rangeOfAqi: rangeOfAqi,
|
||||
filteredRangeOfAqi: _arrangeChartDataByType(
|
||||
rangeOfAqi,
|
||||
state.selectedAqiType,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e'));
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: RangeOfAqiStatus.failure,
|
||||
errorMessage: '$e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateAqiTypeEvent(
|
||||
UpdateAqiTypeEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedAqiType: event.aqiType,
|
||||
filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<RangeOfAqi> _arrangeChartDataByType(
|
||||
List<RangeOfAqi> rangeOfAqi,
|
||||
AqiType aqiType,
|
||||
) {
|
||||
final filteredRangeOfAqi = rangeOfAqi.map(
|
||||
(data) => RangeOfAqi(
|
||||
date: data.date,
|
||||
data: data.data.where((value) => value.type == aqiType.code).toList(),
|
||||
),
|
||||
);
|
||||
return filteredRangeOfAqi.toList();
|
||||
}
|
||||
|
||||
void _onClearRangeOfAqiEvent(
|
||||
ClearRangeOfAqiEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
|
@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
class UpdateAqiTypeEvent extends RangeOfAqiEvent {
|
||||
const UpdateAqiTypeEvent(this.aqiType);
|
||||
|
||||
final AqiType aqiType;
|
||||
|
||||
@override
|
||||
List<Object> get props => [aqiType];
|
||||
}
|
||||
|
||||
class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
|
||||
const ClearRangeOfAqiEvent();
|
||||
}
|
||||
|
@ -5,14 +5,35 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure }
|
||||
final class RangeOfAqiState extends Equatable {
|
||||
const RangeOfAqiState({
|
||||
this.rangeOfAqi = const [],
|
||||
this.filteredRangeOfAqi = const [],
|
||||
this.status = RangeOfAqiStatus.initial,
|
||||
this.errorMessage,
|
||||
this.selectedAqiType = AqiType.aqi,
|
||||
});
|
||||
|
||||
final RangeOfAqiStatus status;
|
||||
final List<RangeOfAqi> rangeOfAqi;
|
||||
final List<RangeOfAqi> filteredRangeOfAqi;
|
||||
final String? errorMessage;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
RangeOfAqiState copyWith({
|
||||
RangeOfAqiStatus? status,
|
||||
List<RangeOfAqi>? rangeOfAqi,
|
||||
List<RangeOfAqi>? filteredRangeOfAqi,
|
||||
String? errorMessage,
|
||||
AqiType? selectedAqiType,
|
||||
}) {
|
||||
return RangeOfAqiState(
|
||||
status: status ?? this.status,
|
||||
rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi,
|
||||
filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, rangeOfAqi, errorMessage];
|
||||
List<Object?> get props =>
|
||||
[status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType];
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_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_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
|
||||
|
||||
abstract final class FetchAirQualityDataHelper {
|
||||
@ -13,8 +16,10 @@ abstract final class FetchAirQualityDataHelper {
|
||||
|
||||
static void loadAirQualityData(
|
||||
BuildContext context, {
|
||||
required DateTime date,
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
loadAnalyticsDevices(
|
||||
@ -26,7 +31,11 @@ abstract final class FetchAirQualityDataHelper {
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
aqiType: AqiType.aqi,
|
||||
);
|
||||
loadAirQualityDistribution(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,8 +46,12 @@ abstract final class FetchAirQualityDataHelper {
|
||||
context.read<RealtimeDeviceChangesBloc>().add(
|
||||
const RealtimeDeviceChangesClosed(),
|
||||
);
|
||||
|
||||
context.read<AirQualityDistributionBloc>().add(
|
||||
const ClearAirQualityDistribution(),
|
||||
);
|
||||
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
|
||||
|
||||
context.read<DeviceLocationBloc>().add(const ClearDeviceLocationEvent());
|
||||
}
|
||||
|
||||
static void loadAnalyticsDevices(
|
||||
@ -52,12 +65,21 @@ abstract final class FetchAirQualityDataHelper {
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['AQI'],
|
||||
requestType: AnalyticsDeviceRequestType.energyManagement,
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
context.read<RealtimeDeviceChangesBloc>()
|
||||
..add(const RealtimeDeviceChangesClosed())
|
||||
..add(RealtimeDeviceChangesStarted(device.uuid));
|
||||
|
||||
context.read<DeviceLocationBloc>().add(
|
||||
LoadDeviceLocationEvent(
|
||||
GetDeviceLocationDataParam(
|
||||
latitude: device.latitude ?? 0,
|
||||
longitude: device.longitude ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -67,16 +89,26 @@ abstract final class FetchAirQualityDataHelper {
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
required AqiType aqiType,
|
||||
}) {
|
||||
context.read<RangeOfAqiBloc>().add(
|
||||
LoadRangeOfAqiEvent(
|
||||
GetRangeOfAqiParam(
|
||||
date: date,
|
||||
spaceUuid: spaceUuid,
|
||||
aqiType: aqiType,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void loadAirQualityDistribution(
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
}) {
|
||||
context.read<AirQualityDistributionBloc>().add(
|
||||
LoadAirQualityDistribution(
|
||||
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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/aqi_distribution_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
|
||||
|
||||
class AirQualityView extends StatelessWidget {
|
||||
@ -23,8 +24,14 @@ class AirQualityView extends StatelessWidget {
|
||||
height: height * 1.2,
|
||||
child: const AirQualityEndSideWidget(),
|
||||
),
|
||||
SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()),
|
||||
SizedBox(height: height * 0.5, child: const Placeholder()),
|
||||
SizedBox(
|
||||
height: height * 0.5,
|
||||
child: const RangeOfAqiChartBox(),
|
||||
),
|
||||
SizedBox(
|
||||
height: height * 0.5,
|
||||
child: const AqiDistributionChartBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -33,7 +40,7 @@ class AirQualityView extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height,
|
||||
height: height * 1.1,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -41,16 +48,16 @@ class AirQualityView extends StatelessWidget {
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
flex: 10,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: RangeOfAqiChartBox()),
|
||||
Expanded(child: Placeholder()),
|
||||
Expanded(child: AqiDistributionChartBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: AirQualityEndSideWidget()),
|
||||
Expanded(flex: 6, child: AirQualityEndSideWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart';
|
||||
|
||||
class AirQualityEndSideGaugeAndInfo extends StatelessWidget {
|
||||
const AirQualityEndSideGaugeAndInfo({
|
||||
super.key,
|
||||
required this.temperature,
|
||||
required this.humidity,
|
||||
required this.aqiLevel,
|
||||
});
|
||||
|
||||
final int temperature;
|
||||
final int humidity;
|
||||
final String aqiLevel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [Expanded(child: AqiGauge(aqi: aqi))],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 20),
|
||||
child: AqiHumidityAndTemperature(
|
||||
temperature: temperature,
|
||||
humidity: humidity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double get aqi => switch (aqiLevel) {
|
||||
'level_1' => 25.0,
|
||||
'level_2' => 75.0,
|
||||
'level_3' => 125.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AirQualityEndSideLiveIndicator extends StatelessWidget {
|
||||
const AirQualityEndSideLiveIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Entrance',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CircleAvatar(
|
||||
backgroundColor: ColorsManager.green.withValues(
|
||||
alpha: 0.5,
|
||||
),
|
||||
radius: 2,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Live',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.green.withValues(alpha: 0.5),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
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/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AirQualityEndSideWidget extends StatelessWidget {
|
||||
@ -17,70 +14,15 @@ class AirQualityEndSideWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(32),
|
||||
child: Column(
|
||||
child: const 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,
|
||||
),
|
||||
),
|
||||
AnalyticsSidebarHeader(title: 'AQI Sensor'),
|
||||
Expanded(flex: 15, child: AqiDeviceInfo()),
|
||||
SizedBox(height: 20),
|
||||
Expanded(flex: 6, child: AqiLocationInfo()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiDeviceInfo extends StatelessWidget {
|
||||
const AqiDeviceInfo({super.key});
|
||||
|
||||
String _getValueForStatus(
|
||||
List<Status> deviceStatusList,
|
||||
String code, {
|
||||
double defaultValue = 0,
|
||||
String Function(int value)? formatter,
|
||||
}) {
|
||||
try {
|
||||
final foundStatus = deviceStatusList.firstWhere((e) => e.code == code);
|
||||
final value = foundStatus.value.toString();
|
||||
final intValue = int.parse(value);
|
||||
return formatter != null ? formatter(intValue) : intValue.toString();
|
||||
} catch (e) {
|
||||
return defaultValue.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
|
||||
builder: (context, state) {
|
||||
final status = state.deviceStatusList;
|
||||
final humidityValue = _getValueForStatus(
|
||||
status,
|
||||
'humidity_value',
|
||||
formatter: (value) => value.toStringAsFixed(0),
|
||||
);
|
||||
|
||||
final tempValue = _getValueForStatus(
|
||||
status,
|
||||
'temp_current',
|
||||
formatter: (value) => (value / 10).toStringAsFixed(0),
|
||||
);
|
||||
final pm25Value = _getValueForStatus(
|
||||
status,
|
||||
'pm25_value',
|
||||
formatter: (value) => value.toString().padLeft(3, '0'),
|
||||
);
|
||||
final pm10Value = _getValueForStatus(
|
||||
status,
|
||||
'pm10',
|
||||
formatter: (value) => value.toString().padLeft(3, '0'),
|
||||
);
|
||||
final co2Value = _getValueForStatus(
|
||||
status,
|
||||
'co2_value',
|
||||
formatter: (value) => value.toString().padLeft(4, '0'),
|
||||
);
|
||||
final ch2oValue = _getValueForStatus(
|
||||
status,
|
||||
'ch2o_value',
|
||||
formatter: (value) => (value / 100).toStringAsFixed(2),
|
||||
);
|
||||
final tvocValue = _getValueForStatus(
|
||||
status,
|
||||
'tvoc_value',
|
||||
formatter: (value) => (value / 100).toStringAsFixed(2),
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
child: Column(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const AirQualityEndSideLiveIndicator(),
|
||||
AirQualityEndSideGaugeAndInfo(
|
||||
aqiLevel: status
|
||||
.firstWhere(
|
||||
(e) => e.code == 'air_quality_index',
|
||||
orElse: () => Status(code: 'air_quality_index', value: ''),
|
||||
)
|
||||
.value
|
||||
.toString(),
|
||||
temperature: int.parse(tempValue),
|
||||
humidity: int.parse(humidityValue),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 999),
|
||||
label: AqiType.pm25.value,
|
||||
value: pm25Value,
|
||||
unit: AqiType.pm25.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 999),
|
||||
label: AqiType.pm10.value,
|
||||
value: pm10Value,
|
||||
unit: AqiType.pm10.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 5),
|
||||
label: AqiType.hcho.value,
|
||||
value: ch2oValue,
|
||||
unit: AqiType.hcho.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 999),
|
||||
label: AqiType.tvoc.value,
|
||||
value: tvocValue,
|
||||
unit: AqiType.tvoc.unit,
|
||||
),
|
||||
AqiSubValueWidget(
|
||||
range: (0, 5000),
|
||||
label: AqiType.co2.value,
|
||||
value: co2Value,
|
||||
unit: AqiType.co2.unit,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_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';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiDistributionChart extends StatelessWidget {
|
||||
const AqiDistributionChart({super.key, required this.chartData});
|
||||
final List<AirQualityDataModel> chartData;
|
||||
|
||||
static const _rodStackItemsSpacing = 0.4;
|
||||
static const _barWidth = 13.0;
|
||||
static final _barBorderRadius = BorderRadius.circular(22);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sortedData = List<AirQualityDataModel>.from(chartData)
|
||||
..sort(
|
||||
(a, b) => a.date.compareTo(b.date),
|
||||
);
|
||||
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.1,
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: 20,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
barGroups: _buildBarGroups(sortedData),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
|
||||
return List.generate(sortedData.length, (index) {
|
||||
final data = sortedData[index];
|
||||
final stackItems = <BarChartRodData>[];
|
||||
double currentY = 0;
|
||||
bool isFirstElement = true;
|
||||
|
||||
// Sort data by type to ensure consistent order
|
||||
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
|
||||
..sort((a, b) => a.type.compareTo(b.type));
|
||||
|
||||
for (final percentageData in sortedPercentageData) {
|
||||
stackItems.add(
|
||||
BarChartRodData(
|
||||
fromY: currentY,
|
||||
toY: currentY + percentageData.percentage ,
|
||||
color: AirQualityDataModel.metricColors[percentageData.name]!,
|
||||
borderRadius: isFirstElement
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(22),
|
||||
topRight: Radius.circular(22),
|
||||
)
|
||||
: _barBorderRadius,
|
||||
width: _barWidth,
|
||||
),
|
||||
);
|
||||
currentY += percentageData.percentage + _rodStackItemsSpacing;
|
||||
isFirstElement = false;
|
||||
}
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
BarTouchData _barTouchData(BuildContext context) {
|
||||
return BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (_) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
final data = chartData[group.x.toInt()];
|
||||
|
||||
final List<TextSpan> children = [];
|
||||
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
);
|
||||
|
||||
// Sort data by type to ensure consistent order
|
||||
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
|
||||
..sort((a, b) => a.type.compareTo(b.type));
|
||||
|
||||
for (final percentageData in sortedPercentageData) {
|
||||
children.add(TextSpan(
|
||||
text:
|
||||
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
|
||||
style: textStyle,
|
||||
));
|
||||
}
|
||||
|
||||
return BarTooltipItem(
|
||||
DateFormat('dd/MM/yyyy').format(data.date),
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlTitlesData _titlesData(BuildContext context) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 20,
|
||||
);
|
||||
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 20,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
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.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
reservedSize: 36,
|
||||
),
|
||||
);
|
||||
|
||||
return titlesData.copyWith(
|
||||
leftTitles: leftTitles,
|
||||
bottomTitles: bottomTitles,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiDistributionChartBox extends StatelessWidget {
|
||||
const AqiDistributionChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AirQualityDistributionBloc, AirQualityDistributionState>(
|
||||
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),
|
||||
],
|
||||
AqiDistributionChartTitle(
|
||||
isLoading: state.status == AirQualityDistributionStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: AqiDistributionChart(chartData: state.filteredChartData),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.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';
|
||||
|
||||
class AqiDistributionChartTitle extends StatelessWidget {
|
||||
const AqiDistributionChartTitle({required this.isLoading, super.key});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
@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('Distribution over Air Quality Index'),
|
||||
),
|
||||
),
|
||||
),
|
||||
FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
context
|
||||
.read<AirQualityDistributionBloc>()
|
||||
.add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
115
lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gauge_indicator/gauge_indicator.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiGauge extends StatelessWidget {
|
||||
const AqiGauge({super.key, required this.aqi});
|
||||
|
||||
final double aqi;
|
||||
|
||||
static const _minRange = 0.0;
|
||||
static const _goodRange = 50.0;
|
||||
static const _moderateRange = 100.0;
|
||||
static const _maxRange = 150.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (status, statusColor) = _getStatusData(aqi);
|
||||
return AnimatedRadialGauge(
|
||||
value: aqi,
|
||||
debug: false,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
initialValue: 0,
|
||||
alignment: Alignment.bottomCenter,
|
||||
builder: (context, child, value) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Air Quality\n',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: status,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: _darkenColor(statusColor),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
axis: GaugeAxis(
|
||||
progressBar: const GaugeProgressBar.basic(color: Colors.transparent),
|
||||
style: const GaugeAxisStyle(
|
||||
cornerRadius: Radius.circular(16),
|
||||
thickness: 14,
|
||||
segmentSpacing: 4,
|
||||
),
|
||||
min: _minRange,
|
||||
max: _maxRange,
|
||||
pointer: GaugePointer.circle(
|
||||
position: const GaugePointerPosition.surface(),
|
||||
radius: MediaQuery.sizeOf(context).width * 0.004,
|
||||
color: ColorsManager.whiteColors,
|
||||
border: GaugePointerBorder(
|
||||
width: 6,
|
||||
color: statusColor,
|
||||
),
|
||||
shadow: const BoxShadow(
|
||||
color: ColorsManager.blackColor,
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
),
|
||||
segments: const [
|
||||
GaugeSegment(
|
||||
from: _minRange,
|
||||
to: _goodRange,
|
||||
cornerRadius: Radius.circular(16),
|
||||
color: ColorsManager.goodGreen,
|
||||
),
|
||||
GaugeSegment(
|
||||
from: _goodRange + 1,
|
||||
to: _moderateRange,
|
||||
cornerRadius: Radius.circular(16),
|
||||
color: ColorsManager.moderateYellow,
|
||||
),
|
||||
GaugeSegment(
|
||||
from: _moderateRange + 1,
|
||||
to: _maxRange,
|
||||
cornerRadius: Radius.circular(16),
|
||||
color: ColorsManager.poorOrange,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(String status, Color color) _getStatusData(double value) {
|
||||
return switch (value) {
|
||||
<= _goodRange => ('Good', ColorsManager.goodGreen),
|
||||
<= _moderateRange => ('Moderate', ColorsManager.moderateYellow),
|
||||
_ => ('Poor', ColorsManager.poorOrange),
|
||||
};
|
||||
}
|
||||
|
||||
Color _darkenColor(Color color) {
|
||||
final black = Colors.black.withValues(alpha: 0.8);
|
||||
return Color.lerp(color, black, 0.4)!;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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';
|
||||
|
||||
class AqiHumidityAndTemperature extends StatelessWidget {
|
||||
const AqiHumidityAndTemperature({
|
||||
required this.temperature,
|
||||
required this.humidity,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int temperature;
|
||||
final int humidity;
|
||||
|
||||
static const iconSize = 12.0;
|
||||
static const colorFilter = ColorFilter.mode(
|
||||
ColorsManager.textPrimaryColor,
|
||||
BlendMode.srcIn,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: context.textTheme.bodySmall!.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildIconAndValue(Assets.temperatureAqiSidebar, '$temperature°C'),
|
||||
const SizedBox(height: 10),
|
||||
_buildIconAndValue(Assets.humidityAqiSidebar, '$humidity%'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconAndValue(String icon, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
icon,
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
colorFilter: colorFilter,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(value),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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 AqiLocation extends StatelessWidget {
|
||||
const AqiLocation({
|
||||
required this.city,
|
||||
required this.country,
|
||||
required this.address,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? city;
|
||||
final String? country;
|
||||
final String? address;
|
||||
|
||||
String _getFormattedLocation() {
|
||||
if (city == null && country == null && address == null) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
final parts = <String>[];
|
||||
|
||||
if (city != null) parts.add(city!);
|
||||
if (address != null) parts.add(address!);
|
||||
final locationPart = parts.join(', ');
|
||||
|
||||
if (country != null) {
|
||||
return locationPart.isEmpty ? country! : '$locationPart - $country';
|
||||
}
|
||||
|
||||
return locationPart;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
boxShadow: const [],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildLocationPin(),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getFormattedLocation(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationPin() {
|
||||
return SvgPicture.asset(
|
||||
Assets.locationPin,
|
||||
height: 12,
|
||||
width: 12,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
ColorsManager.vividBlue,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiLocationInfo extends StatelessWidget {
|
||||
const AqiLocationInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DeviceLocationBloc, DeviceLocationState>(
|
||||
builder: (context, state) {
|
||||
final info = state.locationInfo;
|
||||
return Container(
|
||||
decoration: secondarySection.copyWith(boxShadow: const []),
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
AqiLocation(
|
||||
city: info?.city,
|
||||
country: info?.country,
|
||||
address: info?.address,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
AqiLocationInfoCell(
|
||||
label: 'Temperature',
|
||||
value: ' ${info?.temperature?.roundToDouble() ?? '--'}°',
|
||||
svgPath: Assets.aqiTemperature,
|
||||
),
|
||||
AqiLocationInfoCell(
|
||||
label: 'Humidity',
|
||||
value: '${info?.humidity?.roundToDouble() ?? '--'}%',
|
||||
svgPath: Assets.aqiHumidity,
|
||||
),
|
||||
AqiLocationInfoCell(
|
||||
label: 'Air Quality',
|
||||
value: ' ${info?.airQuality?.roundToDouble() ?? '--'}',
|
||||
svgPath: Assets.aqiAirQuality,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiLocationInfoCell extends StatelessWidget {
|
||||
const AqiLocationInfoCell({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.svgPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String svgPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Text(
|
||||
label,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 120,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Text(
|
||||
value,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.7),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SizedBox.square(
|
||||
dimension: MediaQuery.sizeOf(context).width * 0.45,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SvgPicture.asset(svgPath),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
||||
final class _AqiRange {
|
||||
const _AqiRange({required this.max, required this.color});
|
||||
|
||||
final double max;
|
||||
final Color color;
|
||||
}
|
||||
|
||||
class AqiSubValueWidget extends StatelessWidget {
|
||||
const AqiSubValueWidget({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
required this.range,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String unit;
|
||||
final (double min, double max) range;
|
||||
|
||||
double get _parsedValue => double.parse(value);
|
||||
|
||||
static const List<_AqiRange> _ranges = [
|
||||
_AqiRange(max: 12, color: ColorsManager.goodGreen),
|
||||
_AqiRange(max: 35, color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: 55, color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: 150, color: ColorsManager.unhealthyRed),
|
||||
_AqiRange(max: 250, color: ColorsManager.severePink),
|
||||
_AqiRange(max: 500, color: ColorsManager.hazardousPurple),
|
||||
];
|
||||
|
||||
static List<_AqiRange> _getRangesForValue((double min, double max) range) {
|
||||
final (double min, double max) = range;
|
||||
final rangeSize = (max - min) / 6;
|
||||
return [
|
||||
_AqiRange(max: min + rangeSize, color: ColorsManager.goodGreen),
|
||||
_AqiRange(max: min + (rangeSize * 2), color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: min + (rangeSize * 3), color: ColorsManager.poorOrange),
|
||||
_AqiRange(max: min + (rangeSize * 4), color: ColorsManager.unhealthyRed),
|
||||
_AqiRange(max: min + (rangeSize * 5), color: ColorsManager.severePink),
|
||||
_AqiRange(max: min, color: ColorsManager.hazardousPurple),
|
||||
];
|
||||
}
|
||||
|
||||
int _getActiveSegmentByRange(double value, (double min, double max) range) {
|
||||
final ranges = _getRangesForValue(range);
|
||||
for (int i = 0; i < ranges.length; i++) {
|
||||
if (value <= ranges[i].max) return i;
|
||||
}
|
||||
return ranges.length - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activeSegment = _getActiveSegmentByRange(_parsedValue, range);
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
spacing: MediaQuery.sizeOf(context).width * 0.0075,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildLabel(context),
|
||||
_buildSegmentedBar(activeSegment),
|
||||
_buildValueAndUnit(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildValueAndUnit(BuildContext context) {
|
||||
return Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
unit,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSegmentedBar(int activeSegment) {
|
||||
return Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(_ranges.length, (index) {
|
||||
final isActive = index == activeSegment;
|
||||
final color = _ranges[index].color.withValues(
|
||||
alpha: isActive ? 1.0 : 0.25,
|
||||
);
|
||||
return Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.linear,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLabel(BuildContext context) {
|
||||
return Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
label,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,16 +3,18 @@ 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');
|
||||
aqi('AQI', '', 'aqi'),
|
||||
pm25('PM2.5', 'µg/m³', 'pm25'),
|
||||
pm10('PM10', 'µg/m³', 'pm10'),
|
||||
hcho('HCHO', 'mg/m³', 'hcho'),
|
||||
tvoc('TVOC', 'µg/m³', 'tvoc'),
|
||||
co2('CO2', 'ppm', 'co2');
|
||||
|
||||
const AqiType(this.value, this.unit, this.code);
|
||||
|
||||
final String value;
|
||||
const AqiType(this.value);
|
||||
final String unit;
|
||||
final String code;
|
||||
}
|
||||
|
||||
class AqiTypeDropdown extends StatefulWidget {
|
||||
|
@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
];
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines {
|
||||
final sortedData = List<RangeOfAqi>.from(chartData)
|
||||
..sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
return [
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.max ?? 0;
|
||||
}).toList(),
|
||||
ColorsManager.maxPurple,
|
||||
ColorsManager.maxPurpleDot,
|
||||
),
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.average ?? 0;
|
||||
}).toList(),
|
||||
Colors.white,
|
||||
null,
|
||||
),
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.min ?? 0;
|
||||
}).toList(),
|
||||
ColorsManager.minBlue,
|
||||
ColorsManager.minBlueDot,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -32,7 +32,7 @@ class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,15 +1,18 @@
|
||||
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/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/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});
|
||||
const RangeOfAqiChartTitle({
|
||||
required this.isLoading,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
static const List<(Color color, String title, bool hasBorder)> _colors = [
|
||||
@ -66,12 +69,9 @@ class RangeOfAqiChartTitle extends StatelessWidget {
|
||||
|
||||
if (spaceUuid == null) return;
|
||||
|
||||
FetchAirQualityDataHelper.loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
aqiType: value ?? AqiType.aqi,
|
||||
);
|
||||
if (value != null) {
|
||||
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'analytics_devices_event.dart';
|
||||
part 'analytics_devices_state.dart';
|
||||
@ -36,6 +37,13 @@ class AnalyticsDevicesBloc
|
||||
if (devices.isNotEmpty) {
|
||||
event.onSuccess(devices.first);
|
||||
}
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
status: AnalyticsDevicesStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
AnalyticsDevicesState(
|
||||
|
@ -1,6 +1,7 @@
|
||||
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/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.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';
|
||||
@ -26,10 +27,10 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
|
||||
if (isSpaceSelected) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
|
||||
if (hasSelectedSpaces) clearData(context);
|
||||
|
||||
if (isSpaceSelected) return;
|
||||
|
||||
spaceTreeBloc
|
||||
..add(const SpaceTreeClearSelectionEvent())
|
||||
@ -39,21 +40,15 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
context,
|
||||
communityUuid: community.uuid,
|
||||
spaceUuid: space.uuid ?? '',
|
||||
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
}
|
||||
|
||||
@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());
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
|
||||
);
|
||||
FetchAirQualityDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
|
@ -13,10 +13,5 @@ abstract class AnalyticsDataLoadingStrategy {
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
);
|
||||
void onChildSpaceSelected(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
SpaceModel child,
|
||||
);
|
||||
void clearData(BuildContext context);
|
||||
}
|
||||
|
@ -14,24 +14,14 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
CommunityModel community,
|
||||
List<SpaceModel> spaces,
|
||||
) {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
OnCommunitySelected(
|
||||
community.uuid,
|
||||
spaces,
|
||||
),
|
||||
);
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isCommunitySelected =
|
||||
spaceTreeBloc.state.selectedCommunities.contains(community.uuid);
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
|
||||
if (isCommunitySelected) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -40,21 +30,31 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
CommunityModel community,
|
||||
SpaceModel space,
|
||||
) {
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
OnSpaceSelected(
|
||||
community,
|
||||
space.uuid ?? '',
|
||||
space.children,
|
||||
),
|
||||
);
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
|
||||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
|
||||
clearData(context);
|
||||
if (isSpaceSelected) {
|
||||
final firstSelectedSpace = spaceTreeBloc.state.selectedSpaces.first;
|
||||
final isTheFirstSelectedSpace = firstSelectedSpace == space.uuid;
|
||||
if (isTheFirstSelectedSpace) {
|
||||
clearData(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSelectedSpaces) {
|
||||
clearData(context);
|
||||
}
|
||||
|
||||
spaceTreeBloc.add(
|
||||
OnSpaceSelected(
|
||||
community,
|
||||
space.uuid ?? '',
|
||||
space.children,
|
||||
),
|
||||
);
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
communityId: community.uuid,
|
||||
@ -62,18 +62,11 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
|
||||
);
|
||||
}
|
||||
|
||||
@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());
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
|
@ -26,10 +26,10 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
|
||||
|
||||
if (isSpaceSelected) {
|
||||
clearData(context);
|
||||
return;
|
||||
}
|
||||
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
|
||||
if (hasSelectedSpaces) clearData(context);
|
||||
|
||||
if (isSpaceSelected) return;
|
||||
|
||||
spaceTreeBloc
|
||||
..add(const SpaceTreeClearSelectionEvent())
|
||||
@ -42,18 +42,11 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
@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());
|
||||
context.read<SpaceTreeBloc>().add(
|
||||
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
|
||||
);
|
||||
FetchOccupancyDataHelper.clearAllData(context);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_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';
|
||||
@ -13,9 +16,12 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_details_service_decorator.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/remote_device_location_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';
|
||||
@ -101,6 +107,23 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
|
||||
FakeRangeOfAqiService(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => AirQualityDistributionBloc(
|
||||
FakeAirQualityDistributionService(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => DeviceLocationBloc(
|
||||
DeviceLocationDetailsServiceDecorator(
|
||||
RemoteDeviceLocationService(_httpService),
|
||||
Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'https://nominatim.openstreetmap.org/',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const AnalyticsPageForm(),
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ class AnalyticsCommunitiesSidebar extends StatelessWidget {
|
||||
strategy.onSpaceSelected(context, community, space);
|
||||
},
|
||||
onSelectChildSpace: (community, child) {
|
||||
strategy.onChildSpaceSelected(context, community, child);
|
||||
strategy.onSpaceSelected(context, community, child);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
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/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';
|
||||
@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
const Spacer(),
|
||||
Visibility(
|
||||
key: ValueKey(selectedTab),
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement,
|
||||
visible: selectedTab == AnalyticsPageTab.energyManagement ||
|
||||
selectedTab == AnalyticsPageTab.airQuality,
|
||||
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 ?? '',
|
||||
);
|
||||
}
|
||||
onDateSelected: (value) {
|
||||
_onDateChanged(context, value, selectedTab);
|
||||
},
|
||||
selectedDate: context
|
||||
.watch<AnalyticsDatePickerBloc>()
|
||||
@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _onDateChanged(
|
||||
BuildContext context,
|
||||
DateTime date,
|
||||
AnalyticsPageTab selectedTab,
|
||||
) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: date),
|
||||
);
|
||||
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
final communities = spaceTreeState.selectedCommunities;
|
||||
final spaces = spaceTreeState.selectedSpaces;
|
||||
if (spaceTreeState.selectedSpaces.isNotEmpty) {
|
||||
switch (selectedTab) {
|
||||
case AnalyticsPageTab.energyManagement:
|
||||
_onEnergyManagementDateChanged(
|
||||
context,
|
||||
date: date,
|
||||
communityUuid: communities.firstOrNull ?? '',
|
||||
spaceUuid: spaces.firstOrNull ?? '',
|
||||
);
|
||||
break;
|
||||
case AnalyticsPageTab.airQuality:
|
||||
_onAirQualityDateChanged(
|
||||
context,
|
||||
date: date,
|
||||
communityUuid: communities.firstOrNull ?? '',
|
||||
spaceUuid: spaces.firstOrNull ?? '',
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onEnergyManagementDateChanged(
|
||||
BuildContext context, {
|
||||
required DateTime date,
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
context.read<AnalyticsDatePickerBloc>().add(
|
||||
UpdateAnalyticsDatePickerEvent(montlyDate: date),
|
||||
);
|
||||
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
shouldFetchAnalyticsDevices: false,
|
||||
selectedDate: date,
|
||||
communityId: communityUuid,
|
||||
spaceId: spaceUuid,
|
||||
);
|
||||
}
|
||||
|
||||
void _onAirQualityDateChanged(
|
||||
BuildContext context, {
|
||||
required DateTime date,
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
FetchAirQualityDataHelper.loadAirQualityData(
|
||||
context,
|
||||
date: date,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
shouldFetchAnalyticsDevices: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'energy_consumption_by_phases_event.dart';
|
||||
part 'energy_consumption_by_phases_state.dart';
|
||||
@ -31,6 +32,13 @@ class EnergyConsumptionByPhasesBloc
|
||||
chartData: chartData,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionByPhasesStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'energy_consumption_per_device_event.dart';
|
||||
part 'energy_consumption_per_device_state.dart';
|
||||
@ -13,7 +14,8 @@ class EnergyConsumptionPerDeviceBloc
|
||||
this._energyConsumptionPerDeviceService,
|
||||
) : super(const EnergyConsumptionPerDeviceState()) {
|
||||
on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent);
|
||||
on<ClearEnergyConsumptionPerDeviceEvent>(_onClearEnergyConsumptionPerDeviceEvent);
|
||||
on<ClearEnergyConsumptionPerDeviceEvent>(
|
||||
_onClearEnergyConsumptionPerDeviceEvent);
|
||||
}
|
||||
|
||||
final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService;
|
||||
@ -31,6 +33,13 @@ class EnergyConsumptionPerDeviceBloc
|
||||
chartData: chartData,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: EnergyConsumptionPerDeviceStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'power_clamp_info_event.dart';
|
||||
part 'power_clamp_info_state.dart';
|
||||
@ -31,6 +32,13 @@ class PowerClampInfoBloc extends Bloc<PowerClampInfoEvent, PowerClampInfoState>
|
||||
powerClampModel: powerClampModel,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: PowerClampInfoStatus.error,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'total_energy_consumption_event.dart';
|
||||
part 'total_energy_consumption_state.dart';
|
||||
@ -31,6 +32,13 @@ class TotalEnergyConsumptionBloc
|
||||
status: TotalEnergyConsumptionStatus.loaded,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: e.message,
|
||||
status: TotalEnergyConsumptionStatus.failure,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -38,7 +38,7 @@ abstract final class EnergyManagementChartsHelper {
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: false,
|
||||
minIncluded: true,
|
||||
interval: leftTitlesInterval,
|
||||
reservedSize: 110,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
|
@ -16,7 +16,6 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_
|
||||
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;
|
||||
}
|
||||
@ -48,7 +47,6 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
loadTotalEnergyConsumption(
|
||||
context,
|
||||
selectedDate: selectedDate0,
|
||||
communityId: communityId,
|
||||
spaceId: spaceId,
|
||||
);
|
||||
final selectedDevice = getSelectedDevice(context);
|
||||
@ -61,7 +59,6 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
}
|
||||
loadEnergyConsumptionPerDevice(
|
||||
context,
|
||||
communityId: communityId,
|
||||
spaceId: spaceId,
|
||||
selectedDate: selectedDate0,
|
||||
);
|
||||
@ -84,12 +81,10 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
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(
|
||||
@ -100,12 +95,10 @@ abstract final class FetchEnergyManagementDataHelper {
|
||||
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(
|
||||
|
@ -1,34 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class AnalyticsEnergyManagementView extends StatefulWidget {
|
||||
class AnalyticsEnergyManagementView extends StatelessWidget {
|
||||
const AnalyticsEnergyManagementView({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsEnergyManagementView> createState() =>
|
||||
_AnalyticsEnergyManagementViewState();
|
||||
}
|
||||
|
||||
class _AnalyticsEnergyManagementViewState
|
||||
extends State<AnalyticsEnergyManagementView> {
|
||||
@override
|
||||
void initState() {
|
||||
final spaceTreeBloc = context.read<SpaceTreeBloc>();
|
||||
final communityId = spaceTreeBloc.state.selectedCommunities.firstOrNull;
|
||||
final spaceId = spaceTreeBloc.state.selectedSpaces.firstOrNull;
|
||||
FetchEnergyManagementDataHelper.loadEnergyManagementData(
|
||||
context,
|
||||
communityId: communityId ?? '',
|
||||
spaceId: spaceId ?? '',
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
static const _padding = EdgeInsetsDirectional.all(32);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -6,9 +6,14 @@ 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});
|
||||
const AnalyticsDeviceDropdown({
|
||||
required this.onChanged,
|
||||
this.showSpaceUuid = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<AnalyticsDevice> onChanged;
|
||||
final bool showSpaceUuid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -72,17 +77,18 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
|
||||
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,
|
||||
if (showSpaceUuid)
|
||||
if (spaceUuid != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
spaceUuid,
|
||||
style: _getTextStyle(context)?.copyWith(
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -23,7 +23,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
@ -52,7 +51,9 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(height: 0),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
|
||||
),
|
||||
|
@ -41,7 +41,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
|
||||
.color;
|
||||
|
||||
return Tooltip(
|
||||
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
|
||||
message: '${device.name}\n${device.spaceUuid ?? ''}',
|
||||
child: ChartInformativeCell(title: Text(device.name), color: deviceColor),
|
||||
);
|
||||
}
|
||||
|
@ -2,19 +2,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_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/analytics/widgets/analytics_sidebar_header.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 {
|
||||
@ -42,26 +39,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
AnalyticsSidebarHeader(
|
||||
title: 'Smart Power Clamp',
|
||||
showSpaceUuidInDevicesDropdown: true,
|
||||
onChanged: (device) {
|
||||
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
|
||||
context,
|
||||
powerClampUuid: device.uuid,
|
||||
selectedDate:
|
||||
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
);
|
||||
},
|
||||
),
|
||||
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(
|
||||
@ -111,51 +100,6 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
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: '--'))
|
||||
|
@ -19,7 +19,6 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
@ -39,7 +38,9 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
const Spacer(flex: 4),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
TotalEnergyConsumptionChart(chartData: state.chartData),
|
||||
],
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'occupancy_event.dart';
|
||||
part 'occupancy_state.dart';
|
||||
@ -23,6 +24,8 @@ class OccupancyBloc extends Bloc<OccupancyEvent, OccupancyState> {
|
||||
try {
|
||||
final chartData = await _occupacyService.load(event.param);
|
||||
emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded));
|
||||
} on APIException catch (e) {
|
||||
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: e.message));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e'));
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'occupancy_heat_map_event.dart';
|
||||
part 'occupancy_heat_map_state.dart';
|
||||
@ -30,6 +31,13 @@ class OccupancyHeatMapBloc
|
||||
heatMapData: occupancyHeatMap,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OccupancyHeatMapStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -16,7 +16,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.0,
|
||||
maxY: 100.001,
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 20,
|
||||
@ -134,7 +134,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
(value + 1).toString(),
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.greyColor,
|
||||
fontSize: 8,
|
||||
|
@ -22,7 +22,6 @@ class OccupancyChartBox extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: containerWhiteDecoration,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -65,7 +64,9 @@ class OccupancyChartBox extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 0),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: OccupancyChart(chartData: state.chartData)),
|
||||
],
|
||||
),
|
||||
|
@ -1,15 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyEndSideBar extends StatelessWidget {
|
||||
@ -27,28 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
Text(
|
||||
'Device ID:',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
|
||||
'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(height: 1, color: ColorsManager.greyColor),
|
||||
const SizedBox(height: 50),
|
||||
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
@ -101,42 +76,4 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
.toString();
|
||||
return value == 'null' ? defaultValue ?? '--' : value;
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectableText(
|
||||
'Presnce Sensor',
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AnalyticsDeviceDropdown(
|
||||
onChanged: (value) =>
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: containerWhiteDecoration,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -66,7 +65,9 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 0),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
|
@ -0,0 +1,9 @@
|
||||
class GetAirQualityDistributionParam {
|
||||
final DateTime date;
|
||||
final String spaceUuid;
|
||||
|
||||
const GetAirQualityDistributionParam({
|
||||
required this.date,
|
||||
required this.spaceUuid,
|
||||
});
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
class GetDeviceLocationDataParam {
|
||||
const GetDeviceLocationDataParam({
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
});
|
||||
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
|
||||
Map<String, dynamic> toJson() => {'lat': latitude, 'lon': longitude};
|
||||
}
|
@ -2,18 +2,15 @@ class GetEnergyConsumptionPerDeviceParam {
|
||||
const GetEnergyConsumptionPerDeviceParam({
|
||||
this.monthDate,
|
||||
this.spaceId,
|
||||
this.communityId,
|
||||
});
|
||||
|
||||
final DateTime? monthDate;
|
||||
final String? spaceId;
|
||||
final String? communityId;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'monthDate':
|
||||
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
|
||||
'communityUuid': communityId,
|
||||
if (spaceId != null) 'spaceUuid': spaceId,
|
||||
'groupByDevice': true,
|
||||
};
|
||||
}
|
||||
|
@ -1,16 +1,12 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
|
||||
class GetRangeOfAqiParam extends Equatable {
|
||||
final DateTime date;
|
||||
final String spaceUuid;
|
||||
final AqiType aqiType;
|
||||
|
||||
const GetRangeOfAqiParam(
|
||||
{
|
||||
const GetRangeOfAqiParam({
|
||||
required this.date,
|
||||
required this.spaceUuid,
|
||||
required this.aqiType,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -1,20 +1,17 @@
|
||||
class GetTotalEnergyConsumptionParam {
|
||||
final DateTime? monthDate;
|
||||
final String? spaceId;
|
||||
final String? communityId;
|
||||
|
||||
const GetTotalEnergyConsumptionParam({
|
||||
this.monthDate,
|
||||
this.spaceId,
|
||||
this.communityId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'monthDate':
|
||||
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
|
||||
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
|
||||
'communityUuid': communityId,
|
||||
if (spaceId != null) 'spaceUuid': spaceId,
|
||||
'groupByDevice': false,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
|
||||
abstract interface class AirQualityDistributionService {
|
||||
Future<List<AirQualityDataModel>> getAirQualityDistribution(
|
||||
GetAirQualityDistributionParam param,
|
||||
);
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
|
||||
|
||||
class FakeAirQualityDistributionService implements AirQualityDistributionService {
|
||||
final _random = Random();
|
||||
|
||||
@override
|
||||
Future<List<AirQualityDataModel>> getAirQualityDistribution(
|
||||
GetAirQualityDistributionParam param,
|
||||
) async {
|
||||
return Future.delayed(
|
||||
const Duration(milliseconds: 400),
|
||||
() => List.generate(30, (index) {
|
||||
final date = DateTime(2025, 5, 1).add(Duration(days: index));
|
||||
|
||||
final values = _generateRandomPercentages();
|
||||
final nullMask = List.generate(6, (_) => _shouldBeNull());
|
||||
|
||||
if (nullMask.every((isNull) => isNull)) {
|
||||
nullMask[_random.nextInt(6)] = false;
|
||||
}
|
||||
|
||||
final nonNullValues = _redistributePercentages(values, nullMask);
|
||||
|
||||
return AirQualityDataModel(
|
||||
date: date,
|
||||
data: [
|
||||
AirQualityPercentageData(
|
||||
type: AqiType.aqi.code,
|
||||
percentage: nonNullValues[0],
|
||||
name: 'good',
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'moderate',
|
||||
type: AqiType.co2.code,
|
||||
percentage: nonNullValues[1],
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'poor',
|
||||
percentage: nonNullValues[2],
|
||||
type: AqiType.hcho.code,
|
||||
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'unhealthy',
|
||||
percentage: nonNullValues[3],
|
||||
type: AqiType.pm10.code,
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'severe',
|
||||
type: AqiType.pm25.code,
|
||||
percentage: nonNullValues[4],
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'hazardous',
|
||||
percentage: nonNullValues[5],
|
||||
type: AqiType.co2.code,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
List<double> _redistributePercentages(
|
||||
List<double> originalValues,
|
||||
List<bool> nullMask,
|
||||
) {
|
||||
double nonNullSum = 0;
|
||||
for (int i = 0; i < originalValues.length; i++) {
|
||||
if (!nullMask[i]) {
|
||||
nonNullSum += originalValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
return List.generate(originalValues.length, (i) {
|
||||
if (nullMask[i]) return 0;
|
||||
return (originalValues[i] / nonNullSum * 100).roundToDouble();
|
||||
});
|
||||
}
|
||||
|
||||
bool _shouldBeNull() => _random.nextDouble() < 0.6;
|
||||
|
||||
List<double> _generateRandomPercentages() {
|
||||
final values = List.generate(6, (_) => _random.nextDouble());
|
||||
|
||||
final sum = values.reduce((a, b) => a + b);
|
||||
|
||||
return values.map((value) => (value / sum * 100).roundToDouble()).toList();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class RemoteAirQualityDistributionService implements AirQualityDistributionService {
|
||||
RemoteAirQualityDistributionService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<AirQualityDataModel>> getAirQualityDistribution(
|
||||
GetAirQualityDistributionParam param,
|
||||
) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
queryParameters: {
|
||||
'spaceUuid': param.spaceUuid,
|
||||
'date': param.date.toIso8601String(),
|
||||
},
|
||||
expectedResponseModel: (data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
return mappedData.map((e) {
|
||||
final jsonData = e as Map<String, dynamic>;
|
||||
return AirQualityDataModel.fromJson(jsonData);
|
||||
}).toList();
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per phase: $e');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteEnergyManagementAnalyticsDevicesService
|
||||
@ -9,6 +11,8 @@ final class RemoteEnergyManagementAnalyticsDevicesService
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load analytics devices';
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
|
||||
try {
|
||||
@ -29,8 +33,14 @@ final class RemoteEnergyManagementAnalyticsDevicesService
|
||||
);
|
||||
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption: $e');
|
||||
throw APIException('$_defaultErrorMessage: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:dio/dio.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';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService {
|
||||
@ -9,6 +11,8 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load analytics devices';
|
||||
|
||||
@override
|
||||
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
|
||||
try {
|
||||
@ -26,8 +30,15 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService
|
||||
|
||||
final result = requests.map((e) => e.first).toList();
|
||||
return result;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption: $e');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, e.toString()].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,8 +65,14 @@ class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
throw APIException('$_defaultErrorMessage: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_location_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart';
|
||||
|
||||
class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
|
||||
const DeviceLocationDetailsServiceDecorator(this._decoratee, this._dio);
|
||||
|
||||
final DeviceLocationService _decoratee;
|
||||
final Dio _dio;
|
||||
|
||||
@override
|
||||
Future<DeviceLocationInfo> get(GetDeviceLocationDataParam param) async {
|
||||
try {
|
||||
final deviceLocationInfo = await _decoratee.get(param);
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
'reverse',
|
||||
queryParameters: {
|
||||
'format': 'json',
|
||||
'lat': param.latitude,
|
||||
'lon': param.longitude,
|
||||
},
|
||||
);
|
||||
|
||||
final data = response.data;
|
||||
if (data != null) {
|
||||
final addressData = data['address'] as Map<String, dynamic>;
|
||||
return deviceLocationInfo.copyWith(
|
||||
city: addressData['city'],
|
||||
country: addressData['country_code'].toString().toUpperCase(),
|
||||
address: addressData['state'],
|
||||
);
|
||||
}
|
||||
|
||||
return deviceLocationInfo;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load device location info: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/device_location_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
|
||||
|
||||
abstract interface class DeviceLocationService {
|
||||
Future<DeviceLocationInfo> get(GetDeviceLocationDataParam param);
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_location_info.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class RemoteDeviceLocationService implements DeviceLocationService {
|
||||
const RemoteDeviceLocationService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load device location';
|
||||
|
||||
@override
|
||||
Future<DeviceLocationInfo> get(GetDeviceLocationDataParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: '/weather',
|
||||
queryParameters: param.toJson(),
|
||||
expectedResponseModel: (data) {
|
||||
final response = data as Map<String, dynamic>;
|
||||
final location = response['data'] as Map<String, dynamic>;
|
||||
|
||||
return DeviceLocationInfo.fromJson(location);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
throw Exception(errorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('$_defaultErrorMessage: $e');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteEnergyConsumptionByPhasesService
|
||||
@ -9,6 +11,8 @@ final class RemoteEnergyConsumptionByPhasesService
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load energy consumption per phase';
|
||||
|
||||
@override
|
||||
Future<List<PhasesEnergyConsumption>> load(
|
||||
GetEnergyConsumptionByPhasesParam param,
|
||||
@ -28,8 +32,15 @@ final class RemoteEnergyConsumptionByPhasesService
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per phase: $e');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class RemoteEnergyConsumptionPerDeviceService
|
||||
@ -11,6 +13,8 @@ class RemoteEnergyConsumptionPerDeviceService
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load energy consumption per device';
|
||||
|
||||
@override
|
||||
Future<List<DeviceEnergyDataModel>> load(
|
||||
GetEnergyConsumptionPerDeviceParam param,
|
||||
@ -23,8 +27,15 @@ class RemoteEnergyConsumptionPerDeviceService
|
||||
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per device: $e');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteOccupancyService implements OccupacyService {
|
||||
@ -8,6 +10,8 @@ final class RemoteOccupancyService implements OccupacyService {
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load occupancy';
|
||||
|
||||
@override
|
||||
Future<List<Occupacy>> load(GetOccupancyParam param) async {
|
||||
try {
|
||||
@ -25,8 +29,15 @@ final class RemoteOccupancyService implements OccupacyService {
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per phase: $e');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||
@ -8,6 +10,8 @@ final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load occupancy heat map';
|
||||
|
||||
@override
|
||||
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
|
||||
try {
|
||||
@ -28,8 +32,15 @@ final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
|
||||
);
|
||||
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption:');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemotePowerClampInfoService implements PowerClampInfoService {
|
||||
@ -7,6 +9,8 @@ final class RemotePowerClampInfoService implements PowerClampInfoService {
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to fetch power clamp info';
|
||||
|
||||
@override
|
||||
Future<PowerClampModel> getInfo(String deviceId) async {
|
||||
try {
|
||||
@ -20,8 +24,15 @@ final class RemotePowerClampInfoService implements PowerClampInfoService {
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch power clamp info: $e');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.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';
|
||||
|
||||
@ -18,10 +19,15 @@ class FakeRangeOfAqiService implements RangeOfAqiService {
|
||||
final avg = (min + avgDelta).clamp(0.0, 301.0);
|
||||
final max = (avg + maxDelta).clamp(0.0, 301.0);
|
||||
|
||||
return RangeOfAqi(
|
||||
min: min,
|
||||
avg: avg,
|
||||
max: max,
|
||||
return RangeOfAqi(
|
||||
data: [
|
||||
RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
|
||||
RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max),
|
||||
RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max),
|
||||
RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max),
|
||||
RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max),
|
||||
RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max),
|
||||
],
|
||||
date: date,
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,34 @@
|
||||
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';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteRangeOfAqiService implements RangeOfAqiService {
|
||||
const RemoteRangeOfAqiService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
queryParameters: {
|
||||
'spaceUuid': param.spaceUuid,
|
||||
'date': param.date.toIso8601String(),
|
||||
},
|
||||
expectedResponseModel: (data) {
|
||||
final json = data as Map<String, dynamic>? ?? {};
|
||||
final mappedData = json['data'] as List<dynamic>? ?? [];
|
||||
return mappedData.map((e) {
|
||||
final jsonData = e as Map<String, dynamic>;
|
||||
return RangeOfAqi.fromJson(jsonData);
|
||||
}).toList();
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load energy consumption per phase: $e');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.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';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
|
||||
@ -8,6 +10,8 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi
|
||||
|
||||
final HTTPService _httpService;
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load total energy consumption';
|
||||
|
||||
@override
|
||||
Future<List<EnergyDataModel>> load(
|
||||
GetTotalEnergyConsumptionParam param,
|
||||
@ -21,8 +25,15 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi
|
||||
);
|
||||
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load total energy consumption: $e');
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,17 @@ class AnalyticsErrorWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Visibility(
|
||||
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
|
||||
child: Text(
|
||||
errorMessage ?? 'Something went wrong',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.red,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(bottom: 10),
|
||||
child: Text(
|
||||
errorMessage ?? 'Something went wrong',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.red,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
91
lib/pages/analytics/widgets/analytics_sidebar_header.dart
Normal file
@ -0,0 +1,91 @@
|
||||
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/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';
|
||||
|
||||
class AnalyticsSidebarHeader extends StatelessWidget {
|
||||
const AnalyticsSidebarHeader({
|
||||
required this.title,
|
||||
this.showSpaceUuidInDevicesDropdown = false,
|
||||
this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool showSpaceUuidInDevicesDropdown;
|
||||
final void Function(AnalyticsDevice device)? onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: SelectableText(
|
||||
title,
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AnalyticsDeviceDropdown(
|
||||
showSpaceUuid: showSpaceUuidInDevicesDropdown,
|
||||
onChanged: (value) {
|
||||
context.read<AnalyticsDevicesBloc>().add(
|
||||
SelectAnalyticsDeviceEvent(value),
|
||||
);
|
||||
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
|
||||
context,
|
||||
deviceUuid: value.uuid,
|
||||
);
|
||||
onChanged?.call(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Device ID:',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ?? 'N/A',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(height: 1, color: ColorsManager.greyColor),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/auth_api.dart';
|
||||
import 'package:syncrow_web/utils/constants/strings_manager.dart';
|
||||
import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart';
|
||||
@ -99,7 +100,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
|
||||
}
|
||||
|
||||
Future<void> changePassword(ChangePasswordEvent event, Emitter<AuthState> emit) async {
|
||||
Future<void> changePassword(
|
||||
ChangePasswordEvent event, Emitter<AuthState> emit) async {
|
||||
emit(LoadingForgetState());
|
||||
try {
|
||||
var response = await AuthenticationAPI.verifyOtp(
|
||||
@ -113,14 +115,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
|
||||
emit(SuccessForgetState());
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
final errorData = e.response!.data;
|
||||
String errorMessage = errorData['error']['message'] ?? 'something went wrong';
|
||||
} on APIException catch (e) {
|
||||
final errorMessage = e.message;
|
||||
validate = errorMessage;
|
||||
emit(AuthInitialState());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String? validateCode(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Code is required';
|
||||
@ -149,6 +151,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
static UserModel? user;
|
||||
bool showValidationMessage = false;
|
||||
|
||||
|
||||
void _login(LoginButtonPressed event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
if (isChecked) {
|
||||
@ -161,25 +164,24 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
|
||||
token = await AuthenticationAPI.loginWithEmail(
|
||||
model: LoginWithEmailModel(
|
||||
email: event.username,
|
||||
email: event.username.toLowerCase(),
|
||||
password: event.password,
|
||||
),
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final errorData = e.response!.data;
|
||||
String errorMessage = errorData['error']['message'];
|
||||
if (errorMessage == "Access denied for web platform") {
|
||||
validate = errorMessage;
|
||||
} else {
|
||||
validate = 'Invalid Credentials!';
|
||||
}
|
||||
} on APIException catch (e) {
|
||||
validate = e.message;
|
||||
emit(LoginInitial());
|
||||
return;
|
||||
} catch (e) {
|
||||
validate = 'Something went wrong';
|
||||
emit(LoginInitial());
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.accessTokenIsNotEmpty) {
|
||||
FlutterSecureStorage storage = const FlutterSecureStorage();
|
||||
await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken);
|
||||
await storage.write(
|
||||
key: Token.loginAccessTokenKey, value: token.accessToken);
|
||||
const FlutterSecureStorage().write(
|
||||
key: UserModel.userUuidKey,
|
||||
value: Token.decodeToken(token.accessToken)['uuid'].toString());
|
||||
@ -195,6 +197,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
checkBoxToggle(
|
||||
CheckBoxEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
|
@ -162,31 +162,34 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalBodyScrollController,
|
||||
child: SizedBox(
|
||||
width: widget.size.width,
|
||||
child: widget.isEmpty
|
||||
? _buildEmptyState()
|
||||
: Column(
|
||||
children:
|
||||
List.generate(widget.data.length, (rowIndex) {
|
||||
final row = widget.data[rowIndex];
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.withCheckBox)
|
||||
_buildRowCheckbox(
|
||||
rowIndex, widget.size.height * 0.08),
|
||||
...row.asMap().entries.map((entry) {
|
||||
return _buildTableCell(
|
||||
entry.value.toString(),
|
||||
widget.size.height * 0.08,
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: entry.key,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: widget.size.width,
|
||||
child: widget.isEmpty
|
||||
? _buildEmptyState()
|
||||
: Column(
|
||||
children: List.generate(widget.data.length,
|
||||
(rowIndex) {
|
||||
final row = widget.data[rowIndex];
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.withCheckBox)
|
||||
_buildRowCheckbox(rowIndex,
|
||||
widget.size.height * 0.08),
|
||||
...row.asMap().entries.map((entry) {
|
||||
return _buildTableCell(
|
||||
entry.value.toString(),
|
||||
widget.size.height * 0.08,
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: entry.key,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -310,7 +313,12 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
|
||||
|
||||
if (isSettingsColumn) {
|
||||
return _buildSettingsIcon(rowIndex, size);
|
||||
return buildSettingsIcon(
|
||||
width: 120,
|
||||
height: 60,
|
||||
iconSize: 40,
|
||||
onTap: () => widget.onSettingsPressed?.call(rowIndex),
|
||||
);
|
||||
}
|
||||
|
||||
Color? statusColor;
|
||||
@ -364,22 +372,63 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsIcon(int rowIndex, double size) {
|
||||
return Container(
|
||||
height: size,
|
||||
width: 120,
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: ColorsManager.boxDivider, width: 1.0),
|
||||
Widget buildSettingsIcon(
|
||||
{double width = 120,
|
||||
double height = 60,
|
||||
double iconSize = 40,
|
||||
VoidCallback? onTap}) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
|
||||
margin: const EdgeInsets.only(right: 15),
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.boxDivider,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 16.0,
|
||||
left: 17.0,
|
||||
),
|
||||
child: Container(
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF7F8FA),
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.17),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
Assets.settings, // ضع المسار الصحيح هنا
|
||||
width: 40,
|
||||
height: 22,
|
||||
color: ColorsManager
|
||||
.primaryColor, // نفس لون الأيقونة في الصورة
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: SvgPicture.asset(Assets.settings),
|
||||
onPressed: () => widget.onSettingsPressed?.call(rowIndex),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,27 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||
import 'package:syncrow_web/services/control_device_service.dart';
|
||||
import 'package:syncrow_web/services/devices_mang_api.dart';
|
||||
|
||||
class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
late AcStatusModel deviceStatus;
|
||||
final String deviceId;
|
||||
Timer? _timer;
|
||||
final ControlDeviceService controlDeviceService;
|
||||
final BatchControlDevicesService batchControlDevicesService;
|
||||
Timer? _countdownTimer;
|
||||
|
||||
AcBloc({required this.deviceId}) : super(AcsInitialState()) {
|
||||
AcBloc({
|
||||
required this.deviceId,
|
||||
required this.controlDeviceService,
|
||||
required this.batchControlDevicesService,
|
||||
}) : super(AcsInitialState()) {
|
||||
on<AcFetchDeviceStatusEvent>(_onFetchAcStatus);
|
||||
on<AcFetchBatchStatusEvent>(_onFetchAcBatchStatus);
|
||||
on<AcControlEvent>(_onAcControl);
|
||||
@ -34,14 +40,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
int scheduledMinutes = 0;
|
||||
|
||||
FutureOr<void> _onFetchAcStatus(
|
||||
AcFetchDeviceStatusEvent event, Emitter<AcsState> emit) async {
|
||||
AcFetchDeviceStatusEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
try {
|
||||
final status =
|
||||
await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status);
|
||||
if (deviceStatus.countdown1 != 0) {
|
||||
// Convert API value to minutes
|
||||
final totalMinutes = deviceStatus.countdown1 * 6;
|
||||
scheduledHours = totalMinutes ~/ 60;
|
||||
scheduledMinutes = totalMinutes % 60;
|
||||
@ -62,30 +68,24 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
}
|
||||
}
|
||||
|
||||
_listenToChanges(deviceId) {
|
||||
void _listenToChanges(deviceId) {
|
||||
try {
|
||||
DatabaseReference ref =
|
||||
FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
Stream<DatabaseEvent> stream = ref.onValue;
|
||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
final stream = ref.onValue;
|
||||
|
||||
stream.listen((DatabaseEvent event) async {
|
||||
if (event.snapshot.value == null) return;
|
||||
|
||||
if (_timer != null) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
Map<dynamic, dynamic> usersMap =
|
||||
event.snapshot.value as Map<dynamic, dynamic>;
|
||||
|
||||
List<Status> statusList = [];
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList
|
||||
.add(Status(code: element['code'], value: element['value']));
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
deviceStatus =
|
||||
AcStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
if (!isClosed) {
|
||||
add(AcStatusUpdated(deviceStatus));
|
||||
}
|
||||
@ -93,146 +93,44 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _onAcStatusUpdated(AcStatusUpdated event, Emitter<AcsState> emit) {
|
||||
void _onAcStatusUpdated(
|
||||
AcStatusUpdated event,
|
||||
Emitter<AcsState> emit,
|
||||
) {
|
||||
deviceStatus = event.deviceStatus;
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
}
|
||||
|
||||
FutureOr<void> _onAcControl(
|
||||
AcControlEvent event, Emitter<AcsState> emit) async {
|
||||
final oldValue = _getValueByCode(event.code);
|
||||
|
||||
_updateLocalValue(event.code, event.value, emit);
|
||||
|
||||
AcControlEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
_updateDeviceFunctionFromCode(event.code, event.value);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
|
||||
await _runDebounce(
|
||||
isBatch: false,
|
||||
deviceId: event.deviceId,
|
||||
code: event.code,
|
||||
value: event.value,
|
||||
oldValue: oldValue,
|
||||
emit: emit,
|
||||
);
|
||||
}
|
||||
try {
|
||||
final success = await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: event.code, value: event.value),
|
||||
);
|
||||
|
||||
Future<void> _runDebounce({
|
||||
required dynamic deviceId,
|
||||
required String code,
|
||||
required dynamic value,
|
||||
required dynamic oldValue,
|
||||
required Emitter<AcsState> emit,
|
||||
required bool isBatch,
|
||||
}) async {
|
||||
late String id;
|
||||
|
||||
if (deviceId is List) {
|
||||
id = deviceId.first;
|
||||
} else {
|
||||
id = deviceId;
|
||||
}
|
||||
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
}
|
||||
_timer = Timer(const Duration(seconds: 1), () async {
|
||||
try {
|
||||
late bool response;
|
||||
if (isBatch) {
|
||||
response = await DevicesManagementApi()
|
||||
.deviceBatchControl(deviceId, code, value);
|
||||
} else {
|
||||
response = await DevicesManagementApi()
|
||||
.deviceControl(deviceId, Status(code: code, value: value));
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
_revertValueAndEmit(id, code, oldValue, emit);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.response != null) {
|
||||
debugPrint('Error response: ${e.response?.data}');
|
||||
}
|
||||
_revertValueAndEmit(id, code, oldValue, emit);
|
||||
if (!success) {
|
||||
emit(const AcsFailedState(error: 'Failed to control device'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _revertValueAndEmit(
|
||||
String deviceId, String code, dynamic oldValue, Emitter<AcsState> emit) {
|
||||
_updateLocalValue(code, oldValue, emit);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
}
|
||||
|
||||
void _updateLocalValue(String code, dynamic value, Emitter<AcsState> emit) {
|
||||
switch (code) {
|
||||
case 'switch':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(acSwitch: value);
|
||||
}
|
||||
break;
|
||||
case 'temp_set':
|
||||
if (value is int) {
|
||||
deviceStatus = deviceStatus.copyWith(tempSet: value);
|
||||
}
|
||||
break;
|
||||
case 'mode':
|
||||
if (value is String) {
|
||||
deviceStatus = deviceStatus.copyWith(
|
||||
modeString: value,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'level':
|
||||
if (value is String) {
|
||||
deviceStatus = deviceStatus.copyWith(
|
||||
fanSpeedsString: value,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'child_lock':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(childLock: value);
|
||||
}
|
||||
|
||||
case 'countdown_time':
|
||||
if (value is int) {
|
||||
deviceStatus = deviceStatus.copyWith(countdown1: value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
}
|
||||
|
||||
dynamic _getValueByCode(String code) {
|
||||
switch (code) {
|
||||
case 'switch':
|
||||
return deviceStatus.acSwitch;
|
||||
case 'temp_set':
|
||||
return deviceStatus.tempSet;
|
||||
case 'mode':
|
||||
return deviceStatus.modeString;
|
||||
case 'level':
|
||||
return deviceStatus.fanSpeedsString;
|
||||
case 'child_lock':
|
||||
return deviceStatus.childLock;
|
||||
case 'countdown_time':
|
||||
return deviceStatus.countdown1;
|
||||
default:
|
||||
return null;
|
||||
} catch (e) {
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onFetchAcBatchStatus(
|
||||
AcFetchBatchStatusEvent event, Emitter<AcsState> emit) async {
|
||||
AcFetchBatchStatusEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
try {
|
||||
final status =
|
||||
await DevicesManagementApi().getBatchStatus(event.devicesIds);
|
||||
deviceStatus =
|
||||
AcStatusModel.fromJson(event.devicesIds.first, status.status);
|
||||
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
|
||||
deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
} catch (e) {
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
@ -240,25 +138,32 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
}
|
||||
|
||||
FutureOr<void> _onAcBatchControl(
|
||||
AcBatchControlEvent event, Emitter<AcsState> emit) async {
|
||||
final oldValue = _getValueByCode(event.code);
|
||||
|
||||
_updateLocalValue(event.code, event.value, emit);
|
||||
|
||||
AcBatchControlEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
_updateDeviceFunctionFromCode(event.code, event.value);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
|
||||
await _runDebounce(
|
||||
isBatch: true,
|
||||
deviceId: event.devicesIds,
|
||||
code: event.code,
|
||||
value: event.value,
|
||||
oldValue: oldValue,
|
||||
emit: emit,
|
||||
);
|
||||
try {
|
||||
final success = await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.devicesIds,
|
||||
code: event.code,
|
||||
value: event.value,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
emit(const AcsFailedState(error: 'Failed to control devices'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onFactoryReset(
|
||||
AcFactoryResetEvent event, Emitter<AcsState> emit) async {
|
||||
Future<void> _onFactoryReset(
|
||||
AcFactoryResetEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
try {
|
||||
final response = await DevicesManagementApi().factoryReset(
|
||||
@ -275,9 +180,11 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onClose(OnClose event, Emitter<AcsState> emit) {
|
||||
void _onClose(
|
||||
OnClose event,
|
||||
Emitter<AcsState> emit,
|
||||
) {
|
||||
_countdownTimer?.cancel();
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
void _handleIncreaseTime(IncreaseTimeEvent event, Emitter<AcsState> emit) {
|
||||
@ -300,7 +207,10 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
));
|
||||
}
|
||||
|
||||
void _handleDecreaseTime(DecreaseTimeEvent event, Emitter<AcsState> emit) {
|
||||
void _handleDecreaseTime(
|
||||
DecreaseTimeEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) {
|
||||
if (state is! ACStatusLoaded) return;
|
||||
final currentState = state as ACStatusLoaded;
|
||||
int totalMinutes = (scheduledHours * 60) + scheduledMinutes;
|
||||
@ -315,7 +225,9 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
}
|
||||
|
||||
Future<void> _handleToggleTimer(
|
||||
ToggleScheduleEvent event, Emitter<AcsState> emit) async {
|
||||
ToggleScheduleEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
if (state is! ACStatusLoaded) return;
|
||||
final currentState = state as ACStatusLoaded;
|
||||
|
||||
@ -331,37 +243,44 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
|
||||
try {
|
||||
final scaledValue = totalMinutes ~/ 6;
|
||||
await _runDebounce(
|
||||
isBatch: false,
|
||||
deviceId: deviceId,
|
||||
code: 'countdown_time',
|
||||
value: scaledValue,
|
||||
oldValue: scaledValue,
|
||||
emit: emit,
|
||||
final success = await controlDeviceService.controlDevice(
|
||||
deviceUuid: deviceId,
|
||||
status: Status(code: 'countdown_time', value: scaledValue),
|
||||
);
|
||||
_startCountdownTimer(emit);
|
||||
emit(currentState.copyWith(isTimerActive: timerActive));
|
||||
|
||||
if (success) {
|
||||
_startCountdownTimer(emit);
|
||||
emit(currentState.copyWith(isTimerActive: timerActive));
|
||||
} else {
|
||||
timerActive = false;
|
||||
emit(const AcsFailedState(error: 'Failed to set timer'));
|
||||
}
|
||||
} catch (e) {
|
||||
timerActive = false;
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
}
|
||||
} else {
|
||||
await _runDebounce(
|
||||
isBatch: false,
|
||||
deviceId: deviceId,
|
||||
code: 'countdown_time',
|
||||
value: 0,
|
||||
oldValue: 0,
|
||||
emit: emit,
|
||||
);
|
||||
_countdownTimer?.cancel();
|
||||
scheduledHours = 0;
|
||||
scheduledMinutes = 0;
|
||||
emit(currentState.copyWith(
|
||||
isTimerActive: timerActive,
|
||||
scheduledHours: 0,
|
||||
scheduledMinutes: 0,
|
||||
));
|
||||
try {
|
||||
final success = await controlDeviceService.controlDevice(
|
||||
deviceUuid: deviceId,
|
||||
status: Status(code: 'countdown_time', value: 0),
|
||||
);
|
||||
|
||||
if (success) {
|
||||
_countdownTimer?.cancel();
|
||||
scheduledHours = 0;
|
||||
scheduledMinutes = 0;
|
||||
emit(currentState.copyWith(
|
||||
isTimerActive: timerActive,
|
||||
scheduledHours: 0,
|
||||
scheduledMinutes: 0,
|
||||
));
|
||||
} else {
|
||||
emit(const AcsFailedState(error: 'Failed to stop timer'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,7 +304,10 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
});
|
||||
}
|
||||
|
||||
void _handleUpdateTimer(UpdateTimerEvent event, Emitter<AcsState> emit) {
|
||||
void _handleUpdateTimer(
|
||||
UpdateTimerEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) {
|
||||
if (state is ACStatusLoaded) {
|
||||
final currentState = state as ACStatusLoaded;
|
||||
emit(currentState.copyWith(
|
||||
@ -400,7 +322,6 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
ApiCountdownValueEvent event, Emitter<AcsState> emit) {
|
||||
if (state is ACStatusLoaded) {
|
||||
final totalMinutes = event.apiValue * 6;
|
||||
final scheduledHours = totalMinutes ~/ 60;
|
||||
scheduledMinutes = totalMinutes % 60;
|
||||
_startCountdownTimer(
|
||||
emit,
|
||||
@ -409,6 +330,43 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _updateDeviceFunctionFromCode(String code, dynamic value) {
|
||||
switch (code) {
|
||||
case 'switch':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(acSwitch: value);
|
||||
}
|
||||
break;
|
||||
case 'temp_set':
|
||||
if (value is int) {
|
||||
deviceStatus = deviceStatus.copyWith(tempSet: value);
|
||||
}
|
||||
break;
|
||||
case 'mode':
|
||||
if (value is String) {
|
||||
deviceStatus = deviceStatus.copyWith(modeString: value);
|
||||
}
|
||||
break;
|
||||
case 'level':
|
||||
if (value is String) {
|
||||
deviceStatus = deviceStatus.copyWith(fanSpeedsString: value);
|
||||
}
|
||||
break;
|
||||
case 'child_lock':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(childLock: value);
|
||||
}
|
||||
break;
|
||||
case 'countdown_time':
|
||||
if (value is int) {
|
||||
deviceStatus = deviceStatus.copyWith(countdown1: value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
add(OnClose());
|
||||
|
18
lib/pages/device_managment/ac/factories/ac_bloc_factory.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
|
||||
|
||||
abstract final class AcBlocFactory {
|
||||
const AcBlocFactory._();
|
||||
|
||||
static AcBloc create({
|
||||
required String deviceId,
|
||||
}) {
|
||||
return AcBloc(
|
||||
deviceId: deviceId,
|
||||
controlDeviceService:
|
||||
DeviceBlocDependenciesFactory.createControlDeviceService(),
|
||||
batchControlDevicesService:
|
||||
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
|
||||
);
|
||||
}
|
||||
}
|