Compare commits
332 Commits
upgrade-fl
...
42c410d982
Author | SHA1 | Date | |
---|---|---|---|
42c410d982 | |||
368b1be3c0 | |||
c13119a4e8 | |||
35e9b606b2 | |||
387586f6f7 | |||
7cf4d0b5a9 | |||
e4a27b5651 | |||
f89660a9ff | |||
1a3dc60bd2 | |||
201348a9bf | |||
e2d4e48875 | |||
50f8158830 | |||
72af55ef98 | |||
b888f516e2 | |||
c1e61ee61d | |||
7750290be4 | |||
7f26c773a7 | |||
1adbae6735 | |||
ede2da6632 | |||
b06e4bd2ba | |||
0847cb8a41 | |||
818bdee745 | |||
0a022d8a8d | |||
f33b3e8bd2 | |||
8f0eb88567 | |||
19739c6e4d | |||
9f86b8d638 | |||
95907661d2 | |||
9c9b7d99dc | |||
037895844a | |||
c07bae5cbc | |||
e6fe9f35b0 | |||
8cb6c13cd5 | |||
949c27938a | |||
4c582b865d | |||
d7467adeda | |||
5486f0832d | |||
fd239a3907 | |||
e2d6f5eea8 | |||
289922071a | |||
8594168548 | |||
bd9a74b380 | |||
15ee79688d | |||
e5e88385e9 | |||
62d5bbce7e | |||
05d784ec11 | |||
9ebf474a60 | |||
af48bbead5 | |||
3c80724c1e | |||
db05331e9a | |||
cdc76c2c8e | |||
44c88fb1c4 | |||
dfb120e7cf | |||
8c3861e83c | |||
b90f25f7b0 | |||
4d51321675 | |||
b5e7776ccb | |||
32938404dd | |||
0cfd58d820 | |||
d4625a8f04 | |||
9f24606613 | |||
e87dffd76b | |||
0c220a1f34 | |||
a526fcbeee | |||
172e1d208a | |||
2c254c1a91 | |||
480e183b91 | |||
d8bb234537 | |||
354d61dfa2 | |||
8916efcebb | |||
175d1e662b | |||
57bd4b8527 | |||
df308fd12a | |||
e0cfe541dd | |||
814cbf787f | |||
df8eff895e | |||
9514200892 | |||
cf4bfc41f6 | |||
01f55c14de | |||
19cdd371f8 | |||
388391eec4 | |||
23cfee1490 | |||
2f5ad03431 | |||
6dd3329288 | |||
d82a050422 | |||
a1562110d5 | |||
46aa5e2ddb | |||
ec1bb5b609 | |||
5827ba4296 | |||
b96f65d2c2 | |||
034a5ef908 | |||
c198047165 | |||
1828ffb87a | |||
bd53388438 | |||
b97183fb61 | |||
07dfe6b206 | |||
c4fd90b3bc | |||
bbcb947313 | |||
13e9a808ab | |||
32208c1e81 | |||
1d95915f57 | |||
e365aa3faa | |||
26e8ff7ee2 | |||
3fc6964e15 | |||
0c0bf96c07 | |||
4744009cb6 | |||
1a4ced195a | |||
812c51400b | |||
5beae81596 | |||
f1144762b0 | |||
ca41aa6224 | |||
396ce3dad8 | |||
2d0019200e | |||
475462301f | |||
731ba7a768 | |||
7fda564ee4 | |||
11e2853403 | |||
9c02bed4c0 | |||
4f932b8c35 | |||
44ae8386df | |||
9d4a665547 | |||
f43826a824 | |||
0b372e1ed8 | |||
4e9bcbdcea | |||
eee6a80c50 | |||
03ba506294 | |||
6c268754a9 | |||
8593055923 | |||
18ab9fd24c | |||
3c9494963d | |||
07dd260593 | |||
f38ac58442 | |||
30e940fdfc | |||
520b73717a | |||
e1bb67d7bd | |||
5e0df09cb6 | |||
22070ca04a | |||
6d667af7dc | |||
3b4952db0a | |||
5f59583696 | |||
7397486e7a | |||
487c5a894b | |||
7e0200aad8 | |||
562c67a958 | |||
52b843d514 | |||
423ad6e687 | |||
932e50f518 | |||
c649044a1f | |||
c46cfb04a7 | |||
8754960713 | |||
c6e98fa245 | |||
277a9ce4f0 | |||
f901983aa5 | |||
010403f1fa | |||
ee1ebeae2e | |||
6e6ef79ed0 | |||
7e5825de45 | |||
db9e856bca | |||
07435ec89e | |||
2a2fb7ffca | |||
df34ded153 | |||
5a2299ea2f | |||
90f8305aa1 | |||
329b2ba472 | |||
0fb9149613 | |||
87b45fff1d | |||
95ae50d12d | |||
95d6e1ecda | |||
479aa4a091 | |||
75efc595b4 | |||
d14cc785a8 | |||
379ecec789 | |||
ad00cf35ba | |||
5276f4186c | |||
8bc7a3daa2 | |||
e6957d566d | |||
71cf0a636e | |||
1200a809c2 | |||
03a6c5474b | |||
ada7daf179 | |||
4bdb487094 | |||
f8e4c89cdb | |||
7d4cdba0ef | |||
a78b5993a9 | |||
0e7109a19e | |||
ff3d5cd996 | |||
5f30a5a61b | |||
0712e6d64b | |||
a493ae08ce | |||
27349a6cc0 | |||
d17d4184be | |||
41d4fbb555 | |||
fccb5cbbab | |||
48d7ab430f | |||
28ac911f3f | |||
a793cc3967 | |||
09446844b0 | |||
f02788eaa5 | |||
614db4333c | |||
b79ab06d95 | |||
0a424300aa | |||
8494f0a8f1 | |||
ec12b970b0 | |||
d2713c5902 | |||
65ed94eb08 | |||
51c088d998 | |||
2f233db332 | |||
1f82e84115 | |||
5da25d8ecb | |||
8cf73e3efc | |||
0b774a6dfc | |||
2267d95795 | |||
23c3bf11f9 | |||
5201a65a97 | |||
e4cc5fce50 | |||
8dea89db0e | |||
ad5ada9d55 | |||
7172a0e3fb | |||
78898968e8 | |||
666c64231f | |||
5b5a94cf65 | |||
d45fa4c957 | |||
ed2a8f6ba2 | |||
d895ed74d2 | |||
e39c6abd32 | |||
fc6ea640a7 | |||
09c44f8a5f | |||
c178c36824 | |||
ce96afd7af | |||
27dfa0a05a | |||
3d95f2bef0 | |||
db513f916f | |||
78979a4375 | |||
ea19387605 | |||
5b33a8617e | |||
34565a7dab | |||
caf1ff5c7e | |||
01e8002c43 | |||
63da660ece | |||
567d0e2d20 | |||
45e6ea3259 | |||
e942957a47 | |||
b9a3b9c719 | |||
f5500dfe50 | |||
20d044f2e5 | |||
8caee32822 | |||
6c4bc0d634 | |||
1ba1aba54e | |||
09f2123946 | |||
8fc6e54ecc | |||
5d3380ef82 | |||
5b0710957d | |||
056a1daadc | |||
0132805713 | |||
35f975b261 | |||
9600f4fb8b | |||
5cd1384000 | |||
0260523121 | |||
6af96fadbd | |||
737762bbaf | |||
6bcfb77a06 | |||
6b76827f21 | |||
519285fa7c | |||
3eb38d28f7 | |||
2108622b5b | |||
ac44af54a3 | |||
aa141ef54d | |||
b0aea94b91 | |||
96f463229c | |||
4d9145a953 | |||
a2f897c3a6 | |||
249c2fb172 | |||
7a8537d39c | |||
1da0cdad4b | |||
d10df2ffb8 | |||
6ff9c602f1 | |||
5f20d52e57 | |||
362557d0d0 | |||
312d185932 | |||
89e12e47da | |||
a0d9819532 | |||
1316820954 | |||
5591c78d88 | |||
eaff7c4a52 | |||
5b3152e833 | |||
c1d3296b59 | |||
b3069ab749 | |||
37b21ecdfb | |||
8d408867bb | |||
57508fe17e | |||
13360fe6f3 | |||
3e5b501167 | |||
4d9f08af31 | |||
28aa3bc406 | |||
51ad74b2be | |||
994e9f4e57 | |||
c642ba2644 | |||
218f43bacb | |||
04250ebc98 | |||
29959f567e | |||
1567f10827 | |||
cdbd90b54c | |||
03f5c869c6 | |||
4f98891902 | |||
7002bbfa04 | |||
f19120c754 | |||
6b3eca23af | |||
4f4f11c330 | |||
8a25fa798c | |||
584845ffdc | |||
6612e91430 | |||
56c613fb0c | |||
8d2d9dd0bb | |||
cfc68f1568 | |||
02e08ad92f | |||
d7899a24f5 | |||
800c0ba47f | |||
fe4e775902 | |||
5247856cb4 | |||
4a8b8a32ba | |||
2abce77eb5 | |||
7efd1c3c87 | |||
7a0d9aefb7 | |||
21cc25cfc4 | |||
e2ec4bbf31 | |||
51b46ae197 | |||
36ee22603a | |||
b0abd42b0c | |||
ba4da78846 | |||
dc20d69f20 | |||
cf6ec231dc | |||
d0530f7fc3 |
10
.github/.github/dependabot.yaml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
@ -25,3 +25,8 @@ linter:
|
||||
prefer_int_literals: false
|
||||
sort_constructors_first: false
|
||||
avoid_redundant_argument_values: false
|
||||
always_put_required_named_parameters_first: false
|
||||
unnecessary_breaks: false
|
||||
avoid_catches_without_on_clauses: false
|
||||
cascade_invocations: false
|
||||
overridden_fields: false
|
||||
|
10
assets/icons/add_button_Icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_9795_9381)">
|
||||
<path d="M9.21875 13.5149V10.7805H6.48438C6.05286 10.7805 5.70312 10.4308 5.70312 9.99924C5.70312 9.56787 6.05286 9.21799 6.48438 9.21799H9.21875V6.48361C9.21875 6.05225 9.56848 5.70236 10 5.70236C10.4315 5.70236 10.7812 6.05225 10.7812 6.48361V9.21799H13.5156C13.9471 9.21799 14.2969 9.56787 14.2969 9.99924C14.2969 10.4308 13.9471 10.7805 13.5156 10.7805H10.7812V13.5149C10.7812 13.9464 10.4315 14.2961 10 14.2961C9.56848 14.2961 9.21875 13.9464 9.21875 13.5149ZM17.0711 2.92892C15.1823 1.04019 12.6711 0 10 0C7.32895 0 4.81766 1.04019 2.92892 2.92892C1.04019 4.81766 0 7.32895 0 10C0 12.6711 1.04019 15.1823 2.92892 17.0711C4.81766 18.9598 7.32895 20 10 20C11.8286 20 13.6179 19.5016 15.1743 18.5588C15.5434 18.3353 15.6613 17.8549 15.4378 17.486C15.2142 17.1169 14.7337 16.9989 14.3648 17.2224C13.0525 18.0173 11.5431 18.4375 10 18.4375C5.3476 18.4375 1.5625 14.6524 1.5625 10C1.5625 5.3476 5.3476 1.5625 10 1.5625C14.6524 1.5625 18.4375 5.3476 18.4375 10C18.4375 11.6637 17.9428 13.2829 17.0068 14.6831C16.767 15.0417 16.8634 15.5269 17.2221 15.7668C17.5807 16.0065 18.0659 15.91 18.3058 15.5515C19.4141 13.8936 20 11.9739 20 10C20 7.32895 18.9598 4.81766 17.0711 2.92892Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9795_9381">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
4
assets/icons/back_button_icon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 20C15.514 20 19.9998 15.514 19.9998 9.99995C19.9998 4.48604 15.514 0 10 0C4.48613 0 0.000183105 4.48604 0.000183105 9.99995C0.000183105 15.514 4.48613 20 10 20ZM10 1.36892C14.7591 1.36892 18.6309 5.24077 18.631 9.99995C18.631 14.7591 14.7592 18.631 10 18.6311C5.24095 18.631 1.36919 14.7591 1.36919 9.99986C1.36919 5.24086 5.24095 1.36892 10 1.36892Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M8.65713 14.2828C8.92444 14.55 9.35784 14.5499 9.62505 14.2828C9.89245 14.0154 9.89245 13.5821 9.62496 13.3147L6.99481 10.6846L14.6112 10.6839C14.9892 10.6838 15.2956 10.3775 15.2956 9.99926C15.2955 9.62126 14.9891 9.31499 14.6111 9.31499L6.99444 9.31572L9.62523 6.68511C9.89254 6.41781 9.89254 5.98432 9.62523 5.7171C9.49154 5.5835 9.3164 5.5166 9.14118 5.5166C8.96605 5.5166 8.79092 5.5835 8.65722 5.71701L4.85811 9.51604C4.7297 9.64435 4.65761 9.81838 4.65761 9.99999C4.6577 10.1816 4.7298 10.3555 4.8582 10.4841L8.65713 14.2828Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
4
assets/icons/clock_icon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.9999 0C4.48595 0 0 4.48586 0 9.99971C0 15.514 4.48595 20 9.9999 20C15.5138 20 19.9996 15.5139 19.9996 9.99971C19.9996 4.48586 15.5138 0 9.9999 0ZM9.9999 18.5665C5.27638 18.5665 1.43349 14.7234 1.43349 9.99971C1.43349 5.27628 5.27638 1.43349 9.9999 1.43349C14.7233 1.43349 18.5661 5.27628 18.5661 9.99971C18.5661 14.7234 14.7233 18.5665 9.9999 18.5665Z" fill="#D5D5D5"/>
|
||||
<path d="M15.1416 9.83211H10.4423V4.69526C10.4423 4.29943 10.1215 3.97852 9.72553 3.97852C9.3297 3.97852 9.00879 4.29943 9.00879 4.69526V10.5489C9.00879 10.9447 9.3297 11.2656 9.72553 11.2656H15.1416C15.5376 11.2656 15.8584 10.9447 15.8584 10.5489C15.8584 10.153 15.5375 9.83211 15.1416 9.83211Z" fill="#D5D5D5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 799 B |
8
assets/icons/close_curtain.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="23" height="13" viewBox="0 0 23 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.24512 2.00263V11L1.90308 11.278L7.5311 6.94877C7.82484 6.72277 7.82484 6.27987 7.5311 6.05388L1.90308 1.72461L1.24512 2.00263Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M1.90344 1.7231L1.68312 1.55364C1.31186 1.2681 0.774414 1.53272 0.774414 2.00105V10.9984C0.774414 11.4668 1.31186 11.7315 1.68312 11.4459L1.90344 11.2764V1.7231Z" fill="#023DFE"/>
|
||||
<path d="M12.0646 0.855469H11.5001C11.1883 0.855469 10.9355 1.10819 10.9355 1.41998V11.5813H12.0646C12.3764 11.5813 12.6291 11.3285 12.6291 11.0167V1.41998C12.6291 1.10826 12.3764 0.855469 12.0646 0.855469Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M12.6291 11.0168C12.0056 11.0168 11.5001 10.5113 11.5001 9.88779V0.855469H10.9356C10.6238 0.855469 10.3711 1.10819 10.3711 1.41998V11.5813C10.3711 11.893 10.6238 12.1458 10.9356 12.1458H12.0646C12.3764 12.1458 12.6291 11.893 12.6291 11.5813V11.0168Z" fill="#023DFE"/>
|
||||
<path d="M21.4247 2.01953L16.1094 6.50343L21.4247 11.1061L22.226 10.7315V2.27062L21.4247 2.01953Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M17.3084 6.94723C17.0147 6.7213 17.0147 6.27833 17.3084 6.05233L22.2263 2.26933V2.00108C22.2263 1.53275 21.6889 1.26807 21.3176 1.55367L15.4693 6.05233C15.1756 6.27833 15.1756 6.7213 15.4693 6.94723L21.3176 11.4459C21.6889 11.7314 22.2263 11.4668 22.2263 10.9985V10.7302L17.3084 6.94723Z" fill="#023DFE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
8
assets/icons/empty_barred_chart.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
|
||||
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
|
||||
<rect x="16.0922" y="66.3794" width="28.1609" height="66.3793" fill="#C7CDD1"/>
|
||||
<rect x="54.3105" y="24.1379" width="28.1609" height="108.621" fill="#ABB4BA"/>
|
||||
<rect x="92.5288" y="78.4484" width="28.1609" height="54.3103" fill="#C7CDD1"/>
|
||||
<rect x="130.747" y="48.2759" width="28.1609" height="84.4828" fill="#ABB4BA"/>
|
||||
</svg>
|
After Width: | Height: | Size: 583 B |
5
assets/icons/empty_energy_management_chart.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="3.05394e-05" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
|
||||
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
|
||||
<path d="M1.5 132.5C13 132.5 6.58852 66.5 29.5 66.5C46.5 66.5 46.1214 24.9349 68.5 24.5C94.2816 23.999 80.7136 78.5065 106.5 78.5C125.715 78.4952 131.5 48.5 145.5 48.5C159.5 48.5 156.5 96 171.5 96" stroke="#ABB4BA" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 520 B |
7
assets/icons/empty_energy_management_per_device.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
|
||||
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
|
||||
<path d="M1.5 132.5C13 132.5 6.58852 66.5 29.5 66.5C46.5 66.5 46.1214 24.9348 68.5 24.5C94.2816 23.999 80.7136 78.5064 106.5 78.5C125.715 78.4951 131.5 48.5 145.5 48.5C159.5 48.5 156.5 95.9999 171.5 95.9999" stroke="#ABB4BA" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
|
||||
<path d="M1.5 132.5C13 132.5 6.58852 78.4999 29.5 78.4999C46.5 78.4999 45.6214 44.9349 68 44.5C93.7816 43.999 80.7136 27.0065 106.5 27C125.715 26.9952 131.5 63.5 145.5 63.5C159.5 63.5 156.5 113.5 171.5 113.5" stroke="#C7CDD1" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
|
||||
<path d="M1.5 132.5C13 132.5 6.58852 85.9999 29.5 85.9999C46.5 85.9999 45.6214 11.9348 68 11.4999C93.7816 10.9989 80.7136 43.5064 106.5 43.4999C125.715 43.4951 131.5 35.4999 145.5 35.4999C159.5 35.4999 156.5 105.5 171.5 105.5" stroke="#D5D5D5" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
99
assets/icons/empty_heatmap.svg
Normal file
@ -0,0 +1,99 @@
|
||||
<svg width="181" height="121" viewBox="0 0 181 121" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="15.5" y1="-2.52181e-08" x2="15.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="45.5" y1="-2.52181e-08" x2="45.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="75.5" y1="-2.52181e-08" x2="75.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="105.5" y1="-2.52181e-08" x2="105.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="135.5" y1="-2.52181e-08" x2="135.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="165.5" y1="-2.52181e-08" x2="165.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="30.5" y1="-2.52181e-08" x2="30.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="60.5" y1="-2.52181e-08" x2="60.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="90.5" y1="-2.52181e-08" x2="90.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="120.5" y1="-2.52181e-08" x2="120.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="150.5" y1="-2.52181e-08" x2="150.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="180.5" y1="-2.52181e-08" x2="180.5" y2="120" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="120.5" x2="-4.52101e-08" y2="120.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="60.5" x2="-4.52101e-08" y2="60.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="90.5" x2="-4.52101e-08" y2="90.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="30.5" x2="-4.52101e-08" y2="30.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="105.5" x2="-4.52101e-08" y2="105.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="45.5" x2="-4.52101e-08" y2="45.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="75.5" x2="-4.52101e-08" y2="75.5" stroke="#B9B9B9"/>
|
||||
<line x1="181" y1="15.5" x2="-4.52101e-08" y2="15.5" stroke="#B9B9B9"/>
|
||||
<rect x="16" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="46" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="61" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="76" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="91" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="106" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="121" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="136" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="151" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="166" y="16" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="16" y="31" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="46" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="61" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="76" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="91" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="106" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="121" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="136" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="151" y="31" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="166" y="31" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="16" y="46" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="46" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="61" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="76" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="91" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="106" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="121" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="136" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="151" y="46" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="166" y="46" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="16" y="61" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="61" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="46" y="61" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="61" y="61" width="14" height="14" fill="#B1B1B1"/>
|
||||
<rect x="76" y="61" width="14" height="14" fill="#B1B1B1"/>
|
||||
<rect x="91" y="61" width="14" height="14" fill="#B1B1B1"/>
|
||||
<rect x="106" y="61" width="14" height="14" fill="#B1B1B1"/>
|
||||
<rect x="121" y="61" width="14" height="14" fill="#B1B1B1"/>
|
||||
<rect x="136" y="61" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="151" y="61" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="166" y="61" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="16" y="76" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="46" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="61" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="76" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="91" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="106" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="121" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="136" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="151" y="76" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="166" y="76" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="16" y="91" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="46" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="61" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="76" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="91" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="106" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="121" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="136" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="151" y="91" width="14" height="14" fill="#D5D5D5"/>
|
||||
<rect x="166" y="91" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="16" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="31" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="46" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="61" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="76" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="91" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="106" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="121" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="136" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="151" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
<rect x="166" y="106" width="14" height="14" fill="#F2F2F2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
7
assets/icons/empty_range_of_aqi.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
|
||||
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
|
||||
<path d="M1.5 95.9999C13 95.9999 6.58853 66.4999 29.5 66.4999C46.5 66.4999 46.1214 34.9348 68.5 34.4999C94.2816 33.9989 80.7136 65.0065 106.5 65C125.715 64.9952 131.5 50.5 145.5 50.5C159.5 50.5 156.5 70.5 171.5 70.5" stroke="#C7CDD1" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M1.5 106C13 106 6.58853 76.4999 29.5 76.4999C46.5 76.4999 46.1214 44.9348 68.5 44.4999C94.2816 43.9989 80.7136 75.0065 106.5 75C125.715 74.9952 131.5 60.5 145.5 60.5C159.5 60.5 156.5 80.5 171.5 80.5" stroke="#F2F2F2" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M1.5 116C13 116 6.58853 86.4999 29.5 86.4999C46.5 86.4999 46.1214 54.9348 68.5 54.4999C94.2816 53.9989 80.7136 85.0065 106.5 85C125.715 84.9952 131.5 70.5 145.5 70.5C159.5 70.5 156.5 90.5 171.5 90.5" stroke="#ABB4BA" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
15
assets/icons/group_icon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_9717_7433)">
|
||||
<path d="M17.1131 10.6766H15.5664C15.7241 11.1083 15.8102 11.5741 15.8102 12.0596V17.9053C15.8102 18.1077 15.775 18.302 15.7109 18.4827H18.2679C19.2231 18.4827 20.0002 17.7056 20.0002 16.7505V13.5637C20.0002 11.9718 18.7051 10.6766 17.1131 10.6766Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M4.19005 12.0596C4.19005 11.5741 4.27618 11.1083 4.43384 10.6766H2.88712C1.29516 10.6766 0 11.9718 0 13.5637V16.7505C0 17.7057 0.777072 18.4828 1.73227 18.4828H4.28938C4.22528 18.302 4.19005 18.1077 4.19005 17.9053V12.0596Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M11.7679 9.17249H8.23184C6.63989 9.17249 5.34473 10.4676 5.34473 12.0596V17.9053C5.34473 18.2242 5.60324 18.4827 5.92215 18.4827H14.0776C14.3965 18.4827 14.655 18.2242 14.655 17.9053V12.0596C14.655 10.4676 13.3598 9.17249 11.7679 9.17249Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M9.99995 1.51721C8.08541 1.51721 6.52783 3.07479 6.52783 4.98937C6.52783 6.288 7.24459 7.42218 8.30311 8.01765C8.80518 8.30008 9.38401 8.46148 9.99995 8.46148C10.6159 8.46148 11.1947 8.30008 11.6968 8.01765C12.7553 7.42218 13.4721 6.28796 13.4721 4.98937C13.4721 3.07483 11.9145 1.51721 9.99995 1.51721Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M3.90284 4.75354C2.471 4.75354 1.30615 5.91839 1.30615 7.35022C1.30615 8.78206 2.471 9.94691 3.90284 9.94691C4.26604 9.94691 4.6119 9.87168 4.92608 9.73644C5.46929 9.50257 5.91718 9.08859 6.19433 8.57003C6.38886 8.20609 6.49952 7.79089 6.49952 7.35022C6.49952 5.91843 5.33468 4.75354 3.90284 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M16.0972 4.75354C14.6653 4.75354 13.5005 5.91839 13.5005 7.35022C13.5005 7.79093 13.6112 8.20612 13.8057 8.57003C14.0828 9.08863 14.5307 9.50261 15.0739 9.73644C15.3881 9.87168 15.734 9.94691 16.0972 9.94691C17.529 9.94691 18.6939 8.78206 18.6939 7.35022C18.6939 5.91839 17.529 4.75354 16.0972 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9717_7433">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
4
assets/icons/home_icon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0002 5.97498L3.12109 11.2683V18.3601H8.64871V13.163H11.5852V18.3601H16.8794V11.2683L10.0002 5.97498Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M17.1673 7.15356V3.52759H14.2702V4.92485L10 1.63989L0 9.33274L1.38043 11.1271L10 4.49458L18.6196 11.1272L20 9.33278L17.1673 7.15356Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 433 B |
24
assets/icons/no_data_table.svg
Normal file
After Width: | Height: | Size: 18 KiB |
8
assets/icons/open_curtain.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="22" height="12" viewBox="0 0 22 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2227 1.27411V10.2715L15.8806 10.5495L21.5086 6.22025C21.8024 5.99426 21.8024 5.55136 21.5086 5.32536L15.8806 0.996094L15.2227 1.27411Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M15.881 0.994589L15.6607 0.825126C15.2894 0.539589 14.752 0.804208 14.752 1.27254V10.2699C14.752 10.7383 15.2894 11.0029 15.6607 10.7173L15.881 10.5479V0.994589Z" fill="#023DFE"/>
|
||||
<path d="M12.0646 0.128906H11.5001C11.1883 0.128906 10.9355 0.381631 10.9355 0.693418V10.8547H12.0646C12.3764 10.8547 12.6291 10.602 12.6291 10.2902V0.693418C12.6291 0.381699 12.3764 0.128906 12.0646 0.128906Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M12.6291 10.2903C12.0056 10.2903 11.5001 9.78474 11.5001 9.16123V0.128906H10.9356C10.6238 0.128906 10.3711 0.381631 10.3711 0.693418V10.8547C10.3711 11.1665 10.6238 11.4192 10.9356 11.4192H12.0646C12.3764 11.4192 12.6291 11.1665 12.6291 10.8547V10.2903Z" fill="#023DFE"/>
|
||||
<path d="M6.95005 1.29297L1.63477 5.77687L6.95005 10.3795L7.75136 10.005V1.54405L6.95005 1.29297Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M2.83379 6.21871C2.54005 5.99278 2.54005 5.54981 2.83379 5.32382L7.7517 1.54081V1.27257C7.7517 0.804238 7.21426 0.539551 6.843 0.825156L0.994719 5.32382C0.700979 5.54981 0.700979 5.99278 0.994719 6.21871L6.843 10.7174C7.21426 11.0029 7.7517 10.7383 7.7517 10.27V10.0017L2.83379 6.21871Z" fill="#023DFE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
6
assets/icons/pause_curtain.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.81262 0.277344H8.24811C7.93632 0.277344 7.68359 0.530068 7.68359 0.841855V11.0031H8.81262C9.1244 11.0031 9.37713 10.7504 9.37713 10.4386V0.841855C9.37713 0.530136 9.1244 0.277344 8.81262 0.277344Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M9.37719 10.4387C8.75361 10.4387 8.24816 9.93317 8.24816 9.30967V0.277344H7.68365C7.37187 0.277344 7.11914 0.530068 7.11914 0.841855V11.0031C7.11914 11.3149 7.37187 11.5676 7.68365 11.5676H8.81268C9.12446 11.5676 9.37719 11.3149 9.37719 11.0031V10.4387Z" fill="#023DFE"/>
|
||||
<path d="M2.5548 0.277344H1.99029C1.67851 0.277344 1.42578 0.530068 1.42578 0.841855V11.0031H2.5548C2.86659 11.0031 3.11932 10.7504 3.11932 10.4386V0.841855C3.11932 0.530136 2.86659 0.277344 2.5548 0.277344Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M3.11937 10.4387C2.4958 10.4387 1.99035 9.93317 1.99035 9.30967V0.277344H1.42584C1.11405 0.277344 0.861328 0.530068 0.861328 0.841855V11.0031C0.861328 11.3149 1.11405 11.5676 1.42584 11.5676H2.55486C2.86665 11.5676 3.11937 11.3149 3.11937 11.0031V10.4387Z" fill="#023DFE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
10
assets/icons/reverse_arrows.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10119_2631)">
|
||||
<path d="M16.4229 10.9077V14.4803C16.4229 15.1238 15.9015 15.6453 15.2579 15.6453C14.6143 15.6453 14.0928 15.1238 14.0928 14.4803V14.3684C12.644 15.7134 10.7197 16.5 8.65572 16.5C5.42291 16.5 2.52657 14.573 1.27576 11.5914C1.21425 11.4446 1.18535 11.2917 1.18535 11.1417C1.18535 10.6854 1.45378 10.2539 1.89977 10.0661C2.49302 9.81722 3.17574 10.0959 3.4246 10.6901C4.31098 12.804 6.36475 14.1699 8.65572 14.1699C10.3973 14.1699 11.9999 13.3804 13.0578 12.0728H11.6849C11.0413 12.0728 10.5198 11.5513 10.5198 10.9077C10.5198 10.2641 11.0413 9.74265 11.6849 9.74265H15.2574C15.901 9.74265 16.4229 10.2641 16.4229 10.9077ZM5.31572 7.413C5.9593 7.413 6.48078 6.89105 6.48078 6.24794C6.48078 5.60436 5.9593 5.08288 5.31572 5.08288H4.13342C5.18897 3.68388 6.84661 2.83012 8.65572 2.83012C10.9472 2.83012 13.0005 4.1965 13.8873 6.31039C14.1357 6.90364 14.8184 7.18278 15.4117 6.93439C16.0049 6.68554 16.2841 6.00328 16.0357 5.40956C14.7844 2.42701 11.8881 0.5 8.65572 0.5C6.4421 0.5 4.38554 1.40455 2.90824 2.93218V2.67493C2.90824 2.03135 2.3863 1.50987 1.74318 1.50987C1.09961 1.50987 0.578125 2.03135 0.578125 2.67493V6.24794C0.578125 6.55691 0.701155 6.8533 0.919255 7.07234C1.13782 7.2909 1.43375 7.413 1.74318 7.413H5.31572Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10119_2631">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/autocad_occupancy_image.png
Normal file
After Width: | Height: | Size: 290 KiB |
4
assets/images/completed_done.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M37.8033 16.3567C37.2313 15.7848 36.3039 15.7847 35.7318 16.3568L21.5532 30.5353L14.2681 23.2504C13.6962 22.6784 12.7686 22.6783 12.1966 23.2505C11.6246 23.8226 11.6246 24.75 12.1966 25.3221L20.5174 33.6427C20.8034 33.9287 21.1783 34.0717 21.5531 34.0717C21.928 34.0717 22.3029 33.9287 22.5888 33.6426L37.8033 18.4283C38.3754 17.8563 38.3754 16.9288 37.8033 16.3567Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M42.6776 7.32236C37.9558 2.60049 31.6776 0 25 0C18.3223 0 12.0442 2.60049 7.32236 7.32236C2.60039 12.0443 0 18.3224 0 25C0 31.6778 2.60039 37.9559 7.32236 42.6777C12.0441 47.3996 18.3223 50 25 50C31.6777 50 37.9558 47.3996 42.6776 42.6777C47.3995 37.9559 50 31.6778 50 25C50 18.3224 47.3995 12.0443 42.6776 7.32236ZM25 47.0703C12.8304 47.0703 2.92969 37.1696 2.92969 25C2.92969 12.8304 12.8304 2.92969 25 2.92969C37.1696 2.92969 47.0703 12.8304 47.0703 25C47.0703 37.1696 37.1696 47.0703 25 47.0703Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
10
lib/common/widgets/app_loading_indicator.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppLoadingIndicator extends StatelessWidget {
|
||||
const AppLoadingIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -21,8 +20,10 @@ import 'package:syncrow_web/utils/theme/theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
const environment =
|
||||
String.fromEnvironment('FLAVOR', defaultValue: 'production');
|
||||
const environment = String.fromEnvironment(
|
||||
'FLAVOR',
|
||||
defaultValue: 'production',
|
||||
);
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
@ -40,7 +41,7 @@ class MyApp extends StatelessWidget {
|
||||
initialLocation: RoutesConst.auth,
|
||||
routes: AppRoutes.getRoutes(),
|
||||
redirect: (context, state) async {
|
||||
String checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final loggedIn = checkToken == 'Success';
|
||||
final goingToLogin = state.uri.toString() == RoutesConst.auth;
|
||||
|
||||
@ -58,8 +59,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
BlocProvider<SpaceTreeBloc>(
|
||||
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
|
||||
create: (context) => SpaceTreeBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
|
@ -11,7 +11,6 @@ import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -21,7 +20,10 @@ import 'package:syncrow_web/utils/theme/theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development');
|
||||
const environment = String.fromEnvironment(
|
||||
'FLAVOR',
|
||||
defaultValue: 'development',
|
||||
);
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
@ -39,7 +41,7 @@ class MyApp extends StatelessWidget {
|
||||
initialLocation: RoutesConst.auth,
|
||||
routes: AppRoutes.getRoutes(),
|
||||
redirect: (context, state) async {
|
||||
String checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final loggedIn = checkToken == 'Success';
|
||||
final goingToLogin = state.uri.toString() == RoutesConst.auth;
|
||||
|
||||
@ -57,7 +59,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
@ -65,7 +67,7 @@ class MyApp extends StatelessWidget {
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
BlocProvider<SpaceTreeBloc>(
|
||||
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
|
||||
create: (context) => SpaceTreeBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
|
@ -11,7 +11,6 @@ import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
|
||||
import 'package:syncrow_web/services/locator.dart';
|
||||
import 'package:syncrow_web/utils/app_routes.dart';
|
||||
@ -39,7 +38,7 @@ class MyApp extends StatelessWidget {
|
||||
initialLocation: RoutesConst.auth,
|
||||
routes: AppRoutes.getRoutes(),
|
||||
redirect: (context, state) async {
|
||||
String checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final checkToken = await AuthBloc.getTokenAndValidate();
|
||||
final loggedIn = checkToken == 'Success';
|
||||
final goingToLogin = state.uri.toString() == RoutesConst.auth;
|
||||
|
||||
@ -57,7 +56,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
@ -65,7 +64,7 @@ class MyApp extends StatelessWidget {
|
||||
create: (context) => RoutineBloc(),
|
||||
),
|
||||
BlocProvider<SpaceTreeBloc>(
|
||||
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
|
||||
create: (context) => SpaceTreeBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
|
@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/constants/routes_const.dart';
|
||||
|
||||
class BookingPage extends StatelessWidget {
|
||||
const BookingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.blueGrey[100],
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Side bar',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {
|
||||
context
|
||||
.go(RoutesConst.manageBookableSapcesPage);
|
||||
}),
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {})
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SvgTextButton extends StatelessWidget {
|
||||
final String svgAsset;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final Color backgroundColor;
|
||||
final Color svgColor;
|
||||
final Color labelColor;
|
||||
final double borderRadius;
|
||||
final List<BoxShadow> boxShadow;
|
||||
final double svgSize;
|
||||
|
||||
const SvgTextButton({
|
||||
super.key,
|
||||
required this.svgAsset,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||
this.svgColor = const Color(0xFF496EFF),
|
||||
this.labelColor = Colors.black87,
|
||||
this.borderRadius = 10.0,
|
||||
this.boxShadow = const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.textGray,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
this.svgSize = 24.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
boxShadow: boxShadow,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
svgAsset,
|
||||
width: svgSize,
|
||||
height: svgSize,
|
||||
color: svgColor,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: labelColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
class DummyBookableSpacesService implements BookableSpacesService {
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
BookableSpacesParams param) async {
|
||||
return PaginatedDataModel(
|
||||
data: [
|
||||
BookableSpacemodel(
|
||||
spaceName: 'space1',
|
||||
spaceConfig: BookableSpaceConfig(
|
||||
configUuid: 'uuid',
|
||||
bookableDays: ['wed', 'saturday'],
|
||||
availability: true,
|
||||
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
bookingStartTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
cost: 6,
|
||||
),
|
||||
spaceUuid: 'uuiiddd',
|
||||
spaceVirtualAddress: 'idvirtual1',
|
||||
),
|
||||
BookableSpacemodel(
|
||||
spaceName: 'space2',
|
||||
spaceConfig: BookableSpaceConfig(
|
||||
configUuid: 'uuid',
|
||||
bookableDays: ['wed', 'thur'],
|
||||
availability: true,
|
||||
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
bookingStartTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
cost: 6,
|
||||
),
|
||||
spaceUuid: 'uuiiddd',
|
||||
spaceVirtualAddress: 'idvirtual1',
|
||||
),
|
||||
BookableSpacemodel(
|
||||
spaceName: 'space3',
|
||||
spaceConfig: BookableSpaceConfig(
|
||||
configUuid: 'uuid',
|
||||
bookableDays: ['wed', 'fri', 'tues'],
|
||||
availability: true,
|
||||
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
bookingStartTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
cost: 6,
|
||||
),
|
||||
spaceUuid: 'uuiiddd',
|
||||
spaceVirtualAddress: 'idvirtual1',
|
||||
)
|
||||
],
|
||||
page: 1,
|
||||
size: 1,
|
||||
hasNext: false,
|
||||
totalItems: 3,
|
||||
totalPages: 1,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
class DummyNonNookableSpaces implements NonBookableSpacesService {
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params) {
|
||||
return Future.value(PaginatedDataModel<BookableSpacemodel>(
|
||||
data: [
|
||||
BookableSpacemodel(
|
||||
spaceName: 'space3',
|
||||
spaceConfig: BookableSpaceConfig(
|
||||
configUuid: 'uuid',
|
||||
bookableDays: ['wed', 'saturday'],
|
||||
availability: true,
|
||||
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
bookingStartTime: const TimeOfDay(hour: 6, minute: 20),
|
||||
cost: 6,
|
||||
),
|
||||
spaceUuid: 'uuiiddd',
|
||||
spaceVirtualAddress: 'idvirtual1',
|
||||
),
|
||||
BookableSpacemodel(
|
||||
spaceName: 'space3',
|
||||
spaceConfig: BookableSpaceConfig(
|
||||
configUuid: 'uuid',
|
||||
bookableDays: ['wed', 'saturday', 'thuresday'],
|
||||
availability: true,
|
||||
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
bookingStartTime: const TimeOfDay(hour: 5, minute: 20),
|
||||
cost: 5,
|
||||
),
|
||||
spaceUuid: 'uuiiddd',
|
||||
spaceVirtualAddress: 'idvirtual2',
|
||||
),
|
||||
BookableSpacemodel(
|
||||
spaceName: 'space3',
|
||||
spaceConfig: BookableSpaceConfig(
|
||||
configUuid: 'uuid',
|
||||
bookableDays: [
|
||||
'saturday',
|
||||
'sunday',
|
||||
'Monday',
|
||||
'tuesday',
|
||||
'wed',
|
||||
'thuresday'
|
||||
],
|
||||
availability: true,
|
||||
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
|
||||
bookingStartTime: const TimeOfDay(hour: 15, minute: 20),
|
||||
cost: 2,
|
||||
),
|
||||
spaceUuid: 'uuiiddd',
|
||||
spaceVirtualAddress: 'idvirtual3',
|
||||
)
|
||||
],
|
||||
page: 1,
|
||||
size: 1,
|
||||
hasNext: false,
|
||||
totalPages: 0,
|
||||
totalItems: 0,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams params) async {}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_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';
|
||||
|
||||
class RemoteBookableSpacesService implements BookableSpacesService {
|
||||
final HTTPService _httpService;
|
||||
RemoteBookableSpacesService(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load tags';
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
BookableSpacesParams param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
//TODO: you have to Chage this API call Path
|
||||
path: ApiEndpoints.listTags,
|
||||
//*************|********** */
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
return PaginatedDataModel.fromJson(
|
||||
result,
|
||||
BookableSpacemodel.fromJsonList,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_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';
|
||||
|
||||
class RemoteNonBookableSpaces implements NonBookableSpacesService {
|
||||
final HTTPService _httpService;
|
||||
RemoteNonBookableSpaces(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load Spaces';
|
||||
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
//TODO: you have to Chage this API call Path
|
||||
path: ApiEndpoints.listTags,
|
||||
//*************|********** */
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
return PaginatedDataModel.fromJson(
|
||||
result,
|
||||
BookableSpacemodel.fromJsonList,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams params) async {
|
||||
try {
|
||||
await _httpService.post(
|
||||
path: ApiEndpoints.addBookableSpaces,
|
||||
body: params.toJson(),
|
||||
expectedResponseModel: (p0) {},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BookableSpaceConfig {
|
||||
String configUuid;
|
||||
List<String> bookableDays;
|
||||
TimeOfDay bookingStartTime;
|
||||
TimeOfDay bookingEndTime;
|
||||
int cost;
|
||||
bool availability;
|
||||
BookableSpaceConfig({
|
||||
required this.configUuid,
|
||||
required this.availability,
|
||||
required this.bookableDays,
|
||||
required this.bookingEndTime,
|
||||
required this.bookingStartTime,
|
||||
required this.cost,
|
||||
});
|
||||
factory BookableSpaceConfig.zero() => BookableSpaceConfig(
|
||||
configUuid: '',
|
||||
bookableDays: [],
|
||||
availability: false,
|
||||
bookingEndTime: TimeOfDay.now(),
|
||||
bookingStartTime: TimeOfDay.now(),
|
||||
cost: -1,
|
||||
);
|
||||
factory BookableSpaceConfig.fromJson(Map<String, dynamic> json) =>
|
||||
BookableSpaceConfig(
|
||||
configUuid: json['uuid'] as String,
|
||||
bookableDays: json['daysAvailable'] as List<String>,
|
||||
availability: (json['active'] as bool?) ?? false,
|
||||
bookingEndTime: parseTimeOfDay(json['startTime'] as String),
|
||||
bookingStartTime: parseTimeOfDay(json['endTime'] as String),
|
||||
cost: json['points'] as int,
|
||||
);
|
||||
|
||||
static TimeOfDay parseTimeOfDay(String timeString) {
|
||||
final parts = timeString.split(':');
|
||||
final hour = int.parse(parts[0]);
|
||||
final minute = int.parse(parts[1]);
|
||||
return TimeOfDay(hour: hour, minute: minute);
|
||||
}
|
||||
|
||||
bool get isValid =>
|
||||
configUuid.isNotEmpty && bookableDays.isNotEmpty && cost > 0;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
|
||||
class BookableSpacemodel {
|
||||
String spaceUuid;
|
||||
String spaceName;
|
||||
BookableSpaceConfig spaceConfig;
|
||||
String spaceVirtualAddress;
|
||||
|
||||
BookableSpacemodel({
|
||||
required this.spaceUuid,
|
||||
required this.spaceName,
|
||||
required this.spaceConfig,
|
||||
required this.spaceVirtualAddress,
|
||||
});
|
||||
factory BookableSpacemodel.zero() => BookableSpacemodel(
|
||||
spaceUuid: '',
|
||||
spaceName: '',
|
||||
spaceConfig: BookableSpaceConfig.zero(),
|
||||
spaceVirtualAddress: '',
|
||||
);
|
||||
factory BookableSpacemodel.fromJson(Map<String, dynamic> json) =>
|
||||
BookableSpacemodel(
|
||||
spaceUuid: json['uuid'] as String,
|
||||
spaceName: json['spaceName'] as String,
|
||||
spaceConfig: BookableSpaceConfig.fromJson(
|
||||
json['bookableConfig'] as Map<String, dynamic>),
|
||||
spaceVirtualAddress: json['spaceVirtualAddress'] as String,
|
||||
);
|
||||
|
||||
static List<BookableSpacemodel> fromJsonList(List<dynamic> jsonList) =>
|
||||
jsonList
|
||||
.map(
|
||||
(e) => BookableSpacemodel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get isValid =>
|
||||
spaceUuid.isNotEmpty &&
|
||||
spaceName.isNotEmpty &&
|
||||
spaceVirtualAddress.isNotEmpty &&
|
||||
spaceConfig.isValid;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
class BookableSpacesParams {
|
||||
int currentPage;
|
||||
BookableSpacesParams({
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'page': currentPage,
|
||||
};
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
class NonBookableSpacesParams {
|
||||
int currentPage;
|
||||
String? searchedWords;
|
||||
NonBookableSpacesParams({
|
||||
required this.currentPage,
|
||||
this.searchedWords,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'page': currentPage,
|
||||
};
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
abstract class BookableSpacesService {
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
BookableSpacesParams param);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
abstract class NonBookableSpacesService {
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params);
|
||||
Future<void> sendBookableSpacesToApi(SendBookableSpacesToApiParams params);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/utils/string_utils.dart';
|
||||
|
||||
class SendBookableSpacesToApiParams {
|
||||
List<String> spaceUuids;
|
||||
List<String> daysAvailable;
|
||||
String startTime;
|
||||
String endTime;
|
||||
int points;
|
||||
SendBookableSpacesToApiParams({
|
||||
required this.spaceUuids,
|
||||
required this.daysAvailable,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.points,
|
||||
});
|
||||
|
||||
static SendBookableSpacesToApiParams fromBookableSpacesModel(
|
||||
List<BookableSpacemodel> bookableSpaces) {
|
||||
return SendBookableSpacesToApiParams(
|
||||
spaceUuids: bookableSpaces.map((space) => space.spaceUuid).toList(),
|
||||
daysAvailable: bookableSpaces
|
||||
.expand((space) => space.spaceConfig.bookableDays)
|
||||
.toSet()
|
||||
.toList(),
|
||||
startTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpaces.first.spaceConfig.bookingStartTime),
|
||||
endTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpaces.first.spaceConfig.bookingEndTime),
|
||||
points: bookableSpaces.first.spaceConfig.cost,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'spaceUuids': spaceUuids,
|
||||
'daysAvailable': daysAvailable,
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
'points': points
|
||||
};
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'bookable_spaces_event.dart';
|
||||
part 'bookable_spaces_state.dart';
|
||||
|
||||
class BookableSpacesBloc
|
||||
extends Bloc<BookableSpacesEvent, BookableSpacesState> {
|
||||
final BookableSpacesService bookableSpacesService;
|
||||
BookableSpacesBloc(this.bookableSpacesService)
|
||||
: super(BookableSpacesInitial()) {
|
||||
on<LoadBookableSpacesEvent>(_onLoadBookableSpaces);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBookableSpaces(
|
||||
LoadBookableSpacesEvent event, Emitter<BookableSpacesState> emit) async {
|
||||
emit(BookableSpacesLoading());
|
||||
try {
|
||||
final bookableSpaces = await bookableSpacesService.load(event.params);
|
||||
emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces));
|
||||
} on APIException catch (e) {
|
||||
emit(BookableSpacesError(error: e.message));
|
||||
} catch (e) {
|
||||
emit(
|
||||
BookableSpacesError(error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
part of 'bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class BookableSpacesEvent extends Equatable {
|
||||
const BookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadBookableSpacesEvent extends BookableSpacesEvent {
|
||||
final BookableSpacesParams params;
|
||||
const LoadBookableSpacesEvent(this.params);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
part of 'bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class BookableSpacesState extends Equatable {
|
||||
const BookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class BookableSpacesInitial extends BookableSpacesState {}
|
||||
|
||||
final class BookableSpacesLoading extends BookableSpacesState {}
|
||||
|
||||
final class BookableSpacesLoaded extends BookableSpacesState {
|
||||
final PaginatedDataModel<BookableSpacemodel> bookableSpacesList;
|
||||
const BookableSpacesLoaded({
|
||||
required this.bookableSpacesList,
|
||||
});
|
||||
}
|
||||
|
||||
final class BookableSpacesError extends BookableSpacesState {
|
||||
final String error;
|
||||
const BookableSpacesError({
|
||||
required this.error,
|
||||
});
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
part 'non_bookaable_spaces_event.dart';
|
||||
part 'non_bookaable_spaces_state.dart';
|
||||
|
||||
class NonBookableSpacesBloc
|
||||
extends Bloc<NonBookableSpacesEvent, NonBookableSpacesState> {
|
||||
NonBookableSpacesService nonBookableSpacesService;
|
||||
List<BookableSpacemodel> selectedBookableSpaces = [];
|
||||
NonBookableSpacesBloc(this.nonBookableSpacesService)
|
||||
: super(NonBookableSpacesInitial()) {
|
||||
on<LoadUnBookableSpacesEvent>(_onLoadUnBookableSpacesEvent);
|
||||
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
|
||||
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
|
||||
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
|
||||
}
|
||||
|
||||
TimeOfDay get endTime =>
|
||||
selectedBookableSpaces.first.spaceConfig.bookingEndTime;
|
||||
|
||||
TimeOfDay get startTime =>
|
||||
selectedBookableSpaces.first.spaceConfig.bookingStartTime;
|
||||
Future<void> _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event,
|
||||
Emitter<NonBookableSpacesState> emit) async {
|
||||
emit(NonBookableSpacesLoading());
|
||||
try {
|
||||
final nonBookableSpacesList = await nonBookableSpacesService.load(
|
||||
event.nonBookableSpacesParams,
|
||||
);
|
||||
emit(
|
||||
NonBookableSpacesLoaded(nonBookableSpaces: nonBookableSpacesList),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
NonBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddToBookableSpaceEvent(
|
||||
AddToBookableSpaceEvent event,
|
||||
Emitter<NonBookableSpacesState> emit,
|
||||
) {
|
||||
if (state is NonBookableSpacesLoaded) {
|
||||
final currentState = state as NonBookableSpacesLoaded;
|
||||
|
||||
final updatedSelectedSpaces =
|
||||
List<BookableSpacemodel>.from(currentState.selectedBookableSpaces)
|
||||
..add(event.nonBookableSpace);
|
||||
|
||||
selectedBookableSpaces.add(event.nonBookableSpace);
|
||||
|
||||
emit(
|
||||
NonBookableSpacesLoaded(
|
||||
nonBookableSpaces: currentState.nonBookableSpaces,
|
||||
selectedBookableSpaces: updatedSelectedSpaces,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event,
|
||||
Emitter<NonBookableSpacesState> emit) {
|
||||
if (state is NonBookableSpacesLoaded) {
|
||||
final currentState = state as NonBookableSpacesLoaded;
|
||||
currentState.selectedBookableSpaces.remove(event.bookableSpace);
|
||||
selectedBookableSpaces.remove(event.bookableSpace);
|
||||
emit(
|
||||
NonBookableSpacesLoaded(
|
||||
nonBookableSpaces: currentState.nonBookableSpaces,
|
||||
selectedBookableSpaces: currentState.selectedBookableSpaces,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSendBookableSpacesToApi(SendBookableSpacesToApi event,
|
||||
Emitter<NonBookableSpacesState> emit) async {
|
||||
emit(NonBookableSpacesLoading());
|
||||
try {
|
||||
await nonBookableSpacesService.sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams.fromBookableSpacesModel(
|
||||
selectedBookableSpaces,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
NonBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'non_bookaable_spaces_bloc.dart';
|
||||
|
||||
sealed class NonBookableSpacesEvent extends Equatable {
|
||||
const NonBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent {
|
||||
final NonBookableSpacesParams nonBookableSpacesParams;
|
||||
const LoadUnBookableSpacesEvent({
|
||||
required this.nonBookableSpacesParams,
|
||||
});
|
||||
}
|
||||
|
||||
class AddToBookableSpaceEvent extends NonBookableSpacesEvent {
|
||||
final BookableSpacemodel nonBookableSpace;
|
||||
const AddToBookableSpaceEvent({
|
||||
required this.nonBookableSpace,
|
||||
});
|
||||
}
|
||||
|
||||
class RemoveFromBookableSpaceEvent extends NonBookableSpacesEvent {
|
||||
final BookableSpacemodel bookableSpace;
|
||||
const RemoveFromBookableSpaceEvent({
|
||||
required this.bookableSpace,
|
||||
});
|
||||
}
|
||||
|
||||
class SendBookableSpacesToApi extends NonBookableSpacesEvent {}
|
@ -0,0 +1,26 @@
|
||||
part of 'non_bookaable_spaces_bloc.dart';
|
||||
|
||||
sealed class NonBookableSpacesState extends Equatable {
|
||||
const NonBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class NonBookableSpacesInitial extends NonBookableSpacesState {}
|
||||
|
||||
class NonBookableSpacesLoading extends NonBookableSpacesState {}
|
||||
|
||||
class NonBookableSpacesLoaded extends NonBookableSpacesState {
|
||||
final PaginatedDataModel<BookableSpacemodel> nonBookableSpaces;
|
||||
final List<BookableSpacemodel> selectedBookableSpaces;
|
||||
const NonBookableSpacesLoaded({
|
||||
required this.nonBookableSpaces,
|
||||
this.selectedBookableSpaces = const [],
|
||||
});
|
||||
}
|
||||
|
||||
class NonBookableSpacesError extends NonBookableSpacesState {
|
||||
final String error;
|
||||
const NonBookableSpacesError(this.error);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'steps_state.dart';
|
||||
|
||||
class StepsCubit extends Cubit<StepsState> {
|
||||
StepsCubit() : super(StepsInitial());
|
||||
|
||||
void initDialogValue() {
|
||||
emit(StepOneState());
|
||||
}
|
||||
|
||||
void goToNextStep() {
|
||||
if (state is StepOneState) {
|
||||
emit(StepTwoState());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
part of 'steps_cubit.dart';
|
||||
|
||||
sealed class StepsState extends Equatable {
|
||||
const StepsState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class StepsInitial extends StepsState {}
|
||||
|
||||
final class StepOneState extends StepsState {}
|
||||
|
||||
final class StepTwoState extends StepsState {}
|
||||
|
||||
final class StepEditMode extends StepsState {}
|
@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/dummy_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class ManageBookableSpacesPage extends StatefulWidget {
|
||||
const ManageBookableSpacesPage({super.key});
|
||||
|
||||
@override
|
||||
State<ManageBookableSpacesPage> createState() =>
|
||||
_ManageBookableSpacesPageState();
|
||||
}
|
||||
|
||||
class _ManageBookableSpacesPageState extends State<ManageBookableSpacesPage> {
|
||||
final PageController _pageController = PageController(initialPage: 1);
|
||||
int _currentPageIndex = 1;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebScaffold(
|
||||
enableMenuSidebar: false,
|
||||
appBarTitle: Text(
|
||||
'Access Management',
|
||||
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||
),
|
||||
centerBody: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _switchPage(0),
|
||||
child: Text(
|
||||
'Access Overview',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
|
||||
fontWeight:
|
||||
_currentPageIndex == 0 ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _switchPage(1),
|
||||
child: Text(
|
||||
'Booking System',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
|
||||
fontWeight:
|
||||
_currentPageIndex == 1 ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
scaffoldBody: BlocProvider(
|
||||
create: (context) => BookableSpacesBloc(
|
||||
DummyBookableSpacesService(),
|
||||
// RemoteBookableSpacesService(HTTPService()),
|
||||
)..add(LoadBookableSpacesEvent(
|
||||
BookableSpacesParams(currentPage: 1),
|
||||
)),
|
||||
child: const ManageBookableSpacesWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _switchPage(int index) {
|
||||
setState(() => _currentPageIndex = index);
|
||||
_pageController.jumpToPage(index);
|
||||
}
|
||||
}
|
||||
|
||||
class ManageBookableSpacesWidget extends StatelessWidget {
|
||||
const ManageBookableSpacesWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.backButtonIcon,
|
||||
label: 'Booking Home',
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
}),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.backButtonIcon,
|
||||
label: 'Set Up a Bookable Spaces',
|
||||
onPressed: () async => showDialog(
|
||||
context: context,
|
||||
builder: (context) => SetupBookableSpacesDialog(),
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
Expanded(
|
||||
flex: 9,
|
||||
child: BlocBuilder<BookableSpacesBloc, BookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
if (state is BookableSpacesLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
} else if (state is BookableSpacesError) {
|
||||
return Text(state.error);
|
||||
} else if (state is BookableSpacesLoaded) {
|
||||
return CustomDataTable<BookableSpacemodel>(
|
||||
items: state.bookableSpacesList.data,
|
||||
cellsWidgets: (space) => [
|
||||
DataCell(
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
child: Text(space.spaceName)),
|
||||
),
|
||||
DataCell(Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
child: Text(space.spaceVirtualAddress))),
|
||||
DataCell(SizedBox(
|
||||
width: 200,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
children: space.spaceConfig.bookableDays
|
||||
.map((day) => Text(
|
||||
day,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
)),
|
||||
DataCell(
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
child: Text(
|
||||
space.spaceConfig.bookingStartTime.format(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
child: Text(
|
||||
space.spaceConfig.bookingEndTime.format(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
child: Text('${space.spaceConfig.cost} Points'))),
|
||||
DataCell(Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.7,
|
||||
child: Switch(
|
||||
value: space.spaceConfig.availability,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
),
|
||||
)),
|
||||
DataCell(Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: SvgPicture.asset(Assets.settings),
|
||||
),
|
||||
)),
|
||||
],
|
||||
columnsTitles: const [
|
||||
'space',
|
||||
'space Virtual Address',
|
||||
'Bookable Days',
|
||||
'Booking Start Time',
|
||||
'Booking End Time',
|
||||
'Cost',
|
||||
'Availability',
|
||||
'Settings',
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/dummy_non_nookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SetupBookableSpacesDialog extends StatelessWidget {
|
||||
final TextEditingController pointsController = TextEditingController();
|
||||
SetupBookableSpacesDialog({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<StepsCubit>(
|
||||
create: (context) => StepsCubit()..initDialogValue(),
|
||||
),
|
||||
BlocProvider<NonBookableSpacesBloc>(
|
||||
create: (context) => NonBookableSpacesBloc(
|
||||
DummyNonNookableSpaces(),
|
||||
)..add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams:
|
||||
NonBookableSpacesParams(currentPage: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'Set Up a Bookable Spaces',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Divider(),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: StepperPartWidget(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 588,
|
||||
child: VerticalDivider(
|
||||
thickness: 0.5,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: DetailsStepsWidget(
|
||||
pointsController: pointsController,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
return ButtonsDividerBottomDialogWidget(
|
||||
onNextPressed: () {
|
||||
final stepsState = context.read<StepsCubit>().state;
|
||||
final selectedSpaces = context
|
||||
.read<NonBookableSpacesBloc>()
|
||||
.selectedBookableSpaces;
|
||||
if (stepsState is StepOneState) {
|
||||
if (selectedSpaces.isNotEmpty) {
|
||||
context.read<StepsCubit>().goToNextStep();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select at least one space.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (stepsState is StepTwoState) {
|
||||
selectedSpaces.forEach(
|
||||
(e) => e.spaceConfig.cost = int.parse(
|
||||
pointsController.text.isEmpty
|
||||
? '0'
|
||||
: pointsController.text),
|
||||
);
|
||||
if (selectedSpaces.any(
|
||||
(element) => !element.isValid,
|
||||
)) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please fill the required fields.'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print(selectedSpaces.first.spaceUuid);
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancelPressed: () => context.pop(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailsStepsWidget extends StatelessWidget {
|
||||
final TextEditingController pointsController;
|
||||
const DetailsStepsWidget({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
child: BlocBuilder<StepsCubit, StepsState>(
|
||||
builder: (context, state) {
|
||||
if (state is StepOneState) {
|
||||
return SpacesStepDetailsWidget();
|
||||
} else if (state is StepTwoState) {
|
||||
return StepTwoDetailsWidget(
|
||||
pointsController: pointsController,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ButtonsDividerBottomDialogWidget extends StatelessWidget {
|
||||
final void Function() onNextPressed;
|
||||
final void Function() onCancelPressed;
|
||||
const ButtonsDividerBottomDialogWidget({
|
||||
super.key,
|
||||
required this.onNextPressed,
|
||||
required this.onCancelPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(
|
||||
thickness: 0.5,
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
onTap: onCancelPressed,
|
||||
child: Container(
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: ColorsManager.grayBorder),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<StepsCubit, StepsState>(
|
||||
builder: (context, state) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
onTap: onNextPressed,
|
||||
child: Container(
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
state is StepOneState ? 'Next' : 'Save',
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ColumnTitleWidget extends StatelessWidget {
|
||||
final bool isFirst;
|
||||
final bool isLast;
|
||||
final String title;
|
||||
const ColumnTitleWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.isFirst,
|
||||
required this.isLast,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.graysColor,
|
||||
borderRadius: isFirst
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
)
|
||||
: isLast
|
||||
? const BorderRadius.only(
|
||||
topRight: Radius.circular(12),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(color: ColorsManager.grayColor),
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class CustomDataTable<T> extends StatelessWidget {
|
||||
final List<String> columnsTitles;
|
||||
final List<DataCell> Function(T item) cellsWidgets;
|
||||
final List<T> items;
|
||||
|
||||
const CustomDataTable({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.cellsWidgets,
|
||||
required this.columnsTitles,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DataTable2(
|
||||
dividerThickness: 0.5,
|
||||
columnSpacing: 2,
|
||||
horizontalMargin: 0,
|
||||
empty: SvgPicture.asset(Assets.emptyDataTable),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.textGray,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: columnsTitles.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final title = entry.value;
|
||||
|
||||
return DataColumn(
|
||||
label: ColumnTitleWidget(
|
||||
title: title,
|
||||
isFirst: index == 0,
|
||||
isLast: index == columnsTitles.length - 1,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
rows: items.map((item) {
|
||||
return DataRow(cells: cellsWidgets(item));
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SearchUnbookableSpacesWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget? suffix;
|
||||
final double? height;
|
||||
final double? width;
|
||||
final TextEditingController? controller;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final void Function(String)? onChanged;
|
||||
const SearchUnbookableSpacesWidget({
|
||||
required this.title,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.suffix,
|
||||
this.height,
|
||||
this.width,
|
||||
this.inputFormatters,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width ?? 480,
|
||||
height: height ?? 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x26000000),
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
inputFormatters: inputFormatters,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
|
||||
hintText: title,
|
||||
hintStyle: const TextStyle(color: Colors.grey),
|
||||
border: InputBorder.none,
|
||||
suffixIcon:
|
||||
suffix ?? const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpacesStepDetailsWidget extends StatelessWidget {
|
||||
SpacesStepDetailsWidget({
|
||||
super.key,
|
||||
});
|
||||
Timer? _debounce;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NonBookableSpacesBloc, NonBookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
if (state is NonBookableSpacesLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is NonBookableSpacesError) {
|
||||
return Text(state.error);
|
||||
} else if (state is NonBookableSpacesLoaded) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Select Space',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Container(
|
||||
width: 450,
|
||||
height: 480,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x40000000),
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 520,
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 15, horizontal: 20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF8F8F8),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: SearchUnbookableSpacesWidget(
|
||||
title: 'Search',
|
||||
onChanged: (p0) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce =
|
||||
Timer(const Duration(milliseconds: 500), () {
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams:
|
||||
NonBookableSpacesParams(
|
||||
currentPage: 1,
|
||||
searchedWords: p0,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 490,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.only(top: 10, left: 20, bottom: 5),
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
itemCount: state.nonBookableSpaces.data.length,
|
||||
itemBuilder: (context, index) => CheckBoxSpaceWidget(
|
||||
nonBookableSpace:
|
||||
state.nonBookableSpaces.data[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CheckBoxSpaceWidget extends StatefulWidget {
|
||||
final BookableSpacemodel nonBookableSpace;
|
||||
|
||||
const CheckBoxSpaceWidget({
|
||||
super.key,
|
||||
required this.nonBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CheckBoxSpaceWidget> createState() => _CheckBoxSpaceWidgetState();
|
||||
}
|
||||
|
||||
class _CheckBoxSpaceWidgetState extends State<CheckBoxSpaceWidget> {
|
||||
bool isChecked = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: isChecked,
|
||||
onChanged: (value) => setState(() {
|
||||
isChecked = value ?? false;
|
||||
if (isChecked) {
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
AddToBookableSpaceEvent(
|
||||
nonBookableSpace: widget.nonBookableSpace,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
RemoveFromBookableSpaceEvent(
|
||||
bookableSpace: widget.nonBookableSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Text(widget.nonBookableSpace.spaceName),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/string_utils.dart';
|
||||
|
||||
class StepTwoDetailsWidget extends StatelessWidget {
|
||||
final TextEditingController pointsController;
|
||||
const StepTwoDetailsWidget({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 450,
|
||||
height: 480,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const WeekDaysCheckboxRow(),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
TitleAndTimePickerWidget(
|
||||
title: 'Booking Start Time',
|
||||
onTimePicked: (timePicked) {
|
||||
if (timePicked == null) {
|
||||
return;
|
||||
}
|
||||
final nonBookableBloc = context.read<NonBookableSpacesBloc>();
|
||||
if (isEndTimeAfterStartTime(
|
||||
timePicked, nonBookableBloc.endTime)) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content:
|
||||
Text("You can't choose start Time Before End time"),
|
||||
duration: Duration(seconds: 2),
|
||||
backgroundColor: ColorsManager.red,
|
||||
));
|
||||
throw Exception();
|
||||
} else {
|
||||
nonBookableBloc.selectedBookableSpaces.forEach(
|
||||
(e) => e.spaceConfig.bookingStartTime = timePicked,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
TitleAndTimePickerWidget(
|
||||
title: 'Booking End Time',
|
||||
onTimePicked: (timePicked) {
|
||||
if (timePicked == null) {
|
||||
return;
|
||||
}
|
||||
final nonBookableBloc = context.read<NonBookableSpacesBloc>();
|
||||
if (isEndTimeAfterStartTime(
|
||||
nonBookableBloc.startTime, timePicked)) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content:
|
||||
Text("You can't choose End Time After Start time"),
|
||||
duration: Duration(seconds: 2),
|
||||
backgroundColor: ColorsManager.red,
|
||||
));
|
||||
throw Exception();
|
||||
} else {
|
||||
nonBookableBloc.selectedBookableSpaces.forEach(
|
||||
(e) => e.spaceConfig.bookingEndTime = timePicked,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'* ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.red),
|
||||
),
|
||||
const Text('Points/hrs'),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
SearchUnbookableSpacesWidget(
|
||||
title: 'Ex: 0',
|
||||
controller: pointsController,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
suffix: const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TitleAndTimePickerWidget extends StatelessWidget {
|
||||
final void Function(TimeOfDay? timePicked) onTimePicked;
|
||||
final String title;
|
||||
const TitleAndTimePickerWidget({
|
||||
super.key,
|
||||
required this.onTimePicked,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'* ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.red),
|
||||
),
|
||||
Text(title),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
TimePickerWidget(
|
||||
onTimePicked: onTimePicked,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class StepperPartWidget extends StatelessWidget {
|
||||
const StepperPartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsetsGeometry.only(left: 20),
|
||||
child: BlocBuilder<StepsCubit, StepsState>(
|
||||
builder: (context, state) {
|
||||
if (state is StepOneState) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Space',
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 50,
|
||||
child: const VerticalDivider(
|
||||
width: 8,
|
||||
)),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Settings',
|
||||
titleColor: ColorsManager.softGray,
|
||||
circleColor: ColorsManager.softGray,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else if (state is StepTwoState) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Space',
|
||||
titleColor: ColorsManager.softGray,
|
||||
cicleIcon: Icon(
|
||||
Icons.check,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 12,
|
||||
),
|
||||
circleColor: ColorsManager.trueIconGreen,
|
||||
radius: 3,
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 50,
|
||||
child: const VerticalDivider(
|
||||
width: 8,
|
||||
)),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Settings',
|
||||
)
|
||||
],
|
||||
);
|
||||
} else if (state is StepEditMode) {
|
||||
return const CircleTitleStepperWidget(
|
||||
title: 'Settings',
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CircleTitleStepperWidget extends StatelessWidget {
|
||||
final double? radius;
|
||||
final Widget? cicleIcon;
|
||||
final Color? circleColor;
|
||||
final Color? titleColor;
|
||||
final String title;
|
||||
const CircleTitleStepperWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.circleColor,
|
||||
this.cicleIcon,
|
||||
this.titleColor,
|
||||
this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
minRadius: radius ?? 5,
|
||||
backgroundColor: circleColor ?? ColorsManager.blue1,
|
||||
child: cicleIcon,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: titleColor ?? ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
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';
|
||||
|
||||
class TimePickerWidget extends StatefulWidget {
|
||||
const TimePickerWidget({
|
||||
super.key,
|
||||
required this.onTimePicked,
|
||||
});
|
||||
|
||||
final void Function(TimeOfDay? timePicked) onTimePicked;
|
||||
@override
|
||||
State<TimePickerWidget> createState() => _TimePickerWidgetState();
|
||||
}
|
||||
|
||||
class _TimePickerWidgetState extends State<TimePickerWidget> {
|
||||
TimeOfDay? timePicked;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () async {
|
||||
final tempTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: ColorsManager.primaryColor,
|
||||
onSurface: Colors.black,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
widget.onTimePicked(tempTime);
|
||||
timePicked = tempTime;
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: SvgPicture.asset(Assets.clockIcon),
|
||||
),
|
||||
Container(
|
||||
width: 120,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
timePicked == null
|
||||
? TimeOfDay.now().format(context)
|
||||
: timePicked!.format(context),
|
||||
style: const TextStyle(color: Color(0xB2D5D5D5)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
|
||||
class WeekDaysCheckboxRow extends StatefulWidget {
|
||||
const WeekDaysCheckboxRow({super.key});
|
||||
|
||||
@override
|
||||
State<WeekDaysCheckboxRow> createState() => _WeekDaysCheckboxRowState();
|
||||
}
|
||||
|
||||
class _WeekDaysCheckboxRowState extends State<WeekDaysCheckboxRow> {
|
||||
final Map<String, bool> _daysChecked = {
|
||||
'Mon': false,
|
||||
'Tue': false,
|
||||
'Wed': false,
|
||||
'Thu': false,
|
||||
'Fri': false,
|
||||
'Sat': false,
|
||||
'Sun': false,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _daysChecked.entries.map((entry) {
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Checkbox(
|
||||
value: entry.value,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_daysChecked[entry.key] = newValue ?? false;
|
||||
final selectedDays = _daysChecked.entries
|
||||
.where((e) => e.value)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
|
||||
for (var space in context
|
||||
.read<NonBookableSpacesBloc>()
|
||||
.selectedBookableSpaces) {
|
||||
space.spaceConfig.bookableDays = selectedDays;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,302 +2,86 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
|
||||
import 'package:syncrow_web/pages/common/custom_table.dart';
|
||||
import 'package:syncrow_web/pages/common/date_time_widget.dart';
|
||||
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
|
||||
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
|
||||
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
|
||||
// import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/app_enum.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout {
|
||||
class AccessManagementPage extends StatefulWidget {
|
||||
const AccessManagementPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = isLargeScreenSize(context);
|
||||
final isSmallScreen = isSmallScreenSize(context);
|
||||
final isHalfMediumScreen = isHafMediumScreenSize(context);
|
||||
final padding =
|
||||
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
|
||||
State<AccessManagementPage> createState() => _AccessManagementPageState();
|
||||
}
|
||||
|
||||
return WebScaffold(
|
||||
class _AccessManagementPageState extends State<AccessManagementPage>
|
||||
with HelperResponsiveLayout {
|
||||
final PageController _pageController = PageController(initialPage: 0);
|
||||
int _currentPageIndex = 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
|
||||
child: WebScaffold(
|
||||
enableMenuSidebar: false,
|
||||
appBarTitle: Text(
|
||||
'Access Management',
|
||||
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||
),
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
scaffoldBody: BlocProvider(
|
||||
create: (BuildContext context) =>
|
||||
AccessBloc()..add(FetchTableData()),
|
||||
child: BlocConsumer<AccessBloc, AccessState>(
|
||||
listener: (context, state) {},
|
||||
builder: (context, state) {
|
||||
final accessBloc = BlocProvider.of<AccessBloc>(context);
|
||||
final filteredData = accessBloc.filteredData;
|
||||
return state is AccessLoaded
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Container(
|
||||
padding: padding,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FilterWidget(
|
||||
size: MediaQuery.of(context).size,
|
||||
tabs: accessBloc.tabs,
|
||||
selectedIndex: accessBloc.selectedIndex,
|
||||
onTabChanged: (index) {
|
||||
accessBloc.add(TabChangedEvent(index));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (isSmallScreen || isHalfMediumScreen)
|
||||
_buildSmallSearchFilters(context, accessBloc)
|
||||
else
|
||||
_buildNormalSearchWidgets(context, accessBloc),
|
||||
const SizedBox(height: 20),
|
||||
_buildVisitorAdminPasswords(context, accessBloc),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: DynamicTable(
|
||||
tableName: 'AccessManagement',
|
||||
uuidIndex: 1,
|
||||
withSelectAll: true,
|
||||
isEmpty: filteredData.isEmpty,
|
||||
withCheckBox: false,
|
||||
size: MediaQuery.of(context).size,
|
||||
cellDecoration: containerDecoration,
|
||||
headers: const [
|
||||
'Name',
|
||||
'Access Type',
|
||||
'Access Start',
|
||||
'Access End',
|
||||
'Accessible Device',
|
||||
'Authorizer',
|
||||
'Authorization Date & Time',
|
||||
'Access Status'
|
||||
],
|
||||
data: filteredData.map((item) {
|
||||
return [
|
||||
item.passwordName,
|
||||
item.passwordType.value,
|
||||
accessBloc
|
||||
.timestampToDate(item.effectiveTime),
|
||||
accessBloc
|
||||
.timestampToDate(item.invalidTime),
|
||||
item.deviceName.toString(),
|
||||
item.authorizerEmail.toString(),
|
||||
accessBloc
|
||||
.timestampToDate(item.invalidTime),
|
||||
item.passwordStatus.value,
|
||||
];
|
||||
}).toList(),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
})));
|
||||
}
|
||||
|
||||
Wrap _buildVisitorAdminPasswords(
|
||||
BuildContext context, AccessBloc accessBloc) {
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
Container(
|
||||
width: 205,
|
||||
height: 42,
|
||||
decoration: containerDecoration,
|
||||
child: DefaultButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return const VisitorPasswordDialog();
|
||||
},
|
||||
).then((v) {
|
||||
if (v != null) {
|
||||
accessBloc.add(FetchTableData());
|
||||
}
|
||||
});
|
||||
},
|
||||
borderRadius: 8,
|
||||
centerBody: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _switchPage(0),
|
||||
child: Text(
|
||||
'Create Visitor Password ',
|
||||
style: context.textTheme.titleSmall!
|
||||
.copyWith(color: Colors.white, fontSize: 12),
|
||||
)),
|
||||
'Access Overview',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
|
||||
fontWeight: _currentPageIndex == 0
|
||||
? FontWeight.w700
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _switchPage(1),
|
||||
child: Text(
|
||||
'Booking System',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
|
||||
fontWeight: _currentPageIndex == 1
|
||||
? FontWeight.w700
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Container(
|
||||
// width: 133,
|
||||
// height: 42,
|
||||
// decoration: containerDecoration,
|
||||
// child: DefaultButton(
|
||||
// borderRadius: 8,
|
||||
// backgroundColor: ColorsManager.whiteColors,
|
||||
// child: Text(
|
||||
// 'Admin Password',
|
||||
// style: context.textTheme.titleSmall!
|
||||
// .copyWith(color: Colors.black, fontSize: 12),
|
||||
// )),
|
||||
// ),
|
||||
],
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
scaffoldBody: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
AccessOverviewContent(),
|
||||
BookingPage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
|
||||
// TimeOfDay _selectedTime = TimeOfDay.now();
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: CustomWebTextField(
|
||||
controller: accessBloc.passwordName,
|
||||
height: 43,
|
||||
isRequired: false,
|
||||
textFieldName: 'Name',
|
||||
description: '',
|
||||
onSubmitted: (value) {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer:
|
||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: CustomWebTextField(
|
||||
controller: accessBloc.emailAuthorizer,
|
||||
height: 43,
|
||||
isRequired: false,
|
||||
textFieldName: 'Authorizer',
|
||||
description: '',
|
||||
onSubmitted: (value) {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer:
|
||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(
|
||||
child: DateTimeWebWidget(
|
||||
icon: Assets.calendarIcon,
|
||||
isRequired: false,
|
||||
title: 'Access Time',
|
||||
size: MediaQuery.of(context).size,
|
||||
endTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||
},
|
||||
startTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||
},
|
||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
SearchResetButtons(
|
||||
onSearch: () {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
onReset: () {
|
||||
accessBloc.add(ResetSearch());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
|
||||
return Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: CustomWebTextField(
|
||||
controller: accessBloc.passwordName,
|
||||
isRequired: true,
|
||||
height: 40,
|
||||
textFieldName: 'Name',
|
||||
description: '',
|
||||
onSubmitted: (value) {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer:
|
||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
}),
|
||||
),
|
||||
DateTimeWebWidget(
|
||||
icon: Assets.calendarIcon,
|
||||
isRequired: false,
|
||||
title: 'Access Time',
|
||||
size: MediaQuery.of(context).size,
|
||||
endTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||
},
|
||||
startTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||
},
|
||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||
),
|
||||
SearchResetButtons(
|
||||
onSearch: () {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
onReset: () {
|
||||
accessBloc.add(ResetSearch());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
void _switchPage(int index) {
|
||||
setState(() => _currentPageIndex = index);
|
||||
_pageController.jumpToPage(index);
|
||||
}
|
||||
}
|
||||
|
289
lib/pages/access_management/view/access_overview_content.dart
Normal file
@ -0,0 +1,289 @@
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
|
||||
import 'package:syncrow_web/pages/common/custom_table.dart';
|
||||
import 'package:syncrow_web/pages/common/date_time_widget.dart';
|
||||
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
|
||||
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
|
||||
import 'package:syncrow_web/utils/constants/app_enum.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AccessOverviewContent extends StatelessWidget
|
||||
with HelperResponsiveLayout {
|
||||
const AccessOverviewContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = isLargeScreenSize(context);
|
||||
final isSmallScreen = isSmallScreenSize(context);
|
||||
final isHalfMediumScreen = isHafMediumScreenSize(context);
|
||||
final padding =
|
||||
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
|
||||
|
||||
return BlocProvider(
|
||||
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
|
||||
child: BlocConsumer<AccessBloc, AccessState>(
|
||||
listener: (context, state) {},
|
||||
builder: (context, state) {
|
||||
final accessBloc = BlocProvider.of<AccessBloc>(context);
|
||||
final filteredData = accessBloc.filteredData;
|
||||
return state is AccessLoaded
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Container(
|
||||
padding: padding,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FilterWidget(
|
||||
size: MediaQuery.of(context).size,
|
||||
tabs: accessBloc.tabs,
|
||||
selectedIndex: accessBloc.selectedIndex,
|
||||
onTabChanged: (index) {
|
||||
accessBloc.add(TabChangedEvent(index));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (isSmallScreen || isHalfMediumScreen)
|
||||
_buildSmallSearchFilters(context, accessBloc)
|
||||
else
|
||||
_buildNormalSearchWidgets(context, accessBloc),
|
||||
const SizedBox(height: 20),
|
||||
_buildVisitorAdminPasswords(context, accessBloc),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: DynamicTable(
|
||||
tableName: 'AccessManagement',
|
||||
uuidIndex: 1,
|
||||
withSelectAll: true,
|
||||
isEmpty: filteredData.isEmpty,
|
||||
withCheckBox: false,
|
||||
size: MediaQuery.of(context).size,
|
||||
cellDecoration: containerDecoration,
|
||||
headers: const [
|
||||
'Name',
|
||||
'Access Type',
|
||||
'Access Start',
|
||||
'Access End',
|
||||
'Accessible Device',
|
||||
'Authorizer',
|
||||
'Authorization Date & Time',
|
||||
'Access Status'
|
||||
],
|
||||
data: filteredData.map((item) {
|
||||
return [
|
||||
item.passwordName,
|
||||
item.passwordType.value,
|
||||
accessBloc.timestampToDate(item.effectiveTime),
|
||||
accessBloc.timestampToDate(item.invalidTime),
|
||||
item.deviceName.toString(),
|
||||
item.authorizerEmail.toString(),
|
||||
accessBloc.timestampToDate(item.invalidTime),
|
||||
item.passwordStatus.value,
|
||||
];
|
||||
}).toList(),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
Wrap _buildVisitorAdminPasswords(
|
||||
BuildContext context, AccessBloc accessBloc) {
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
Container(
|
||||
width: 205,
|
||||
height: 42,
|
||||
decoration: containerDecoration,
|
||||
child: DefaultButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return const VisitorPasswordDialog();
|
||||
},
|
||||
).then((v) {
|
||||
if (v != null) {
|
||||
accessBloc.add(FetchTableData());
|
||||
}
|
||||
});
|
||||
},
|
||||
borderRadius: 8,
|
||||
child: Text(
|
||||
'Create Visitor Password ',
|
||||
style: context.textTheme.titleSmall!
|
||||
.copyWith(color: Colors.white, fontSize: 12),
|
||||
)),
|
||||
),
|
||||
// Container(
|
||||
// width: 133,
|
||||
// height: 42,
|
||||
// decoration: containerDecoration,
|
||||
// child: DefaultButton(
|
||||
// borderRadius: 8,
|
||||
// backgroundColor: ColorsManager.whiteColors,
|
||||
// child: Text(
|
||||
// 'Admin Password',
|
||||
// style: context.textTheme.titleSmall!
|
||||
// .copyWith(color: Colors.black, fontSize: 12),
|
||||
// )),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
|
||||
// TimeOfDay _selectedTime = TimeOfDay.now();
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: CustomWebTextField(
|
||||
controller: accessBloc.passwordName,
|
||||
height: 43,
|
||||
isRequired: false,
|
||||
textFieldName: 'Name',
|
||||
description: '',
|
||||
onSubmitted: (value) {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer:
|
||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: CustomWebTextField(
|
||||
controller: accessBloc.emailAuthorizer,
|
||||
height: 43,
|
||||
isRequired: false,
|
||||
textFieldName: 'Authorizer',
|
||||
description: '',
|
||||
onSubmitted: (value) {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer:
|
||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(
|
||||
child: DateTimeWebWidget(
|
||||
icon: Assets.calendarIcon,
|
||||
isRequired: false,
|
||||
title: 'Access Time',
|
||||
size: MediaQuery.of(context).size,
|
||||
endTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||
},
|
||||
startTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||
},
|
||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
SearchResetButtons(
|
||||
onSearch: () {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
onReset: () {
|
||||
accessBloc.add(ResetSearch());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
|
||||
return Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: CustomWebTextField(
|
||||
controller: accessBloc.passwordName,
|
||||
isRequired: true,
|
||||
height: 40,
|
||||
textFieldName: 'Name',
|
||||
description: '',
|
||||
onSubmitted: (value) {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer:
|
||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
}),
|
||||
),
|
||||
DateTimeWebWidget(
|
||||
icon: Assets.calendarIcon,
|
||||
isRequired: false,
|
||||
title: 'Access Time',
|
||||
size: MediaQuery.of(context).size,
|
||||
endTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||
},
|
||||
startTime: () {
|
||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||
},
|
||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||
),
|
||||
SearchResetButtons(
|
||||
onSearch: () {
|
||||
accessBloc.add(FilterDataEvent(
|
||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||
selectedTabIndex:
|
||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||
endTime: accessBloc.expirationTimeTimeStamp));
|
||||
},
|
||||
onReset: () {
|
||||
accessBloc.add(ResetSearch());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,7 +15,9 @@ class AirQualityDataModel extends Equatable {
|
||||
return AirQualityDataModel(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
|
||||
.map(
|
||||
(e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
@ -23,9 +25,9 @@ class AirQualityDataModel extends Equatable {
|
||||
static final Map<String, Color> metricColors = {
|
||||
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
|
||||
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
|
||||
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7),
|
||||
'unhealthy_sensitive': ColorsManager.poorOrange.withValues(alpha: 0.7),
|
||||
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
|
||||
'severe': ColorsManager.severePink.withValues(alpha: 0.7),
|
||||
'very_unhealthy': ColorsManager.severePink.withValues(alpha: 0.7),
|
||||
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
|
||||
};
|
||||
|
||||
@ -36,22 +38,19 @@ class AirQualityDataModel extends Equatable {
|
||||
class AirQualityPercentageData extends Equatable {
|
||||
const AirQualityPercentageData({
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.percentage,
|
||||
});
|
||||
|
||||
final String type;
|
||||
final String name;
|
||||
final double percentage;
|
||||
|
||||
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
|
||||
return AirQualityPercentageData(
|
||||
type: json['type'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, name, percentage];
|
||||
List<Object?> get props => [type, percentage];
|
||||
}
|
||||
|
@ -25,8 +25,8 @@ class AnalyticsDevice {
|
||||
|
||||
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsDevice(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: null,
|
||||
@ -39,8 +39,12 @@ class AnalyticsDevice {
|
||||
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
|
||||
: null,
|
||||
spaceUuid: json['spaceUuid'] as String?,
|
||||
latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null,
|
||||
longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null,
|
||||
latitude: json['lat'] != null && json['lat'] != ''
|
||||
? double.tryParse(json['lat']?.toString() ?? '0.0')
|
||||
: null,
|
||||
longitude: json['lon'] != null && json['lon'] != ''
|
||||
? double.tryParse(json['lon']?.toString() ?? '0.0')
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,21 @@ class OccupancyHeatMapModel extends Equatable {
|
||||
});
|
||||
|
||||
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
|
||||
final eventDate = json['event_date'] as String?;
|
||||
final year = eventDate?.split('-')[0];
|
||||
final month = eventDate?.split('-')[1];
|
||||
final day = eventDate?.split('-')[2];
|
||||
|
||||
return OccupancyHeatMapModel(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
eventDate: DateTime.parse(
|
||||
json['event_date'] as String? ?? '${DateTime.now()}',
|
||||
eventDate: DateTime.utc(
|
||||
int.parse(year ?? '2025'),
|
||||
int.parse(month ?? '1'),
|
||||
int.parse(day ?? '1'),
|
||||
),
|
||||
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
|
||||
countTotalPresenceDetected: num.parse(
|
||||
json['count_total_presence_detected']?.toString() ?? '0',
|
||||
).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable {
|
||||
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
|
||||
return RangeOfAqiValue(
|
||||
type: json['type'] as String,
|
||||
min: (json['min'] as num).toDouble(),
|
||||
average: (json['average'] as num).toDouble(),
|
||||
max: (json['max'] as num).toDouble(),
|
||||
min: (json['min'] as num? ?? 0).toDouble(),
|
||||
average: (json['average'] as num? ?? 0).toDouble(),
|
||||
max: (json['max'] as num? ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,6 @@ class AirQualityDistributionBloc
|
||||
state.copyWith(
|
||||
status: AirQualityDistributionStatus.success,
|
||||
chartData: result,
|
||||
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@ -47,35 +46,17 @@ class AirQualityDistributionBloc
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onClearAirQualityDistribution(
|
||||
void _onClearAirQualityDistribution(
|
||||
ClearAirQualityDistribution event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) async {
|
||||
emit(const AirQualityDistributionState());
|
||||
) {
|
||||
emit(AirQualityDistributionState(selectedAqiType: state.selectedAqiType));
|
||||
}
|
||||
|
||||
void _onUpdateAqiTypeEvent(
|
||||
UpdateAqiTypeEvent event,
|
||||
Emitter<AirQualityDistributionState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedAqiType: event.aqiType,
|
||||
filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<AirQualityDataModel> _arrangeChartDataByType(
|
||||
List<AirQualityDataModel> data,
|
||||
AqiType aqiType,
|
||||
) {
|
||||
final filteredData = data.map(
|
||||
(data) => AirQualityDataModel(
|
||||
date: data.date,
|
||||
data: data.data.where((value) => value.type == aqiType.code).toList(),
|
||||
),
|
||||
);
|
||||
return filteredData.toList();
|
||||
emit(state.copyWith(selectedAqiType: event.aqiType));
|
||||
}
|
||||
}
|
||||
|
@ -11,28 +11,24 @@ class AirQualityDistributionState extends Equatable {
|
||||
const AirQualityDistributionState({
|
||||
this.status = AirQualityDistributionStatus.initial,
|
||||
this.chartData = const [],
|
||||
this.filteredChartData = const [],
|
||||
this.errorMessage,
|
||||
this.selectedAqiType = AqiType.aqi,
|
||||
});
|
||||
|
||||
final AirQualityDistributionStatus status;
|
||||
final List<AirQualityDataModel> chartData;
|
||||
final List<AirQualityDataModel> filteredChartData;
|
||||
final String? errorMessage;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
AirQualityDistributionState copyWith({
|
||||
AirQualityDistributionStatus? status,
|
||||
List<AirQualityDataModel>? chartData,
|
||||
List<AirQualityDataModel>? filteredChartData,
|
||||
String? errorMessage,
|
||||
AqiType? selectedAqiType,
|
||||
}) {
|
||||
return AirQualityDistributionState(
|
||||
status: status ?? this.status,
|
||||
chartData: chartData ?? this.chartData,
|
||||
filteredChartData: filteredChartData ?? this.filteredChartData,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
|
||||
);
|
||||
|
@ -75,6 +75,6 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
|
||||
ClearRangeOfAqiEvent event,
|
||||
Emitter<RangeOfAqiState> emit,
|
||||
) {
|
||||
emit(const RangeOfAqiState());
|
||||
emit(RangeOfAqiState(selectedAqiType: state.selectedAqiType));
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
@ -21,12 +21,14 @@ abstract final class FetchAirQualityDataHelper {
|
||||
required String spaceUuid,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
);
|
||||
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
|
||||
if (shouldFetchAnalyticsDevices) {
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
);
|
||||
}
|
||||
loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
@ -36,6 +38,7 @@ abstract final class FetchAirQualityDataHelper {
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
aqiType: aqiType,
|
||||
);
|
||||
}
|
||||
|
||||
@ -104,10 +107,15 @@ abstract final class FetchAirQualityDataHelper {
|
||||
BuildContext context, {
|
||||
required String spaceUuid,
|
||||
required DateTime date,
|
||||
required AqiType aqiType,
|
||||
}) {
|
||||
context.read<AirQualityDistributionBloc>().add(
|
||||
LoadAirQualityDistribution(
|
||||
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date),
|
||||
GetAirQualityDistributionParam(
|
||||
spaceUuid: spaceUuid,
|
||||
date: date,
|
||||
aqiType: aqiType,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -18,11 +18,16 @@ abstract final class RangeOfAqiChartsHelper {
|
||||
(ColorsManager.hazardousPurple, 'Hazardous'),
|
||||
];
|
||||
|
||||
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) {
|
||||
static FlTitlesData titlesData(
|
||||
BuildContext context,
|
||||
List<RangeOfAqi> data, {
|
||||
double leftSideInterval = 50,
|
||||
}) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(context);
|
||||
return titlesData.copyWith(
|
||||
bottomTitles: titlesData.bottomTitles.copyWith(
|
||||
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
|
||||
reservedSize: 36,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
@ -38,10 +43,11 @@ abstract final class RangeOfAqiChartsHelper {
|
||||
leftTitles: titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 50,
|
||||
interval: leftSideInterval,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final text = value >= 300 ? '301+' : value.toInt().toString();
|
||||
final text = value.toInt().toString();
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
|
||||
|
||||
class AirQualityView extends StatelessWidget {
|
||||
@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: height * 0.1,
|
||||
child: const AqiLegend(),
|
||||
),
|
||||
SizedBox(
|
||||
height: height * 1.2,
|
||||
child: const AirQualityEndSideWidget(),
|
||||
@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height * 1.1,
|
||||
height: height * 1.2,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: RangeOfAqiChartBox()),
|
||||
Expanded(child: AqiDistributionChartBox()),
|
||||
Expanded(flex: 2, child: AqiLegend()),
|
||||
Expanded(flex: 12, child: RangeOfAqiChartBox()),
|
||||
Expanded(flex: 12, child: AqiDistributionChartBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget {
|
||||
);
|
||||
final tvocValue = _getValueForStatus(
|
||||
status,
|
||||
'tvoc_value',
|
||||
'voc_value',
|
||||
formatter: (value) => (value / 100).toStringAsFixed(2),
|
||||
);
|
||||
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -16,43 +17,40 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sortedData = List<AirQualityDataModel>.from(chartData)
|
||||
..sort(
|
||||
(a, b) => a.date.compareTo(b.date),
|
||||
);
|
||||
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.1,
|
||||
alignment: BarChartAlignment.start,
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: 20,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
barGroups: _buildBarGroups(sortedData),
|
||||
barGroups: _buildBarGroups(),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
|
||||
return List.generate(sortedData.length, (index) {
|
||||
final data = sortedData[index];
|
||||
List<BarChartGroupData> _buildBarGroups() {
|
||||
final groups = <BarChartGroupData>[];
|
||||
for (var i = 0; i < chartData.length; i++) {
|
||||
final data = chartData[i];
|
||||
final isAllZero = data.data.every((d) => d.percentage == 0);
|
||||
if (isAllZero) {
|
||||
continue;
|
||||
}
|
||||
final stackItems = <BarChartRodData>[];
|
||||
double currentY = 0;
|
||||
bool isFirstElement = true;
|
||||
var isFirstElement = true;
|
||||
|
||||
// Sort data by type to ensure consistent order
|
||||
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
|
||||
..sort((a, b) => a.type.compareTo(b.type));
|
||||
|
||||
for (final percentageData in sortedPercentageData) {
|
||||
for (final percentageData in data.data) {
|
||||
stackItems.add(
|
||||
BarChartRodData(
|
||||
fromY: currentY,
|
||||
toY: currentY + percentageData.percentage ,
|
||||
color: AirQualityDataModel.metricColors[percentageData.name]!,
|
||||
toY: currentY + percentageData.percentage,
|
||||
color: AirQualityDataModel.metricColors[percentageData.type],
|
||||
borderRadius: isFirstElement
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(22),
|
||||
@ -65,13 +63,15 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
currentY += percentageData.percentage + _rodStackItemsSpacing;
|
||||
isFirstElement = false;
|
||||
}
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
groups.add(
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
BarTouchData _barTouchData(BuildContext context) {
|
||||
@ -82,25 +82,27 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
maxContentWidth: 500,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
final data = chartData[group.x.toInt()];
|
||||
final data = chartData[group.x];
|
||||
|
||||
final List<TextSpan> children = [];
|
||||
final children = <TextSpan>[];
|
||||
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
);
|
||||
|
||||
// Sort data by type to ensure consistent order
|
||||
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
|
||||
..sort((a, b) => a.type.compareTo(b.type));
|
||||
|
||||
for (final percentageData in sortedPercentageData) {
|
||||
for (final percentageData in data.data) {
|
||||
if (percentageData.percentage == 0) {
|
||||
continue;
|
||||
}
|
||||
final percentage = percentageData.percentage.toStringAsFixed(1);
|
||||
final type = percentageData.type[0].toUpperCase() +
|
||||
percentageData.type.substring(1).replaceAll('_', ' ');
|
||||
children.add(TextSpan(
|
||||
text:
|
||||
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
|
||||
text: '\n$type: $percentage%',
|
||||
style: textStyle,
|
||||
));
|
||||
}
|
||||
@ -109,9 +111,10 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
DateFormat('dd/MM/yyyy').format(data.date),
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 16,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
@ -128,7 +131,6 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 20,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
@ -149,8 +151,9 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
axisNameWidget: const ChartsXAxisTitle(),
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
showTitles: chartData.isNotEmpty,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
@ -158,7 +161,7 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 8,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiDistributionChartBox extends StatelessWidget {
|
||||
@ -32,8 +34,20 @@ class AqiDistributionChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: AqiDistributionChart(chartData: state.filteredChartData),
|
||||
Visibility(
|
||||
visible: state.chartData.isNotEmpty,
|
||||
replacement: AnalyticsChartEmptyStateWidget(
|
||||
isLoading: state.status == AirQualityDistributionStatus.loading,
|
||||
isError: state.status == AirQualityDistributionStatus.failure,
|
||||
isInitial: state.status == AirQualityDistributionStatus.initial,
|
||||
errorMessage: state.errorMessage,
|
||||
iconPath: Assets.emptyBarredChart,
|
||||
),
|
||||
child: Expanded(
|
||||
child: AqiDistributionChart(
|
||||
chartData: state.chartData,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class AqiDistributionChartTitle extends StatelessWidget {
|
||||
const AqiDistributionChartTitle({required this.isLoading, super.key});
|
||||
@ -16,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
|
||||
children: [
|
||||
ChartsLoadingWidget(isLoading: isLoading),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@ -25,20 +28,45 @@ class AqiDistributionChartTitle extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
context
|
||||
.read<AirQualityDistributionBloc>()
|
||||
.add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
},
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
selectedAqiType: context.watch<AirQualityDistributionBloc>().state.selectedAqiType,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
final bloc = context.read<AirQualityDistributionBloc>();
|
||||
try {
|
||||
final param = _makeLoadAqiDistributionParam(context, value);
|
||||
bloc.add(LoadAirQualityDistribution(param));
|
||||
} catch (_) {
|
||||
return;
|
||||
} finally {
|
||||
bloc.add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
GetAirQualityDistributionParam _makeLoadAqiDistributionParam(
|
||||
BuildContext context,
|
||||
AqiType aqiType,
|
||||
) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
final spaceUuid =
|
||||
context.read<SpaceTreeBloc>().state.selectedSpaces.firstOrNull ?? '';
|
||||
if (spaceUuid.isEmpty) throw Exception('Space UUID is empty');
|
||||
return GetAirQualityDistributionParam(
|
||||
date: date,
|
||||
spaceUuid: spaceUuid,
|
||||
aqiType: aqiType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiLegend extends StatelessWidget {
|
||||
const AqiLegend({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: RangeOfAqiChartsHelper.gradientData.map((e) {
|
||||
return Flexible(
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: ChartInformativeCell(
|
||||
color: e.$1,
|
||||
title: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Text(e.$2),
|
||||
),
|
||||
height: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -47,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
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,
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SvgPicture.asset(
|
||||
svgPath,
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -6,8 +6,8 @@ enum AqiType {
|
||||
aqi('AQI', '', 'aqi'),
|
||||
pm25('PM2.5', 'µg/m³', 'pm25'),
|
||||
pm10('PM10', 'µg/m³', 'pm10'),
|
||||
hcho('HCHO', 'mg/m³', 'hcho'),
|
||||
tvoc('TVOC', 'µg/m³', 'tvoc'),
|
||||
hcho('HCHO', 'mg/m³', 'ch2o'),
|
||||
tvoc('TVOC', 'mg/m³', 'voc'),
|
||||
co2('CO2', 'ppm', 'co2');
|
||||
|
||||
const AqiType(this.value, this.unit, this.code);
|
||||
@ -18,19 +18,20 @@ enum AqiType {
|
||||
}
|
||||
|
||||
class AqiTypeDropdown extends StatefulWidget {
|
||||
const AqiTypeDropdown({super.key, required this.onChanged});
|
||||
const AqiTypeDropdown({
|
||||
required this.onChanged,
|
||||
this.selectedAqiType,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<AqiType?> onChanged;
|
||||
final AqiType? selectedAqiType;
|
||||
|
||||
@override
|
||||
State<AqiTypeDropdown> createState() => _AqiTypeDropdownState();
|
||||
}
|
||||
|
||||
class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
|
||||
AqiType? _selectedItem = AqiType.aqi;
|
||||
|
||||
void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -41,8 +42,8 @@ class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: DropdownButton<AqiType?>(
|
||||
value: _selectedItem,
|
||||
child: DropdownButton<AqiType>(
|
||||
value: widget.selectedAqiType,
|
||||
isDense: true,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
dropdownColor: ColorsManager.whiteColors,
|
||||
@ -59,10 +60,7 @@ class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
|
||||
items: AqiType.values
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
_updateSelectedItem(value);
|
||||
widget.onChanged(value);
|
||||
},
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RangeOfAqiChart extends StatelessWidget {
|
||||
final List<RangeOfAqi> chartData;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
const RangeOfAqiChart({
|
||||
super.key,
|
||||
required this.chartData,
|
||||
required this.selectedAqiType,
|
||||
});
|
||||
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines {
|
||||
@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
(double maxY, double interval) get _maxYForAqiType {
|
||||
const aqiMaxValues = <AqiType, (double maxY, double interval)>{
|
||||
AqiType.aqi: (401, 100),
|
||||
AqiType.pm25: (351, 50),
|
||||
AqiType.pm10: (501, 100),
|
||||
AqiType.hcho: (301, 50),
|
||||
AqiType.tvoc: (501, 50),
|
||||
AqiType.co2: (1251, 250),
|
||||
};
|
||||
|
||||
return aqiMaxValues[selectedAqiType]!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: 301,
|
||||
maxY: _maxYForAqiType.$1,
|
||||
clipData: const FlClipData.vertical(),
|
||||
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
|
||||
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: _maxYForAqiType.$2,
|
||||
),
|
||||
titlesData: RangeOfAqiChartsHelper.titlesData(
|
||||
context,
|
||||
chartData,
|
||||
leftSideInterval: _maxYForAqiType.$2,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
|
||||
betweenBarsData: [
|
||||
@ -63,7 +85,7 @@ class RangeOfAqiChart extends StatelessWidget {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
|
||||
stops: const [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
|
||||
colors: RangeOfAqiChartsHelper.gradientData.map((e) {
|
||||
final (color, _) = e;
|
||||
return color.withValues(alpha: 0.6);
|
||||
|
@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class RangeOfAqiChartBox extends StatelessWidget {
|
||||
@ -32,7 +34,22 @@ class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
|
||||
Visibility(
|
||||
visible: state.filteredRangeOfAqi.isNotEmpty,
|
||||
replacement: AnalyticsChartEmptyStateWidget(
|
||||
isLoading: state.status == RangeOfAqiStatus.loading,
|
||||
isError: state.status == RangeOfAqiStatus.failure,
|
||||
isInitial: state.status == RangeOfAqiStatus.initial,
|
||||
errorMessage: state.errorMessage,
|
||||
iconPath: Assets.emptyRangeOfAqi,
|
||||
),
|
||||
child: Expanded(
|
||||
child: RangeOfAqiChart(
|
||||
chartData: state.filteredRangeOfAqi,
|
||||
selectedAqiType: state.selectedAqiType,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -63,15 +63,15 @@ class RangeOfAqiChartTitle extends StatelessWidget {
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: AqiTypeDropdown(
|
||||
selectedAqiType: context.watch<RangeOfAqiBloc>().state.selectedAqiType,
|
||||
onChanged: (value) {
|
||||
final spaceTreeState = context.read<SpaceTreeBloc>().state;
|
||||
final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull;
|
||||
|
||||
if (spaceUuid == null) return;
|
||||
|
||||
if (value != null) {
|
||||
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
|
||||
if (spaceUuid == null) return;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -16,7 +16,7 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
|
||||
@ -27,10 +27,12 @@ import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_devi
|
||||
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/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/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
@ -104,12 +106,12 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => RangeOfAqiBloc(
|
||||
FakeRangeOfAqiService(),
|
||||
RemoteRangeOfAqiService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => AirQualityDistributionBloc(
|
||||
FakeAirQualityDistributionService(),
|
||||
RemoteAirQualityDistributionService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
@ -130,9 +132,19 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsPageForm extends StatelessWidget {
|
||||
class AnalyticsPageForm extends StatefulWidget {
|
||||
const AnalyticsPageForm({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsPageForm> createState() => _AnalyticsPageFormState();
|
||||
}
|
||||
|
||||
class _AnalyticsPageFormState extends State<AnalyticsPageForm> {
|
||||
@override
|
||||
void initState() {
|
||||
context.read<SpaceTreeBloc>().add(InitialEvent());
|
||||
super.initState();
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebScaffold(
|
||||
|
@ -20,7 +20,7 @@ class AnalyticsDateFilterButton extends StatefulWidget {
|
||||
final void Function(DateTime)? onDateSelected;
|
||||
final DatePickerType datePickerType;
|
||||
|
||||
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
static final Color _color = ColorsManager.blackColor.withValues(alpha: 0.8);
|
||||
|
||||
@override
|
||||
State<AnalyticsDateFilterButton> createState() =>
|
||||
@ -60,23 +60,21 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return switch (widget.datePickerType) {
|
||||
DatePickerType.month => MonthPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
DatePickerType.year => YearPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
};
|
||||
builder: (_) => switch (widget.datePickerType) {
|
||||
DatePickerType.month => MonthPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
DatePickerType.year => YearPickerWidget(
|
||||
selectedDate: widget.selectedDate,
|
||||
onDateSelected: (value) {
|
||||
widget.onDateSelected?.call(value);
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -118,7 +118,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
communityUuid: communities.firstOrNull ?? '',
|
||||
spaceUuid: spaces.firstOrNull ?? '',
|
||||
);
|
||||
break;
|
||||
return;
|
||||
case AnalyticsPageTab.airQuality:
|
||||
_onAirQualityDateChanged(
|
||||
context,
|
||||
@ -126,8 +126,9 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
communityUuid: communities.firstOrNull ?? '',
|
||||
spaceUuid: spaces.firstOrNull ?? '',
|
||||
);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,6 +158,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
|
||||
required String communityUuid,
|
||||
required String spaceUuid,
|
||||
}) {
|
||||
if (spaceUuid.isEmpty) return;
|
||||
FetchAirQualityDataHelper.loadAirQualityData(
|
||||
context,
|
||||
date: date,
|
||||
|
@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget {
|
||||
required this.title,
|
||||
required this.color,
|
||||
this.hasBorder = false,
|
||||
this.height,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Color color;
|
||||
final bool hasBorder;
|
||||
final double? height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.sizeOf(context).height * 0.0385,
|
||||
height: height ?? MediaQuery.sizeOf(context).height * 0.0385,
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper {
|
||||
return FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: const ChartsXAxisTitle(),
|
||||
drawBelowEverything: true,
|
||||
sideTitles: SideTitles(
|
||||
interval: 1,
|
||||
@ -38,7 +40,7 @@ abstract final class EnergyManagementChartsHelper {
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
minIncluded: false,
|
||||
interval: leftTitlesInterval,
|
||||
reservedSize: 110,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper {
|
||||
);
|
||||
}
|
||||
|
||||
static String getToolTipLabel(num month, double value) {
|
||||
final monthLabel = month.toString();
|
||||
final valueLabel = value.formatNumberToKwh;
|
||||
final labels = [monthLabel, valueLabel];
|
||||
return labels.where((element) => element.isNotEmpty).join(', ');
|
||||
}
|
||||
static String getToolTipLabel(double value) => value.formatNumberToKwh;
|
||||
|
||||
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
return LineTooltipItem(
|
||||
getToolTipLabel(spot.x, spot.y),
|
||||
getToolTipLabel(spot.y),
|
||||
const TextStyle(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: MediaQuery.sizeOf(context).height * 1,
|
||||
height: MediaQuery.sizeOf(context).height * 1.05,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -46,7 +46,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 7,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
@ -55,7 +55,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: PowerClampEnergyDataWidget()),
|
||||
Expanded(flex: 4, child: PowerClampEnergyDataWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Visibility(
|
||||
visible: state.devices.isNotEmpty,
|
||||
replacement: _buildNoDevicesFound(context),
|
||||
child: _buildDevicesDropdown(context, state),
|
||||
visible: state.status != AnalyticsDevicesStatus.loading,
|
||||
replacement: _buildLoadingIndicator(),
|
||||
child: Visibility(
|
||||
visible: state.devices.isNotEmpty,
|
||||
replacement: _buildNoDevicesFound(context),
|
||||
child: _buildDevicesDropdown(context, state),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator() {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 2,
|
||||
|
@ -5,8 +5,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
@ -37,7 +39,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ChartTitle(
|
||||
title: Text('Energy Consumption per Device'),
|
||||
title: Text('Device energy consumed'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -54,8 +56,24 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
const Divider(height: 0),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
|
||||
Visibility(
|
||||
visible: state.chartData.isNotEmpty &&
|
||||
state.chartData
|
||||
.every((e) => e.energy.every((e) => e.value != 0)),
|
||||
replacement: AnalyticsChartEmptyStateWidget(
|
||||
isLoading:
|
||||
state.status == EnergyConsumptionPerDeviceStatus.loading,
|
||||
isError: state.status == EnergyConsumptionPerDeviceStatus.failure,
|
||||
isInitial:
|
||||
state.status == EnergyConsumptionPerDeviceStatus.initial,
|
||||
errorMessage: state.errorMessage,
|
||||
iconPath: Assets.emptyEnergyManagementPerDevice,
|
||||
),
|
||||
child: Expanded(
|
||||
child: EnergyConsumptionPerDeviceChart(
|
||||
chartData: state.chartData,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
maxY: chartData.isEmpty
|
||||
? null
|
||||
: chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250,
|
||||
clipData: const FlClipData.vertical(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
leftTitlesInterval: 500,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
horizontalInterval: 500,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
),
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
@ -32,7 +34,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(title: Text('Total Energy Consumption')),
|
||||
child: ChartTitle(title: Text('Space energy consumed')),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
@ -41,7 +43,18 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
TotalEnergyConsumptionChart(chartData: state.chartData),
|
||||
Visibility(
|
||||
visible: state.chartData.isNotEmpty &&
|
||||
state.chartData.every((e) => e.value != 0),
|
||||
replacement: AnalyticsChartEmptyStateWidget(
|
||||
isLoading: state.status == TotalEnergyConsumptionStatus.loading,
|
||||
isError: state.status == TotalEnergyConsumptionStatus.failure,
|
||||
isInitial: state.status == TotalEnergyConsumptionStatus.initial,
|
||||
errorMessage: state.errorMessage,
|
||||
iconPath: Assets.emptyEnergyManagementChart,
|
||||
),
|
||||
child: TotalEnergyConsumptionChart(chartData: state.chartData),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper {
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['WPS', 'CPS'],
|
||||
deviceTypes: ['WPS', 'CPS', 'NCPS'],
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
|
@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
|
||||
],
|
||||
@ -31,12 +31,12 @@ class AnalyticsOccupancyView extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height * 0.9,
|
||||
height: height * 1,
|
||||
child: const Row(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
flex: 7,
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
@ -45,7 +45,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: OccupancyEndSideBar()),
|
||||
Expanded(flex: 4, child: OccupancyEndSideBar()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget {
|
||||
),
|
||||
const Divider(height: 2, thickness: 1),
|
||||
Text(
|
||||
'$value Occupants',
|
||||
'Occupancy detected: $value',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
|
||||
color: Colors.transparent,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-(widget.cellSize * 2.5), -50),
|
||||
child: HeatMapTooltip(date: item.date, value: item.value),
|
||||
child: HeatMapTooltip(date: item.date.toUtc(), value: item.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -17,6 +18,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.001,
|
||||
alignment: BarChartAlignment.start,
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 20,
|
||||
@ -88,8 +90,8 @@ class OccupancyChart extends StatelessWidget {
|
||||
}) {
|
||||
final data = chartData;
|
||||
|
||||
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
|
||||
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
|
||||
final occupancyValue = double.parse(data[group.x].occupancy);
|
||||
final percentage = '${occupancyValue.toStringAsFixed(0)}%';
|
||||
|
||||
return BarTooltipItem(
|
||||
percentage,
|
||||
@ -116,7 +118,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${(value).toStringAsFixed(0)}%',
|
||||
'${value.toStringAsFixed(0)}%',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.greyColor,
|
||||
@ -128,6 +130,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
axisNameWidget: const ChartsXAxisTitle(),
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
|
@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyChartBox extends StatelessWidget {
|
||||
@ -67,7 +69,24 @@ class OccupancyChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: OccupancyChart(chartData: state.chartData)),
|
||||
Visibility(
|
||||
visible: state.chartData.isNotEmpty &&
|
||||
state.chartData.every(
|
||||
(e) => e.occupancy.isNotEmpty,
|
||||
),
|
||||
replacement: AnalyticsChartEmptyStateWidget(
|
||||
isLoading: state.status == OccupancyStatus.loading,
|
||||
isError: state.status == OccupancyStatus.failure,
|
||||
isInitial: state.status == OccupancyStatus.initial,
|
||||
errorMessage: state.errorMessage,
|
||||
iconPath: Assets.emptyBarredChart,
|
||||
),
|
||||
child: Expanded(
|
||||
child: OccupancyChart(
|
||||
chartData: state.chartData,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -23,38 +23,45 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
|
||||
SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
status: [
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.presenceState,
|
||||
title: 'Presence Status',
|
||||
value: _valueFromCode(
|
||||
'presence_state',
|
||||
state.deviceStatusList,
|
||||
const AnalyticsSidebarHeader(title: 'Presence Sensor'),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
status: [
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.presenceState,
|
||||
title: 'Presence Status',
|
||||
value: _valueFromCode(
|
||||
'presence_state',
|
||||
state.deviceStatusList,
|
||||
),
|
||||
unit: '',
|
||||
),
|
||||
unit: '',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.presenceTimeIcon,
|
||||
title: 'Presence Time',
|
||||
value:
|
||||
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
|
||||
unit: '',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.currentDistanceIcon,
|
||||
title: 'Detection Distance',
|
||||
value:
|
||||
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
|
||||
unit: '',
|
||||
),
|
||||
],
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.presenceTimeIcon,
|
||||
title: 'Presence Time',
|
||||
value:
|
||||
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
|
||||
unit: '',
|
||||
),
|
||||
PowerClampEnergyStatus(
|
||||
iconPath: Assets.currentDistanceIcon,
|
||||
title: 'Detection Distance',
|
||||
value:
|
||||
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
|
||||
unit: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
child: Image.asset(Assets.autocadOccupancyImage),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMap extends StatelessWidget {
|
||||
const OccupancyHeatMap({required this.heatMapData, super.key});
|
||||
const OccupancyHeatMap({
|
||||
required this.heatMapData,
|
||||
required this.selectedDate,
|
||||
super.key,
|
||||
});
|
||||
final Map<DateTime, int> heatMapData;
|
||||
final DateTime selectedDate;
|
||||
|
||||
static const _cellSize = 16.0;
|
||||
static const _totalWeeks = 53;
|
||||
@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget {
|
||||
: 0;
|
||||
|
||||
DateTime _getStartingDate() {
|
||||
final jan1 = DateTime(DateTime.now().year, 1, 1);
|
||||
final jan1 = DateTime.utc(selectedDate.year, 1, 1);
|
||||
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
|
||||
return startOfWeek;
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class OccupancyHeatMapBox extends StatelessWidget {
|
||||
@ -68,14 +70,29 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
(_, value) => MapEntry(
|
||||
value.eventDate,
|
||||
value.countTotalPresenceDetected,
|
||||
Visibility(
|
||||
visible: state.heatMapData.isNotEmpty &&
|
||||
state.heatMapData.every(
|
||||
(e) => e.countTotalPresenceDetected != 0,
|
||||
),
|
||||
replacement: AnalyticsChartEmptyStateWidget(
|
||||
isLoading: state.status == OccupancyHeatMapStatus.loading,
|
||||
isError: state.status == OccupancyHeatMapStatus.failure,
|
||||
isInitial: state.status == OccupancyHeatMapStatus.initial,
|
||||
errorMessage: state.errorMessage,
|
||||
iconPath: Assets.emptyHeatmap,
|
||||
),
|
||||
child: Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
selectedDate:
|
||||
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
(_, value) => MapEntry(
|
||||
value.eventDate,
|
||||
value.countTotalPresenceDetected,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -34,8 +34,8 @@ class OccupancyHeatMapGradient extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
begin: AlignmentDirectional.centerEnd,
|
||||
end: AlignmentDirectional.centerStart,
|
||||
begin: AlignmentDirectional.centerStart,
|
||||
end: AlignmentDirectional.centerEnd,
|
||||
colors: _heatMapColors(),
|
||||
),
|
||||
),
|
||||
|
@ -28,11 +28,11 @@ class OccupancyPainter extends CustomPainter {
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint fillPaint = Paint();
|
||||
final Paint borderPaint = Paint()
|
||||
final fillPaint = Paint();
|
||||
final borderPaint = Paint()
|
||||
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
|
||||
..style = PaintingStyle.stroke;
|
||||
final Paint hoveredBorderPaint = Paint()
|
||||
final hoveredBorderPaint = Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
@ -48,7 +48,6 @@ class OccupancyPainter extends CustomPainter {
|
||||
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
|
||||
canvas.drawRect(rect, fillPaint);
|
||||
|
||||
// Highlight the hovered item
|
||||
if (hoveredItem != null && hoveredItem!.index == item.index) {
|
||||
canvas.drawRect(rect, hoveredBorderPaint);
|
||||
} else {
|
||||
@ -73,16 +72,16 @@ class OccupancyPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
|
||||
const double dashWidth = 2.0;
|
||||
const double dashSpace = 4.0;
|
||||
final double totalLength = (end - start).distance;
|
||||
final Offset direction = (end - start) / (end - start).distance;
|
||||
const dashWidth = 2.0;
|
||||
const dashSpace = 4.0;
|
||||
final totalLength = (end - start).distance;
|
||||
final direction = (end - start) / (end - start).distance;
|
||||
|
||||
double currentLength = 0.0;
|
||||
var currentLength = 0.0;
|
||||
while (currentLength < totalLength) {
|
||||
final Offset dashStart = start + direction * currentLength;
|
||||
final double nextLength = currentLength + dashWidth;
|
||||
final Offset dashEnd =
|
||||
final dashStart = start + direction * currentLength;
|
||||
final nextLength = currentLength + dashWidth;
|
||||
final dashEnd =
|
||||
start + direction * (nextLength < totalLength ? nextLength : totalLength);
|
||||
canvas.drawLine(dashStart, dashEnd, paint);
|
||||
currentLength = nextLength + dashSpace;
|
||||
@ -91,8 +90,9 @@ class OccupancyPainter extends CustomPainter {
|
||||
|
||||
Color _getColor(int value) {
|
||||
if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0);
|
||||
final opacity = value.clamp(0, maxValue) / maxValue;
|
||||
return ColorsManager.vividBlue.withValues(alpha: opacity);
|
||||
final clampedValue = 0.075 + (1 * value.clamp(0, maxValue) / maxValue);
|
||||
final opacity = value == 0 ? 0 : clampedValue;
|
||||
return ColorsManager.vividBlue.withValues(alpha: opacity.toDouble());
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
|
||||
class GetAirQualityDistributionParam {
|
||||
final DateTime date;
|
||||
final String spaceUuid;
|
||||
final AqiType aqiType;
|
||||
|
||||
const GetAirQualityDistributionParam({
|
||||
const GetAirQualityDistributionParam(
|
||||
{
|
||||
required this.date,
|
||||
required this.spaceUuid,
|
||||
required this.aqiType,
|
||||
});
|
||||
}
|
||||
|
@ -1,95 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
|
||||
|
||||
class FakeAirQualityDistributionService implements AirQualityDistributionService {
|
||||
final _random = Random();
|
||||
|
||||
@override
|
||||
Future<List<AirQualityDataModel>> getAirQualityDistribution(
|
||||
GetAirQualityDistributionParam param,
|
||||
) async {
|
||||
return Future.delayed(
|
||||
const Duration(milliseconds: 400),
|
||||
() => List.generate(30, (index) {
|
||||
final date = DateTime(2025, 5, 1).add(Duration(days: index));
|
||||
|
||||
final values = _generateRandomPercentages();
|
||||
final nullMask = List.generate(6, (_) => _shouldBeNull());
|
||||
|
||||
if (nullMask.every((isNull) => isNull)) {
|
||||
nullMask[_random.nextInt(6)] = false;
|
||||
}
|
||||
|
||||
final nonNullValues = _redistributePercentages(values, nullMask);
|
||||
|
||||
return AirQualityDataModel(
|
||||
date: date,
|
||||
data: [
|
||||
AirQualityPercentageData(
|
||||
type: AqiType.aqi.code,
|
||||
percentage: nonNullValues[0],
|
||||
name: 'good',
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'moderate',
|
||||
type: AqiType.co2.code,
|
||||
percentage: nonNullValues[1],
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'poor',
|
||||
percentage: nonNullValues[2],
|
||||
type: AqiType.hcho.code,
|
||||
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'unhealthy',
|
||||
percentage: nonNullValues[3],
|
||||
type: AqiType.pm10.code,
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'severe',
|
||||
type: AqiType.pm25.code,
|
||||
percentage: nonNullValues[4],
|
||||
),
|
||||
AirQualityPercentageData(
|
||||
name: 'hazardous',
|
||||
percentage: nonNullValues[5],
|
||||
type: AqiType.co2.code,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
List<double> _redistributePercentages(
|
||||
List<double> originalValues,
|
||||
List<bool> nullMask,
|
||||
) {
|
||||
double nonNullSum = 0;
|
||||
for (int i = 0; i < originalValues.length; i++) {
|
||||
if (!nullMask[i]) {
|
||||
nonNullSum += originalValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
return List.generate(originalValues.length, (i) {
|
||||
if (nullMask[i]) return 0;
|
||||
return (originalValues[i] / nonNullSum * 100).roundToDouble();
|
||||
});
|
||||
}
|
||||
|
||||
bool _shouldBeNull() => _random.nextDouble() < 0.6;
|
||||
|
||||
List<double> _generateRandomPercentages() {
|
||||
final values = List.generate(6, (_) => _random.nextDouble());
|
||||
|
||||
final sum = values.reduce((a, b) => a + b);
|
||||
|
||||
return values.map((value) => (value / sum * 100).roundToDouble()).toList();
|
||||
}
|
||||
}
|