Compare commits
64 Commits
SP-1660-fe
...
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 | |||
2f5ad03431 |
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/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 |
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 |
@ -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());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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? ?? '0.0') : null,
|
||||
longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.1,
|
||||
alignment: BarChartAlignment.start,
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: 20,
|
||||
),
|
||||
|
@ -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.chartData),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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,10 +34,20 @@ class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: RangeOfAqiChart(
|
||||
chartData: state.filteredRangeOfAqi,
|
||||
selectedAqiType: state.selectedAqiType,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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 {
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -18,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,
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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,16 +70,29 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
selectedDate:
|
||||
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
|
||||
'reverse',
|
||||
queryParameters: {
|
||||
'format': 'json',
|
||||
'lat': param.latitude,
|
||||
'lon': param.longitude,
|
||||
'lat': 25.1880567,
|
||||
'lon': 55.266608,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AnalyticsChartEmptyStateWidget extends StatelessWidget {
|
||||
const AnalyticsChartEmptyStateWidget({
|
||||
required this.iconPath,
|
||||
this.isLoading = false,
|
||||
this.isError = false,
|
||||
this.isInitial = false,
|
||||
this.errorMessage,
|
||||
this.noDataMessage = 'No data to display',
|
||||
this.initialMessage = 'Please select a space to see data',
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
final bool isError;
|
||||
final bool isInitial;
|
||||
final String? errorMessage;
|
||||
final String noDataMessage;
|
||||
final String initialMessage;
|
||||
final String iconPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: _buildWidgetBasedOnState(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWidgetBasedOnState(BuildContext context) {
|
||||
final widgetsMap = {
|
||||
isLoading: const AppLoadingIndicator(),
|
||||
isInitial: _buildState(context, initialMessage),
|
||||
isError: _buildState(context, errorMessage ?? 'Something went wrong'),
|
||||
};
|
||||
|
||||
return widgetsMap[true] ?? _buildState(context, noDataMessage);
|
||||
}
|
||||
|
||||
Widget _buildState(BuildContext context, String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Expanded(child: SvgPicture.asset(iconPath, fit: BoxFit.contain)),
|
||||
SelectableText(
|
||||
message,
|
||||
style: isError
|
||||
? context.textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.red,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
)
|
||||
: null,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -50,6 +50,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
bool _selectAll = false;
|
||||
final ScrollController _verticalScrollController = ScrollController();
|
||||
final ScrollController _horizontalScrollController = ScrollController();
|
||||
static const double _fixedRowHeight = 60;
|
||||
static const double _checkboxColumnWidth = 50;
|
||||
static const double _settingsColumnWidth = 100;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -67,7 +70,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
|
||||
bool _compareListOfLists(
|
||||
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||
// Check if the old and new lists are the same
|
||||
if (oldList.length != newList.length) return false;
|
||||
|
||||
for (int i = 0; i < oldList.length; i++) {
|
||||
@ -104,73 +106,130 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
|
||||
}
|
||||
|
||||
double get _totalTableWidth {
|
||||
final hasSettings = widget.headers.contains('Settings');
|
||||
final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) +
|
||||
(hasSettings ? _settingsColumnWidth : 0);
|
||||
final regularCount = widget.headers.length - (hasSettings ? 1 : 0);
|
||||
final regularWidth = (widget.size.width - base) / regularCount;
|
||||
return base + regularCount * regularWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: widget.size.width,
|
||||
height: widget.size.height,
|
||||
decoration: widget.cellDecoration,
|
||||
child: Scrollbar(
|
||||
controller: _verticalScrollController,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
||||
child: Scrollbar(
|
||||
//fixed the horizontal scrollbar issue
|
||||
controller: _horizontalScrollController,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
notificationPredicate: (notif) => notif.depth == 1,
|
||||
notificationPredicate: (notif) =>
|
||||
notif.metrics.axis == Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
controller: _verticalScrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: _horizontalScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: widget.size.width,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: widget.headerDecoration ??
|
||||
const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
controller: _horizontalScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: _totalTableWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: _fixedRowHeight,
|
||||
decoration: widget.headerDecoration ??
|
||||
const BoxDecoration(color: ColorsManager.boxColor),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.withCheckBox)
|
||||
_buildSelectAllCheckbox(_checkboxColumnWidth),
|
||||
for (var i = 0; i < widget.headers.length; i++)
|
||||
_buildTableHeaderCell(
|
||||
widget.headers[i],
|
||||
widget.headers[i] == 'Settings'
|
||||
? _settingsColumnWidth
|
||||
: (_totalTableWidth -
|
||||
(widget.withCheckBox
|
||||
? _checkboxColumnWidth
|
||||
: 0) -
|
||||
(widget.headers.contains('Settings')
|
||||
? _settingsColumnWidth
|
||||
: 0)) /
|
||||
(widget.headers.length -
|
||||
(widget.headers.contains('Settings')
|
||||
? 1
|
||||
: 0)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.withCheckBox) _buildSelectAllCheckbox(),
|
||||
...List.generate(widget.headers.length, (index) {
|
||||
return _buildTableHeaderCell(
|
||||
widget.headers[index], index);
|
||||
})
|
||||
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: widget.size.width,
|
||||
child: widget.isEmpty
|
||||
? _buildEmptyState()
|
||||
: Column(
|
||||
children:
|
||||
List.generate(widget.data.length, (rowIndex) {
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: widget.isEmpty
|
||||
? _buildEmptyState()
|
||||
: Scrollbar(
|
||||
controller: _verticalScrollController,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
notificationPredicate: (notif) =>
|
||||
notif.metrics.axis == Axis.vertical,
|
||||
child: ListView.builder(
|
||||
controller: _verticalScrollController,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (_, rowIndex) {
|
||||
final row = widget.data[rowIndex];
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.withCheckBox)
|
||||
_buildRowCheckbox(
|
||||
rowIndex, widget.size.height * 0.08),
|
||||
...row.asMap().entries.map((entry) {
|
||||
return _buildTableCell(
|
||||
entry.value.toString(),
|
||||
widget.size.height * 0.08,
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: entry.key,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
return SizedBox(
|
||||
height: _fixedRowHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.withCheckBox)
|
||||
_buildRowCheckbox(
|
||||
rowIndex,
|
||||
_checkboxColumnWidth,
|
||||
),
|
||||
for (var colIndex = 0;
|
||||
colIndex < row.length;
|
||||
colIndex++)
|
||||
widget.headers[colIndex] == 'Settings'
|
||||
? buildSettingsIcon(
|
||||
width: _settingsColumnWidth,
|
||||
onTap: () => widget
|
||||
.onSettingsPressed
|
||||
?.call(rowIndex),
|
||||
)
|
||||
: _buildTableCell(
|
||||
row[colIndex].toString(),
|
||||
width: widget.headers[
|
||||
colIndex] ==
|
||||
'Settings'
|
||||
? _settingsColumnWidth
|
||||
: (_totalTableWidth -
|
||||
(widget.withCheckBox
|
||||
? _checkboxColumnWidth
|
||||
: 0) -
|
||||
(widget.headers
|
||||
.contains(
|
||||
'Settings')
|
||||
? _settingsColumnWidth
|
||||
: 0)) /
|
||||
(widget.headers.length -
|
||||
(widget.headers
|
||||
.contains(
|
||||
'Settings')
|
||||
? 1
|
||||
: 0)),
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: colIndex,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -210,9 +269,10 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget _buildSelectAllCheckbox() {
|
||||
|
||||
Widget _buildSelectAllCheckbox(double width) {
|
||||
return Container(
|
||||
width: 50,
|
||||
width: width,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||
@ -227,11 +287,11 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRowCheckbox(int index, double size) {
|
||||
Widget _buildRowCheckbox(int index, double width) {
|
||||
return Container(
|
||||
width: 50,
|
||||
width: width,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
height: size,
|
||||
height: _fixedRowHeight,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@ -253,50 +313,47 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableHeaderCell(String title, int index) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||
),
|
||||
Widget _buildTableHeaderCell(String title, double width) {
|
||||
return Container(
|
||||
width: width,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||
),
|
||||
constraints: const BoxConstraints.expand(height: 40),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
|
||||
vertical: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall!.copyWith(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall!.copyWith(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableCell(String content, double size,
|
||||
{required int rowIndex, required int columnIndex}) {
|
||||
Widget _buildTableCell(String content,
|
||||
{required double width,
|
||||
required int rowIndex,
|
||||
required int columnIndex}) {
|
||||
bool isBatteryLevel = content.endsWith('%');
|
||||
double? batteryLevel;
|
||||
|
||||
if (isBatteryLevel) {
|
||||
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
|
||||
}
|
||||
|
||||
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
|
||||
if (isSettingsColumn) {
|
||||
return buildSettingsIcon(
|
||||
width: 120,
|
||||
height: 60,
|
||||
iconSize: 40,
|
||||
onTap: () => widget.onSettingsPressed?.call(rowIndex),
|
||||
);
|
||||
width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex));
|
||||
}
|
||||
|
||||
Color? statusColor;
|
||||
@ -320,92 +377,82 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
statusColor = Colors.black;
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: size,
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.boxDivider,
|
||||
width: 1.0,
|
||||
),
|
||||
return Container(
|
||||
width: width,
|
||||
height: _fixedRowHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.boxDivider,
|
||||
width: 1.0,
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
color: (batteryLevel != null && batteryLevel < 20)
|
||||
? ColorsManager.red
|
||||
: (batteryLevel != null && batteryLevel > 20)
|
||||
? ColorsManager.green
|
||||
: statusColor,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400),
|
||||
maxLines: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
color: (batteryLevel != null && batteryLevel < 20)
|
||||
? ColorsManager.red
|
||||
: (batteryLevel != null && batteryLevel > 20)
|
||||
? ColorsManager.green
|
||||
: statusColor,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSettingsIcon(
|
||||
{double width = 120,
|
||||
double height = 60,
|
||||
double iconSize = 40,
|
||||
VoidCallback? onTap}) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
|
||||
margin: const EdgeInsets.only(right: 15),
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.boxDivider,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
Widget buildSettingsIcon({required double width, VoidCallback? onTap}) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: _fixedRowHeight,
|
||||
padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10),
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.boxDivider,
|
||||
width: 1.0,
|
||||
),
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 16.0,
|
||||
left: 17.0,
|
||||
),
|
||||
child: Container(
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF7F8FA),
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.17),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF7F8FA),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.17),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
Assets.settings,
|
||||
width: 40,
|
||||
height: 22,
|
||||
color: ColorsManager.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
Assets.settings,
|
||||
width: 40,
|
||||
height: 20,
|
||||
color: ColorsManager.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ 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';
|
||||
|
||||
class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout {
|
||||
class AcDeviceBatchControlView extends StatelessWidget
|
||||
with HelperResponsiveLayout {
|
||||
const AcDeviceBatchControlView({super.key, required this.devicesIds});
|
||||
|
||||
final List<String> devicesIds;
|
||||
@ -51,7 +52,7 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
|
||||
deviceId: devicesIds.first,
|
||||
code: 'switch',
|
||||
value: state.status.acSwitch,
|
||||
label: 'ThermoState',
|
||||
label: 'Thermostat',
|
||||
icon: Assets.ac,
|
||||
onChange: (value) {
|
||||
context.read<AcBloc>().add(AcBatchControlEvent(
|
||||
@ -100,8 +101,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
|
||||
),
|
||||
Text(
|
||||
'h',
|
||||
style:
|
||||
context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor),
|
||||
style: context.textTheme.bodySmall!
|
||||
.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
Text(
|
||||
'30',
|
||||
@ -148,7 +149,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
|
||||
callFactoryReset: () {
|
||||
context.read<AcBloc>().add(AcFactoryResetEvent(
|
||||
deviceId: state.status.uuid,
|
||||
factoryResetModel: FactoryResetModel(devicesUuid: devicesIds),
|
||||
factoryResetModel:
|
||||
FactoryResetModel(devicesUuid: devicesIds),
|
||||
));
|
||||
},
|
||||
),
|
||||
|
@ -60,4 +60,13 @@ class Status {
|
||||
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
Status copyWith({
|
||||
String? code,
|
||||
dynamic value,
|
||||
}) {
|
||||
return Status(
|
||||
code: code ?? this.code,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
children: [
|
||||
Expanded(child: SpaceTreeView(
|
||||
onSelect: () {
|
||||
context.read<DeviceManagementBloc>().add(ResetFilters());
|
||||
context.read<DeviceManagementBloc>().add(FetchDevices(context));
|
||||
},
|
||||
)),
|
||||
|
@ -62,9 +62,10 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
|
||||
BlocProvider.of<CurtainModuleBloc>(context),
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'CUR_2',
|
||||
category: 'Timer',
|
||||
code: 'control',
|
||||
|
||||
countdownCode: 'Timer',
|
||||
deviceType: 'CUR_2',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AccurteCalibratingDialog extends StatelessWidget {
|
||||
final String deviceId;
|
||||
@ -17,6 +18,7 @@ class AccurteCalibratingDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Calibrating',
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AccurateCalibrationDialog extends StatelessWidget {
|
||||
final String deviceId;
|
||||
@ -15,6 +16,7 @@ class AccurateCalibrationDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Accurate Calibration',
|
||||
|
@ -22,6 +22,7 @@ class AccurateDialogWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
@ -43,17 +44,22 @@ class AccurateDialogWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: body,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Divider(),
|
||||
const Expanded(child: Divider()),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
onTap: leftOnTap,
|
||||
child: Container(
|
||||
height: 40,
|
||||
@ -64,6 +70,9 @@ class AccurateDialogWidget extends StatelessWidget {
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
@ -74,6 +83,9 @@ class AccurateDialogWidget extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
onTap: rightOnTap,
|
||||
child: Container(
|
||||
height: 40,
|
||||
@ -84,6 +96,9 @@ class AccurateDialogWidget extends StatelessWidget {
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Next',
|
||||
|
@ -17,6 +17,7 @@ class CalibrateCompletedDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: SizedBox(
|
||||
height: 250,
|
||||
|
@ -28,7 +28,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 17,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -42,7 +42,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
|
||||
const Text('1. ',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 17,
|
||||
fontSize: 15,
|
||||
)),
|
||||
SizedBox(
|
||||
width: 450,
|
||||
@ -50,7 +50,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
|
||||
step1,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 17,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -67,13 +67,13 @@ class NormalTextBodyForDialog extends StatelessWidget {
|
||||
const Text('2. ',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 17,
|
||||
fontSize: 15,
|
||||
)),
|
||||
Text(
|
||||
step2,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 17,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -63,6 +63,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Calibrating',
|
||||
@ -71,7 +72,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 75),
|
||||
child: Text(
|
||||
@ -85,17 +86,21 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 110,
|
||||
width: 130,
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
color: ColorsManager.neutralGray.withValues(
|
||||
alpha: 0.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: NumberInputField(controller: _controller),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 5),
|
||||
child: NumberInputField(controller: _controller)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@ -127,7 +132,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
|
||||
),
|
||||
const Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Text(
|
||||
'2.click Next to Complete the calibration',
|
||||
style: TextStyle(color: ColorsManager.lightGrayColor),
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class QuickCalibrationDialog extends StatelessWidget {
|
||||
final int timControl;
|
||||
@ -17,6 +18,7 @@ class QuickCalibrationDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Quick Calibration',
|
||||
|
@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc
|
||||
emit(OneGangGlassSwitchLoading());
|
||||
try {
|
||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
_listenToChanges(event.deviceId, emit);
|
||||
_listenToChanges(event.deviceId);
|
||||
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
} catch (e) {
|
||||
@ -48,42 +48,28 @@ class OneGangGlassSwitchBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToChanges(
|
||||
String deviceId,
|
||||
Emitter<OneGangGlassSwitchState> emit,
|
||||
) {
|
||||
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||
|
||||
void _listenToChanges(String deviceId) {
|
||||
try {
|
||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
final stream = ref.onValue;
|
||||
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||
if (event.snapshot.value == null) return;
|
||||
|
||||
stream.listen((DatabaseEvent event) {
|
||||
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
||||
if (data == null) return;
|
||||
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||
|
||||
final statusList = <Status>[];
|
||||
if (data['status'] != null) {
|
||||
for (var element in data['status']) {
|
||||
statusList.add(
|
||||
Status(
|
||||
code: element['code'].toString(),
|
||||
value: element['value'].toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (statusList.isNotEmpty) {
|
||||
final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList);
|
||||
if (newStatus != deviceStatus) {
|
||||
deviceStatus = newStatus;
|
||||
if (!isClosed) {
|
||||
add(StatusUpdated(deviceStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
deviceStatus =
|
||||
OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
|
||||
add(StatusUpdated(deviceStatus));
|
||||
});
|
||||
} catch (e) {
|
||||
emit(OneGangGlassSwitchError('Failed to listen to changes: $e'));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _onStatusUpdated(
|
||||
@ -174,4 +160,10 @@ class OneGangGlassSwitchBloc
|
||||
deviceStatus = deviceStatus.copyWith(switch1: value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_deviceStatusSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_1',
|
||||
deviceUuid: deviceId,
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: '1GT',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -80,6 +80,8 @@ class WallLightDeviceControl extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_1',
|
||||
deviceUuid: deviceId,
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: '1G',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -277,6 +277,32 @@ class SmartPowerDeviceControl extends StatelessWidget
|
||||
totalConsumption: 10000,
|
||||
date: blocProvider.formattedDate,
|
||||
),
|
||||
EnergyConsumptionPage(
|
||||
formattedDate:
|
||||
'${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}',
|
||||
onTap: () {
|
||||
blocProvider.add(SelectDateEvent(context: context));
|
||||
},
|
||||
widget: blocProvider.dateSwitcher(),
|
||||
chartData: blocProvider.energyDataList.isNotEmpty
|
||||
? blocProvider.energyDataList
|
||||
: [
|
||||
EnergyData('12:00 AM', 4.0),
|
||||
EnergyData('01:00 AM', 6.5),
|
||||
EnergyData('02:00 AM', 3.8),
|
||||
EnergyData('03:00 AM', 3.2),
|
||||
EnergyData('04:00 AM', 6.0),
|
||||
EnergyData('05:00 AM', 3.4),
|
||||
EnergyData('06:00 AM', 5.2),
|
||||
EnergyData('07:00 AM', 3.5),
|
||||
EnergyData('08:00 AM', 6.8),
|
||||
EnergyData('09:00 AM', 5.6),
|
||||
EnergyData('10:00 AM', 3.9),
|
||||
EnergyData('11:00 AM', 4.0),
|
||||
],
|
||||
totalConsumption: 10000,
|
||||
date: blocProvider.formattedDate,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
final success = await RemoteControlDeviceService().controlDevice(
|
||||
deviceUuid: deviceId,
|
||||
status: Status(
|
||||
code: 'countdown_1',
|
||||
code: event.countdownCode,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
@ -80,15 +80,18 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
) {
|
||||
if (state is ScheduleLoaded) {
|
||||
final currentState = state as ScheduleLoaded;
|
||||
|
||||
emit(currentState.copyWith(
|
||||
countdownSeconds: currentState.countdownSeconds,
|
||||
selectedTime: currentState.selectedTime,
|
||||
deviceId: deviceId,
|
||||
scheduleMode: event.scheduleMode,
|
||||
countdownRemaining: Duration.zero,
|
||||
countdownHours: 0,
|
||||
countdownMinutes: 0,
|
||||
inchingHours: 0,
|
||||
inchingMinutes: 0,
|
||||
isCountdownActive: false,
|
||||
countdownHours: currentState.countdownHours,
|
||||
countdownMinutes: currentState.countdownMinutes,
|
||||
inchingHours: currentState.inchingHours,
|
||||
inchingMinutes: currentState.inchingMinutes,
|
||||
isInchingActive: false,
|
||||
isCountdownActive: currentState.countdownRemaining > Duration.zero,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -221,7 +224,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
deviceId,
|
||||
event.category,
|
||||
);
|
||||
|
||||
if (state is ScheduleLoaded) {
|
||||
final currentState = state as ScheduleLoaded;
|
||||
emit(currentState.copyWith(
|
||||
@ -230,7 +232,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
selectedDays: List.filled(7, false),
|
||||
functionOn: false,
|
||||
isEditing: false,
|
||||
countdownRemaining: Duration.zero,
|
||||
));
|
||||
} else {
|
||||
emit(ScheduleLoaded(
|
||||
@ -285,12 +286,22 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
) async {
|
||||
try {
|
||||
if (state is ScheduleLoaded) {
|
||||
Status status = Status(code: '', value: '');
|
||||
if (event.deviceType == 'CUR_2') {
|
||||
status = status.copyWith(
|
||||
code: 'control',
|
||||
value: event.functionOn == true ? 'open' : 'close');
|
||||
} else {
|
||||
status =
|
||||
status.copyWith(code: event.category, value: event.functionOn);
|
||||
}
|
||||
|
||||
final dateTime = DateTime.parse(event.time);
|
||||
final updatedSchedule = ScheduleEntry(
|
||||
scheduleId: event.scheduleId,
|
||||
category: event.category,
|
||||
time: getTimeStampWithoutSeconds(dateTime).toString(),
|
||||
function: Status(code: event.category, value: event.functionOn),
|
||||
function: status,
|
||||
days: event.selectedDays,
|
||||
);
|
||||
final success = await DevicesManagementApi().editScheduleRecord(
|
||||
@ -396,7 +407,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
final totalSeconds =
|
||||
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
|
||||
final code = event.mode == ScheduleModes.countdown
|
||||
? 'countdown_1'
|
||||
? event.countDownCode
|
||||
: 'switch_inching';
|
||||
final currentState = state as ScheduleLoaded;
|
||||
final duration = Duration(seconds: totalSeconds);
|
||||
@ -423,7 +434,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
);
|
||||
|
||||
if (success) {
|
||||
if (code == 'countdown_1') {
|
||||
if (code == event.countDownCode) {
|
||||
final countdownDuration = Duration(seconds: totalSeconds);
|
||||
|
||||
emit(
|
||||
@ -437,7 +448,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
);
|
||||
|
||||
if (countdownDuration.inSeconds > 0) {
|
||||
_startCountdownTimer(emit, countdownDuration);
|
||||
_startCountdownTimer(emit, countdownDuration, event.countDownCode);
|
||||
} else {
|
||||
_countdownTimer?.cancel();
|
||||
emit(
|
||||
@ -467,9 +478,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
}
|
||||
|
||||
void _startCountdownTimer(
|
||||
Emitter<ScheduleState> emit,
|
||||
Duration duration,
|
||||
) {
|
||||
Emitter<ScheduleState> emit, Duration duration, String countdownCode) {
|
||||
_countdownTimer?.cancel();
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
|
||||
@ -479,6 +488,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
} else {
|
||||
timer.cancel();
|
||||
add(StopScheduleEvent(
|
||||
countdownCode: countdownCode,
|
||||
mode: _currentCountdown == null
|
||||
? ScheduleModes.countdown
|
||||
: ScheduleModes.inching,
|
||||
@ -515,70 +525,75 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
try {
|
||||
final status =
|
||||
await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
print(status.status);
|
||||
int totalSeconds = 0;
|
||||
final countdownItem = status.status.firstWhere(
|
||||
(item) => item.code == event.countdownCode,
|
||||
orElse: () => Status(code: '', value: 0),
|
||||
);
|
||||
totalSeconds = (countdownItem.value as int?) ?? 0;
|
||||
final countdownHours = totalSeconds ~/ 3600;
|
||||
final countdownMinutes = (totalSeconds % 3600) ~/ 60;
|
||||
final countdownSeconds = totalSeconds % 60;
|
||||
|
||||
final deviceStatus =
|
||||
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
|
||||
final isCountdownActive = totalSeconds > 0;
|
||||
final isInchingActive = !isCountdownActive &&
|
||||
(deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0);
|
||||
|
||||
final scheduleMode =
|
||||
deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0
|
||||
? ScheduleModes.countdown
|
||||
: deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0
|
||||
? ScheduleModes.inching
|
||||
: ScheduleModes.schedule;
|
||||
final isCountdown = scheduleMode == ScheduleModes.countdown;
|
||||
final isInching = scheduleMode == ScheduleModes.inching;
|
||||
final newState = state is ScheduleLoaded
|
||||
? (state as ScheduleLoaded).copyWith(
|
||||
scheduleMode: ScheduleModes.schedule,
|
||||
countdownHours: countdownHours,
|
||||
countdownMinutes: countdownMinutes,
|
||||
countdownSeconds: countdownSeconds,
|
||||
inchingHours: deviceStatus.inchingHours,
|
||||
inchingMinutes: deviceStatus.inchingMinutes,
|
||||
isCountdownActive: isCountdownActive,
|
||||
isInchingActive: isInchingActive,
|
||||
countdownRemaining: isCountdownActive
|
||||
? Duration(seconds: totalSeconds)
|
||||
: Duration.zero,
|
||||
)
|
||||
: ScheduleLoaded(
|
||||
scheduleMode: ScheduleModes.schedule,
|
||||
schedules: const [],
|
||||
selectedTime: null,
|
||||
selectedDays: List.filled(7, false),
|
||||
functionOn: false,
|
||||
isEditing: false,
|
||||
deviceId: event.deviceId,
|
||||
countdownHours: countdownHours,
|
||||
countdownMinutes: countdownMinutes,
|
||||
countdownSeconds: countdownSeconds,
|
||||
inchingHours: deviceStatus.inchingHours,
|
||||
inchingMinutes: deviceStatus.inchingMinutes,
|
||||
isCountdownActive: isCountdownActive,
|
||||
isInchingActive: isInchingActive,
|
||||
countdownRemaining: isCountdownActive
|
||||
? Duration(seconds: totalSeconds)
|
||||
: Duration.zero,
|
||||
);
|
||||
emit(newState);
|
||||
|
||||
Duration? countdownRemaining;
|
||||
var isCountdownActive = false;
|
||||
var isInchingActive = false;
|
||||
if (isCountdownActive) {
|
||||
_countdownTimer?.cancel();
|
||||
_currentCountdown = Duration(seconds: totalSeconds);
|
||||
countdownRemaining = _currentCountdown!;
|
||||
|
||||
if (isCountdown) {
|
||||
countdownRemaining = Duration(
|
||||
hours: deviceStatus.countdownHours,
|
||||
minutes: deviceStatus.countdownMinutes,
|
||||
);
|
||||
isCountdownActive = countdownRemaining > Duration.zero;
|
||||
} else if (isInching) {
|
||||
isInchingActive = Duration(
|
||||
hours: deviceStatus.inchingHours,
|
||||
minutes: deviceStatus.inchingMinutes,
|
||||
) >
|
||||
Duration.zero;
|
||||
}
|
||||
if (state is ScheduleLoaded) {
|
||||
final currentState = state as ScheduleLoaded;
|
||||
emit(currentState.copyWith(
|
||||
scheduleMode: scheduleMode,
|
||||
countdownHours: deviceStatus.countdownHours,
|
||||
countdownMinutes: deviceStatus.countdownMinutes,
|
||||
inchingHours: deviceStatus.inchingHours,
|
||||
inchingMinutes: deviceStatus.inchingMinutes,
|
||||
isCountdownActive: isCountdownActive,
|
||||
isInchingActive: isInchingActive,
|
||||
countdownRemaining: countdownRemaining ?? Duration.zero,
|
||||
));
|
||||
if (totalSeconds > 0) {
|
||||
_startCountdownTimer(
|
||||
emit, Duration(seconds: totalSeconds), event.countdownCode);
|
||||
} else {
|
||||
add(StopScheduleEvent(
|
||||
countdownCode: event.countdownCode,
|
||||
mode: ScheduleModes.countdown,
|
||||
deviceId: event.deviceId,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
emit(ScheduleLoaded(
|
||||
schedules: const [],
|
||||
selectedTime: null,
|
||||
selectedDays: List.filled(7, false),
|
||||
functionOn: false,
|
||||
isEditing: false,
|
||||
deviceId: deviceId,
|
||||
scheduleMode: scheduleMode,
|
||||
countdownHours: deviceStatus.countdownHours,
|
||||
countdownMinutes: deviceStatus.countdownMinutes,
|
||||
inchingHours: deviceStatus.inchingHours,
|
||||
inchingMinutes: deviceStatus.inchingMinutes,
|
||||
isCountdownActive: isCountdownActive,
|
||||
isInchingActive: isInchingActive,
|
||||
countdownRemaining: countdownRemaining ?? Duration.zero,
|
||||
));
|
||||
_countdownTimer?.cancel();
|
||||
}
|
||||
|
||||
// if (isCountdownActive && countdownRemaining != null) {
|
||||
// _startCountdownTimer(emit, countdownRemaining);
|
||||
// }
|
||||
} catch (e) {
|
||||
emit(ScheduleError('Failed to fetch device status: $e'));
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
||||
final String time;
|
||||
final List<String> selectedDays;
|
||||
final bool functionOn;
|
||||
final String deviceType;
|
||||
|
||||
const ScheduleEditEvent({
|
||||
required this.scheduleId,
|
||||
@ -98,6 +99,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
||||
required this.time,
|
||||
required this.selectedDays,
|
||||
required this.functionOn,
|
||||
required this.deviceType,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -107,6 +109,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
||||
time,
|
||||
selectedDays,
|
||||
functionOn,
|
||||
deviceType,
|
||||
];
|
||||
}
|
||||
|
||||
@ -138,11 +141,13 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent {
|
||||
|
||||
class UpdateScheduleModeEvent extends ScheduleEvent {
|
||||
final ScheduleModes scheduleMode;
|
||||
final String countdownCode;
|
||||
|
||||
const UpdateScheduleModeEvent({required this.scheduleMode});
|
||||
const UpdateScheduleModeEvent(
|
||||
{required this.scheduleMode, required this.countdownCode});
|
||||
|
||||
@override
|
||||
List<Object> get props => [scheduleMode];
|
||||
List<Object> get props => [scheduleMode, countdownCode!];
|
||||
}
|
||||
|
||||
class UpdateCountdownTimeEvent extends ScheduleEvent {
|
||||
@ -177,28 +182,32 @@ class StartScheduleEvent extends ScheduleEvent {
|
||||
final ScheduleModes mode;
|
||||
final int hours;
|
||||
final int minutes;
|
||||
final String countDownCode;
|
||||
|
||||
const StartScheduleEvent({
|
||||
required this.mode,
|
||||
required this.hours,
|
||||
required this.minutes,
|
||||
required this.countDownCode,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mode, hours, minutes];
|
||||
List<Object?> get props => [mode, hours, minutes, countDownCode];
|
||||
}
|
||||
|
||||
class StopScheduleEvent extends ScheduleEvent {
|
||||
final ScheduleModes mode;
|
||||
final String deviceId;
|
||||
final String countdownCode;
|
||||
|
||||
const StopScheduleEvent({
|
||||
required this.mode,
|
||||
required this.deviceId,
|
||||
required this.countdownCode,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mode, deviceId];
|
||||
List<Object?> get props => [mode, deviceId, countdownCode];
|
||||
}
|
||||
|
||||
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
||||
@ -210,11 +219,13 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
||||
|
||||
class ScheduleFetchStatusEvent extends ScheduleEvent {
|
||||
final String deviceId;
|
||||
final String countdownCode;
|
||||
|
||||
const ScheduleFetchStatusEvent(this.deviceId);
|
||||
const ScheduleFetchStatusEvent(
|
||||
{required this.deviceId, required this.countdownCode});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
List<Object> get props => [deviceId, countdownCode];
|
||||
}
|
||||
|
||||
class DeleteScheduleEvent extends ScheduleEvent {
|
||||
|
@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState {
|
||||
final int inchingSeconds;
|
||||
final bool isInchingActive;
|
||||
final ScheduleModes scheduleMode;
|
||||
final Duration? countdownRemaining;
|
||||
final Duration countdownRemaining;
|
||||
final int? countdownSeconds;
|
||||
|
||||
const ScheduleLoaded({
|
||||
@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState {
|
||||
this.inchingMinutes = 0,
|
||||
this.isInchingActive = false,
|
||||
this.scheduleMode = ScheduleModes.countdown,
|
||||
this.countdownRemaining,
|
||||
this.countdownRemaining = Duration.zero,
|
||||
});
|
||||
|
||||
ScheduleLoaded copyWith({
|
||||
|
@ -11,6 +11,7 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
final String deviceId;
|
||||
final int hours;
|
||||
final int minutes;
|
||||
final String countDownCode;
|
||||
|
||||
const CountdownModeButtons({
|
||||
super.key,
|
||||
@ -18,6 +19,7 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
required this.deviceId,
|
||||
required this.hours,
|
||||
required this.minutes,
|
||||
required this.countDownCode,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -43,6 +45,7 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
StopScheduleEvent(
|
||||
mode: ScheduleModes.countdown,
|
||||
deviceId: deviceId,
|
||||
countdownCode: countDownCode,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -54,10 +57,10 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
onPressed: () {
|
||||
context.read<ScheduleBloc>().add(
|
||||
StartScheduleEvent(
|
||||
mode: ScheduleModes.countdown,
|
||||
hours: hours,
|
||||
minutes: minutes,
|
||||
),
|
||||
mode: ScheduleModes.countdown,
|
||||
hours: hours,
|
||||
minutes: minutes,
|
||||
countDownCode: countDownCode),
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.primaryColor,
|
||||
|
@ -75,23 +75,33 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
|
||||
final isActive =
|
||||
isCountDown ? state.isCountdownActive : state.isInchingActive;
|
||||
final displayHours = isActive && state.countdownRemaining != null
|
||||
? state.countdownRemaining!.inHours
|
||||
: (isCountDown ? state.countdownHours : state.inchingHours);
|
||||
final displayMinutes = isActive && state.countdownRemaining != null
|
||||
? state.countdownRemaining!.inMinutes.remainder(60)
|
||||
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
||||
final displaySeconds = isActive && state.countdownRemaining != null
|
||||
? state.countdownRemaining!.inSeconds.remainder(60)
|
||||
: (isCountDown ? state.countdownSeconds : state.inchingSeconds);
|
||||
|
||||
_updateControllers(displayHours, displayMinutes, displaySeconds!);
|
||||
final displayHours =
|
||||
isActive && state.countdownRemaining != Duration.zero
|
||||
? state.countdownRemaining.inHours
|
||||
: (isCountDown ? state.countdownHours : state.inchingHours);
|
||||
|
||||
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
|
||||
final displayMinutes =
|
||||
isActive && state.countdownRemaining != Duration.zero
|
||||
? state.countdownRemaining.inMinutes.remainder(60)
|
||||
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
||||
|
||||
final displaySeconds =
|
||||
isActive && state.countdownRemaining != Duration.zero
|
||||
? state.countdownRemaining.inSeconds.remainder(60)
|
||||
: (isCountDown ? (state.countdownSeconds ?? 0) : 0);
|
||||
|
||||
_updateControllers(displayHours, displayMinutes, displaySeconds);
|
||||
|
||||
if (isActive &&
|
||||
displayHours == 0 &&
|
||||
displayMinutes == 0 &&
|
||||
displaySeconds == 0) {
|
||||
context.read<ScheduleBloc>().add(
|
||||
StopScheduleEvent(
|
||||
mode: ScheduleModes.countdown,
|
||||
deviceId: widget.deviceId,
|
||||
countdownCode: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -43,7 +43,9 @@ class InchingModeButtons extends StatelessWidget {
|
||||
onPressed: () {
|
||||
context.read<ScheduleBloc>().add(
|
||||
StopScheduleEvent(
|
||||
deviceId: deviceId, mode: ScheduleModes.inching),
|
||||
deviceId: deviceId,
|
||||
mode: ScheduleModes.inching,
|
||||
countdownCode: ''),
|
||||
);
|
||||
},
|
||||
backgroundColor: Colors.red,
|
||||
|
@ -18,11 +18,15 @@ class BuildScheduleView extends StatelessWidget {
|
||||
super.key,
|
||||
required this.deviceUuid,
|
||||
required this.category,
|
||||
required this.countdownCode,
|
||||
this.code,
|
||||
required this.deviceType,
|
||||
});
|
||||
final String deviceUuid;
|
||||
final String category;
|
||||
final String? code;
|
||||
final String? countdownCode;
|
||||
final String deviceType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -31,7 +35,8 @@ class BuildScheduleView extends StatelessWidget {
|
||||
deviceId: deviceUuid,
|
||||
)
|
||||
..add(ScheduleGetEvent(category: category))
|
||||
..add(ScheduleFetchStatusEvent(deviceUuid)),
|
||||
..add(ScheduleFetchStatusEvent(
|
||||
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
|
||||
child: Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
@ -52,28 +57,32 @@ class BuildScheduleView extends StatelessWidget {
|
||||
children: [
|
||||
const ScheduleHeader(),
|
||||
const SizedBox(height: 20),
|
||||
ScheduleModeSelector(
|
||||
currentMode: state.scheduleMode,
|
||||
),
|
||||
if (deviceType == 'CUR_2')
|
||||
const SizedBox()
|
||||
else
|
||||
ScheduleModeSelector(
|
||||
countdownCode: countdownCode ?? '',
|
||||
currentMode: state.scheduleMode,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (state.scheduleMode == ScheduleModes.schedule)
|
||||
ScheduleManagementUI(
|
||||
deviceType: deviceType,
|
||||
category: category,
|
||||
deviceUuid: deviceUuid,
|
||||
onAddSchedule: () async {
|
||||
final entry = await ScheduleDialogHelper
|
||||
.showAddScheduleDialog(
|
||||
context,
|
||||
schedule: ScheduleEntry(
|
||||
category: category,
|
||||
time: '',
|
||||
function: Status(
|
||||
code: code.toString(), value: null),
|
||||
days: [],
|
||||
),
|
||||
isEdit: false,
|
||||
code: code,
|
||||
);
|
||||
.showAddScheduleDialog(context,
|
||||
schedule: ScheduleEntry(
|
||||
category: category,
|
||||
time: '',
|
||||
function: Status(
|
||||
code: code.toString(), value: null),
|
||||
days: [],
|
||||
),
|
||||
isEdit: false,
|
||||
code: code,
|
||||
deviceType: deviceType);
|
||||
if (entry != null) {
|
||||
context.read<ScheduleBloc>().add(
|
||||
ScheduleAddEvent(
|
||||
@ -87,14 +96,16 @@ class BuildScheduleView extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
if (state.scheduleMode == ScheduleModes.countdown ||
|
||||
state.scheduleMode == ScheduleModes.inching)
|
||||
CountdownInchingView(
|
||||
deviceId: deviceUuid,
|
||||
),
|
||||
if (deviceType != 'CUR_2')
|
||||
if (state.scheduleMode == ScheduleModes.countdown ||
|
||||
state.scheduleMode == ScheduleModes.inching)
|
||||
CountdownInchingView(
|
||||
deviceId: deviceUuid,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (state.scheduleMode == ScheduleModes.countdown)
|
||||
CountdownModeButtons(
|
||||
countDownCode: countdownCode ?? '',
|
||||
isActive: state.isCountdownActive,
|
||||
deviceId: deviceUuid,
|
||||
hours: state.countdownHours,
|
||||
|
@ -5,14 +5,16 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ScheduleManagementUI extends StatelessWidget {
|
||||
final String deviceUuid;
|
||||
final String deviceUuid;
|
||||
final VoidCallback onAddSchedule;
|
||||
final String category;
|
||||
final String deviceType;
|
||||
|
||||
const ScheduleManagementUI({
|
||||
super.key,
|
||||
required this.deviceUuid,
|
||||
required this.onAddSchedule,
|
||||
required this.deviceType,
|
||||
this.category = 'switch_1',
|
||||
});
|
||||
|
||||
@ -44,7 +46,11 @@ class ScheduleManagementUI extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ScheduleTableWidget(deviceUuid: deviceUuid, category: category),
|
||||
ScheduleTableWidget(
|
||||
deviceUuid: deviceUuid,
|
||||
category: category,
|
||||
deviceType: deviceType,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ScheduleModeSelector extends StatelessWidget {
|
||||
final ScheduleModes currentMode;
|
||||
final String countdownCode;
|
||||
|
||||
const ScheduleModeSelector({
|
||||
super.key,
|
||||
required this.currentMode,
|
||||
required this.countdownCode,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -71,7 +73,8 @@ class ScheduleModeSelector extends StatelessWidget {
|
||||
onChanged: (ScheduleModes? value) {
|
||||
if (value != null) {
|
||||
context.read<ScheduleBloc>().add(
|
||||
UpdateScheduleModeEvent(scheduleMode: value),
|
||||
UpdateScheduleModeEvent(
|
||||
scheduleMode: value, countdownCode: countdownCode),
|
||||
);
|
||||
if (value == ScheduleModes.schedule) {
|
||||
context.read<ScheduleBloc>().add(
|
||||
|
@ -12,11 +12,13 @@ import 'package:syncrow_web/utils/format_date_time.dart';
|
||||
class ScheduleTableWidget extends StatelessWidget {
|
||||
final String deviceUuid;
|
||||
final String category;
|
||||
final String deviceType;
|
||||
|
||||
const ScheduleTableWidget({
|
||||
super.key,
|
||||
required this.deviceUuid,
|
||||
this.category = 'switch_1',
|
||||
required this.deviceType,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -25,13 +27,14 @@ class ScheduleTableWidget extends StatelessWidget {
|
||||
create: (_) => ScheduleBloc(
|
||||
deviceId: deviceUuid,
|
||||
)..add(ScheduleGetEvent(category: category)),
|
||||
child: _ScheduleTableView(),
|
||||
child: _ScheduleTableView(deviceType),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScheduleTableView extends StatelessWidget {
|
||||
const _ScheduleTableView();
|
||||
final String deviceType;
|
||||
const _ScheduleTableView(this.deviceType);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -81,7 +84,7 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20)),
|
||||
),
|
||||
child: _buildTableBody(state.schedules, context));
|
||||
child: _buildTableBody(state.schedules, context, deviceType));
|
||||
}
|
||||
if (state is ScheduleError) {
|
||||
return Center(child: Text(state.error));
|
||||
@ -123,7 +126,8 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableBody(List<ScheduleModel> schedules, BuildContext context) {
|
||||
Widget _buildTableBody(
|
||||
List<ScheduleModel> schedules, BuildContext context, String deviceType) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: SingleChildScrollView(
|
||||
@ -132,7 +136,8 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
for (int i = 0; i < schedules.length; i++)
|
||||
_buildScheduleRow(schedules[i], i, context),
|
||||
_buildScheduleRow(schedules[i], i, context,
|
||||
deviceType: deviceType),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -155,25 +160,19 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
}
|
||||
|
||||
TableRow _buildScheduleRow(
|
||||
ScheduleModel schedule, int index, BuildContext context) {
|
||||
ScheduleModel schedule, int index, BuildContext context,
|
||||
{required String deviceType}) {
|
||||
return TableRow(
|
||||
children: [
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
bool temp;
|
||||
if (schedule.category == 'CUR_2') {
|
||||
temp = schedule.function.value == 'open' ? true : false;
|
||||
} else {
|
||||
temp = schedule.function.value as bool;
|
||||
}
|
||||
context.read<ScheduleBloc>().add(
|
||||
ScheduleUpdateEntryEvent(
|
||||
category: schedule.category,
|
||||
scheduleId: schedule.scheduleId,
|
||||
functionOn: temp,
|
||||
// schedule.function.value,
|
||||
functionOn: schedule.function.value,
|
||||
enable: !schedule.enable,
|
||||
),
|
||||
);
|
||||
@ -195,8 +194,9 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
child: Text(_getSelectedDays(
|
||||
ScheduleModel.parseSelectedDays(schedule.days)))),
|
||||
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
||||
if (schedule.category == 'CUR_2')
|
||||
Center(child: Text(schedule.function.value))
|
||||
if (deviceType == 'CUR_2')
|
||||
Center(
|
||||
child: Text(schedule.function.value == true ? 'open' : 'close'))
|
||||
else
|
||||
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
||||
Center(
|
||||
@ -206,18 +206,27 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||
onPressed: () {
|
||||
ScheduleDialogHelper.showAddScheduleDialog(
|
||||
context,
|
||||
schedule: ScheduleEntry.fromScheduleModel(schedule),
|
||||
isEdit: true,
|
||||
).then((updatedSchedule) {
|
||||
ScheduleDialogHelper.showAddScheduleDialog(context,
|
||||
schedule: ScheduleEntry.fromScheduleModel(schedule),
|
||||
isEdit: true,
|
||||
deviceType: deviceType)
|
||||
.then((updatedSchedule) {
|
||||
if (updatedSchedule != null) {
|
||||
bool temp;
|
||||
if (deviceType == 'CUR_2') {
|
||||
updatedSchedule.function.value == 'open'
|
||||
? temp = true
|
||||
: temp = false;
|
||||
} else {
|
||||
temp = updatedSchedule.function.value;
|
||||
}
|
||||
context.read<ScheduleBloc>().add(
|
||||
ScheduleEditEvent(
|
||||
deviceType: deviceType,
|
||||
scheduleId: schedule.scheduleId,
|
||||
category: schedule.category,
|
||||
time: updatedSchedule.time,
|
||||
functionOn: updatedSchedule.function.value,
|
||||
functionOn: temp,
|
||||
selectedDays: updatedSchedule.days),
|
||||
);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc
|
||||
emit(ThreeGangGlassSwitchLoading());
|
||||
try {
|
||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
_listenToChanges(event.deviceId, emit);
|
||||
_listenToChanges(event.deviceId);
|
||||
deviceStatus =
|
||||
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
@ -50,42 +50,28 @@ class ThreeGangGlassSwitchBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToChanges(
|
||||
String deviceId,
|
||||
Emitter<ThreeGangGlassSwitchState> emit,
|
||||
) {
|
||||
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||
|
||||
void _listenToChanges(String deviceId) {
|
||||
try {
|
||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
final stream = ref.onValue;
|
||||
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||
if (event.snapshot.value == null) return;
|
||||
|
||||
stream.listen((DatabaseEvent event) {
|
||||
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
||||
if (data == null) return;
|
||||
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||
|
||||
final statusList = <Status>[];
|
||||
if (data['status'] != null) {
|
||||
for (var element in data['status']) {
|
||||
statusList.add(
|
||||
Status(
|
||||
code: element['code'].toString(),
|
||||
value: element['value'].toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (statusList.isNotEmpty) {
|
||||
final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList);
|
||||
if (newStatus != deviceStatus) {
|
||||
deviceStatus = newStatus;
|
||||
if (!isClosed) {
|
||||
add(StatusUpdated(deviceStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
deviceStatus =
|
||||
ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
|
||||
add(StatusUpdated(deviceStatus));
|
||||
});
|
||||
} catch (e) {
|
||||
emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e'));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _onStatusUpdated(
|
||||
@ -184,4 +170,10 @@ class ThreeGangGlassSwitchBloc
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_deviceStatusSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +111,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_1',
|
||||
deviceUuid: deviceId,
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: '3GT',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -127,6 +129,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_2',
|
||||
deviceUuid: deviceId,
|
||||
countdownCode: 'countdown_2',
|
||||
deviceType: '3GT',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -143,6 +147,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_3',
|
||||
deviceUuid: deviceId,
|
||||
countdownCode: 'countdown_3',
|
||||
deviceType: '3GT',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -102,6 +102,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_1',
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: '3G',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -118,6 +120,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_2',
|
||||
countdownCode: 'countdown_2',
|
||||
deviceType: '3G',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -134,6 +138,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_3',
|
||||
countdownCode: 'countdown_3',
|
||||
deviceType: '3G',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -1,173 +1,177 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
|
||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||
import 'package:syncrow_web/services/control_device_service.dart';
|
||||
import 'package:syncrow_web/services/devices_mang_api.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
|
||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||
import 'package:syncrow_web/services/control_device_service.dart';
|
||||
import 'package:syncrow_web/services/devices_mang_api.dart';
|
||||
|
||||
part 'two_gang_glass_switch_event.dart';
|
||||
part 'two_gang_glass_switch_state.dart';
|
||||
part 'two_gang_glass_switch_event.dart';
|
||||
part 'two_gang_glass_switch_state.dart';
|
||||
|
||||
class TwoGangGlassSwitchBloc
|
||||
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
|
||||
final String deviceId;
|
||||
final ControlDeviceService controlDeviceService;
|
||||
final BatchControlDevicesService batchControlDevicesService;
|
||||
class TwoGangGlassSwitchBloc
|
||||
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
|
||||
final String deviceId;
|
||||
final ControlDeviceService controlDeviceService;
|
||||
final BatchControlDevicesService batchControlDevicesService;
|
||||
|
||||
late TwoGangGlassStatusModel deviceStatus;
|
||||
late TwoGangGlassStatusModel deviceStatus;
|
||||
|
||||
TwoGangGlassSwitchBloc({
|
||||
required this.deviceId,
|
||||
required this.controlDeviceService,
|
||||
required this.batchControlDevicesService,
|
||||
}) : super(TwoGangGlassSwitchInitial()) {
|
||||
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
|
||||
on<TwoGangGlassSwitchControl>(_onControl);
|
||||
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
|
||||
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
|
||||
on<TwoGangGlassFactoryReset>(_onFactoryReset);
|
||||
on<StatusUpdated>(_onStatusUpdated);
|
||||
}
|
||||
|
||||
Future<void> _onFetchDeviceStatus(
|
||||
TwoGangGlassSwitchFetchDeviceEvent event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
try {
|
||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||
_listenToChanges(event.deviceId);
|
||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
} catch (e) {
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
TwoGangGlassSwitchBloc({
|
||||
required this.deviceId,
|
||||
required this.controlDeviceService,
|
||||
required this.batchControlDevicesService,
|
||||
}) : super(TwoGangGlassSwitchInitial()) {
|
||||
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
|
||||
on<TwoGangGlassSwitchControl>(_onControl);
|
||||
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
|
||||
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
|
||||
on<TwoGangGlassFactoryReset>(_onFactoryReset);
|
||||
on<StatusUpdated>(_onStatusUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToChanges(String deviceId) {
|
||||
try {
|
||||
final ref = FirebaseDatabase.instance.ref(
|
||||
'device-status/$deviceId',
|
||||
);
|
||||
|
||||
ref.onValue.listen((event) {
|
||||
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
|
||||
|
||||
List<Status> statusList = [];
|
||||
eventsMap['status'].forEach((element) {
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList);
|
||||
add(StatusUpdated(deviceStatus));
|
||||
});
|
||||
} catch (_) {
|
||||
log(
|
||||
'Error listening to changes',
|
||||
name: 'TwoGangGlassSwitchBloc._listenToChanges',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onControl(
|
||||
TwoGangGlassSwitchControl event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
_updateLocalValue(event.code, event.value);
|
||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: event.code, value: event.value),
|
||||
);
|
||||
} catch (e) {
|
||||
_updateLocalValue(event.code, !event.value);
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onBatchControl(
|
||||
TwoGangGlassSwitchBatchControl event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
_updateLocalValue(event.code, event.value);
|
||||
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
||||
|
||||
try {
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.deviceIds,
|
||||
code: event.code,
|
||||
value: event.value,
|
||||
);
|
||||
} catch (e) {
|
||||
_updateLocalValue(event.code, !event.value);
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onFetchBatchStatus(
|
||||
TwoGangGlassSwitchFetchBatchStatusEvent event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
try {
|
||||
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
|
||||
deviceStatus = TwoGangGlassStatusModel.fromJson(
|
||||
event.deviceIds.first,
|
||||
status.status,
|
||||
);
|
||||
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
||||
} catch (e) {
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onFactoryReset(
|
||||
TwoGangGlassFactoryReset event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
try {
|
||||
final response = await DevicesManagementApi().factoryReset(
|
||||
event.factoryReset,
|
||||
event.deviceId,
|
||||
);
|
||||
if (!response) {
|
||||
emit(TwoGangGlassSwitchError('Failed to reset device'));
|
||||
} else {
|
||||
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
|
||||
Future<void> _onFetchDeviceStatus(
|
||||
TwoGangGlassSwitchFetchDeviceEvent event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
try {
|
||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||
_listenToChanges(event.deviceId);
|
||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
} catch (e) {
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onStatusUpdated(
|
||||
StatusUpdated event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) {
|
||||
deviceStatus = event.deviceStatus;
|
||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
}
|
||||
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||
|
||||
void _updateLocalValue(String code, bool value) {
|
||||
switch (code) {
|
||||
case 'switch_1':
|
||||
deviceStatus = deviceStatus.copyWith(switch1: value);
|
||||
break;
|
||||
case 'switch_2':
|
||||
deviceStatus = deviceStatus.copyWith(switch2: value);
|
||||
break;
|
||||
void _listenToChanges(String deviceId) {
|
||||
try {
|
||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||
if (event.snapshot.value == null) return;
|
||||
|
||||
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||
|
||||
final statusList = <Status>[];
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
deviceStatus =
|
||||
TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
|
||||
add(StatusUpdated(deviceStatus));
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _onControl(
|
||||
TwoGangGlassSwitchControl event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
_updateLocalValue(event.code, event.value);
|
||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: event.code, value: event.value),
|
||||
);
|
||||
} catch (e) {
|
||||
_updateLocalValue(event.code, !event.value);
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onBatchControl(
|
||||
TwoGangGlassSwitchBatchControl event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
_updateLocalValue(event.code, event.value);
|
||||
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
||||
|
||||
try {
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.deviceIds,
|
||||
code: event.code,
|
||||
value: event.value,
|
||||
);
|
||||
} catch (e) {
|
||||
_updateLocalValue(event.code, !event.value);
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onFetchBatchStatus(
|
||||
TwoGangGlassSwitchFetchBatchStatusEvent event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
try {
|
||||
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
|
||||
deviceStatus = TwoGangGlassStatusModel.fromJson(
|
||||
event.deviceIds.first,
|
||||
status.status,
|
||||
);
|
||||
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
||||
} catch (e) {
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onFactoryReset(
|
||||
TwoGangGlassFactoryReset event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) async {
|
||||
emit(TwoGangGlassSwitchLoading());
|
||||
try {
|
||||
final response = await DevicesManagementApi().factoryReset(
|
||||
event.factoryReset,
|
||||
event.deviceId,
|
||||
);
|
||||
if (!response) {
|
||||
emit(TwoGangGlassSwitchError('Failed to reset device'));
|
||||
} else {
|
||||
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(TwoGangGlassSwitchError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onStatusUpdated(
|
||||
StatusUpdated event,
|
||||
Emitter<TwoGangGlassSwitchState> emit,
|
||||
) {
|
||||
deviceStatus = event.deviceStatus;
|
||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||
}
|
||||
|
||||
void _updateLocalValue(String code, bool value) {
|
||||
switch (code) {
|
||||
case 'switch_1':
|
||||
deviceStatus = deviceStatus.copyWith(switch1: value);
|
||||
break;
|
||||
case 'switch_2':
|
||||
deviceStatus = deviceStatus.copyWith(switch2: value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_deviceStatusSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
||||
child: BuildScheduleView(
|
||||
deviceType: '2GT',
|
||||
countdownCode: 'countdown_1',
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_1',
|
||||
),
|
||||
@ -118,6 +120,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
||||
child: BuildScheduleView(
|
||||
deviceType: '2GT',
|
||||
countdownCode: 'countdown_2',
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_2',
|
||||
),
|
||||
|
@ -97,6 +97,8 @@ class TwoGangBatchControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_1',
|
||||
deviceUuid: deviceIds.first,
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: '2G',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -114,6 +116,8 @@ class TwoGangBatchControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
category: 'switch_2',
|
||||
deviceUuid: deviceIds.first,
|
||||
countdownCode: 'countdown_2',
|
||||
deviceType: '2G',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -121,10 +125,7 @@ class TwoGangBatchControlView extends StatelessWidget
|
||||
subtitle: 'Scheduling',
|
||||
iconPath: Assets.scheduling,
|
||||
),
|
||||
// FirmwareUpdateWidget(
|
||||
// deviceId: deviceIds.first,
|
||||
// version: 12,
|
||||
// ),
|
||||
|
||||
FactoryResetWidget(callFactoryReset: () {
|
||||
context.read<TwoGangSwitchBloc>().add(
|
||||
TwoGangFactoryReset(
|
||||
|
@ -103,6 +103,8 @@ class TwoGangDeviceControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_1',
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: '2G',
|
||||
),
|
||||
));
|
||||
},
|
||||
@ -125,6 +127,8 @@ class TwoGangDeviceControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'switch_2',
|
||||
countdownCode: 'countdown_2',
|
||||
deviceType: '2G',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -18,9 +18,10 @@ class ScheduleDialogHelper {
|
||||
ScheduleEntry? schedule,
|
||||
bool isEdit = false,
|
||||
String? code,
|
||||
required String deviceType,
|
||||
}) {
|
||||
bool temp;
|
||||
if (schedule?.category == 'CUR_2') {
|
||||
if (deviceType == 'CUR_2') {
|
||||
temp = schedule!.function.value == 'open' ? true : false;
|
||||
} else {
|
||||
temp = schedule!.function.value;
|
||||
@ -103,8 +104,7 @@ class ScheduleDialogHelper {
|
||||
setState(() => selectedDays[i] = v);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
|
||||
(v) {
|
||||
_buildFunctionSwitch(deviceType, ctx, functionOn!, (v) {
|
||||
setState(() => functionOn = v);
|
||||
}),
|
||||
],
|
||||
@ -120,32 +120,29 @@ class ScheduleDialogHelper {
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
dynamic temp;
|
||||
if (schedule?.category == 'CUR_2') {
|
||||
temp = functionOn! ? 'open' : 'close';
|
||||
} else {
|
||||
temp = functionOn;
|
||||
}
|
||||
print(temp);
|
||||
final entry = ScheduleEntry(
|
||||
category: schedule?.category ?? 'switch_1',
|
||||
time: _formatTimeOfDayToISO(selectedTime),
|
||||
function: Status(
|
||||
code: code ?? 'switch_1',
|
||||
value: temp,
|
||||
// functionOn,
|
||||
),
|
||||
days: _convertSelectedDaysToStrings(selectedDays),
|
||||
scheduleId: schedule?.scheduleId,
|
||||
);
|
||||
Navigator.pop(ctx, entry);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
width: 100,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
dynamic temp;
|
||||
if (deviceType == 'CUR_2') {
|
||||
temp = functionOn! ? 'open' : 'close';
|
||||
} else {
|
||||
temp = functionOn;
|
||||
}
|
||||
final entry = ScheduleEntry(
|
||||
category: schedule?.category ?? 'switch_1',
|
||||
time: _formatTimeOfDayToISO(selectedTime),
|
||||
function: Status(
|
||||
code: code ?? 'switch_1',
|
||||
value: temp,
|
||||
),
|
||||
days: _convertSelectedDaysToStrings(selectedDays),
|
||||
scheduleId: schedule.scheduleId,
|
||||
);
|
||||
Navigator.pop(ctx, entry);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -84,6 +84,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: device.uuid ?? '',
|
||||
category: 'switch_1',
|
||||
countdownCode: 'countdown_1',
|
||||
deviceType: 'WH',
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
Future<void> checkEmail(
|
||||
CheckEmailEvent event, Emitter<UsersState> emit) async {
|
||||
emit(UsersLoadingState());
|
||||
String? res = await UserPermissionApi().checkEmail(
|
||||
String? res = await UserPermissionApi().checkEmail(
|
||||
emailController.text,
|
||||
);
|
||||
checkEmailValid = res!;
|
||||
|
@ -34,7 +34,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(20))),
|
||||
width: 900,
|
||||
child: Column(
|
||||
children: [
|
||||
@ -63,7 +64,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
children: [
|
||||
_buildStep1Indicator(1, "Basics", _blocRole),
|
||||
_buildStep2Indicator(2, "Spaces", _blocRole),
|
||||
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
|
||||
_buildStep3Indicator(
|
||||
3, "Role & Permissions", _blocRole),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -105,18 +107,32 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final isBasicsStep = currentStep == 1;
|
||||
|
||||
if (isBasicsStep) {
|
||||
// Validate the form first
|
||||
final isValid = _blocRole.formKey.currentState
|
||||
?.validate() ??
|
||||
false;
|
||||
|
||||
if (!isValid)
|
||||
return; // Stop if form is not valid
|
||||
}
|
||||
_blocRole.add(const CheckEmailEvent());
|
||||
|
||||
setState(() {
|
||||
if (currentStep < 3) {
|
||||
currentStep++;
|
||||
if (currentStep == 2) {
|
||||
_blocRole.add(const CheckStepStatus(isEditUser: false));
|
||||
_blocRole.add(const CheckStepStatus(
|
||||
isEditUser: false));
|
||||
} else if (currentStep == 3) {
|
||||
_blocRole.add(const CheckSpacesStepStatus());
|
||||
_blocRole
|
||||
.add(const CheckSpacesStepStatus());
|
||||
}
|
||||
} else {
|
||||
_blocRole.add(SendInviteUsers(context: context));
|
||||
_blocRole
|
||||
.add(SendInviteUsers(context: context));
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -124,8 +140,11 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
currentStep < 3 ? "Next" : "Save",
|
||||
style: TextStyle(
|
||||
color: (_blocRole.isCompleteSpaces == false ||
|
||||
_blocRole.isCompleteBasics == false ||
|
||||
_blocRole.isCompleteRolePermissions == false) &&
|
||||
_blocRole.isCompleteBasics ==
|
||||
false ||
|
||||
_blocRole
|
||||
.isCompleteRolePermissions ==
|
||||
false) &&
|
||||
currentStep == 3
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.secondaryColor),
|
||||
@ -143,7 +162,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
Widget _getFormContent() {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return const BasicsView(
|
||||
return BasicsView(
|
||||
userId: '',
|
||||
);
|
||||
case 2:
|
||||
@ -196,8 +215,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
||||
color: currentStep == step
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -260,8 +283,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
||||
color: currentStep == step
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -318,8 +345,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
||||
color: currentStep == step
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl_phone_field/countries.dart';
|
||||
import 'package:intl_phone_field/country_picker_dialog.dart';
|
||||
import 'package:intl_phone_field/intl_phone_field.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
@ -11,7 +14,9 @@ import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class BasicsView extends StatelessWidget {
|
||||
final String? userId;
|
||||
const BasicsView({super.key, this.userId = ''});
|
||||
Timer? _debounce;
|
||||
|
||||
BasicsView({super.key, this.userId = ''});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
|
||||
@ -21,6 +26,7 @@ class BasicsView extends StatelessWidget {
|
||||
}
|
||||
return Form(
|
||||
key: _blocRole.formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
@ -208,6 +214,14 @@ class BasicsView extends StatelessWidget {
|
||||
fontSize: 12,
|
||||
color: ColorsManager.textGray),
|
||||
),
|
||||
|
||||
onChanged: (value) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 800), () {
|
||||
_blocRole.add(const CheckEmailEvent());
|
||||
});
|
||||
},
|
||||
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Enter Email Address';
|
||||
|
@ -32,113 +32,114 @@ class SpaceDropdown extends StatelessWidget {
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
child: Container(
|
||||
DropdownButton2<String>(
|
||||
underline: const SizedBox(),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
decoration:
|
||||
BoxDecoration(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
value: selectedValue,
|
||||
items: spaces.map((space) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: space.uuid,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
' ${space.name}',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: selectedValue == space.uuid
|
||||
? ColorsManager.dialogBlueTitle
|
||||
: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' ${space.lastThreeParents}',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 12,
|
||||
color: selectedValue == space.uuid
|
||||
? ColorsManager.dialogBlueTitle
|
||||
: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 13,
|
||||
),
|
||||
hint: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
hintMessage,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
customButton: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.textGray, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
selectedValue != null
|
||||
? spaces
|
||||
.firstWhere((e) => e.uuid == selectedValue)
|
||||
.name
|
||||
: hintMessage,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 13,
|
||||
color: selectedValue != null
|
||||
? Colors.black
|
||||
: ColorsManager.textGray,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
height: 45,
|
||||
child: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: DropdownButton2<String>(
|
||||
underline: const SizedBox(),
|
||||
value: selectedValue,
|
||||
items: spaces.map((space) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: space.uuid,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
' ${space.name}',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' ${space.lastThreeParents}',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
style: TextStyle(color: Colors.black),
|
||||
hint: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
hintMessage,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
customButton: Container(
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: ColorsManager.textGray, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
selectedValue != null
|
||||
? spaces
|
||||
.firstWhere((e) => e.uuid == selectedValue)
|
||||
.name
|
||||
: hintMessage,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: selectedValue != null
|
||||
? Colors.black
|
||||
: ColorsManager.textGray,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
height: 45,
|
||||
child: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 60,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 60,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -121,7 +121,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
child:
|
||||
CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -159,8 +160,9 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
Image.asset(
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Image.asset(
|
||||
Assets.logo,
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
@ -203,7 +205,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
||||
maxLines: 1,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: widget.isSmallScreenSize(context) ? 10 : 12,
|
||||
fontSize:
|
||||
widget.isSmallScreenSize(context) ? 10 : 12,
|
||||
),
|
||||
),
|
||||
if (widget.spaceName != '')
|
||||
@ -222,8 +225,9 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
||||
maxLines: 1,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize:
|
||||
widget.isSmallScreenSize(context) ? 10 : 12,
|
||||
fontSize: widget.isSmallScreenSize(context)
|
||||
? 10
|
||||
: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -58,11 +58,14 @@ class ProductModel {
|
||||
'3G': Assets.Gang3SwitchIcon,
|
||||
'3GT': Assets.threeTouchSwitch,
|
||||
'CUR': Assets.curtain,
|
||||
'CUR_2': Assets.curtain,
|
||||
'GD': Assets.garageDoor,
|
||||
'GW': Assets.SmartGatewayIcon,
|
||||
'DL': Assets.DoorLockIcon,
|
||||
'WL': Assets.waterLeakSensor,
|
||||
'WH': Assets.waterHeater,
|
||||
'WM': Assets.waterLeakSensor,
|
||||
'SOS': Assets.sos,
|
||||
'AC': Assets.ac,
|
||||
'CPS': Assets.presenceSensor,
|
||||
'PC': Assets.powerClamp,
|
||||
|
@ -21,7 +21,6 @@ import 'package:syncrow_web/utils/snack_bar.dart';
|
||||
|
||||
class VisitorPasswordBloc
|
||||
extends Bloc<VisitorPasswordEvent, VisitorPasswordState> {
|
||||
|
||||
VisitorPasswordBloc() : super(VisitorPasswordInitial()) {
|
||||
on<SelectUsageFrequency>(selectUsageFrequency);
|
||||
on<FetchDevice>(_onFetchDevice);
|
||||
@ -87,6 +86,9 @@ class VisitorPasswordBloc
|
||||
SelectTimeVisitorPassword event,
|
||||
Emitter<VisitorPasswordState> emit,
|
||||
) async {
|
||||
// Ensure expirationTimeTimeStamp has a value
|
||||
effectiveTimeTimeStamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: event.context,
|
||||
initialDate: DateTime.now(),
|
||||
@ -94,86 +96,124 @@ class VisitorPasswordBloc
|
||||
lastDate: DateTime.now().add(const Duration(days: 5095)),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
final TimeOfDay? timePicked = await showTimePicker(
|
||||
context: event.context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: ColorsManager.primaryColor,
|
||||
onSurface: Colors.black,
|
||||
),
|
||||
buttonTheme: const ButtonThemeData(
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Colors.green,
|
||||
),
|
||||
),
|
||||
if (picked == null) return;
|
||||
|
||||
final TimeOfDay? timePicked = await showTimePicker(
|
||||
context: event.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!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (timePicked != null) {
|
||||
final selectedDateTime = DateTime(
|
||||
picked.year,
|
||||
picked.month,
|
||||
picked.day,
|
||||
timePicked.hour,
|
||||
timePicked.minute,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final selectedTimestamp =
|
||||
selectedDateTime.millisecondsSinceEpoch ~/ 1000;
|
||||
if (timePicked == null) return;
|
||||
|
||||
if (event.isStart) {
|
||||
if (expirationTimeTimeStamp != null &&
|
||||
selectedTimestamp > expirationTimeTimeStamp!) {
|
||||
CustomSnackBar.displaySnackBar(
|
||||
final selectedDateTime = DateTime(
|
||||
picked.year,
|
||||
picked.month,
|
||||
picked.day,
|
||||
timePicked.hour,
|
||||
timePicked.minute,
|
||||
);
|
||||
final selectedTimestamp = selectedDateTime.millisecondsSinceEpoch ~/ 1000;
|
||||
final currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
if (event.isStart) {
|
||||
// START TIME VALIDATION
|
||||
if (expirationTimeTimeStamp != null &&
|
||||
selectedTimestamp > expirationTimeTimeStamp!) {
|
||||
await showDialog<void>(
|
||||
context: event.context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text(
|
||||
'Effective Time cannot be later than Expiration Time.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
||||
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
||||
await showDialog<void>(
|
||||
context: event.context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Effective Time cannot be earlier than current time.'),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
content:
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(event.context).pop();
|
||||
add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false));
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
effectiveTimeTimeStamp = selectedTimestamp;
|
||||
startTimeAccess = selectedDateTime.toString().split('.').first;
|
||||
} else {
|
||||
if (effectiveTimeTimeStamp != null &&
|
||||
selectedTimestamp < effectiveTimeTimeStamp!) {
|
||||
CustomSnackBar.displaySnackBar(
|
||||
'Expiration Time cannot be earlier than Effective Time.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
expirationTimeTimeStamp = selectedTimestamp;
|
||||
endTimeAccess = selectedDateTime.toString().split('.').first;
|
||||
}
|
||||
emit(ChangeTimeState());
|
||||
emit(VisitorPasswordInitial());
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
content: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(event.context).pop();
|
||||
add(SelectTimeVisitorPassword(
|
||||
context: event.context,
|
||||
isStart: true,
|
||||
isRepeat: false,
|
||||
));
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedTimestamp < currentTimestamp) {
|
||||
await showDialog<void>(
|
||||
context: event.context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text(
|
||||
'Effective Time cannot be earlier than current time.',
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
content: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(event.context).pop();
|
||||
add(SelectTimeVisitorPassword(
|
||||
context: event.context,
|
||||
isStart: true,
|
||||
isRepeat: false,
|
||||
));
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save effective time
|
||||
effectiveTimeTimeStamp = selectedTimestamp;
|
||||
startTimeAccess = selectedDateTime.toString().split('.').first;
|
||||
} else {
|
||||
|
||||
if (effectiveTimeTimeStamp != null &&
|
||||
selectedTimestamp < effectiveTimeTimeStamp!) {
|
||||
await showDialog<void>(
|
||||
context: event.context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text(
|
||||
'Expiration Time cannot be earlier than Effective Time.',
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
content: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(event.context).pop();
|
||||
add(SelectTimeVisitorPassword(
|
||||
context: event.context,
|
||||
isStart: false,
|
||||
isRepeat: false,
|
||||
));
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save expiration time
|
||||
expirationTimeTimeStamp = selectedTimestamp;
|
||||
endTimeAccess = selectedDateTime.toString().split('.').first;
|
||||
}
|
||||
|
||||
emit(ChangeTimeState());
|
||||
emit(VisitorPasswordInitial());
|
||||
}
|
||||
|
||||
bool toggleRepeat(
|
||||
@ -213,7 +253,7 @@ class VisitorPasswordBloc
|
||||
FetchDevice event, Emitter<VisitorPasswordState> emit) async {
|
||||
try {
|
||||
emit(DeviceLoaded());
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
|
||||
data = await AccessMangApi().fetchDoorLockDeviceList(projectUuid);
|
||||
emit(TableLoaded(data));
|
||||
|