Compare commits
217 Commits
On-power-c
...
c473325883
Author | SHA1 | Date | |
---|---|---|---|
c473325883 | |||
c8eb07c166 | |||
518e9c8914 | |||
9754b3b589 | |||
327be5aa54 | |||
b12903059d | |||
729b0caac3 | |||
ebfdfe1762 | |||
631766e0ec | |||
a55ff24257 | |||
0cb2875672 | |||
d12b4c0c65 | |||
22c8c54fab | |||
95300071e9 | |||
04fcf0a140 | |||
9396f37fea | |||
0e4d37ccac | |||
06f00da02c | |||
7876af9756 | |||
fe2f4a872b | |||
c9b8fbb0c2 | |||
7b5b40a03c | |||
8522c0bbc3 | |||
c6729f476f | |||
fc70669f1d | |||
9d130139f7 | |||
bee4e05404 | |||
f03c28f7fd | |||
6db60a2a97 | |||
7f9f39811b | |||
739b491bd8 | |||
6ec972a520 | |||
62f67f5a5f | |||
3e634dc7a2 | |||
8e303af0d7 | |||
db157f30c5 | |||
4ef4858cee | |||
9a203b2fd9 | |||
39c5fd1bca | |||
308eb65d46 | |||
6b8827f4d9 | |||
e8c36f5af6 | |||
762dade195 | |||
f5def4b4d7 | |||
ab326fa820 | |||
e9b4d35f97 | |||
d65f9ceea9 | |||
f539b0ac8d | |||
5a3cf93748 | |||
e740652507 | |||
c60078c96a | |||
903c5dd29b | |||
df39fca050 | |||
f832c5d884 | |||
fa930571dc | |||
75b9f4a4e6 | |||
fe4063ef8f | |||
acefe7b355 | |||
029b5d32e0 | |||
b223194950 | |||
428c81efff | |||
288c252f46 | |||
7399dee687 | |||
08e2ed4b4c | |||
59e04708cd | |||
338d4f5737 | |||
466f5b89c7 | |||
de5d8df01c | |||
5532935a3a | |||
5218641705 | |||
ab6a6851f2 | |||
249cbfc242 | |||
8167926620 | |||
beb33e37fa | |||
3bee17c574 | |||
f4b5c6fb52 | |||
086f3cedf8 | |||
559091faa0 | |||
035c03c6b2 | |||
eba351c9be | |||
cf1b34ee0a | |||
c112cde634 | |||
5663e2084e | |||
3cd0125310 | |||
e0980b324c | |||
65d541d594 | |||
7331c8440b | |||
a409e34643 | |||
7cc59e43df | |||
2681c837f5 | |||
b6664ec1ba | |||
a9895f5462 | |||
6e4f0c3c0c | |||
bbf2891804 | |||
aab2b4a52a | |||
d58da9644f | |||
df46a5b905 | |||
a1fa049a05 | |||
494a000590 | |||
b5d72b2a2a | |||
55a73eee7f | |||
21f8b2962c | |||
645a07287e | |||
df29aab111 | |||
e55e793081 | |||
885ef61114 | |||
bfd6b5c3a0 | |||
2b638940ae | |||
3e95bf4473 | |||
2d16bda61d | |||
5c90d5f6b9 | |||
d6a48850a7 | |||
6cac94a1c4 | |||
9f28e1ccef | |||
83202204b0 | |||
d87739f1fd | |||
b128618bfd | |||
5cd083a37b | |||
6534bfae5b | |||
2b8d987c69 | |||
707cb4791f | |||
03c45ed8d0 | |||
9e0ea4ad6f | |||
9e6b14737f | |||
7c2aed2d58 | |||
bcf62027bc | |||
b001713ce4 | |||
bab3226c73 | |||
fa1eaa570c | |||
4cfb984d2c | |||
42c410d982 | |||
368b1be3c0 | |||
c13119a4e8 | |||
35e9b606b2 | |||
387586f6f7 | |||
7cf4d0b5a9 | |||
e4a27b5651 | |||
f89660a9ff | |||
1a3dc60bd2 | |||
201348a9bf | |||
e2d4e48875 | |||
4c06479469 | |||
3101960201 | |||
ddfd4ee153 | |||
7f0484eec6 | |||
dc7064d142 | |||
e523a83912 | |||
e917225c3d | |||
66ed30b50c | |||
47bd6ff89e | |||
138390496c | |||
df87e41d61 | |||
f0bfe085a4 | |||
bb846f797f | |||
e234c9f3b2 | |||
bcd0ae4a2a | |||
cebce2ce7f | |||
97e3fb68bf | |||
46a7add90d | |||
73de1e6ff9 | |||
826dea8054 | |||
fdea4b1cd0 | |||
823d86fd80 | |||
dd735032ea | |||
6dcc851d97 | |||
15b36fd052 | |||
a4024067c7 | |||
95cded4bf5 | |||
757a96ed9f | |||
b857736e10 | |||
1fccd51440 | |||
c07ddb0ccd | |||
58e99f95b2 | |||
227df6fe3d | |||
9451ec0cc4 | |||
fc797c2646 | |||
318e1d9af7 | |||
d47dc349bc | |||
c221c8499f | |||
71cf4b9feb | |||
c43cf9347f | |||
9990b1805e | |||
50f8158830 | |||
009b7c0316 | |||
72af55ef98 | |||
779c0fe916 | |||
e448eabda6 | |||
9dfb3ed369 | |||
63353af38b | |||
68b6c9b18c | |||
fa6ee9a0af | |||
3601b02bc3 | |||
fdd0526c78 | |||
b888f516e2 | |||
bdeec7d325 | |||
50ff17a0c1 | |||
87c2e3261d | |||
62a6f9c993 | |||
c1e61ee61d | |||
7750290be4 | |||
f7e4d6ff07 | |||
7f26c773a7 | |||
1adbae6735 | |||
ede2da6632 | |||
b06e4bd2ba | |||
0847cb8a41 | |||
818bdee745 | |||
0a022d8a8d | |||
f33b3e8bd2 | |||
8f0eb88567 | |||
19739c6e4d | |||
9f86b8d638 | |||
95907661d2 | |||
9c9b7d99dc | |||
c07bae5cbc | |||
e6fe9f35b0 | |||
dcf1df9b4a |
@ -1,2 +1,3 @@
|
||||
ENV_NAME=development
|
||||
BASE_URL=https://syncrow-dev.azurewebsites.net
|
||||
BASE_URL=https://syncrow-dev.azurewebsites.net
|
||||
RTDB_URL=https://syncrow-dev-79446.asia-southeast1.firebasedatabase.app/
|
||||
|
@ -1,2 +1,3 @@
|
||||
ENV_NAME=production
|
||||
BASE_URL=https://syncrow-staging.azurewebsites.net
|
||||
BASE_URL=https://syncrow-staging.azurewebsites.net
|
||||
RTDB_URL=https://syncrow-prod-79446.asia-southeast1.firebasedatabase.app/
|
||||
|
@ -1,2 +1,3 @@
|
||||
ENV_NAME=staging
|
||||
BASE_URL=https://syncrow-staging.azurewebsites.net
|
||||
BASE_URL=https://syncrow-staging.azurewebsites.net
|
||||
RTDB_URL=https://syncrow-staging-79446.asia-southeast1.firebasedatabase.app/
|
||||
|
112
.vscode/launch.json
vendored
@ -1,67 +1,49 @@
|
||||
{
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
|
||||
"name": "DEVELOPMENT",
|
||||
|
||||
"request": "launch",
|
||||
|
||||
"type": "dart",
|
||||
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_dev.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
||||
},{
|
||||
|
||||
"name": "STAGING",
|
||||
|
||||
"request": "launch",
|
||||
|
||||
"type": "dart",
|
||||
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_staging.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
||||
},{
|
||||
|
||||
"name": "PRODUCTION",
|
||||
|
||||
"request": "launch",
|
||||
|
||||
"type": "dart",
|
||||
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
||||
},
|
||||
|
||||
]
|
||||
"configurations": [
|
||||
{
|
||||
"name": "DEVELOPMENT",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_dev.dart",
|
||||
"--web-experimental-hot-reload"
|
||||
],
|
||||
"flutterMode": "debug"
|
||||
},
|
||||
{
|
||||
"name": "STAGING",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_staging.dart",
|
||||
"--web-experimental-hot-reload"
|
||||
],
|
||||
"flutterMode": "debug"
|
||||
},
|
||||
{
|
||||
"name": "PRODUCTION",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main.dart",
|
||||
"--web-experimental-hot-reload"
|
||||
],
|
||||
"flutterMode": "debug"
|
||||
}
|
||||
]
|
||||
}
|
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 |
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 |
5
assets/icons/x_delete.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 34.9999C27.1649 34.9999 34.9999 27.1649 34.9999 17.4999C34.9999 7.83499 27.1649 0 17.5 0C7.83499 0 0 7.83499 0 17.5C0 27.1651 7.83499 34.9999 17.5 34.9999Z" fill="#FF6465"/>
|
||||
<path opacity="0.1" d="M4.70804 17.5C4.70804 8.63343 11.3024 1.30805 19.854 0.158115C19.0839 0.0545507 18.2984 0 17.5 0C7.835 0 0 7.835 0 17.5C0 27.1651 7.83499 35 17.4999 35C18.2983 35 19.0839 34.9455 19.8539 34.8419C11.3024 33.6919 4.70804 26.3665 4.70804 17.5Z" fill="black"/>
|
||||
<path d="M21.4229 17.5003L26.0301 12.8931C26.365 12.5582 26.365 12.0152 26.0301 11.6804L23.3197 8.96992C22.9848 8.63503 22.4418 8.63503 22.107 8.96992L17.4997 13.5772L12.8924 8.96992C12.5576 8.63503 12.0146 8.63503 11.6798 8.96992L8.96931 11.6804C8.63442 12.0153 8.63442 12.5582 8.96931 12.8931L13.5766 17.5003L8.96931 22.1076C8.63442 22.4425 8.63442 22.9855 8.96931 23.3204L11.6798 26.0308C12.0146 26.3657 12.5576 26.3657 12.8924 26.0308L17.4997 21.4235L22.1071 26.0308C22.442 26.3657 22.9849 26.3657 23.3198 26.0308L26.0302 23.3204C26.3651 22.9855 26.3651 22.4425 26.0302 22.1076L21.4229 17.5003Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -1 +1 @@
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"test2-8a3d2","configurations":{"android":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","ios":"1:427332280600:ios:14346b200780dc760c7e6d","macos":"1:427332280600:ios:14346b200780dc760c7e6d","web":"1:427332280600:web:ad50516a87a35a1a0c7e6d","windows":"1:427332280600:web:f7a25537ccd5a7bd0c7e6d"}}}}}}
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"syncrow-prod-79446","configurations":{"web":"1:255001682464:web:a03e2d6214c13101561245"}}}}}}
|
16
lib/firebase_options.dart
Normal file
@ -0,0 +1,16 @@
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
|
||||
final class DefaultFirebaseOptions extends FirebaseOptions {
|
||||
const DefaultFirebaseOptions({
|
||||
required String databaseUrl,
|
||||
}) : super(
|
||||
apiKey: 'AIzaSyDgq5ywsnFVbbQO-Xz1Z4sR5bBcuiDaS9g',
|
||||
appId: '1:255001682464:web:a03e2d6214c13101561245',
|
||||
messagingSenderId: '255001682464',
|
||||
projectId: 'syncrow-prod-79446',
|
||||
authDomain: 'syncrow-prod-79446.firebaseapp.com',
|
||||
storageBucket: 'syncrow-prod-79446.firebasestorage.app',
|
||||
databaseURL: databaseUrl,
|
||||
measurementId: 'G-1850Q89RMK',
|
||||
);
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
// File generated by FlutterFire CLI.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'firebase_options.dart';
|
||||
/// // ...
|
||||
/// await Firebase.initializeApp(
|
||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||
/// );
|
||||
/// ```
|
||||
class DefaultFirebaseOptionsDev {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
return web;
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return ios;
|
||||
case TargetPlatform.macOS:
|
||||
return macos;
|
||||
case TargetPlatform.windows:
|
||||
return windows;
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
apiKey: 'AIzaSyCVEvKsJYzhWDFM-9Od68FE0nPpP933st0',
|
||||
appId: '1:427332280600:web:ad50516a87a35a1a0c7e6d',
|
||||
messagingSenderId: '427332280600',
|
||||
projectId: 'test2-8a3d2',
|
||||
authDomain: 'test2-8a3d2.firebaseapp.com',
|
||||
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'test2-8a3d2.firebasestorage.app',
|
||||
measurementId: 'G-Z1RTTTV5H9',
|
||||
);
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
apiKey: 'AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0',
|
||||
appId: '1:427332280600:android:2bc36fbe82994a3e0c7e6d',
|
||||
messagingSenderId: '427332280600',
|
||||
projectId: 'test2-8a3d2',
|
||||
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'test2-8a3d2.firebasestorage.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions ios = FirebaseOptions(
|
||||
apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw',
|
||||
appId: '1:427332280600:ios:14346b200780dc760c7e6d',
|
||||
messagingSenderId: '427332280600',
|
||||
projectId: 'test2-8a3d2',
|
||||
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'test2-8a3d2.firebasestorage.app',
|
||||
iosBundleId: 'com.example.syncrowWeb',
|
||||
);
|
||||
|
||||
static const FirebaseOptions macos = FirebaseOptions(
|
||||
apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw',
|
||||
appId: '1:427332280600:ios:14346b200780dc760c7e6d',
|
||||
messagingSenderId: '427332280600',
|
||||
projectId: 'test2-8a3d2',
|
||||
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'test2-8a3d2.firebasestorage.app',
|
||||
iosBundleId: 'com.example.syncrowWeb',
|
||||
);
|
||||
|
||||
static const FirebaseOptions windows = FirebaseOptions(
|
||||
apiKey: 'AIzaSyDizKjPC5rdkEjDxwXjM-RU5unB0Ziq3iw',
|
||||
appId: '1:427332280600:web:f7a25537ccd5a7bd0c7e6d',
|
||||
messagingSenderId: '427332280600',
|
||||
projectId: 'test2-8a3d2',
|
||||
authDomain: 'test2-8a3d2.firebaseapp.com',
|
||||
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'test2-8a3d2.firebasestorage.app',
|
||||
measurementId: 'G-4LFVXEXWKY',
|
||||
);
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
// File generated by FlutterFire CLI.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'firebase_options.dart';
|
||||
/// // ...
|
||||
/// await Firebase.initializeApp(
|
||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||
/// );
|
||||
/// ```
|
||||
class DefaultFirebaseOptionsStaging {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
return web;
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return ios;
|
||||
case TargetPlatform.macOS:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for macos - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.windows:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for windows - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
apiKey: 'AIzaSyDP9GpYfLE8gHTj3kZ1hW8fx_FkJqOqSQk',
|
||||
appId: '1:786692570726:android:0ef7079c2b978d4417b7a7',
|
||||
messagingSenderId: '786692570726',
|
||||
projectId: 'syncrow-staging',
|
||||
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'syncrow-staging.appspot.com',
|
||||
);
|
||||
|
||||
static const FirebaseOptions ios = FirebaseOptions(
|
||||
apiKey: 'AIzaSyAWlRiuJ75FMlf2_UDdri1voWKvkaSHtRg',
|
||||
appId: '1:786692570726:ios:455a6fcff77e130f17b7a7',
|
||||
messagingSenderId: '786692570726',
|
||||
projectId: 'syncrow-staging',
|
||||
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'syncrow-staging.appspot.com',
|
||||
iosBundleId: 'com.example.syncrow.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
apiKey: 'AIzaSyDyGaQ3sZhb4meaY6sGke-YglhdhJ2is8Q',
|
||||
appId: '1:786692570726:web:93c931e6701797b317b7a7',
|
||||
messagingSenderId: '786692570726',
|
||||
projectId: 'syncrow-staging',
|
||||
authDomain: 'syncrow-staging.firebaseapp.com',
|
||||
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
|
||||
storageBucket: 'syncrow-staging.appspot.com',
|
||||
measurementId: 'G-CZ3J3G6LMQ',
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/firebase_options_prod.dart';
|
||||
import 'package:syncrow_web/firebase_options.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
@ -27,7 +27,9 @@ Future<void> main() async {
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptionsStaging.currentPlatform,
|
||||
options: DefaultFirebaseOptions(
|
||||
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||
),
|
||||
);
|
||||
initialSetup();
|
||||
} catch (_) {}
|
||||
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/firebase_options_dev.dart';
|
||||
import 'package:syncrow_web/firebase_options.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
@ -27,7 +27,9 @@ Future<void> main() async {
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptionsDev.currentPlatform,
|
||||
options: DefaultFirebaseOptions(
|
||||
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||
),
|
||||
);
|
||||
initialSetup();
|
||||
} catch (_) {}
|
||||
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/firebase_options_prod.dart';
|
||||
import 'package:syncrow_web/firebase_options.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
@ -24,7 +24,9 @@ Future<void> main() async {
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptionsStaging.currentPlatform,
|
||||
options: DefaultFirebaseOptions(
|
||||
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||
),
|
||||
);
|
||||
initialSetup();
|
||||
} catch (_) {}
|
||||
@ -56,7 +58,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
||||
import 'package:syncrow_web/services/access_mang_api.dart';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
|
||||
abstract class AccessState extends Equatable {
|
||||
const AccessState();
|
||||
|
@ -0,0 +1,63 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
|
||||
class MemoryCalendarService implements CalendarSystemService {
|
||||
final Map<String, CalendarEventsResponse> _eventsCache = {};
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final key = params.generateKey();
|
||||
|
||||
return _eventsCache[key]!;
|
||||
}
|
||||
|
||||
void setEvents(
|
||||
LoadEventsParam param,
|
||||
CalendarEventsResponse events,
|
||||
) {
|
||||
final key = param.generateKey();
|
||||
_eventsCache[key] = events;
|
||||
}
|
||||
|
||||
void addEvent(LoadEventsParam param, CalendarEventsResponse event) {
|
||||
final key = param.generateKey();
|
||||
|
||||
_eventsCache[key] = event;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_eventsCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryCalendarServiceWithRemoteFallback implements CalendarSystemService {
|
||||
final MemoryCalendarService memoryService;
|
||||
final RemoteCalendarService remoteService;
|
||||
|
||||
MemoryCalendarServiceWithRemoteFallback({
|
||||
required this.memoryService,
|
||||
required this.remoteService,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final key = params.generateKey();
|
||||
final doesExistInMemory = memoryService._eventsCache.containsKey(key);
|
||||
|
||||
if (doesExistInMemory) {
|
||||
return memoryService.getCalendarEvents(params: params);
|
||||
} else {
|
||||
final remoteResult =
|
||||
await remoteService.getCalendarEvents(params: params);
|
||||
memoryService.setEvents(params, remoteResult);
|
||||
|
||||
return remoteResult;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.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 BookableSystemService {
|
||||
const RemoteBookableSpacesService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
static const _defaultErrorMessage = 'Failed to load bookable spaces';
|
||||
|
||||
@override
|
||||
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||
required LoadBookableSpacesParam param,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'page': param.page,
|
||||
'size': param.size,
|
||||
'active': true,
|
||||
'configured': true,
|
||||
if (param.search != null &&
|
||||
param.search.isNotEmpty &&
|
||||
param.search != 'null')
|
||||
'search': param.search,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
return PaginatedBookableSpaces.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final responseData = e.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
final errorMessage = responseData['error']?['message'] as String? ??
|
||||
responseData['message'] as String? ??
|
||||
_defaultErrorMessage;
|
||||
throw APIException(errorMessage);
|
||||
}
|
||||
throw APIException(_defaultErrorMessage);
|
||||
} catch (e) {
|
||||
throw APIException('$_defaultErrorMessage: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.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 RemoteCalendarService implements CalendarSystemService {
|
||||
const RemoteCalendarService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
static const _defaultErrorMessage = 'Failed to load Calendar';
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final month = params.startDate.month.toString().padLeft(2, '0');
|
||||
final year = params.startDate.year.toString();
|
||||
|
||||
try {
|
||||
return await _httpService.get<CalendarEventsResponse>(
|
||||
path: ApiEndpoints.getBookings
|
||||
.replaceAll('{mm}', month)
|
||||
.replaceAll('{yyyy}', year)
|
||||
.replaceAll('{space}', params.id),
|
||||
expectedResponseModel: (json) {
|
||||
return CalendarEventsResponse.fromJson(json as Map<String, dynamic>);
|
||||
},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final responseData = e.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
final errorMessage = responseData['error']?['message'] as String? ??
|
||||
responseData['message'] as String? ??
|
||||
_defaultErrorMessage;
|
||||
throw APIException(errorMessage);
|
||||
}
|
||||
throw APIException(_defaultErrorMessage);
|
||||
} catch (e) {
|
||||
throw APIException('$_defaultErrorMessage: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class LoadEventsParam extends Equatable {
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String id;
|
||||
|
||||
const LoadEventsParam({
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [startDate, endDate, id];
|
||||
|
||||
LoadEventsParam copyWith({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
String? id,
|
||||
}) {
|
||||
return LoadEventsParam(
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
id: id ?? this.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyGenerator on LoadEventsParam {
|
||||
String generateKey() {
|
||||
return '$id-${startDate.year}-${startDate.month.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class LoadBookableSpacesParam extends Equatable {
|
||||
const LoadBookableSpacesParam({
|
||||
this.page = 1,
|
||||
this.size = 25,
|
||||
this.search = '',
|
||||
this.active = true,
|
||||
this.configured = true,
|
||||
});
|
||||
|
||||
final int page;
|
||||
final int size;
|
||||
final String search;
|
||||
final bool active;
|
||||
final bool configured;
|
||||
|
||||
LoadBookableSpacesParam copyWith({
|
||||
int? page,
|
||||
int? size,
|
||||
String? search,
|
||||
bool? active,
|
||||
bool? configured,
|
||||
}) {
|
||||
return LoadBookableSpacesParam(
|
||||
page: page ?? this.page,
|
||||
size: size ?? this.size,
|
||||
search: search ?? this.search,
|
||||
active: active ?? this.active,
|
||||
configured: configured ?? this.configured,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, search, active, configured];
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
class BookableSpaceModel {
|
||||
final String uuid;
|
||||
final String spaceName;
|
||||
final String virtualLocation;
|
||||
final BookableConfig bookableConfig;
|
||||
|
||||
BookableSpaceModel({
|
||||
required this.uuid,
|
||||
required this.spaceName,
|
||||
required this.virtualLocation,
|
||||
required this.bookableConfig,
|
||||
});
|
||||
|
||||
factory BookableSpaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return BookableSpaceModel(
|
||||
uuid: json['uuid'] as String,
|
||||
spaceName: json['spaceName'] as String,
|
||||
virtualLocation: json['virtualLocation'] as String,
|
||||
bookableConfig: BookableConfig.fromJson(
|
||||
json['bookableConfig'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookableConfig {
|
||||
final String uuid;
|
||||
final List<String> daysAvailable;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final bool active;
|
||||
final int points;
|
||||
|
||||
BookableConfig({
|
||||
required this.uuid,
|
||||
required this.daysAvailable,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.active,
|
||||
required this.points,
|
||||
});
|
||||
|
||||
factory BookableConfig.fromJson(Map<String, dynamic> json) {
|
||||
return BookableConfig(
|
||||
uuid: json['uuid'] as String,
|
||||
daysAvailable: (json['daysAvailable'] as List).cast<String>(),
|
||||
startTime: json['startTime'] as String,
|
||||
endTime: json['endTime'] as String,
|
||||
active: json['active'] as bool,
|
||||
points: json['points'] as int,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
class CalendarEventBooking {
|
||||
final String uuid;
|
||||
final DateTime date;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final int cost;
|
||||
final BookingUser user;
|
||||
final BookingSpace space;
|
||||
|
||||
CalendarEventBooking({
|
||||
required this.uuid,
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.cost,
|
||||
required this.user,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
factory CalendarEventBooking.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEventBooking(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
date: json['date'] != null
|
||||
? DateTime.parse(json['date'] as String)
|
||||
: DateTime.now(),
|
||||
startTime: json['startTime'] as String? ?? '',
|
||||
endTime: json['endTime'] as String? ?? '',
|
||||
cost: _parseInt(json['cost']),
|
||||
user: json['user'] != null
|
||||
? BookingUser.fromJson(json['user'] as Map<String, dynamic>)
|
||||
: BookingUser.empty(),
|
||||
space: json['space'] != null
|
||||
? BookingSpace.fromJson(json['space'] as Map<String, dynamic>)
|
||||
: BookingSpace.empty(),
|
||||
);
|
||||
}
|
||||
|
||||
static int _parseInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class BookingUser {
|
||||
final String uuid;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
final String? companyName;
|
||||
|
||||
BookingUser({
|
||||
required this.uuid,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
this.companyName,
|
||||
});
|
||||
|
||||
factory BookingUser.fromJson(Map<String, dynamic> json) {
|
||||
return BookingUser(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
firstName: json['firstName'] as String? ?? '',
|
||||
lastName: json['lastName'] as String? ?? '',
|
||||
email: json['email'] as String? ?? '',
|
||||
companyName: json['companyName'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
factory BookingUser.empty() {
|
||||
return BookingUser(
|
||||
uuid: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
companyName: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookingSpace {
|
||||
final String uuid;
|
||||
final String spaceName;
|
||||
|
||||
BookingSpace({
|
||||
required this.uuid,
|
||||
required this.spaceName,
|
||||
});
|
||||
|
||||
factory BookingSpace.fromJson(Map<String, dynamic> json) {
|
||||
return BookingSpace(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
spaceName: json['spaceName'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
factory BookingSpace.empty() {
|
||||
return BookingSpace(
|
||||
uuid: '',
|
||||
spaceName: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarEventsResponse {
|
||||
final int statusCode;
|
||||
final String message;
|
||||
final List<CalendarEventBooking> data;
|
||||
final bool success;
|
||||
|
||||
CalendarEventsResponse({
|
||||
required this.statusCode,
|
||||
required this.message,
|
||||
required this.data,
|
||||
required this.success,
|
||||
});
|
||||
|
||||
factory CalendarEventsResponse.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEventsResponse(
|
||||
statusCode: _parseInt(json['statusCode']),
|
||||
message: json['message'] as String? ?? '',
|
||||
data: (json['data'] as List? ?? [])
|
||||
.map((e) => CalendarEventBooking.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
success: json['success'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _parseInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
|
||||
class PaginatedBookableSpaces {
|
||||
final List<BookableSpaceModel> data;
|
||||
final String message;
|
||||
final int page;
|
||||
final int size;
|
||||
final int totalItem;
|
||||
final int totalPage;
|
||||
final bool hasNext;
|
||||
final bool hasPrevious;
|
||||
|
||||
PaginatedBookableSpaces({
|
||||
required this.data,
|
||||
required this.message,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.totalItem,
|
||||
required this.totalPage,
|
||||
required this.hasNext,
|
||||
required this.hasPrevious,
|
||||
});
|
||||
|
||||
factory PaginatedBookableSpaces.fromJson(Map<String, dynamic> json) {
|
||||
return PaginatedBookableSpaces(
|
||||
data: (json['data'] as List)
|
||||
.map((item) => BookableSpaceModel.fromJson(item))
|
||||
.toList(),
|
||||
message: json['message'] as String,
|
||||
page: json['page'] as int,
|
||||
size: json['size'] as int,
|
||||
totalItem: json['totalItem'] as int,
|
||||
totalPage: json['totalPage'] as int,
|
||||
hasNext: json['hasNext'] as bool,
|
||||
hasPrevious: json['hasPrevious'] as bool,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||
|
||||
abstract class BookableSystemService {
|
||||
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||
required LoadBookableSpacesParam param,
|
||||
});
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
|
||||
abstract class CalendarSystemService {
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
});
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'dart:async';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
|
||||
|
||||
class DebouncedBookableSpacesService implements BookableSystemService {
|
||||
final BookableSystemService _inner;
|
||||
final Duration debounceDuration;
|
||||
|
||||
Timer? _debounceTimer;
|
||||
Completer<PaginatedBookableSpaces>? _lastCompleter;
|
||||
|
||||
DebouncedBookableSpacesService(
|
||||
this._inner, {
|
||||
this.debounceDuration = const Duration(milliseconds: 500),
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||
required LoadBookableSpacesParam param,
|
||||
}) {
|
||||
_debounceTimer?.cancel();
|
||||
if (_lastCompleter != null && !_lastCompleter!.isCompleted) {
|
||||
_lastCompleter!.completeError(StateError("Cancelled by new search"));
|
||||
}
|
||||
|
||||
final completer = Completer<PaginatedBookableSpaces>();
|
||||
_lastCompleter = completer;
|
||||
|
||||
_debounceTimer = Timer(debounceDuration, () async {
|
||||
try {
|
||||
final result = await _inner.getBookableSpaces(param: param);
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(result);
|
||||
}
|
||||
} catch (e, st) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(e, st);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
|
||||
part 'events_event.dart';
|
||||
part 'events_state.dart';
|
||||
|
||||
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
final EventController eventController = EventController();
|
||||
final CalendarSystemService calendarService;
|
||||
|
||||
CalendarEventsBloc({
|
||||
required this.calendarService,
|
||||
}) : super(EventsInitial()) {
|
||||
on<LoadEvents>(_onLoadEvents);
|
||||
on<AddEvent>(_onAddEvent);
|
||||
on<DisposeResources>(_onDisposeResources);
|
||||
on<GoToWeek>(_onGoToWeek);
|
||||
}
|
||||
Future<void> _onLoadEvents(
|
||||
LoadEvents event,
|
||||
Emitter<CalendarEventState> emit,
|
||||
) async {
|
||||
final param = event.param;
|
||||
final month = param.endDate.month;
|
||||
final year = param.endDate.year;
|
||||
final spaceId = param.id;
|
||||
|
||||
emit(EventsLoading());
|
||||
try {
|
||||
final response = await calendarService.getCalendarEvents(params: param);
|
||||
|
||||
final events = response.data.map(_toCalendarEventData).toList();
|
||||
eventController.addAll(events);
|
||||
emit(EventsLoaded(
|
||||
events: events,
|
||||
spaceId: spaceId,
|
||||
month: month,
|
||||
year: year,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EventsError('Failed to load events'));
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
|
||||
eventController.add(event.event);
|
||||
|
||||
if (state is EventsLoaded) {
|
||||
final loaded = state as EventsLoaded;
|
||||
|
||||
emit(EventsLoaded(
|
||||
events: [...eventController.events],
|
||||
spaceId: loaded.spaceId,
|
||||
month: loaded.month,
|
||||
year: loaded.year,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onDisposeResources(
|
||||
DisposeResources event, Emitter<CalendarEventState> emit) {
|
||||
eventController.dispose();
|
||||
}
|
||||
|
||||
void _onGoToWeek(GoToWeek event, Emitter<CalendarEventState> emit) {
|
||||
if (state is EventsLoaded) {
|
||||
final loaded = state as EventsLoaded;
|
||||
final newWeekDays = _getWeekDays(event.weekDate);
|
||||
emit(EventsLoaded(
|
||||
events: loaded.events,
|
||||
spaceId: loaded.spaceId,
|
||||
month: loaded.month,
|
||||
year: loaded.year,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
|
||||
final date = booking.date;
|
||||
|
||||
final localDate = date.toLocal();
|
||||
|
||||
final startParts = booking.startTime.split(':').map(int.parse).toList();
|
||||
final endParts = booking.endTime.split(':').map(int.parse).toList();
|
||||
|
||||
final startTime = DateTime(
|
||||
localDate.year,
|
||||
localDate.month,
|
||||
localDate.day,
|
||||
startParts[0],
|
||||
startParts[1],
|
||||
);
|
||||
|
||||
final endTime = DateTime(
|
||||
localDate.year,
|
||||
localDate.month,
|
||||
localDate.day,
|
||||
endParts[0],
|
||||
endParts[1],
|
||||
);
|
||||
|
||||
return CalendarEventData(
|
||||
date: startTime,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
title: '${booking.user.firstName} ${booking.user.lastName}',
|
||||
description: 'Cost: ${booking.cost}',
|
||||
color: Colors.blue,
|
||||
event: booking,
|
||||
);
|
||||
}
|
||||
|
||||
List<DateTime> _getWeekDays(DateTime date) {
|
||||
final int weekday = date.weekday;
|
||||
final DateTime monday = date.subtract(Duration(days: weekday - 1));
|
||||
return List.generate(7, (i) => monday.add(Duration(days: i)));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
eventController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'events_bloc.dart';
|
||||
|
||||
@immutable
|
||||
abstract class CalendarEventsEvent {
|
||||
const CalendarEventsEvent();
|
||||
}
|
||||
|
||||
class LoadEvents extends CalendarEventsEvent {
|
||||
final LoadEventsParam param;
|
||||
const LoadEvents(this.param);
|
||||
}
|
||||
|
||||
|
||||
class AddEvent extends CalendarEventsEvent {
|
||||
final CalendarEventData event;
|
||||
const AddEvent(this.event);
|
||||
}
|
||||
|
||||
class StartTimer extends CalendarEventsEvent {}
|
||||
|
||||
class DisposeResources extends CalendarEventsEvent {}
|
||||
|
||||
class GoToWeek extends CalendarEventsEvent {
|
||||
final DateTime weekDate;
|
||||
GoToWeek(this.weekDate);
|
||||
}
|
||||
|
||||
class CheckWeekHasEvents extends CalendarEventsEvent {
|
||||
final DateTime weekStart;
|
||||
const CheckWeekHasEvents(this.weekStart);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
part of 'events_bloc.dart';
|
||||
|
||||
@immutable
|
||||
abstract class CalendarEventState {}
|
||||
|
||||
class EventsInitial extends CalendarEventState {}
|
||||
|
||||
class EventsLoading extends CalendarEventState {}
|
||||
|
||||
final class EventsLoaded extends CalendarEventState {
|
||||
final List<CalendarEventData> events;
|
||||
final String spaceId;
|
||||
final int month;
|
||||
final int year;
|
||||
|
||||
EventsLoaded({
|
||||
required this.events,
|
||||
required this.spaceId,
|
||||
required this.month,
|
||||
required this.year,
|
||||
});
|
||||
}
|
||||
|
||||
class EventsError extends CalendarEventState {
|
||||
final String message;
|
||||
EventsError(this.message);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
|
||||
import 'date_selection_state.dart';
|
||||
|
||||
|
||||
class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
|
||||
DateSelectionBloc() : super(DateSelectionState.initial()) {
|
||||
on<SelectDate>((event, emit) {
|
||||
final newWeekStart = _getStartOfWeek(event.selectedDate);
|
||||
emit(state.copyWith(
|
||||
selectedDate: event.selectedDate,
|
||||
weekStart: newWeekStart,
|
||||
));
|
||||
});
|
||||
|
||||
on<NextWeek>((event, emit) {
|
||||
final newWeekStart = state.weekStart.add(const Duration(days: 7));
|
||||
emit(state.copyWith(
|
||||
weekStart: newWeekStart,
|
||||
));
|
||||
});
|
||||
|
||||
on<PreviousWeek>((event, emit) {
|
||||
final newWeekStart = state.weekStart.subtract(const Duration(days: 7));
|
||||
emit(state.copyWith(
|
||||
weekStart: newWeekStart,
|
||||
));
|
||||
});
|
||||
|
||||
on<SelectDateFromSidebarCalendar>((event, emit) {
|
||||
emit(state.copyWith(
|
||||
selectedDateFromSideBarCalender: event.selectedDate,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
static DateTime _getStartOfWeek(DateTime date) {
|
||||
return date.subtract(Duration(days: date.weekday - 1));
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
|
||||
abstract class DateSelectionEvent {
|
||||
const DateSelectionEvent();
|
||||
}
|
||||
|
||||
class SelectDate extends DateSelectionEvent {
|
||||
final DateTime selectedDate;
|
||||
const SelectDate(this.selectedDate);
|
||||
}
|
||||
|
||||
class NextWeek extends DateSelectionEvent {}
|
||||
|
||||
class PreviousWeek extends DateSelectionEvent {}
|
||||
|
||||
class SelectDateFromSidebarCalendar extends DateSelectionEvent {
|
||||
final DateTime selectedDate;
|
||||
SelectDateFromSidebarCalendar(this.selectedDate);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
|
||||
class DateSelectionState {
|
||||
final DateTime selectedDate;
|
||||
final DateTime weekStart;
|
||||
final DateTime? selectedDateFromSideBarCalender;
|
||||
|
||||
DateSelectionState({
|
||||
required this.selectedDate,
|
||||
required this.weekStart,
|
||||
this.selectedDateFromSideBarCalender,
|
||||
});
|
||||
|
||||
factory DateSelectionState.initial() {
|
||||
final now = DateTime.now();
|
||||
final weekStart = now.subtract(Duration(days: now.weekday - 1));
|
||||
return DateSelectionState(
|
||||
selectedDate: now,
|
||||
weekStart: weekStart,
|
||||
selectedDateFromSideBarCalender: null,
|
||||
);
|
||||
}
|
||||
|
||||
DateSelectionState copyWith({
|
||||
DateTime? selectedDate,
|
||||
DateTime? weekStart,
|
||||
DateTime? selectedDateFromSideBarCalender,
|
||||
}) {
|
||||
return DateSelectionState(
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
weekStart: weekStart ?? this.weekStart,
|
||||
selectedDateFromSideBarCalender: selectedDateFromSideBarCalender ?? this.selectedDateFromSideBarCalender,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
part 'selected_bookable_space_event.dart';
|
||||
part 'selected_bookable_space_state.dart';
|
||||
|
||||
class SelectedBookableSpaceBloc
|
||||
extends Bloc<SelectedBookableSpaceEvent, SelectedBookableSpaceState> {
|
||||
SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) {
|
||||
on<SelectBookableSpace>((event, emit) {
|
||||
emit(SelectedBookableSpaceState(
|
||||
selectedBookableSpace: event.bookableSpace));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
part of 'selected_bookable_space_bloc.dart';
|
||||
|
||||
abstract class SelectedBookableSpaceEvent {
|
||||
const SelectedBookableSpaceEvent();
|
||||
}
|
||||
|
||||
class SelectBookableSpace extends SelectedBookableSpaceEvent {
|
||||
final BookableSpaceModel bookableSpace;
|
||||
|
||||
const SelectBookableSpace(this.bookableSpace);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
part of 'selected_bookable_space_bloc.dart';
|
||||
|
||||
class SelectedBookableSpaceState {
|
||||
final BookableSpaceModel? selectedBookableSpace;
|
||||
|
||||
const SelectedBookableSpaceState(
|
||||
{ this.selectedBookableSpace,}
|
||||
);
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
|
||||
|
||||
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
|
||||
final BookableSystemService _bookingService;
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
String _currentSearch = '';
|
||||
|
||||
SidebarBloc(this._bookingService)
|
||||
: super(SidebarState(
|
||||
allRooms: [],
|
||||
displayedRooms: [],
|
||||
isLoading: true,
|
||||
hasMore: true,
|
||||
)) {
|
||||
on<LoadBookableSpaces>(_onLoadBookableSpaces);
|
||||
on<LoadMoreSpaces>(_onLoadMoreSpaces);
|
||||
on<SelectRoomEvent>(_onSelectRoom);
|
||||
on<SearchRoomsEvent>(_onSearchRooms);
|
||||
on<ResetSearch>(_onResetSearch);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBookableSpaces(
|
||||
LoadBookableSpaces event,
|
||||
Emitter<SidebarState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
_currentPage = 1;
|
||||
_currentSearch = '';
|
||||
|
||||
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||
param: LoadBookableSpacesParam(
|
||||
page: _currentPage,
|
||||
size: _pageSize,
|
||||
search: _currentSearch,
|
||||
),
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
allRooms: paginatedSpaces.data,
|
||||
displayedRooms: paginatedSpaces.data,
|
||||
isLoading: false,
|
||||
hasMore: paginatedSpaces.hasNext,
|
||||
totalPages: paginatedSpaces.totalPage,
|
||||
currentPage: _currentPage,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: 'Failed to load rooms: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreSpaces(
|
||||
LoadMoreSpaces event,
|
||||
Emitter<SidebarState> emit,
|
||||
) async {
|
||||
if (!state.hasMore || state.isLoadingMore) return;
|
||||
|
||||
try {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
_currentPage++;
|
||||
|
||||
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||
param: LoadBookableSpacesParam(
|
||||
page: _currentPage,
|
||||
size: _pageSize,
|
||||
search: _currentSearch,
|
||||
),
|
||||
);
|
||||
|
||||
final updatedRooms = [...state.allRooms, ...paginatedSpaces.data];
|
||||
|
||||
emit(state.copyWith(
|
||||
allRooms: updatedRooms,
|
||||
displayedRooms: updatedRooms,
|
||||
isLoadingMore: false,
|
||||
hasMore: paginatedSpaces.hasNext,
|
||||
totalPages: paginatedSpaces.totalPage,
|
||||
currentPage: _currentPage,
|
||||
));
|
||||
} catch (e) {
|
||||
_currentPage--;
|
||||
emit(state.copyWith(
|
||||
isLoadingMore: false,
|
||||
errorMessage: 'Failed to load more rooms: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSearchRooms(
|
||||
SearchRoomsEvent event,
|
||||
Emitter<SidebarState> emit,
|
||||
) async {
|
||||
try {
|
||||
_currentSearch = event.query;
|
||||
_currentPage = 1;
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||
param: LoadBookableSpacesParam(
|
||||
page: _currentPage,
|
||||
size: _pageSize,
|
||||
search: _currentSearch,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
allRooms: paginatedSpaces.data,
|
||||
displayedRooms: paginatedSpaces.data,
|
||||
isLoading: false,
|
||||
hasMore: paginatedSpaces.hasNext,
|
||||
totalPages: paginatedSpaces.totalPage,
|
||||
currentPage: _currentPage,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: 'Search failed: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onResetSearch(
|
||||
ResetSearch event,
|
||||
Emitter<SidebarState> emit,
|
||||
) {
|
||||
_currentSearch = '';
|
||||
add(LoadBookableSpaces());
|
||||
}
|
||||
|
||||
void _onSelectRoom(
|
||||
SelectRoomEvent event,
|
||||
Emitter<SidebarState> emit,
|
||||
) {
|
||||
emit(state.copyWith(selectedRoomId: event.roomId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
abstract class SidebarEvent {}
|
||||
|
||||
class LoadBookableSpaces extends SidebarEvent {}
|
||||
|
||||
class SelectRoomEvent extends SidebarEvent {
|
||||
final String roomId;
|
||||
|
||||
SelectRoomEvent(this.roomId);
|
||||
}
|
||||
|
||||
class SearchRoomsEvent extends SidebarEvent {
|
||||
final String query;
|
||||
|
||||
SearchRoomsEvent(this.query);
|
||||
}
|
||||
|
||||
class LoadMoreSpaces extends SidebarEvent {}
|
||||
|
||||
class ResetSearch extends SidebarEvent {}
|
||||
|
||||
class ExecuteSearch extends SidebarEvent {
|
||||
final String query;
|
||||
|
||||
ExecuteSearch(this.query);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
|
||||
class SidebarState {
|
||||
final List<BookableSpaceModel> allRooms;
|
||||
final List<BookableSpaceModel> displayedRooms;
|
||||
final bool isLoading;
|
||||
final bool isLoadingMore;
|
||||
final String? errorMessage;
|
||||
final String? selectedRoomId;
|
||||
final bool hasMore;
|
||||
final int totalPages;
|
||||
final int currentPage;
|
||||
|
||||
SidebarState({
|
||||
required this.allRooms,
|
||||
required this.displayedRooms,
|
||||
required this.isLoading,
|
||||
this.isLoadingMore = false,
|
||||
this.errorMessage,
|
||||
this.selectedRoomId,
|
||||
this.hasMore = true,
|
||||
this.totalPages = 0,
|
||||
this.currentPage = 1,
|
||||
});
|
||||
|
||||
SidebarState copyWith({
|
||||
List<BookableSpaceModel>? allRooms,
|
||||
List<BookableSpaceModel>? displayedRooms,
|
||||
bool? isLoading,
|
||||
bool? isLoadingMore,
|
||||
String? errorMessage,
|
||||
String? selectedRoomId,
|
||||
bool? hasMore,
|
||||
int? totalPages,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return SidebarState(
|
||||
allRooms: allRooms ?? this.allRooms,
|
||||
displayedRooms: displayedRooms ?? this.displayedRooms,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedRoomId: selectedRoomId ?? this.selectedRoomId,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingPage extends StatefulWidget {
|
||||
const BookingPage({super.key});
|
||||
|
||||
@override
|
||||
State<BookingPage> createState() => _BookingPageState();
|
||||
}
|
||||
|
||||
class _BookingPageState extends State<BookingPage> {
|
||||
late final EventController _eventController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_eventController = EventController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispatchLoadEvents(BuildContext context) {
|
||||
final selectedRoom =
|
||||
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
|
||||
final dateState = context.read<DateSelectionBloc>().state;
|
||||
|
||||
if (selectedRoom != null) {
|
||||
context.read<CalendarEventsBloc>().add(
|
||||
LoadEvents(
|
||||
LoadEventsParam(
|
||||
startDate: dateState.weekStart,
|
||||
endDate: dateState.weekStart.add(const Duration(days: 6)),
|
||||
id: selectedRoom.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
|
||||
BlocProvider(create: (_) => DateSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (_) => CalendarEventsBloc(
|
||||
calendarService: MemoryCalendarServiceWithRemoteFallback(
|
||||
remoteService: RemoteCalendarService(
|
||||
HTTPService(),
|
||||
),
|
||||
memoryService: MemoryCalendarService(),
|
||||
),
|
||||
)),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) =>
|
||||
BlocListener<CalendarEventsBloc, CalendarEventState>(
|
||||
listenWhen: (prev, curr) => curr is EventsLoaded,
|
||||
listener: (context, state) {
|
||||
if (state is EventsLoaded) {
|
||||
_eventController.removeWhere((_) => true);
|
||||
_eventController.addAll(state.events);
|
||||
}
|
||||
},
|
||||
child: BlocListener<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
listener: (context, state) => _dispatchLoadEvents(context),
|
||||
child: BlocListener<DateSelectionBloc, DateSelectionState>(
|
||||
listener: (context, state) => _dispatchLoadEvents(context),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(3, 0),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, state) {
|
||||
return BookingSidebar(
|
||||
onRoomSelected: (selectedRoom) {
|
||||
context
|
||||
.read<SelectedBookableSpaceBloc>()
|
||||
.add(SelectBookableSpace(selectedRoom));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return CustomCalendarPage(
|
||||
selectedDate: dateState.selectedDate,
|
||||
onDateChanged: (day, month, year) {
|
||||
final newDate = DateTime(year, month, day);
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(SelectDate(newDate));
|
||||
context.read<DateSelectionBloc>().add(
|
||||
SelectDateFromSidebarCalendar(newDate));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {},
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, state) {
|
||||
final weekStart = state.weekStart;
|
||||
final weekEnd =
|
||||
weekStart.add(const Duration(days: 6));
|
||||
return WeekNavigation(
|
||||
weekStart: weekStart,
|
||||
weekEnd: weekEnd,
|
||||
onPreviousWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(PreviousWeek());
|
||||
},
|
||||
onNextWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(NextWeek());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, roomState) {
|
||||
final selectedRoom =
|
||||
roomState.selectedBookableSpace;
|
||||
return BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return BlocListener<CalendarEventsBloc,
|
||||
CalendarEventState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
curr is EventsLoaded,
|
||||
listener: (context, state) {
|
||||
if (state is EventsLoaded) {
|
||||
_eventController
|
||||
.removeWhere((_) => true);
|
||||
_eventController.addAll(state.events);
|
||||
}
|
||||
},
|
||||
child: WeeklyCalendarPage(
|
||||
startTime: selectedRoom
|
||||
?.bookableConfig.startTime,
|
||||
endTime: selectedRoom
|
||||
?.bookableConfig.endTime,
|
||||
weekStart: dateState.weekStart,
|
||||
selectedDate: dateState.selectedDate,
|
||||
eventController: _eventController,
|
||||
selectedDateFromSideBarCalender: context
|
||||
.watch<DateSelectionBloc>()
|
||||
.state
|
||||
.selectedDateFromSideBarCalender,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingSidebar extends StatelessWidget {
|
||||
final void Function(BookableSpaceModel) onRoomSelected;
|
||||
|
||||
const BookingSidebar({
|
||||
super.key,
|
||||
required this.onRoomSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SidebarBloc(RemoteBookableSpacesService(
|
||||
HTTPService(),
|
||||
))
|
||||
..add(LoadBookableSpaces()),
|
||||
child: _SidebarContent(onRoomSelected: onRoomSelected),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarContent extends StatefulWidget {
|
||||
final void Function(BookableSpaceModel) onRoomSelected;
|
||||
|
||||
const _SidebarContent({
|
||||
required this.onRoomSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SidebarContent> createState() => __SidebarContentState();
|
||||
}
|
||||
|
||||
class __SidebarContentState extends State<_SidebarContent> {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_scrollListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (_scrollController.position.pixels ==
|
||||
_scrollController.position.maxScrollExtent) {
|
||||
context.read<SidebarBloc>().add(LoadMoreSpaces());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSearch(String value) {
|
||||
context.read<SidebarBloc>().add(SearchRoomsEvent(value));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<SidebarBloc, SidebarState>(
|
||||
listener: (context, state) {},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
const _SidebarHeader(title: 'Spaces'),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(0, -2),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.counterBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
controller: searchController,
|
||||
onChanged: _handleSearch,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search',
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: SvgPicture.asset(
|
||||
Assets.searchIconUser,
|
||||
color: ColorsManager.primaryTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8, horizontal: 12),
|
||||
border: const OutlineInputBorder(
|
||||
borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
context.read<SidebarBloc>().add(ResetSearch());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isLoading)
|
||||
const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (state.errorMessage != null)
|
||||
Expanded(
|
||||
child: Center(child: Text(state.errorMessage!)),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount:
|
||||
state.displayedRooms.length + (state.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.displayedRooms.length) {
|
||||
return _buildLoadMoreIndicator(state);
|
||||
}
|
||||
|
||||
final room = state.displayedRooms[index];
|
||||
return RoomListItem(
|
||||
room: room,
|
||||
isSelected: state.selectedRoomId == room.uuid,
|
||||
onTap: () {
|
||||
context
|
||||
.read<SidebarBloc>()
|
||||
.add(SelectRoomEvent(room.uuid));
|
||||
widget.onRoomSelected(room);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadMoreIndicator(SidebarState state) {
|
||||
if (state.isLoadingMore) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
} else if (state.hasMore) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(child: Text('Scroll to load more')),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _SidebarHeader({
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.primaryTextColor,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CustomCalendarPage extends StatefulWidget {
|
||||
final DateTime selectedDate;
|
||||
final Function(int day, int month, int year) onDateChanged;
|
||||
|
||||
const CustomCalendarPage({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onDateChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomCalendarPage> createState() => _CustomCalendarPageState();
|
||||
}
|
||||
|
||||
class _CustomCalendarPageState extends State<CustomCalendarPage> {
|
||||
late DateTime _selectedDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDate = widget.selectedDate;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomCalendarPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedDate != oldWidget.selectedDate) {
|
||||
setState(() {
|
||||
_selectedDate = widget.selectedDate;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = CalendarDatePicker2Config(
|
||||
calendarType: CalendarDatePicker2Type.single,
|
||||
selectedDayHighlightColor: const Color(0xFF3B82F6),
|
||||
selectedDayTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
dayTextStyle: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
weekdayLabelTextStyle: const TextStyle(
|
||||
color: ColorsManager.grey50,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
controlsTextStyle: const TextStyle(
|
||||
color: Color(0xFF232D3A),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 18,
|
||||
),
|
||||
centerAlignModePicker: false,
|
||||
disableMonthPicker: true,
|
||||
firstDayOfWeek: 1,
|
||||
weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
|
||||
);
|
||||
|
||||
return CalendarDatePicker2(
|
||||
config: config,
|
||||
value: [_selectedDate],
|
||||
onValueChanged: (dates) {
|
||||
final picked = dates.first;
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
});
|
||||
widget.onDateChanged(picked.day, picked.month, picked.year);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class EventTileWidget extends StatelessWidget {
|
||||
final List<CalendarEventData<Object?>> events;
|
||||
const EventTileWidget({
|
||||
super.key,
|
||||
required this.events,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: events.map((event) {
|
||||
final booking = event.event is CalendarEventBooking
|
||||
? event.event! as CalendarEventBooking
|
||||
: null;
|
||||
|
||||
final companyName = booking?.user.companyName ?? 'Unknown Company';
|
||||
final startTime = DateFormat('hh:mm a').format(event.startTime!);
|
||||
final endTime = DateFormat('hh:mm a').format(event.endTime!);
|
||||
final isEventEnded =
|
||||
event.endTime != null && event.endTime!.isBefore(DateTime.now());
|
||||
|
||||
final duration = event.endTime!.difference(event.startTime!);
|
||||
final bool isLongEnough = duration.inMinutes >= 31;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor.withOpacity(0.1)
|
||||
: ColorsManager.blue1.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: const Border(
|
||||
left: BorderSide(
|
||||
color: ColorsManager.grayColor,
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: isLongEnough
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$startTime - $endTime',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor.withOpacity(0.9)
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
companyName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor.withOpacity(0.9)
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
event.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class HatchedColumnBackground extends StatelessWidget {
|
||||
final Color backgroundColor;
|
||||
final Color lineColor;
|
||||
final double opacity;
|
||||
final double stripeSpacing;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
const HatchedColumnBackground({
|
||||
super.key,
|
||||
required this.backgroundColor,
|
||||
required this.lineColor,
|
||||
this.opacity = 0.15,
|
||||
this.stripeSpacing = 12,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _HatchedBackgroundPainter(
|
||||
backgroundColor: backgroundColor,
|
||||
opacity: opacity,
|
||||
lineColor: lineColor,
|
||||
stripeSpacing: stripeSpacing,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
size: Size.infinite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HatchedBackgroundPainter extends CustomPainter {
|
||||
final Color backgroundColor;
|
||||
final double opacity;
|
||||
final Color lineColor;
|
||||
final double stripeSpacing;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
_HatchedBackgroundPainter({
|
||||
required this.backgroundColor,
|
||||
required this.opacity,
|
||||
required this.lineColor,
|
||||
required this.stripeSpacing,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
final RRect rrect = borderRadius?.toRRect(rect) ??
|
||||
RRect.fromRectAndRadius(rect, Radius.zero);
|
||||
final backgroundPaint = Paint()
|
||||
..color = backgroundColor.withOpacity(0.02)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawRRect(rrect, backgroundPaint);
|
||||
canvas.save();
|
||||
canvas.clipRRect(rrect);
|
||||
final linePaint = Paint()
|
||||
..color = lineColor
|
||||
..strokeWidth = 0.5
|
||||
..style = PaintingStyle.stroke;
|
||||
final maxExtent =
|
||||
math.sqrt(size.width * size.width + size.height * size.height);
|
||||
|
||||
canvas.translate(0, size.height);
|
||||
canvas.rotate(-math.pi / 4);
|
||||
double y = -maxExtent;
|
||||
while (y < maxExtent) {
|
||||
canvas.drawLine(
|
||||
Offset(-maxExtent, y),
|
||||
Offset(maxExtent, y),
|
||||
linePaint,
|
||||
);
|
||||
y += stripeSpacing;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) {
|
||||
return backgroundColor != oldDelegate.backgroundColor ||
|
||||
opacity != oldDelegate.opacity ||
|
||||
lineColor != oldDelegate.lineColor ||
|
||||
stripeSpacing != oldDelegate.stripeSpacing ||
|
||||
borderRadius != oldDelegate.borderRadius;
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
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 double? horizontalPadding;
|
||||
final double? verticalPadding;
|
||||
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;
|
||||
final double? fontSize;
|
||||
final FontWeight? fontWeight;
|
||||
const SvgTextButton({
|
||||
super.key,
|
||||
required this.svgAsset,
|
||||
this.fontSize,
|
||||
this.fontWeight,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||
this.svgColor = const Color(0xFF496EFF),
|
||||
this.labelColor = Colors.black,
|
||||
this.borderRadius = 10.0,
|
||||
this.horizontalPadding,
|
||||
this.verticalPadding,
|
||||
this.boxShadow = const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
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: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding ?? 24,
|
||||
vertical: verticalPadding ?? 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
boxShadow: boxShadow,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
svgAsset,
|
||||
width: svgSize,
|
||||
height: svgSize,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: labelColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RoomListItem extends StatelessWidget {
|
||||
final BookableSpaceModel room;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const RoomListItem({
|
||||
required this.room,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioListTile(
|
||||
value: room.uuid,
|
||||
contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
|
||||
groupValue: isSelected ? room.uuid : null,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
onChanged: (value) => onTap(),
|
||||
activeColor: ColorsManager.primaryColor,
|
||||
title: Text(
|
||||
room.spaceName,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12),
|
||||
),
|
||||
subtitle: Text(
|
||||
room.virtualLocation,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.textGray,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class TimeLineWidget extends StatelessWidget {
|
||||
final DateTime date;
|
||||
|
||||
const TimeLineWidget({Key? key, required this.date}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int hour =
|
||||
date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour);
|
||||
String period = date.hour >= 12 ? 'PM' : 'AM';
|
||||
return Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$hour',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2, top: 6),
|
||||
child: Text(
|
||||
period,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeekDayHeader extends StatelessWidget {
|
||||
final DateTime date;
|
||||
final bool isSelectedDay;
|
||||
|
||||
const WeekDayHeader({
|
||||
Key? key,
|
||||
required this.date,
|
||||
required this.isSelectedDay,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('EEE').format(date).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
color: isSelectedDay ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 20,
|
||||
color:
|
||||
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeekNavigation extends StatelessWidget {
|
||||
final DateTime weekStart;
|
||||
final DateTime weekEnd;
|
||||
final VoidCallback onPreviousWeek;
|
||||
final VoidCallback onNextWeek;
|
||||
|
||||
const WeekNavigation({
|
||||
Key? key,
|
||||
required this.weekStart,
|
||||
required this.weekEnd,
|
||||
required this.onPreviousWeek,
|
||||
required this.onNextWeek,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 15,
|
||||
icon: const Icon(Icons.arrow_back_ios,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
onPressed: onPreviousWeek,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
_getMonthYearText(weekStart, weekEnd),
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
iconSize: 15,
|
||||
icon: const Icon(Icons.arrow_forward_ios,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
onPressed: onNextWeek,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthYearText(DateTime start, DateTime end) {
|
||||
final startMonth = DateFormat('MMM').format(start);
|
||||
final endMonth = DateFormat('MMM').format(end);
|
||||
final year = start.year == end.year
|
||||
? start.year.toString()
|
||||
: '${start.year}-${end.year}';
|
||||
|
||||
if (start.month == end.month) {
|
||||
return '$startMonth $year';
|
||||
} else {
|
||||
return '$startMonth - $endMonth $year';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeeklyCalendarPage extends StatelessWidget {
|
||||
final DateTime weekStart;
|
||||
final DateTime selectedDate;
|
||||
final EventController eventController;
|
||||
final String? startTime;
|
||||
final String? endTime;
|
||||
final DateTime? selectedDateFromSideBarCalender;
|
||||
|
||||
const WeeklyCalendarPage({
|
||||
super.key,
|
||||
required this.weekStart,
|
||||
required this.selectedDate,
|
||||
required this.eventController,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.selectedDateFromSideBarCalender,
|
||||
});
|
||||
static const double timeLineWidth = 65;
|
||||
static const int totalDays = 7;
|
||||
static const double dayColumnWidth = 220;
|
||||
|
||||
final double calendarContentWidth =
|
||||
timeLineWidth + (totalDays * dayColumnWidth);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final startHour = _parseHour(startTime, defaultValue: 0);
|
||||
final endHour = _parseHour(endTime, defaultValue: 24);
|
||||
|
||||
if (endTime == null || endTime!.isEmpty) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
size: 80,
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Please select a bookable space to view the calendar.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const double timeLineWidth = 65;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
||||
bool isInRange(DateTime date, DateTime start, DateTime end) {
|
||||
!date.isBefore(start) && !date.isAfter(end);
|
||||
// remove this line and Check if the date is within the range
|
||||
return false;
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: calendarContentWidth,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
child: WeekView(
|
||||
minuteSlotSize: MinuteSlotSize.minutes15,
|
||||
weekDetectorBuilder: ({
|
||||
required date,
|
||||
required height,
|
||||
required heightPerMinute,
|
||||
required minuteSlotSize,
|
||||
required width,
|
||||
}) {
|
||||
final isSelected = isSameDay(date, selectedDate);
|
||||
final isSidebarSelected =
|
||||
selectedDateFromSideBarCalender != null &&
|
||||
isSameDay(
|
||||
date, selectedDateFromSideBarCalender!);
|
||||
if (isSidebarSelected && !isSelected) {
|
||||
return Container(
|
||||
height: height,
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.13),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
} else if (isSelected) {
|
||||
return Container(
|
||||
height: height,
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
ColorsManager.spaceColor.withOpacity(0.07),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
|
||||
// weekDetectorBuilder: ({
|
||||
// required date,
|
||||
// required height,
|
||||
// required heightPerMinute,
|
||||
// required minuteSlotSize,
|
||||
// required width,
|
||||
// }) {
|
||||
// return isInRange(date, highlightStart, highlightEnd)
|
||||
// ? HatchedColumnBackground(
|
||||
// backgroundColor: ColorsManager.grey800,
|
||||
// lineColor: ColorsManager.textGray,
|
||||
// opacity: 0.3,
|
||||
// stripeSpacing: 12,
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// )
|
||||
// : const SizedBox();
|
||||
// },
|
||||
pageViewPhysics: const NeverScrollableScrollPhysics(),
|
||||
key: ValueKey(weekStart),
|
||||
controller: eventController,
|
||||
initialDay: weekStart,
|
||||
startHour: startHour - 1,
|
||||
endHour: endHour,
|
||||
heightPerMinute: 1.7,
|
||||
showLiveTimeLineInAllDays: false,
|
||||
showVerticalLines: true,
|
||||
emulateVerticalOffsetBy: -80,
|
||||
startDay: WeekDays.monday,
|
||||
liveTimeIndicatorSettings:
|
||||
const LiveTimeIndicatorSettings(
|
||||
showBullet: false,
|
||||
height: 0,
|
||||
),
|
||||
weekDayBuilder: (date) {
|
||||
return WeekDayHeader(
|
||||
date: date,
|
||||
isSelectedDay: isSameDay(date, selectedDate),
|
||||
);
|
||||
},
|
||||
timeLineBuilder: (date) {
|
||||
return TimeLineWidget(date: date);
|
||||
},
|
||||
timeLineWidth: timeLineWidth,
|
||||
weekPageHeaderBuilder: (start, end) => Container(),
|
||||
weekTitleHeight: 60,
|
||||
weekNumberBuilder: (firstDayOfWeek) => Padding(
|
||||
padding: const EdgeInsets.only(right: 15, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
firstDayOfWeek.timeZoneName
|
||||
.replaceAll(':00', ''),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
eventTileBuilder: (date, events, boundary, start, end) {
|
||||
return EventTileWidget(
|
||||
events: events,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 50,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 1,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
bool isSameDay(DateTime d1, DateTime d2) {
|
||||
return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day;
|
||||
}
|
||||
|
||||
int _parseHour(String? time, {required int defaultValue}) {
|
||||
if (time == null || time.isEmpty || !time.contains(':')) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return int.parse(time.split(':')[0]);
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
|
||||
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingPage extends StatelessWidget {
|
||||
final PageController pageController;
|
||||
const BookingPage({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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: () {
|
||||
pageController.jumpToPage(2);
|
||||
}),
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {})
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
class NonBookableSpacesDebouncerDecoratorService
|
||||
implements NonBookableSpacesService {
|
||||
final NonBookableSpacesService _delegate;
|
||||
Timer? _debounce;
|
||||
|
||||
NonBookableSpacesDebouncerDecoratorService(this._delegate);
|
||||
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params) {
|
||||
final completer = Completer<PaginatedDataModel<BookableSpacemodel>>();
|
||||
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () async {
|
||||
try {
|
||||
final result = await _delegate.load(params);
|
||||
completer.complete(result);
|
||||
} catch (e) {
|
||||
completer.completeError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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 Bookable Spaces';
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
BookableSpacesParams param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'configured': true,
|
||||
'page': param.currentPage,
|
||||
},
|
||||
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,53 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/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(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'configured': false,
|
||||
'page': params.currentPage,
|
||||
'search': params.searchedWords,
|
||||
},
|
||||
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,35 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.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 RemoteSendBookableSpaces implements SendBookableSpacesService {
|
||||
final HTTPService _httpService;
|
||||
RemoteSendBookableSpaces(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load Spaces';
|
||||
@override
|
||||
Future<void> sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams params) async {
|
||||
try {
|
||||
await _httpService.post(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
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,40 @@
|
||||
import 'package:dio/dio.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/update_bookable_space_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.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 RemoteUpdateBookableSpaceService implements UpdateBookableSpaceService {
|
||||
final HTTPService _httpService;
|
||||
RemoteUpdateBookableSpaceService(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
|
||||
@override
|
||||
Future<BookableSpaceConfig> update(
|
||||
UpdateBookableSpaceParam updateParam) async {
|
||||
try {
|
||||
final response = await _httpService.put(
|
||||
path: '${ApiEndpoints.bookableSpaces}/${updateParam.spaceUuid}',
|
||||
body: updateParam.toJson(),
|
||||
expectedResponseModel: (json) {
|
||||
return BookableSpaceConfig.fromJson(
|
||||
json['data'] as Map<String, dynamic>);
|
||||
},
|
||||
);
|
||||
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,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BookableSpaceConfig {
|
||||
final String configUuid;
|
||||
final List<String> bookableDays;
|
||||
final TimeOfDay? bookingStartTime;
|
||||
final TimeOfDay? bookingEndTime;
|
||||
final int cost;
|
||||
final bool availability;
|
||||
BookableSpaceConfig({
|
||||
required this.configUuid,
|
||||
required this.availability,
|
||||
required this.bookableDays,
|
||||
this.bookingEndTime,
|
||||
this.bookingStartTime,
|
||||
required this.cost,
|
||||
});
|
||||
factory BookableSpaceConfig.zero() => BookableSpaceConfig(
|
||||
configUuid: '',
|
||||
bookableDays: [],
|
||||
availability: false,
|
||||
cost: -1,
|
||||
);
|
||||
factory BookableSpaceConfig.fromJson(Map<String, dynamic> json) =>
|
||||
BookableSpaceConfig(
|
||||
configUuid: json['uuid'] as String,
|
||||
bookableDays: (json['daysAvailable'] as List).cast<String>(),
|
||||
availability: (json['active'] as bool?) ?? false,
|
||||
bookingStartTime: parseTimeOfDay(json['startTime'] as String),
|
||||
bookingEndTime: 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 =>
|
||||
bookableDays.isNotEmpty &&
|
||||
cost >= 0 &&
|
||||
bookingStartTime != null &&
|
||||
bookingEndTime != null;
|
||||
|
||||
BookableSpaceConfig copyWith({
|
||||
List<String>? bookableDays,
|
||||
TimeOfDay? bookingStartTime,
|
||||
TimeOfDay? bookingEndTime,
|
||||
int? cost,
|
||||
bool? availability,
|
||||
}) {
|
||||
return BookableSpaceConfig(
|
||||
configUuid: configUuid,
|
||||
availability: availability ?? this.availability,
|
||||
bookableDays: bookableDays ?? this.bookableDays,
|
||||
cost: cost ?? this.cost,
|
||||
bookingEndTime: bookingEndTime ?? this.bookingEndTime,
|
||||
bookingStartTime: bookingStartTime ?? this.bookingStartTime,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
|
||||
class BookableSpacemodel {
|
||||
final String spaceUuid;
|
||||
final String spaceName;
|
||||
final BookableSpaceConfig? spaceConfig;
|
||||
final String spaceVirtualAddress;
|
||||
|
||||
BookableSpacemodel({
|
||||
required this.spaceUuid,
|
||||
required this.spaceName,
|
||||
this.spaceConfig,
|
||||
required this.spaceVirtualAddress,
|
||||
});
|
||||
factory BookableSpacemodel.zero() => BookableSpacemodel(
|
||||
spaceUuid: '',
|
||||
spaceName: '',
|
||||
spaceVirtualAddress: '',
|
||||
);
|
||||
factory BookableSpacemodel.fromJson(Map<String, dynamic> json) =>
|
||||
BookableSpacemodel(
|
||||
spaceUuid: json['uuid'] as String,
|
||||
spaceName: json['spaceName'] as String,
|
||||
spaceConfig: json['bookableConfig'] == null
|
||||
? BookableSpaceConfig.zero()
|
||||
: BookableSpaceConfig.fromJson(
|
||||
json['bookableConfig'] as Map<String, dynamic>),
|
||||
spaceVirtualAddress: json['virtualLocation'] 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 != null &&
|
||||
spaceConfig!.isValid;
|
||||
|
||||
BookableSpacemodel copyWith({
|
||||
String? spaceUuid,
|
||||
String? spaceName,
|
||||
BookableSpaceConfig? spaceConfig,
|
||||
String? spaceVirtualAddress,
|
||||
}) {
|
||||
return BookableSpacemodel(
|
||||
spaceUuid: spaceUuid ?? this.spaceUuid,
|
||||
spaceName: spaceName ?? this.spaceName,
|
||||
spaceConfig: spaceConfig ?? this.spaceConfig,
|
||||
spaceVirtualAddress: spaceVirtualAddress ?? this.spaceVirtualAddress,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
class BookableSpacesParams {
|
||||
final int currentPage;
|
||||
BookableSpacesParams({
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'page': currentPage,
|
||||
};
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
class NonBookableSpacesParams {
|
||||
final int currentPage;
|
||||
String? searchedWords;
|
||||
NonBookableSpacesParams({
|
||||
required this.currentPage,
|
||||
this.searchedWords,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'page': currentPage,
|
||||
};
|
||||
}
|
@ -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 {
|
||||
final List<String> spaceUuids;
|
||||
final List<String> daysAvailable;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final 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,40 @@
|
||||
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 UpdateBookableSpaceParam {
|
||||
final String spaceUuid;
|
||||
final List<String>? bookableDays;
|
||||
final String? bookingStartTime;
|
||||
final String? bookingEndTime;
|
||||
final int? cost;
|
||||
final bool? availability;
|
||||
UpdateBookableSpaceParam({
|
||||
required this.spaceUuid,
|
||||
this.bookingStartTime,
|
||||
this.bookingEndTime,
|
||||
this.bookableDays,
|
||||
this.availability,
|
||||
this.cost,
|
||||
});
|
||||
factory UpdateBookableSpaceParam.fromBookableModel(
|
||||
BookableSpacemodel bookableSpace) {
|
||||
return UpdateBookableSpaceParam(
|
||||
spaceUuid: bookableSpace.spaceUuid,
|
||||
availability: bookableSpace.spaceConfig!.availability,
|
||||
bookableDays: bookableSpace.spaceConfig!.bookableDays,
|
||||
cost: bookableSpace.spaceConfig!.cost,
|
||||
bookingStartTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpace.spaceConfig!.bookingStartTime!),
|
||||
bookingEndTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpace.spaceConfig!.bookingEndTime!),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
if (bookableDays != null) 'daysAvailable': bookableDays,
|
||||
if (bookingStartTime != null) 'startTime': bookingStartTime,
|
||||
if (bookingEndTime != null) 'endTime': bookingEndTime,
|
||||
if (cost != null) 'points': cost,
|
||||
if (availability != null) 'active': availability,
|
||||
};
|
||||
}
|
@ -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,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/non_bookable_spaces_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);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
|
||||
|
||||
abstract class SendBookableSpacesService{
|
||||
Future<void> sendBookableSpacesToApi(SendBookableSpacesToApiParams params);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
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/update_bookable_space_param.dart';
|
||||
|
||||
abstract class UpdateBookableSpaceService {
|
||||
Future<BookableSpaceConfig> update(UpdateBookableSpaceParam updateParam);
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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';
|
||||
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);
|
||||
on<InsertUpdatedSpaceEvent>(_onInsertUpdatedSpaceEven);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBookableSpaces(
|
||||
LoadBookableSpacesEvent event, Emitter<BookableSpacesState> emit) async {
|
||||
emit(BookableSpacesLoading());
|
||||
try {
|
||||
final bookableSpaces = await bookableSpacesService.load(event.param);
|
||||
emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces));
|
||||
} on APIException catch (e) {
|
||||
emit(BookableSpacesError(error: e.message));
|
||||
} catch (e) {
|
||||
emit(
|
||||
BookableSpacesError(error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onInsertUpdatedSpaceEven(
|
||||
InsertUpdatedSpaceEvent event, Emitter<BookableSpacesState> emit) {
|
||||
emit(InsertingUpdatedSpaceState());
|
||||
|
||||
if (event.bookableSpace.spaceConfig!.configUuid ==
|
||||
event.updatedBookableSpaceConfig.configUuid) {
|
||||
final editedBookableSpace = event.bookableSpaces.data.firstWhere(
|
||||
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
|
||||
);
|
||||
final config = editedBookableSpace.spaceConfig!.copyWith(
|
||||
availability: event.updatedBookableSpaceConfig.availability,
|
||||
bookableDays: event.updatedBookableSpaceConfig.bookableDays,
|
||||
bookingEndTime: event.updatedBookableSpaceConfig.bookingEndTime,
|
||||
bookingStartTime: event.updatedBookableSpaceConfig.bookingStartTime,
|
||||
cost: event.updatedBookableSpaceConfig.cost,
|
||||
);
|
||||
editedBookableSpace.copyWith(spaceConfig: config);
|
||||
final index = event.bookableSpaces.data.indexWhere(
|
||||
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
|
||||
);
|
||||
event.bookableSpaces.data.removeAt(index);
|
||||
event.bookableSpaces.data.insert(index, editedBookableSpace);
|
||||
}
|
||||
|
||||
emit(BookableSpacesLoaded(bookableSpacesList: event.bookableSpaces));
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
part of 'bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class BookableSpacesEvent extends Equatable {
|
||||
const BookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadBookableSpacesEvent extends BookableSpacesEvent {
|
||||
final BookableSpacesParams param;
|
||||
const LoadBookableSpacesEvent(this.param);
|
||||
}
|
||||
|
||||
class InsertUpdatedSpaceEvent extends BookableSpacesEvent {
|
||||
final PaginatedDataModel<BookableSpacemodel> bookableSpaces;
|
||||
final BookableSpacemodel bookableSpace;
|
||||
final BookableSpaceConfig updatedBookableSpaceConfig;
|
||||
const InsertUpdatedSpaceEvent({
|
||||
required this.bookableSpaces,
|
||||
required this.bookableSpace,
|
||||
required this.updatedBookableSpaceConfig,
|
||||
});
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
class InsertingUpdatedSpaceState extends BookableSpacesState {}
|
@ -0,0 +1,65 @@
|
||||
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/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/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;
|
||||
|
||||
NonBookableSpacesBloc(this.nonBookableSpacesService)
|
||||
: super(NonBookableSpacesInitial()) {
|
||||
on<CallInitStateEvent>(_onCallInitStateEvent);
|
||||
on<LoadUnBookableSpacesEvent>(_onLoadUnBookableSpacesEvent);
|
||||
}
|
||||
|
||||
void _onCallInitStateEvent(
|
||||
CallInitStateEvent event, Emitter<NonBookableSpacesState> emit) {
|
||||
emit(NonBookableSpacesInitial());
|
||||
}
|
||||
|
||||
Future<void> _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event,
|
||||
Emitter<NonBookableSpacesState> emit) async {
|
||||
if (state is NonBookableSpacesLoaded) {
|
||||
final currState = state as NonBookableSpacesLoaded;
|
||||
try {
|
||||
emit(NonBookableSpacesLoading(
|
||||
lastNonBookableSpaces: currState.nonBookableSpaces));
|
||||
|
||||
final nonBookableSpacesList = await nonBookableSpacesService.load(
|
||||
event.nonBookableSpacesParams,
|
||||
);
|
||||
nonBookableSpacesList.data.addAll(currState.nonBookableSpaces.data);
|
||||
|
||||
emit(
|
||||
NonBookableSpacesLoaded(
|
||||
nonBookableSpaces: nonBookableSpacesList,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
NonBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
emit(const NonBookableSpacesLoading());
|
||||
final nonBookableSpacesList = await nonBookableSpacesService.load(
|
||||
event.nonBookableSpacesParams,
|
||||
);
|
||||
emit(
|
||||
NonBookableSpacesLoaded(nonBookableSpaces: nonBookableSpacesList),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
NonBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
part of 'non_bookaable_spaces_bloc.dart';
|
||||
|
||||
sealed class NonBookableSpacesEvent extends Equatable {
|
||||
const NonBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CallInitStateEvent extends NonBookableSpacesEvent {}
|
||||
|
||||
class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent {
|
||||
final NonBookableSpacesParams nonBookableSpacesParams;
|
||||
const LoadUnBookableSpacesEvent({
|
||||
required this.nonBookableSpacesParams,
|
||||
});
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
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 {
|
||||
final PaginatedDataModel<BookableSpacemodel>? lastNonBookableSpaces;
|
||||
const NonBookableSpacesLoading({
|
||||
this.lastNonBookableSpaces,
|
||||
});
|
||||
}
|
||||
|
||||
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,33 @@
|
||||
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/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart';
|
||||
|
||||
part 'send_bookable_spaces_event.dart';
|
||||
part 'send_bookable_spaces_state.dart';
|
||||
|
||||
class SendBookableSpacesBloc
|
||||
extends Bloc<SendBookableSpacesEvent, SendBookableSpacesState> {
|
||||
SendBookableSpacesService sendBookableSpacesService;
|
||||
SendBookableSpacesBloc(
|
||||
this.sendBookableSpacesService,
|
||||
) : super(SendBookableSpacesInitial()) {
|
||||
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
|
||||
}
|
||||
Future<void> _onSendBookableSpacesToApi(SendBookableSpacesToApi event,
|
||||
Emitter<SendBookableSpacesState> emit) async {
|
||||
emit(SendBookableSpacesLoading());
|
||||
try {
|
||||
await sendBookableSpacesService.sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams.fromBookableSpacesModel(
|
||||
event.selectedBookableSpaces),
|
||||
);
|
||||
emit(SendBookableSpacesSuccess());
|
||||
} catch (e) {
|
||||
emit(
|
||||
SendBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
part of 'send_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SendBookableSpacesEvent extends Equatable {
|
||||
const SendBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class SendBookableSpacesToApi extends SendBookableSpacesEvent {
|
||||
final List<BookableSpacemodel> selectedBookableSpaces;
|
||||
const SendBookableSpacesToApi({required this.selectedBookableSpaces});
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
part of 'send_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SendBookableSpacesState extends Equatable {
|
||||
const SendBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class SendBookableSpacesInitial extends SendBookableSpacesState {}
|
||||
|
||||
class SendBookableSpacesLoading extends SendBookableSpacesState {}
|
||||
|
||||
class SendBookableSpacesSuccess extends SendBookableSpacesState {}
|
||||
|
||||
class SendBookableSpacesError extends SendBookableSpacesState {
|
||||
final String error;
|
||||
const SendBookableSpacesError(this.error);
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
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/service/non_bookable_spaces_service.dart';
|
||||
|
||||
part 'setup_bookable_spaces_event.dart';
|
||||
part 'setup_bookable_spaces_state.dart';
|
||||
|
||||
class SetupBookableSpacesBloc
|
||||
extends Bloc<SetupBookableSpacesEvent, SetupBookableSpacesState> {
|
||||
NonBookableSpacesService nonBookableSpacesService;
|
||||
List<BookableSpacemodel> selectedBookableSpaces = [];
|
||||
SetupBookableSpacesBloc(this.nonBookableSpacesService)
|
||||
: super(SetupBookableSpacesInitial()) {
|
||||
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
|
||||
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
|
||||
|
||||
on<CheckConfigurValidityEvent>(_onCheckConfigurValidityEvent);
|
||||
on<EditModeSelected>(_onEditModeSelected);
|
||||
}
|
||||
TimeOfDay? get endTime =>
|
||||
selectedBookableSpaces.first.spaceConfig!.bookingEndTime;
|
||||
|
||||
TimeOfDay? get startTime =>
|
||||
selectedBookableSpaces.first.spaceConfig!.bookingStartTime;
|
||||
|
||||
void _onAddToBookableSpaceEvent(
|
||||
AddToBookableSpaceEvent event,
|
||||
Emitter<SetupBookableSpacesState> emit,
|
||||
) {
|
||||
emit(InProgressState());
|
||||
selectedBookableSpaces.add(event.nonBookableSpace);
|
||||
emit(AddNonBookableSpaceIntoBookableState(
|
||||
bookableSpaces: selectedBookableSpaces));
|
||||
}
|
||||
|
||||
void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event,
|
||||
Emitter<SetupBookableSpacesState> emit) {
|
||||
emit(InProgressState());
|
||||
selectedBookableSpaces.remove(event.bookableSpace);
|
||||
emit(RemoveBookableSpaceIntoNonBookableState(
|
||||
bookableSpaces: selectedBookableSpaces));
|
||||
}
|
||||
|
||||
void _onCheckConfigurValidityEvent(CheckConfigurValidityEvent event,
|
||||
Emitter<SetupBookableSpacesState> emit) {
|
||||
if (selectedBookableSpaces.first.spaceConfig!.isValid) {
|
||||
emit(ValidSaveButtonState());
|
||||
} else {
|
||||
emit(UnValidSaveButtonState());
|
||||
}
|
||||
}
|
||||
|
||||
void _onEditModeSelected(
|
||||
EditModeSelected event, Emitter<SetupBookableSpacesState> emit) {
|
||||
selectedBookableSpaces.clear();
|
||||
selectedBookableSpaces.add(event.editingBookableSpace);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'setup_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SetupBookableSpacesEvent extends Equatable {
|
||||
const SetupBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AddToBookableSpaceEvent extends SetupBookableSpacesEvent {
|
||||
final BookableSpacemodel nonBookableSpace;
|
||||
const AddToBookableSpaceEvent({
|
||||
required this.nonBookableSpace,
|
||||
});
|
||||
}
|
||||
|
||||
class RemoveFromBookableSpaceEvent extends SetupBookableSpacesEvent {
|
||||
final BookableSpacemodel bookableSpace;
|
||||
const RemoveFromBookableSpaceEvent({
|
||||
required this.bookableSpace,
|
||||
});
|
||||
}
|
||||
|
||||
class CheckConfigurValidityEvent extends SetupBookableSpacesEvent {}
|
||||
|
||||
class EditModeSelected extends SetupBookableSpacesEvent {
|
||||
final BookableSpacemodel editingBookableSpace;
|
||||
const EditModeSelected({
|
||||
required this.editingBookableSpace,
|
||||
});
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
part of 'setup_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SetupBookableSpacesState extends Equatable {
|
||||
const SetupBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class SetupBookableSpacesInitial extends SetupBookableSpacesState {}
|
||||
|
||||
class AddNonBookableSpaceIntoBookableState extends SetupBookableSpacesState {
|
||||
final List<BookableSpacemodel> bookableSpaces;
|
||||
const AddNonBookableSpaceIntoBookableState({
|
||||
required this.bookableSpaces,
|
||||
});
|
||||
}
|
||||
|
||||
class InProgressState extends SetupBookableSpacesState {}
|
||||
|
||||
class RemoveBookableSpaceIntoNonBookableState extends SetupBookableSpacesState {
|
||||
final List<BookableSpacemodel> bookableSpaces;
|
||||
const RemoveBookableSpaceIntoNonBookableState({
|
||||
required this.bookableSpaces,
|
||||
});
|
||||
}
|
||||
|
||||
class ValidSaveButtonState extends SetupBookableSpacesState {}
|
||||
|
||||
class UnValidSaveButtonState extends SetupBookableSpacesState {}
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
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 editValueInit() {
|
||||
emit(StepTwoState());
|
||||
}
|
||||
|
||||
void goToNextStep() {
|
||||
if (state is StepOneState) {
|
||||
emit(StepTwoState());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
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 {}
|
||||
|
@ -0,0 +1,16 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'toggle_points_switch_state.dart';
|
||||
|
||||
class TogglePointsSwitchCubit extends Cubit<TogglePointsSwitchState> {
|
||||
TogglePointsSwitchCubit() : super(TogglePointsSwitchInitial());
|
||||
|
||||
void activateSwitch() {
|
||||
emit(ActivatePointsSwitch());
|
||||
}
|
||||
|
||||
void unActivateSwitch() {
|
||||
emit(UnActivatePointsSwitch());
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
part of 'toggle_points_switch_cubit.dart';
|
||||
|
||||
sealed class TogglePointsSwitchState extends Equatable {
|
||||
const TogglePointsSwitchState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class TogglePointsSwitchInitial extends TogglePointsSwitchState {}
|
||||
|
||||
class ActivatePointsSwitch extends TogglePointsSwitchState {}
|
||||
|
||||
class UnActivatePointsSwitch extends TogglePointsSwitchState {}
|
@ -0,0 +1,36 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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/update_bookable_space_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'update_bookable_spaces_event.dart';
|
||||
part 'update_bookable_spaces_state.dart';
|
||||
|
||||
class UpdateBookableSpacesBloc
|
||||
extends Bloc<UpdateBookableSpaceEvent, UpdateBookableSpacesState> {
|
||||
final UpdateBookableSpaceService updateBookableSpaceService;
|
||||
UpdateBookableSpacesBloc(this.updateBookableSpaceService)
|
||||
: super(UpdateBookableSpacesInitial()) {
|
||||
on<UpdateBookableSpace>(_onUpdateBookableSpace);
|
||||
}
|
||||
|
||||
Future<void> _onUpdateBookableSpace(UpdateBookableSpace event,
|
||||
Emitter<UpdateBookableSpacesState> emit) async {
|
||||
emit(UpdateBookableSpaceLoading(event.updatedParams.spaceUuid));
|
||||
try {
|
||||
final updatedSpace =
|
||||
await updateBookableSpaceService.update(event.updatedParams);
|
||||
|
||||
emit(UpdateBookableSpaceSuccess(bookableSpaceConfig: updatedSpace));
|
||||
event.onSuccess?.call();
|
||||
} on APIException catch (e) {
|
||||
emit(UpdateBookableSpaceFailure(error: e.message));
|
||||
} catch (e) {
|
||||
emit(
|
||||
UpdateBookableSpaceFailure(error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
part of 'update_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class UpdateBookableSpaceEvent extends Equatable {
|
||||
const UpdateBookableSpaceEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class UpdateBookableSpace extends UpdateBookableSpaceEvent {
|
||||
final void Function()? onSuccess;
|
||||
final UpdateBookableSpaceParam updatedParams;
|
||||
const UpdateBookableSpace({
|
||||
required this.updatedParams,
|
||||
this.onSuccess,
|
||||
});
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'update_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class UpdateBookableSpacesState extends Equatable {
|
||||
const UpdateBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class UpdateBookableSpacesInitial extends UpdateBookableSpacesState {}
|
||||
|
||||
final class UpdateBookableSpaceLoading extends UpdateBookableSpacesState {
|
||||
final String updatingSpaceUuid;
|
||||
|
||||
const UpdateBookableSpaceLoading(this.updatingSpaceUuid);
|
||||
}
|
||||
|
||||
final class UpdateBookableSpaceSuccess extends UpdateBookableSpacesState {
|
||||
final BookableSpaceConfig bookableSpaceConfig;
|
||||
const UpdateBookableSpaceSuccess({
|
||||
required this.bookableSpaceConfig,
|
||||
});
|
||||
}
|
||||
|
||||
final class UpdateBookableSpaceFailure extends UpdateBookableSpacesState {
|
||||
final String error;
|
||||
const UpdateBookableSpaceFailure({
|
||||
required this.error,
|
||||
});
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.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/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart';
|
||||
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class ManageBookableSpacesPage extends StatefulWidget {
|
||||
final PageController pageController;
|
||||
const ManageBookableSpacesPage({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ManageBookableSpacesPage> createState() =>
|
||||
_ManageBookableSpacesPageState();
|
||||
}
|
||||
|
||||
class _ManageBookableSpacesPageState extends State<ManageBookableSpacesPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => BookableSpacesBloc(
|
||||
RemoteBookableSpacesService(HTTPService()),
|
||||
)..add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParams(currentPage: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => UpdateBookableSpacesBloc(
|
||||
RemoteUpdateBookableSpaceService(HTTPService()),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: ManageBookableSpacesWidget(
|
||||
pageController: widget.pageController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ManageBookableSpacesWidget extends StatelessWidget {
|
||||
final PageController pageController;
|
||||
|
||||
const ManageBookableSpacesWidget({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 35,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 10,
|
||||
child: RowOfButtonsTitleWidget(pageController: pageController)),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 85,
|
||||
child: TableOfBookableSpacesWidget(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 5,
|
||||
child: PaginationButtonsWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.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/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_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/details_steps_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SetupBookableSpacesDialog extends StatefulWidget {
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
SetupBookableSpacesDialog({
|
||||
super.key,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SetupBookableSpacesDialog> createState() =>
|
||||
_SetupBookableSpacesDialogState();
|
||||
}
|
||||
|
||||
class _SetupBookableSpacesDialogState extends State<SetupBookableSpacesDialog> {
|
||||
final TextEditingController pointsController = TextEditingController();
|
||||
@override
|
||||
void dispose() {
|
||||
pointsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<StepsCubit>(
|
||||
create: widget.editingBookableSpace == null
|
||||
? (context) => StepsCubit()..initDialogValue()
|
||||
: (context) => StepsCubit()..editValueInit(),
|
||||
),
|
||||
BlocProvider<NonBookableSpacesBloc>(
|
||||
create: (context) => NonBookableSpacesBloc(
|
||||
NonBookableSpacesDebouncerDecoratorService(
|
||||
RemoteNonBookableSpaces(HTTPService()),
|
||||
),
|
||||
)..add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams:
|
||||
NonBookableSpacesParams(currentPage: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider<SetupBookableSpacesBloc>(
|
||||
create: widget.editingBookableSpace == null
|
||||
? (context) => SetupBookableSpacesBloc(
|
||||
RemoteNonBookableSpaces(HTTPService()))
|
||||
: (context) => SetupBookableSpacesBloc(
|
||||
RemoteNonBookableSpaces(HTTPService()))
|
||||
..add(EditModeSelected(
|
||||
editingBookableSpace: widget.editingBookableSpace!,
|
||||
)),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SendBookableSpacesBloc(
|
||||
RemoteSendBookableSpaces(HTTPService()),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Center(
|
||||
child: 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,
|
||||
editingBookableSpace: widget.editingBookableSpace,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final stepsState = context.watch<StepsCubit>().state;
|
||||
final setupBookableSpacesBloc =
|
||||
context.watch<SetupBookableSpacesBloc>();
|
||||
final selectedSpaces =
|
||||
setupBookableSpacesBloc.selectedBookableSpaces;
|
||||
return stepsState is StepOneState
|
||||
? NextFirstStepButton(selectedSpaces: selectedSpaces)
|
||||
: SaveSecondStepButton(
|
||||
selectedSpaces: selectedSpaces,
|
||||
pointsController: pointsController,
|
||||
isEditingMode: widget.editingBookableSpace != null,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
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/update_bookable_space_param.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/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class BookableSpaceSwitchActivationWidget extends StatelessWidget {
|
||||
final PaginatedDataModel<BookableSpacemodel> bookableSpaces;
|
||||
final BookableSpacemodel space;
|
||||
const BookableSpaceSwitchActivationWidget({
|
||||
super.key,
|
||||
required this.bookableSpaces,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.7,
|
||||
child:
|
||||
BlocConsumer<UpdateBookableSpacesBloc, UpdateBookableSpacesState>(
|
||||
listener: (context, updateState) {
|
||||
if (updateState is UpdateBookableSpaceSuccess) {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
InsertUpdatedSpaceEvent(
|
||||
bookableSpaces: bookableSpaces,
|
||||
bookableSpace: space,
|
||||
updatedBookableSpaceConfig:
|
||||
updateState.bookableSpaceConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, updateState) {
|
||||
final isLoading = updateState is UpdateBookableSpaceLoading &&
|
||||
updateState.updatingSpaceUuid == space.spaceUuid;
|
||||
if (isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Switch(
|
||||
trackOutlineColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) {
|
||||
return ColorsManager.whiteColors;
|
||||
}),
|
||||
value: space.spaceConfig!.availability,
|
||||
activeTrackColor: ColorsManager.blueColor,
|
||||
inactiveTrackColor: ColorsManager.grayBorder,
|
||||
thumbColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) {
|
||||
return ColorsManager.whiteColors;
|
||||
}),
|
||||
onChanged: (value) {
|
||||
context.read<UpdateBookableSpacesBloc>().add(
|
||||
UpdateBookableSpace(
|
||||
updatedParams: UpdateBookableSpaceParam(
|
||||
spaceUuid: space.spaceUuid,
|
||||
availability: value,
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.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/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/string_utils.dart';
|
||||
|
||||
class BookingPeriodWidget extends StatelessWidget {
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
const BookingPeriodWidget({
|
||||
super.key,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'* ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.red),
|
||||
),
|
||||
const Text('Booking Period'),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 300,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: ColorsManager.graysColor,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TimePickerWidget(
|
||||
title: editingBookableSpace == null
|
||||
? 'Start Time'
|
||||
: editingBookableSpace!.spaceConfig!.bookingStartTime!
|
||||
.format(context),
|
||||
onTimePicked: (timePicked) {
|
||||
if (timePicked == null) {
|
||||
return;
|
||||
}
|
||||
final setupBookableSpacesBloc =
|
||||
context.read<SetupBookableSpacesBloc>();
|
||||
|
||||
if (setupBookableSpacesBloc.endTime != null &&
|
||||
isEndTimeAfterStartTime(
|
||||
timePicked, setupBookableSpacesBloc.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 {
|
||||
setupBookableSpacesBloc.selectedBookableSpaces.forEach(
|
||||
(e) =>
|
||||
e.spaceConfig!.copyWith(bookingStartTime: timePicked),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_right_alt,
|
||||
color: ColorsManager.grayColor,
|
||||
),
|
||||
TimePickerWidget(
|
||||
title: editingBookableSpace == null
|
||||
? 'End Time'
|
||||
: editingBookableSpace!.spaceConfig!.bookingEndTime!
|
||||
.format(context),
|
||||
onTimePicked: (timePicked) {
|
||||
if (timePicked == null) {
|
||||
return;
|
||||
}
|
||||
final setupBookableSpacesBloc =
|
||||
context.read<SetupBookableSpacesBloc>();
|
||||
if (setupBookableSpacesBloc.startTime != null &&
|
||||
isEndTimeAfterStartTime(
|
||||
setupBookableSpacesBloc.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 {
|
||||
setupBookableSpacesBloc.selectedBookableSpaces.forEach(
|
||||
(e) =>
|
||||
e.spaceConfig!.copyWith(bookingEndTime: timePicked),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: 50,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: SvgPicture.asset(
|
||||
Assets.clockIcon,
|
||||
height: 15,
|
||||
color: ColorsManager.blackColor.withValues(alpha: 0.4),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
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/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/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ButtonsDividerBottomDialogWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final void Function()? onNextPressed;
|
||||
final void Function() onCancelPressed;
|
||||
const ButtonsDividerBottomDialogWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
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.blackColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
BlocConsumer<NonBookableSpacesBloc, NonBookableSpacesState>(
|
||||
listener: (context, nonBookableState) {
|
||||
if (nonBookableState is NonBookableSpacesInitial) {
|
||||
context.pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Operation Done Successfully',
|
||||
style: TextStyle(color: ColorsManager.activeGreen),
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParams(currentPage: 1),
|
||||
),
|
||||
);
|
||||
} else if (nonBookableState is NonBookableSpacesError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
nonBookableState.error,
|
||||
style:
|
||||
const TextStyle(color: ColorsManager.red),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, nonBookableState) {
|
||||
return TextButton(
|
||||
onPressed: onNextPressed,
|
||||
child: Text(
|
||||
title,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
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/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CheckBoxSpaceWidget extends StatelessWidget {
|
||||
final BookableSpacemodel nonBookableSpace;
|
||||
final List<BookableSpacemodel> selectedSpaces;
|
||||
|
||||
const CheckBoxSpaceWidget({
|
||||
super.key,
|
||||
required this.nonBookableSpace,
|
||||
required this.selectedSpaces,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
BlocBuilder<SetupBookableSpacesBloc, SetupBookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
final isChecked = switch (state) {
|
||||
AddNonBookableSpaceIntoBookableState(
|
||||
bookableSpaces: final spaces
|
||||
) =>
|
||||
spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid),
|
||||
RemoveBookableSpaceIntoNonBookableState(
|
||||
bookableSpaces: final spaces
|
||||
) =>
|
||||
spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
return Checkbox(
|
||||
value: isChecked,
|
||||
onChanged: (value) {
|
||||
final bloc = context.read<SetupBookableSpacesBloc>();
|
||||
if (value ?? false) {
|
||||
bloc.add(AddToBookableSpaceEvent(
|
||||
nonBookableSpace: nonBookableSpace));
|
||||
} else {
|
||||
bloc.add(RemoveFromBookableSpaceEvent(
|
||||
bookableSpace: nonBookableSpace));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
nonBookableSpace.spaceName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
nonBookableSpace.spaceVirtualAddress,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
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,
|
||||
fontSize: 12,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
@ -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,33 @@
|
||||
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/presentation/blocs/steps_cubit/cubit/steps_cubit.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';
|
||||
|
||||
class DetailsStepsWidget extends StatelessWidget {
|
||||
final TextEditingController pointsController;
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
const DetailsStepsWidget({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
child: BlocBuilder<StepsCubit, StepsState>(builder: (context, state) {
|
||||
return switch (state) {
|
||||
StepOneState() => const SpacesStepDetailsWidget(),
|
||||
StepTwoState() => StepTwoDetailsWidget(
|
||||
pointsController: pointsController,
|
||||
editingBookableSpace: editingBookableSpace,
|
||||
),
|
||||
StepsInitial() => const SizedBox(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_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/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_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/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class EditBookableSpaceButtonWidget extends StatelessWidget {
|
||||
final BookableSpacemodel? space;
|
||||
const EditBookableSpaceButtonWidget({
|
||||
super.key,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final bookableBloc = context.read<BookableSpacesBloc>();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: bookableBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => UpdateBookableSpacesBloc(
|
||||
RemoteUpdateBookableSpaceService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: SetupBookableSpacesDialog(
|
||||
editingBookableSpace: space,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
fixedSize: const Size(50, 30),
|
||||
elevation: 1,
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.settings,
|
||||
height: 15,
|
||||
color: ColorsManager.blue1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|