Compare commits

..

52 Commits

Author SHA1 Message Date
3c98365338 change the validation from static code to backend 2025-06-03 10:44:34 +03:00
f07dbad1ea Merge pull request #220 from SyncrowIOT/SP-1664-FE-Sider-bar-tree-behavior-issues-on-Analytics-page
Sp 1664 fe sider bar tree behavior issues on analytics page
2025-06-01 16:45:19 +03:00
87df8e4091 Merge pull request #222 from SyncrowIOT/SP-1389-FE-On-Login-page-Email-field-is-case-sensitive-it-should-not-be
Normalize email to lowercase when logging in
2025-06-01 16:38:53 +03:00
2d68fc23a3 Normalize email to lowercase when logging in 2025-06-01 16:21:22 +03:00
15ea1b4c5a Merge pull request #221 from SyncrowIOT/enable-hot-reload
enable hot reload on web.
2025-06-01 16:00:46 +03:00
17f6985dbf enable hot reload on web. 2025-06-01 15:59:29 +03:00
d1ddf75a42 Merge pull request #219 from SyncrowIOT/SP-1607-FE-Adjust-Padding-Between-Comparison-Signs-for-Visual-Consistency
Sp 1607 fe adjust padding between comparison signs for visual consistency
2025-06-01 15:50:53 +03:00
393a5361f0 Apply correct business logic in AirQualityDataLoadingStrategy. 2025-06-01 15:40:12 +03:00
a56e93d0d7 removed the interface method onSelectChildSpace, because all the clients dont use it and instead pass the onSpaceSelected, which isn't a good design. 2025-06-01 15:38:14 +03:00
94847fa936 SP-1664-Fe-Sider-bar-tree-behavior-issues-on-Analytics-page. 2025-06-01 15:36:52 +03:00
78f42dacf6 Adjust ConditionToggle widget dimensions and colors for improved UI consistency 2025-06-01 14:37:42 +03:00
fdabfe5d95 Merge pull request #217 from SyncrowIOT/SP-1584-FE-Block-Energy-Device-from-Being-Added-to-Then-Section-with-Validation-Message
Refactor energy clamp dialog to handle empty functions list gracefully
2025-06-01 14:13:53 +03:00
8916000696 Refactor visibility logic in Energy Clamp Dialog to handle empty functions list more elegantly 2025-06-01 14:11:21 +03:00
305d695358 Refactor energy clamp dialog to handle empty functions list gracefully 2025-06-01 13:12:58 +03:00
cde79fc168 Merge pull request #212 from SyncrowIOT/SP-1594-FE-Implement-Real-Time-AQI-Data-Panel-for-Selected-Sensor
Sp 1594 fe implement real time aqi data panel for selected sensor
2025-05-29 15:27:08 +03:00
283a0dd536 Updated AqiSubValueWidget to use minimum value for range calculations, improving accuracy in AQI range display. 2025-05-29 14:59:03 +03:00
5636fbe6c9 sorted constructor dependencies. 2025-05-29 14:57:26 +03:00
3d4c17214c Refactored AqiGauge to consolidate status text and color logic into a single method, improving code readability and maintainability. 2025-05-29 14:56:56 +03:00
b95f4063d9 removed unused widget. 2025-05-29 14:54:53 +03:00
bc289a0ddf removed testing code. 2025-05-29 14:45:03 +03:00
d9448d9709 Merge pull request #209 from SyncrowIOT/SP-1546-FE-Garage-door-opener-Countdown-counter-is-throwing-Device-not-found-error
Refactor event handling in GarageDoorBloc to use local variable for d…
2025-05-29 14:39:40 +03:00
7bd0c061d4 enhanced design of AqiLocation. 2025-05-29 13:29:52 +03:00
36ddebb5ae Implemented new gauge design. 2025-05-29 13:28:44 +03:00
43cb985e74 finished integrating realtime data. 2025-05-29 13:05:49 +03:00
7bfd08238e Refactor event handling in GarageDoorBloc to use local variable for deviceId 2025-05-29 12:19:04 +03:00
94b4aa7c46 Extracted big widgets into smaller ones, and integrated aqi device info with RealtimeChangesBloc. 2025-05-29 11:26:21 +03:00
0a9d53e5bd Refactor ConditionToggle widget to display icons with corresponding conditions 2025-05-29 10:48:12 +03:00
3d133581ff Implemented and used a reusable widget for analytics sidebars headers. 2025-05-29 09:59:27 +03:00
a75e6a89a9 Enhanced responsiveness of AqiLocationInfoCell. 2025-05-29 09:24:29 +03:00
010960c89b Merge pull request #208 from SyncrowIOT/SP-1603-FE-Freeze-First-Row-in-All-Table-Views-Across-the-Platform
Refactor table layout to accommodate dynamic table size
2025-05-28 16:57:56 +03:00
cc5f107ccb Extracted AqiHumidityAndTemperature into its own widget and file. 2025-05-28 16:55:36 +03:00
79c5fe1651 add icons for side bar info (humidity and tempreture). 2025-05-28 16:13:23 +03:00
fd186a00fd add shadow to pointer to match the design. 2025-05-28 15:41:18 +03:00
5b91ceb639 enhanced animation of AqiGague 2025-05-28 15:33:28 +03:00
5d3ef95cb7 Refactor AqiGauge to use constants for range values, to allow for ease of change, and readability. 2025-05-28 15:30:12 +03:00
a87b11d084 adjusted the size of AqiGauge and removed unnecessary code. 2025-05-28 15:25:17 +03:00
7c69c7ddbd fixed responsiveness of end side bar. 2025-05-28 15:19:26 +03:00
16dc066440 removed unnecessary comment. 2025-05-28 14:57:16 +03:00
9a41e0c4f5 moved ApiGauge to its own file. 2025-05-28 14:50:46 +03:00
a23370471c improved sizing of AqiLocationInfoCell. 2025-05-28 14:39:41 +03:00
595966d306 implemented gauge. 2025-05-28 14:22:35 +03:00
fc330d6e17 Making good progress towards finalizing the end side bar. 2025-05-28 09:32:58 +03:00
42319cc4f9 added unit property to AqiType. 2025-05-27 16:56:01 +03:00
aded80fb9a modified sizing of AirQualityView. 2025-05-27 16:55:43 +03:00
077c6e99d6 added aqi informative icons. 2025-05-27 16:55:17 +03:00
1f444ccfcb Created AqiLocationInfoCell widget. 2025-05-27 16:11:13 +03:00
fe716baba7 created AqiLocation widget. 2025-05-27 16:10:58 +03:00
34279cfdae added location_pin.svg icon. 2025-05-27 16:10:49 +03:00
0bf34c66aa Animated AqiSubValueWidget. 2025-05-27 15:45:32 +03:00
7726ceecb8 made AqiSubValueWidget use the correct colors. 2025-05-27 15:21:45 +03:00
ae2078d28c Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1594-FE-Implement-Real-Time-AQI-Data-Panel-for-Selected-Sensor 2025-05-27 15:17:59 +03:00
95eca869c9 Implemented AqiSubValueWidget. 2025-05-27 15:12:11 +03:00
41 changed files with 1410 additions and 517 deletions

3
.vscode/launch.json vendored
View File

@ -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"

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -33,7 +33,7 @@ class AirQualityView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height,
height: height * 1.1,
child: const Column(
children: [
Expanded(
@ -41,7 +41,7 @@ class AirQualityView extends StatelessWidget {
spacing: 32,
children: [
Expanded(
flex: 5,
flex: 10,
child: Column(
spacing: 20,
children: [
@ -50,7 +50,7 @@ class AirQualityView extends StatelessWidget {
],
),
),
Expanded(flex: 2, child: AirQualityEndSideWidget()),
Expanded(flex: 6, child: AirQualityEndSideWidget()),
],
),
),

View File

@ -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,
};
}

View File

@ -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,
),
),
],
);
}
}

View File

@ -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,
);
},
),
),
),
],
);
}
}

View File

@ -0,0 +1,129 @@
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: Expanded(
child: Column(
spacing: 6,
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,
),
],
),
),
);
},
);
}
}

View 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)!;
}
}

View File

@ -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),
],
);
}
}

View File

@ -0,0 +1,52 @@
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({super.key});
@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(
'Business Bay, Dubai - UAE',
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,
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.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 Container(
decoration: secondarySection.copyWith(boxShadow: const []),
padding: const EdgeInsetsDirectional.all(20),
child: const Column(
spacing: 8,
children: [
AqiLocation(),
Expanded(
child: Row(
spacing: 8,
children: [
AqiLocationInfoCell(
label: 'Temperature',
value: ' 25°',
svgPath: Assets.aqiTemperature,
),
AqiLocationInfoCell(
label: 'Humidity',
value: '25%',
svgPath: Assets.aqiHumidity,
),
AqiLocationInfoCell(
label: 'Air Quality',
value: ' 120',
svgPath: Assets.aqiAirQuality,
),
],
),
),
],
),
);
}
}

View File

@ -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),
),
),
),
],
),
),
);
}
}

View File

@ -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,
),
),
),
);
}
}

View File

@ -3,16 +3,17 @@ 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', ''),
pm25('PM2.5', 'µg/m³'),
pm10('PM10', 'µg/m³'),
hcho('HCHO', 'mg/m³'),
tvoc('TVOC', 'µg/m³'),
co2('CO2', 'ppm');
const AqiType(this.value, this.unit);
final String value;
const AqiType(this.value);
final String unit;
}
class AqiTypeDropdown extends StatefulWidget {

View File

@ -26,10 +26,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())
@ -42,18 +42,11 @@ final class AirQualityDataLoadingStrategy 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(),
);
FetchAirQualityDataHelper.clearAllData(context);
}
}

View File

@ -13,10 +13,5 @@ abstract class AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel space,
);
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
);
void clearData(BuildContext context);
}

View File

@ -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,7 +30,24 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
SpaceModel space,
) {
context.read<SpaceTreeBloc>().add(
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
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 ?? '',
@ -48,13 +55,6 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
},
),
);

View File

@ -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,6 +77,7 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(e.name),
if (showSpaceUuid)
if (spaceUuid != null)
FittedBox(
fit: BoxFit.scaleDown,

View File

@ -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',
showSpaceUuid: 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: '--'))

View File

@ -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,
),
),
),
),
],
);
}
}

View File

@ -0,0 +1,90 @@
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.showSpaceUuid = false,
this.onChanged,
super.key,
});
final String title;
final bool showSpaceUuid;
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(
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),
],
);
}
}

View File

@ -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,

View File

@ -360,7 +360,7 @@ class GarageDoorBloc extends Bloc<GarageDoorEvent, GarageDoorState> {
delay: deviceStatus.delay + Duration(minutes: 10));
emit(GarageDoorLoadedState(status: deviceStatus));
add(GarageDoorControlEvent(
deviceId: event.deviceId,
deviceId: deviceId,
value: deviceStatus.delay.inSeconds,
code: 'countdown_1'));
} catch (e) {

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
@ -15,6 +16,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/routine_details_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_model.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:syncrow_web/services/routines_api.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -64,7 +66,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
TriggerSwitchTabsEvent event,
Emitter<RoutineState> emit,
) {
emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false));
emit(state.copyWith(
routineTab: event.isRoutineTab, createRoutineView: false));
add(ResetRoutineState());
if (event.isRoutineTab) {
add(const LoadScenes());
@ -90,8 +93,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
final updatedIfItems = List<Map<String, dynamic>>.from(state.ifItems);
// Find the index of the item in teh current itemsList
int index =
updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = updatedIfItems.indexWhere(
(map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
updatedIfItems[index] = event.item;
@ -100,18 +103,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
if (event.isTabToRun) {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
} else {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
}
}
void _onAddToThenContainer(AddToThenContainer event, Emitter<RoutineState> emit) {
void _onAddToThenContainer(
AddToThenContainer event, Emitter<RoutineState> emit) {
final currentItems = List<Map<String, dynamic>>.from(state.thenItems);
// Find the index of the item in teh current itemsList
int index =
currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = currentItems.indexWhere(
(map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
currentItems[index] = event.item;
@ -122,7 +128,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
emit(state.copyWith(thenItems: currentItems));
}
void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter<RoutineState> emit) {
void _onAddFunctionsToRoutine(
AddFunctionToRoutine event, Emitter<RoutineState> emit) {
try {
if (event.functions.isEmpty) return;
@ -157,7 +164,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions);
// }
currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions);
currentSelectedFunctions[event.uniqueCustomId] =
List.from(event.functions);
emit(state.copyWith(selectedFunctions: currentSelectedFunctions));
} catch (e) {
@ -165,24 +173,30 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadScenes(LoadScenes event, Emitter<RoutineState> emit) async {
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid));
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectUuid));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith(
@ -199,7 +213,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadAutomation(LoadAutomation event, Emitter<RoutineState> emit) async {
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> automations = [];
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -207,17 +222,22 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
try {
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
automations.addAll(await SceneApi.getAutomation(spaceId, communityId, projectId));
automations.addAll(
await SceneApi.getAutomation(spaceId, communityId, projectId));
}
}
} else {
automations.addAll(await SceneApi.getAutomation(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectId));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectId));
}
emit(state.copyWith(
automations: automations,
@ -233,14 +253,16 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onSearchRoutines(SearchRoutines event, Emitter<RoutineState> emit) async {
FutureOr<void> _onSearchRoutines(
SearchRoutines event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(isLoading: false, errorMessage: null));
emit(state.copyWith(searchText: event.query));
}
FutureOr<void> _onAddSelectedIcon(AddSelectedIcon event, Emitter<RoutineState> emit) {
FutureOr<void> _onAddSelectedIcon(
AddSelectedIcon event, Emitter<RoutineState> emit) {
emit(state.copyWith(selectedIcon: event.icon));
}
@ -254,7 +276,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
return actions.last['deviceId'] == 'delay';
}
Future<void> _onCreateScene(CreateSceneEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateScene(
CreateSceneEvent event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -267,7 +290,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
return;
@ -335,15 +359,18 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
errorMessage: 'Something went wrong',
));
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorMessage,
));
}
}
Future<void> _onCreateAutomation(CreateAutomationEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateAutomation(
CreateAutomationEvent event, Emitter<RoutineState> emit) async {
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (state.routineName == null || state.routineName!.isEmpty) {
@ -365,7 +392,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
CustomSnackBar.redSnackBar('Cannot have delay as the last action');
@ -456,7 +484,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.createAutomation(createAutomationModel, projectUuid);
final result =
await SceneApi.createAutomation(createAutomationModel, projectUuid);
if (result['success']) {
add(ResetRoutineState());
add(const LoadAutomation());
@ -468,26 +497,32 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
CustomSnackBar.redSnackBar('Something went wrong');
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorMessage,
));
CustomSnackBar.redSnackBar('Something went wrong');
CustomSnackBar.redSnackBar(errorMessage);
}
}
FutureOr<void> _onRemoveDragCard(RemoveDragCard event, Emitter<RoutineState> emit) {
FutureOr<void> _onRemoveDragCard(
RemoveDragCard event, Emitter<RoutineState> emit) {
if (event.isFromThen) {
final thenItems = List<Map<String, dynamic>>.from(state.thenItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
thenItems.removeAt(event.index);
selectedFunctions.remove(event.key);
emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
thenItems: thenItems, selectedFunctions: selectedFunctions));
} else {
final ifItems = List<Map<String, dynamic>>.from(state.ifItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
ifItems.removeAt(event.index);
selectedFunctions.remove(event.key);
@ -498,7 +533,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
isAutomation: false,
isTabToRun: false));
} else {
emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
ifItems: ifItems, selectedFunctions: selectedFunctions));
}
}
}
@ -510,11 +546,13 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
}
FutureOr<void> _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
FutureOr<void> _onEffectiveTimeEvent(
EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
emit(state.copyWith(effectiveTime: event.effectiveTime));
}
FutureOr<void> _onSetRoutineName(SetRoutineName event, Emitter<RoutineState> emit) {
FutureOr<void> _onSetRoutineName(
SetRoutineName event, Emitter<RoutineState> emit) {
emit(state.copyWith(
routineName: event.name,
));
@ -641,7 +679,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// return (thenItems, ifItems, currentFunctions);
// }
Future<void> _onGetSceneDetails(GetSceneDetails event, Emitter<RoutineState> emit) async {
Future<void> _onGetSceneDetails(
GetSceneDetails event, Emitter<RoutineState> emit) async {
try {
emit(state.copyWith(
isLoading: true,
@ -689,8 +728,10 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// if (!deviceCards.containsKey(deviceId)) {
deviceCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay'
'deviceId':
action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId':
action.type == 'automation' || action.actionExecutor == 'delay'
? action.entityId
: const Uuid().v4(),
'title': action.actionExecutor == 'delay'
@ -732,7 +773,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
),
);
// emit(state.copyWith(automationActionExecutor: action.actionExecutor));
} else if (action.executorProperty != null && action.actionExecutor != 'delay') {
} else if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice?.functions ?? [];
final functionCode = action.executorProperty?.functionCode;
for (DeviceFunction function in functions) {
@ -798,7 +840,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onResetRoutineState(ResetRoutineState event, Emitter<RoutineState> emit) {
FutureOr<void> _onResetRoutineState(
ResetRoutineState event, Emitter<RoutineState> emit) {
emit(state.copyWith(
ifItems: [],
thenItems: [],
@ -822,7 +865,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
createRoutineView: false));
}
FutureOr<void> _deleteScene(DeleteScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _deleteScene(
DeleteScene event, Emitter<RoutineState> emit) async {
try {
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -831,7 +875,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var spaceBloc = context.read<SpaceTreeBloc>();
if (state.isTabToRun) {
await SceneApi.deleteScene(
unitUuid: spaceBloc.state.selectedSpaces[0], sceneId: state.sceneId ?? '');
unitUuid: spaceBloc.state.selectedSpaces[0],
sceneId: state.sceneId ?? '');
} else {
await SceneApi.deleteAutomation(
unitUuid: spaceBloc.state.selectedSpaces[0],
@ -854,11 +899,14 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
add(const LoadAutomation());
add(ResetRoutineState());
emit(state.copyWith(isLoading: false, createRoutineView: false));
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Failed to delete scene',
errorMessage: errorMessage,
));
CustomSnackBar.redSnackBar(errorMessage);
}
}
@ -876,7 +924,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// }
// }
FutureOr<void> _fetchDevices(FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
FutureOr<void> _fetchDevices(
FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true));
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@ -885,17 +934,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var createRoutineBloc = context.read<CreateRoutineBloc>();
var spaceBloc = context.read<SpaceTreeBloc>();
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
devices.addAll(
await DevicesManagementApi().fetchDevices(communityId, spaceId, projectUuid));
devices.addAll(await DevicesManagementApi()
.fetchDevices(communityId, spaceId, projectUuid));
}
}
} else {
devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId, createRoutineBloc.selectedSpaceId, projectUuid));
createRoutineBloc.selectedCommunityId,
createRoutineBloc.selectedSpaceId,
projectUuid));
}
emit(state.copyWith(isLoading: false, devices: devices));
@ -904,7 +957,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateScene(UpdateScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateScene(
UpdateScene event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -918,7 +972,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
return;
@ -971,7 +1026,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
final result =
await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
if (result['success']) {
add(ResetRoutineState());
add(const LoadScenes());
@ -990,7 +1046,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateAutomation(UpdateAutomation event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateAutomation(
UpdateAutomation event, Emitter<RoutineState> emit) async {
try {
if (state.routineName == null || state.routineName!.isEmpty) {
emit(state.copyWith(
@ -1114,10 +1171,11 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
errorMessage: result['message'],
));
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorData,
));
}
}
@ -1214,7 +1272,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// if (!deviceThenCards.containsKey(deviceId)) {
deviceThenCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'deviceId':
action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': const Uuid().v4(),
'title': action.actionExecutor == 'delay'
? 'Delay'
@ -1249,7 +1308,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
updatedFunctions[uniqueCustomId] = [];
}
if (action.executorProperty != null && action.actionExecutor != 'delay') {
if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice.functions;
final functionCode = action.executorProperty!.functionCode;
for (var function in functions) {
@ -1291,10 +1351,14 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
final ifItems = deviceIfCards.values.where((card) => card['type'] == 'condition').toList();
final ifItems = deviceIfCards.values
.where((card) => card['type'] == 'condition')
.toList();
final thenItems = deviceThenCards.values
.where((card) =>
card['type'] == 'action' || card['type'] == 'automation' || card['type'] == 'scene')
card['type'] == 'action' ||
card['type'] == 'automation' ||
card['type'] == 'scene')
.toList();
emit(state.copyWith(
@ -1316,7 +1380,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onSceneTrigger(SceneTrigger event, Emitter<RoutineState> emit) async {
Future<void> _onSceneTrigger(
SceneTrigger event, Emitter<RoutineState> emit) async {
emit(state.copyWith(loadingSceneId: event.sceneId));
try {
@ -1358,24 +1423,29 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (success) {
final updatedAutomations = await SceneApi.getAutomationByUnitId(
event.automationStatusUpdate.spaceUuid, event.communityId, projectId);
event.automationStatusUpdate.spaceUuid,
event.communityId,
projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
));
} else {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update failed',
));
}
} catch (e) {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update error: ${e.toString()}',

View File

@ -12,22 +12,53 @@ class ConditionToggle extends StatelessWidget {
});
static const _conditions = ["<", "==", ">"];
static const _icons = [
Icons.chevron_left,
Icons.drag_handle,
Icons.chevron_right
];
@override
Widget build(BuildContext context) {
return ToggleButtons(
onPressed: (index) => onChanged(_conditions[index]),
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
final selectedIndex = _conditions.indexOf(currentCondition ?? "==");
return Container(
height: 30,
width: MediaQuery.of(context).size.width * 0.1,
decoration: BoxDecoration(
color: ColorsManager.softGray.withOpacity(0.5),
borderRadius: BorderRadius.circular(50),
),
clipBehavior: Clip.antiAlias,
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(_conditions.length, (index) {
final isSelected = index == selectedIndex;
return Expanded(
child: InkWell(
onTap: () => onChanged(_conditions[index]),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.ease,
decoration: BoxDecoration(
color:
isSelected ? ColorsManager.vividBlue : Colors.transparent,
),
child: Center(
child: Icon(
_icons[index],
size: 20,
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor,
weight: isSelected ? 700 : 500,
),
),
),
),
);
}),
),
isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: _conditions.map((c) => Text(c)).toList(),
);
}
}

View File

@ -99,7 +99,27 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('Energy Clamp Conditions'),
Expanded(child: _buildMainContent(context, state)),
Expanded(
child: Visibility(
visible: _functions.isNotEmpty,
replacement: SizedBox(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Text(
'You Cant add\n the Power Clamp to Then Section',
textAlign: TextAlign.center,
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w400),
)),
],
),
),
child: _buildMainContent(context, state),
)),
_buildDialogFooter(context, state),
],
),

View File

@ -24,6 +24,9 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
on<PaginationEvent>(_fetchPaginationSpaces);
on<DebouncedSearchEvent>(_onDebouncedSearch);
on<SpaceTreeClearSelectionEvent>(_onSpaceTreeClearSelectionEvent);
on<AnalyticsClearAllSpaceTreeSelectionsEvent>(
_onAnalyticsClearAllSpaceTreeSelectionsEvent,
);
}
Timer _timer = Timer(const Duration(microseconds: 0), () {});
@ -493,6 +496,20 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
);
}
void _onAnalyticsClearAllSpaceTreeSelectionsEvent(
AnalyticsClearAllSpaceTreeSelectionsEvent event,
Emitter<SpaceTreeState> emit,
) async {
emit(
state.copyWith(
selectedCommunities: [],
selectedCommunityAndSpaces: {},
selectedSpaces: [],
soldCheck: [],
),
);
}
@override
Future<void> close() async {
_timer.cancel();

View File

@ -112,3 +112,7 @@ class ClearCachedData extends SpaceTreeEvent {}
class SpaceTreeClearSelectionEvent extends SpaceTreeEvent {
const SpaceTreeClearSelectionEvent();
}
final class AnalyticsClearAllSpaceTreeSelectionsEvent extends SpaceTreeEvent {
const AnalyticsClearAllSpaceTreeSelectionsEvent();
}

View File

@ -48,7 +48,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
@ -132,7 +133,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
)
else
CustomSearchBar(
onSearchChanged: (query) => context.read<SpaceTreeBloc>().add(
onSearchChanged: (query) =>
context.read<SpaceTreeBloc>().add(
SearchQueryEvent(query),
),
),
@ -140,6 +142,15 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
Expanded(
child: state.isSearching
? const Center(child: CircularProgressIndicator())
: communities.isEmpty
? Center(
child: Text(
'No communities found',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.textGray,
),
),
)
: SidebarCommunitiesList(
onScrollToEnd: () {
if (!state.paginationIsLoading) {
@ -166,7 +177,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
communities[index].uuid,
),
),
isExpanded: state.expandedCommunities.contains(
isExpanded:
state.expandedCommunities.contains(
communities[index].uuid,
),
onItemSelected: () {
@ -182,10 +194,11 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
(space) {
return CustomExpansionTileSpaceTree(
title: space.name,
isExpanded:
state.expandedSpaces.contains(space.uuid),
isExpanded: state.expandedSpaces
.contains(space.uuid),
onItemSelected: () {
final isParentSelected = _isParentSelected(
final isParentSelected =
_isParentSelected(
state,
communities[index],
space,
@ -213,9 +226,10 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
),
isSelected: state.selectedSpaces
.contains(space.uuid) ||
state.soldCheck.contains(space.uuid),
isSoldCheck:
state.soldCheck.contains(space.uuid),
state.soldCheck
.contains(space.uuid),
isSoldCheck: state.soldCheck
.contains(space.uuid),
children: _buildNestedSpaces(
context,
state,
@ -229,7 +243,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
},
),
),
if (state.paginationIsLoading) const CircularProgressIndicator(),
if (state.paginationIsLoading)
const CircularProgressIndicator(),
],
),
);

View File

@ -0,0 +1,10 @@
class APIException implements Exception {
final String message;
APIException(this.message);
@override
String toString() {
return message;
}
}

View File

@ -1,10 +1,13 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/auth/model/token.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class AuthenticationAPI {
static Future<Token> loginWithEmail({required var model}) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.login,
body: model.toJson(),
@ -13,6 +16,11 @@ class AuthenticationAPI {
return Token.fromJson(json['data']);
});
return response;
} on DioException catch (e) {
final message = e.response?.data['error']['message'] ??
'An error occurred while logging in';
throw APIException(message);
}
}
static Future forgetPassword({
@ -20,12 +28,18 @@ class AuthenticationAPI {
required var password,
required var otpCode,
}) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.forgetPassword,
body: {"email": email, "password": password, "otpCode": otpCode},
showServerMessage: true,
expectedResponseModel: (json) {});
return response;
} on DioException catch (e) {
final message = e.response?.data['error']['message'] ??
'An error occurred while resetting the password';
throw APIException(message);
}
}
static Future<int?> sendOtp({required String email}) async {
@ -39,7 +53,9 @@ class AuthenticationAPI {
return response;
}
static Future verifyOtp({required String email, required String otpCode}) async {
static Future verifyOtp(
{required String email, required String otpCode}) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.verifyOtp,
body: {"email": email, "type": "PASSWORD", "otpCode": otpCode},
@ -52,6 +68,11 @@ class AuthenticationAPI {
}
});
return response;
} on APIException catch (e) {
throw APIException(e.message);
} catch (e) {
throw APIException('An error occurred while verifying the OTP');
}
}
static Future<List<RegionModel>> fetchRegion() async {
@ -59,7 +80,9 @@ class AuthenticationAPI {
path: ApiEndpoints.getRegion,
showServerMessage: true,
expectedResponseModel: (json) {
return (json as List).map((zone) => RegionModel.fromJson(zone)).toList();
return (json as List)
.map((zone) => RegionModel.fromJson(zone))
.toList();
});
return response;
}

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart';
import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart';
@ -5,6 +6,7 @@ import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/cr
import 'package:syncrow_web/pages/routines/models/icon_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_details_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
@ -26,9 +28,10 @@ class SceneApi {
);
debugPrint('create scene response: $response');
return response;
} catch (e) {
debugPrint(e.toString());
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -48,9 +51,10 @@ class SceneApi {
);
debugPrint('create automation response: $response');
return response;
} catch (e) {
debugPrint(e.toString());
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -165,8 +169,10 @@ class SceneApi {
},
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -185,8 +191,10 @@ class SceneApi {
},
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -217,8 +225,10 @@ class SceneApi {
expectedResponseModel: (json) => json['statusCode'] == 200,
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -236,8 +246,10 @@ class SceneApi {
expectedResponseModel: (json) => json['statusCode'] == 200,
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}

View File

@ -452,4 +452,10 @@ class Assets {
'assets/icons/refresh_status_icon.svg';
static const String energyConsumedIcon =
'assets/icons/energy_consumed_icon.svg';
static const String locationPin = 'assets/icons/location_pin.svg';
static const String aqiTemperature = 'assets/icons/aqi_temperature.svg';
static const String aqiHumidity = 'assets/icons/aqi_humidity.svg';
static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg';
static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg';
static const String humidityAqiSidebar = 'assets/icons/humidity.svg';
}

View File

@ -61,6 +61,7 @@ dependencies:
firebase_crashlytics: ^4.3.2
firebase_database: ^11.3.2
bloc: ^9.0.0
gauge_indicator: ^0.4.3
dev_dependencies: