mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Compare commits
117 Commits
roles_perm
...
bugfix/emp
Author | SHA1 | Date | |
---|---|---|---|
916b606cb1 | |||
29c444eede | |||
9dd6c9e1e7 | |||
bf5b39e742 | |||
a294988558 | |||
09c1a785e5 | |||
c173df934d | |||
e4262d08a5 | |||
4bae7bb9fb | |||
073916d4ac | |||
8870467fe4 | |||
9331193e90 | |||
45b0b67fe0 | |||
9091af2661 | |||
4b7f4d4279 | |||
e61cfd7e49 | |||
788fb75a68 | |||
c72297e0c8 | |||
d0c6b13072 | |||
812dc4792b | |||
4907eebc42 | |||
9167c8da29 | |||
4258ccdfbd | |||
cb71b51565 | |||
d4ed4efcd8 | |||
dac045146e | |||
5563197e9d | |||
7268253e35 | |||
24f7ab6af8 | |||
65d00c923a | |||
830725254f | |||
ba7db3a5fb | |||
f35b699d4c | |||
7ffdc67016 | |||
18afc4f563 | |||
44d95f5701 | |||
e47f3d6d59 | |||
788ea27de1 | |||
81e9e58627 | |||
25eae3dfaa | |||
0e912207e5 | |||
eb53671e3a | |||
2f6bd31aa2 | |||
fe680d15f2 | |||
440263e2f9 | |||
ec5b7d4395 | |||
7109421358 | |||
145086b9de | |||
bae5ae17a7 | |||
9706c2655c | |||
8a95f93556 | |||
60028cdf78 | |||
c12c73f20a | |||
a7256c8d5d | |||
5975adb5e2 | |||
0bb24604bc | |||
cf2690123e | |||
a220483310 | |||
12df07e681 | |||
59eafc99a5 | |||
a4e7f30411 | |||
210fbf7497 | |||
acbb6ca7c0 | |||
408c40aa60 | |||
2abe7a6feb | |||
a381fd317d | |||
a588351482 | |||
1be52adcc8 | |||
3c5e0a7778 | |||
6591ef1664 | |||
cfc1b544b7 | |||
15640ff0df | |||
bfbc32d51b | |||
8aa493a15e | |||
e70df16de3 | |||
a7e7554813 | |||
1ab8c8341d | |||
67516817ec | |||
097e70b906 | |||
390f7288bd | |||
625b7b8304 | |||
d0b853b188 | |||
339a242e74 | |||
48c064c711 | |||
17e025b69f | |||
9b3e4a59af | |||
9f5e9af5fa | |||
6b79254a89 | |||
08f322165e | |||
1228e5e737 | |||
e7e0149b3a | |||
6ee650e9f8 | |||
a31eb27c92 | |||
0fda5457ae | |||
5bd257ee56 | |||
e44c3ae796 | |||
58b469b92a | |||
ae09cbda1e | |||
b59e7e4836 | |||
ed06b0ebd6 | |||
0bed2573a0 | |||
69092823a2 | |||
d2d5e76102 | |||
691beb2e86 | |||
819670867d | |||
1c256cc55c | |||
1d35377137 | |||
67ad986fcc | |||
90e5499f92 | |||
944b981ee0 | |||
e0ff139f30 | |||
e12252db96 | |||
80dea5c12d | |||
65ad9c5edf | |||
fa16eaf82f | |||
7935864208 | |||
edf8bdfdcd |
16
assets/icons/duplicate.svg
Normal file
16
assets/icons/duplicate.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.9807 2.67199L16.6413 0.219727H7.91051C7.43122 0.219727 7.04266 0.608281 7.04266 1.08758V16.605C7.04266 17.0843 7.43122 17.4729 7.91051 17.4729H18.2257C18.705 17.4729 19.0935 17.0843 19.0935 16.605V16.5459H19.7312V3.8127L17.9807 2.67199Z" fill="#60B7FF"/>
|
||||
<path d="M18.1513 6.23445H9.12553C8.95881 6.23445 8.82373 6.09934 8.82373 5.93266C8.82373 5.76598 8.95885 5.63086 9.12553 5.63086H18.1513C18.318 5.63086 18.4531 5.76598 18.4531 5.93266C18.4531 6.09934 18.318 6.23445 18.1513 6.23445Z" fill="#0055A3"/>
|
||||
<path d="M18.1513 8.54891H9.12553C8.95881 8.54891 8.82373 8.41379 8.82373 8.24711C8.82373 8.08043 8.95885 7.94531 9.12553 7.94531H18.1513C18.318 7.94531 18.4531 8.08043 18.4531 8.24711C18.4531 8.41379 18.318 8.54891 18.1513 8.54891Z" fill="#0055A3"/>
|
||||
<path d="M18.1513 10.8634H9.12553C8.95881 10.8634 8.82373 10.7282 8.82373 10.5616C8.82373 10.3949 8.95885 10.2598 9.12553 10.2598H18.1513C18.318 10.2598 18.4531 10.3949 18.4531 10.5616C18.4531 10.7282 18.318 10.8634 18.1513 10.8634Z" fill="#0055A3"/>
|
||||
<path d="M18.1513 13.1778H9.12556C8.95884 13.1778 8.82376 13.0427 8.82376 12.876C8.82376 12.7093 8.95888 12.5742 9.12556 12.5742H18.1513C18.3181 12.5742 18.4531 12.7093 18.4531 12.876C18.4531 13.0427 18.3181 13.1778 18.1513 13.1778Z" fill="#0055A3"/>
|
||||
<path d="M19.0935 3.39648V16.6044C19.0935 17.0837 18.7049 17.4722 18.2256 17.4722H19.3663C19.8456 17.4722 20.2342 17.0837 20.2342 16.6044V3.81203L19.0935 3.39648Z" fill="#26A6FE"/>
|
||||
<path d="M17.5091 3.8127H20.2342L16.6413 0.219727V2.94484C16.6413 3.42414 17.0298 3.8127 17.5091 3.8127Z" fill="#004281"/>
|
||||
<path d="M11.4172 19.7805C11.8965 19.7805 12.2851 19.392 12.2851 18.9127V18.8937H12.8297V6.12031L11.039 4.78906L9.83279 2.52734H1.10204C0.622747 2.52734 0.234192 2.9159 0.234192 3.3952V18.9127C0.234192 19.392 0.622747 19.7805 1.10204 19.7805H11.4172Z" fill="#D5EDFF"/>
|
||||
<path d="M12.285 4.97852V6.11922V18.9116C12.285 19.3909 11.8964 19.7794 11.4171 19.7794H12.5578C13.0371 19.7794 13.4257 19.3909 13.4257 18.9116V6.11922L12.285 4.97852Z" fill="#D8ECFE"/>
|
||||
<path d="M10.7006 6.12031H13.4258L9.83279 2.52734V5.25246C9.83276 5.73176 10.2213 6.12031 10.7006 6.12031Z" fill="#B3DAFE"/>
|
||||
<path d="M11.3429 8.54891H2.31709C2.15037 8.54891 2.01529 8.41379 2.01529 8.24711C2.01529 8.08043 2.15041 7.94531 2.31709 7.94531H11.3429C11.5096 7.94531 11.6447 8.08043 11.6447 8.24711C11.6447 8.41379 11.5096 8.54891 11.3429 8.54891Z" fill="#82AEE3"/>
|
||||
<path d="M11.3428 10.8634H2.31706C2.15034 10.8634 2.01526 10.7282 2.01526 10.5616C2.01526 10.3949 2.15038 10.2598 2.31706 10.2598H11.3428C11.5096 10.2598 11.6446 10.3949 11.6446 10.5616C11.6446 10.7282 11.5096 10.8634 11.3428 10.8634Z" fill="#82AEE3"/>
|
||||
<path d="M11.3428 13.1778H2.31706C2.15034 13.1778 2.01526 13.0427 2.01526 12.876C2.01526 12.7093 2.15038 12.5742 2.31706 12.5742H11.3428C11.5096 12.5742 11.6446 12.7093 11.6446 12.876C11.6446 13.0427 11.5096 13.1778 11.3428 13.1778Z" fill="#82AEE3"/>
|
||||
<path d="M11.3428 15.4923H2.31706C2.15034 15.4923 2.01526 15.3571 2.01526 15.1905C2.01526 15.0238 2.15038 14.8887 2.31706 14.8887H11.3428C11.5096 14.8887 11.6446 15.0238 11.6446 15.1905C11.6447 15.3571 11.5096 15.4923 11.3428 15.4923Z" fill="#82AEE3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
22
assets/icons/edit_space.svg
Normal file
22
assets/icons/edit_space.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_4618_3290)">
|
||||
<path d="M18.7967 7.35156V19.8515C18.7967 21.0362 17.8329 22 16.6482 22H4.14825C2.9636 22 1.99982 21.0362 1.99982 19.8515V7.35156C1.99982 6.16691 2.9636 5.20312 4.14825 5.20312H16.6482C17.8329 5.20312 18.7967 6.16691 18.7967 7.35156Z" fill="#EDF2F2"/>
|
||||
<path d="M18.7967 19.8515C18.7967 21.0361 17.8329 21.9999 16.6482 21.9999H4.14825C3.55591 21.9999 3.0188 21.7589 2.62978 21.3699L18.1667 5.83301C18.5557 6.22203 18.7967 6.75914 18.7967 7.35148V19.8515Z" fill="#C9DCDC"/>
|
||||
<path d="M9.28417 14.7153C9.12722 14.5583 9.07241 14.3262 9.14265 14.1157L9.97128 11.6297C10 11.5434 10.0485 11.465 10.1128 11.4007L17.8468 3.66674C18.0756 3.43791 18.4466 3.43791 18.6754 3.66674L20.3327 5.324C20.5615 5.55283 20.5615 5.9238 20.3327 6.15263L12.5987 13.8866C12.5344 13.9509 12.456 13.9994 12.3697 14.0281L9.88374 14.8567C9.67323 14.927 9.44109 14.8722 9.28417 14.7153Z" fill="#4998EE"/>
|
||||
<path d="M19.504 4.49512L9.28413 14.715C9.44105 14.8719 9.6732 14.9267 9.88374 14.8565L12.3697 14.0279C12.456 13.9992 12.5344 13.9507 12.5987 13.8864L20.3327 6.15242C20.5615 5.92359 20.5615 5.55261 20.3327 5.32379L19.504 4.49512Z" fill="#176EDE"/>
|
||||
<path d="M20.3327 6.15305L17.8467 3.66711L19.2278 2.28602C19.6092 1.90466 20.2275 1.90466 20.6089 2.28602L21.7137 3.39087C22.0951 3.77223 22.0951 4.39055 21.7137 4.77192L20.3327 6.15305Z" fill="#FFE137"/>
|
||||
<path d="M21.1613 2.83789L19.0897 4.90949L20.3327 6.15245L21.7138 4.77136C22.0951 4.39 22.0951 3.77168 21.7138 3.39031L21.1613 2.83789Z" fill="#FAC814"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_4618_3290" x="-0.000183105" y="0" width="24" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4618_3290"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4618_3290" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
12
assets/icons/link.svg
Normal file
12
assets/icons/link.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 440">
|
||||
<path id="Vector" d="M6.7986 0.892983L4.84001 2.85148C4.48185 3.20973 4.23182 3.63572 4.08964 4.08854C3.62464 4.23503 3.19865 4.49284 2.85148 4.84001L0.892983 6.7986C-0.29757 7.98915 -0.297753 9.91625 0.892983 11.107C2.08354 12.2976 4.01072 12.2977 5.20146 11.107L7.15996 9.14848C7.5182 8.79033 7.76814 8.36433 7.91032 7.91142C8.37541 7.76503 8.80132 7.50713 9.14848 7.15996L11.107 5.20146C12.2976 4.01081 12.2978 2.08372 11.107 0.892983C9.91643 -0.29757 7.98933 -0.297753 6.7986 0.892983ZM4.17735 6.1656C4.32576 6.5276 4.54658 6.86653 4.84001 7.15996C5.1251 7.44496 5.46431 7.67027 5.83418 7.82289L3.87577 9.78139C3.41893 10.2381 2.67552 10.2382 2.21867 9.78139C1.76182 9.32445 1.76182 8.58113 2.21867 8.12428L4.17717 6.16569C4.17726 6.16569 4.17726 6.16569 4.17735 6.1656ZM6.82854 8.81706L4.86995 10.7756C3.86259 11.783 2.23194 11.7831 1.2244 10.7756C0.216957 9.76821 0.216866 8.13747 1.2244 7.13002L3.1829 5.17143C3.41105 4.94328 3.67939 4.76082 3.9719 4.63255C3.92823 4.9897 3.94819 5.34263 4.02427 5.67991C3.96128 5.72697 3.90159 5.77843 3.84574 5.83427L1.88725 7.79277C1.24766 8.43245 1.24766 9.47313 1.88725 10.1127C2.52683 10.7523 3.56752 10.7523 4.2072 10.1127L6.16569 8.15422C6.80592 7.5139 6.80601 6.47459 6.16569 5.83427C5.82365 5.49223 5.74034 4.99492 5.90358 4.57753C6.24891 4.70598 6.56587 4.90876 6.82854 5.17143C7.8336 6.1765 7.83369 7.8119 6.82854 8.81706ZM10.7756 4.86995L8.81706 6.82854C8.58891 7.05669 8.32057 7.23915 8.02806 7.36742C8.07173 7.01027 8.05177 6.65733 7.97578 6.32005C8.03868 6.27299 8.09846 6.22154 8.15422 6.16569L10.1128 4.2072C10.7524 3.56761 10.7524 2.52683 10.1128 1.88725C9.5385 1.31303 8.64028 1.25379 7.99913 1.71229C7.89384 1.78755 7.86958 1.93394 7.94484 2.03922C8.0201 2.14451 8.16649 2.16886 8.27177 2.09352C8.73952 1.75907 9.37434 1.81162 9.78139 2.21867C10.2382 2.67552 10.2382 3.41883 9.78139 3.87577L7.8228 5.83427C7.8228 5.83427 7.8228 5.83427 7.82271 5.83436C7.67421 5.47236 7.45338 5.13344 7.15996 4.84001C6.87495 4.555 6.53566 4.32969 6.16578 4.17707L6.96431 3.37855C7.05577 3.28709 7.05577 3.13868 6.96431 3.04713C6.87276 2.95567 6.72444 2.95567 6.63289 3.04713L5.83427 3.84574C5.19404 4.48597 5.19395 5.52528 5.83427 6.16569C6.17631 6.50764 6.25963 7.00505 6.09639 7.42244C5.75105 7.29399 5.43409 7.0912 5.17143 6.82844C4.16645 5.82347 4.16636 4.18797 5.17143 3.1829L7.13002 1.2244C8.13737 0.216957 9.76811 0.216774 10.7756 1.2244C11.783 2.23176 11.7831 3.8625 10.7756 4.86995Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_2" d="M7.69571 2.55103C7.69571 2.68048 7.59079 2.7854 7.46143 2.7854C7.33197 2.7854 7.22705 2.68048 7.22705 2.55103C7.22705 2.42157 7.33197 2.31665 7.46143 2.31665C7.59079 2.31665 7.69571 2.42157 7.69571 2.55103Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_3" d="M3.18286 3.1831C3.27441 3.09164 3.27441 2.94323 3.18286 2.85168L2.18859 1.85741C2.09704 1.76595 1.94872 1.76595 1.85717 1.85741C1.76571 1.94897 1.76571 2.09737 1.85717 2.18893L2.85143 3.18319C2.94308 3.27465 3.09139 3.27465 3.18286 3.1831Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_4" d="M0.891022 3.9375C0.761658 3.9375 0.656738 4.04242 0.656738 4.17178C0.656738 4.30124 0.761658 4.40616 0.891022 4.40616H2.29718C2.42655 4.40616 2.53147 4.30124 2.53147 4.17178C2.53147 4.04242 2.42655 3.9375 2.29718 3.9375H0.891022Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_5" d="M3.93774 0.821289V2.22736C3.93774 2.35672 4.04266 2.46173 4.17203 2.46173C4.30148 2.46173 4.4064 2.35672 4.4064 2.22736V0.821289C4.4064 0.691834 4.30148 0.586914 4.17203 0.586914C4.04266 0.586914 3.93774 0.691834 3.93774 0.821289Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_6" d="M8.8172 8.81738C8.72565 8.90884 8.72565 9.05724 8.8172 9.1488L9.81146 10.1431C9.85724 10.1888 9.91721 10.2117 9.97717 10.2117C10.184 10.2117 10.291 9.95986 10.1429 9.81164L9.14862 8.81738C9.05707 8.72591 8.90875 8.72591 8.8172 8.81738Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_7" d="M8.03882 11.1785V9.77237C8.03882 9.64301 7.93381 9.53809 7.80444 9.53809C7.67499 9.53809 7.57007 9.64301 7.57007 9.77237V11.1785C7.57007 11.3079 7.67499 11.4128 7.80444 11.4128C7.93381 11.4128 8.03882 11.3079 8.03882 11.1785Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path id="Vector_8" d="M11.1793 8.03906C11.3086 8.03906 11.4135 7.93405 11.4135 7.80469C11.4135 7.67523 11.3086 7.57031 11.1793 7.57031H9.7731C9.64374 7.57031 9.53882 7.67523 9.53882 7.80469C9.53882 7.93405 9.64374 8.03906 9.7731 8.03906H11.1793Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
9
assets/icons/space_delete.svg
Normal file
9
assets/icons/space_delete.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.8069 19.9998H3.50035C2.16411 19.9998 1.08087 18.9165 1.08087 17.5804V4.51562H14.2262V17.5804C14.2262 18.9165 13.1432 19.9998 11.8069 19.9998Z" fill="#D8D8D8"/>
|
||||
<path d="M1.08087 4.51562V5.48335H12.3715C12.8168 5.48335 13.1779 5.84438 13.1779 6.2898V17.3384C13.1779 18.2292 12.4557 18.9513 11.5649 18.9513H2.4519C2.05409 18.9513 1.67887 18.8547 1.34775 18.6844C1.74907 19.4652 2.56207 19.9998 3.50035 19.9998H11.8069C13.1432 19.9998 14.2262 18.9165 14.2262 17.5803V4.51562H1.08087Z" fill="#BABABA"/>
|
||||
<path d="M14.2667 1.77417H10.7439L10.1633 0.612956C9.9742 0.234837 9.5941 0 9.17142 0H6.13594C5.71311 0 5.33316 0.234837 5.1441 0.612956L4.56349 1.77417H1.04063C0.595221 1.77417 0.234192 2.1352 0.234192 2.58061V3.70978C0.234192 4.15504 0.595221 4.51622 1.04063 4.51622H14.2667C14.7121 4.51622 15.0732 4.15504 15.0732 3.70978V2.58077C15.0732 2.1352 14.7121 1.77417 14.2667 1.77417ZM5.68503 0.8835C5.77094 0.71153 5.94368 0.604869 6.13594 0.604869H9.17142C9.36354 0.604869 9.53627 0.71153 9.62218 0.8835L10.0676 1.77417H5.23977L5.68503 0.8835Z" fill="#757575"/>
|
||||
<path d="M14.2668 1.77441H12.9763C13.4217 1.77441 13.7829 2.13544 13.7829 2.58086V3.71003C13.7829 4.15529 13.4217 4.51647 12.9763 4.51647H14.2668C14.7122 4.51647 15.0732 4.15529 15.0732 3.71003V2.58101C15.0732 2.13544 14.7122 1.77441 14.2668 1.77441Z" fill="#595959"/>
|
||||
<path d="M11.3634 17.5C10.918 17.5 10.5569 17.139 10.5569 16.6935V9.15312C10.5569 8.70771 10.918 8.34668 11.3634 8.34668C11.8088 8.34668 12.1698 8.70771 12.1698 9.15312V16.6935C12.1698 17.139 11.8088 17.5 11.3634 17.5Z" fill="#757575"/>
|
||||
<path d="M3.94398 17.5C4.38924 17.5 4.75043 17.139 4.75043 16.6935V9.15312C4.75043 8.70771 4.38924 8.34668 3.94398 8.34668C3.49857 8.34668 3.13739 8.70771 3.13739 9.15312V16.6935C3.13739 17.139 3.49857 17.5 3.94398 17.5Z" fill="#757575"/>
|
||||
<path d="M7.65361 17.5C7.2082 17.5 6.84717 17.139 6.84717 16.6935V9.15312C6.84717 8.70771 7.2082 8.34668 7.65361 8.34668C8.09902 8.34668 8.46005 8.70771 8.46005 9.15312V16.6935C8.46005 17.139 8.09902 17.5 7.65361 17.5Z" fill="#757575"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
138
lib/common/dialog_dropdown.dart
Normal file
138
lib/common/dialog_dropdown.dart
Normal file
@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DialogDropdown extends StatefulWidget {
|
||||
final List<String> items;
|
||||
final ValueChanged<String> onSelected;
|
||||
final String? selectedValue;
|
||||
|
||||
const DialogDropdown({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.selectedValue,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DialogDropdownState createState() => _DialogDropdownState();
|
||||
}
|
||||
|
||||
class _DialogDropdownState extends State<DialogDropdown> {
|
||||
bool _isOpen = false;
|
||||
late OverlayEntry _overlayEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _toggleDropdown() {
|
||||
if (_isOpen) {
|
||||
_closeDropdown();
|
||||
} else {
|
||||
_openDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
void _openDropdown() {
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry);
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
void _closeDropdown() {
|
||||
_overlayEntry.remove();
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
OverlayEntry _createOverlayEntry() {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
return OverlayEntry(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_closeDropdown();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy + size.height,
|
||||
width: size.width,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 200.0, // Set max height for dropdown
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.items[index];
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.lightGrayBorderColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
item,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
widget.onSelected(item);
|
||||
_closeDropdown();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _toggleDropdown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.selectedValue ?? 'Select an item',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
160
lib/common/dialog_textfield_dropdown.dart
Normal file
160
lib/common/dialog_textfield_dropdown.dart
Normal file
@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DialogTextfieldDropdown extends StatefulWidget {
|
||||
final List<String> items;
|
||||
final ValueChanged<String> onSelected;
|
||||
final String? initialValue;
|
||||
|
||||
const DialogTextfieldDropdown({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.initialValue,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DialogTextfieldDropdownState createState() =>
|
||||
_DialogTextfieldDropdownState();
|
||||
}
|
||||
|
||||
class _DialogTextfieldDropdownState extends State<DialogTextfieldDropdown> {
|
||||
bool _isOpen = false;
|
||||
late OverlayEntry _overlayEntry;
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
late List<String> _filteredItems; // Filtered items list
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.text = widget.initialValue ?? 'Select Tag';
|
||||
_filteredItems = List.from(widget.items); // Initialize filtered items
|
||||
}
|
||||
|
||||
void _toggleDropdown() {
|
||||
if (_isOpen) {
|
||||
_closeDropdown();
|
||||
} else {
|
||||
_openDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
void _openDropdown() {
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry);
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
void _closeDropdown() {
|
||||
_overlayEntry.remove();
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
OverlayEntry _createOverlayEntry() {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
return OverlayEntry(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_closeDropdown();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy + size.height,
|
||||
width: size.width,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 200.0,
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _filteredItems[index];
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.lightGrayBorderColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(item,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor)),
|
||||
onTap: () {
|
||||
_controller.text = item;
|
||||
widget.onSelected(item);
|
||||
setState(() {
|
||||
_filteredItems
|
||||
.remove(item); // Remove selected item
|
||||
});
|
||||
_closeDropdown();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _toggleDropdown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_filteredItems = widget.items
|
||||
.where((item) =>
|
||||
item.toLowerCase().contains(value.toLowerCase()))
|
||||
.toList(); // Filter items dynamically
|
||||
});
|
||||
widget.onSelected(value);
|
||||
},
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter or Select tag',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
39
lib/common/edit_chip.dart
Normal file
39
lib/common/edit_chip.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class EditChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final Color labelColor;
|
||||
final Color backgroundColor;
|
||||
final Color borderColor;
|
||||
final double borderRadius;
|
||||
|
||||
const EditChip({
|
||||
Key? key,
|
||||
this.label = 'Edit',
|
||||
required this.onTap,
|
||||
this.labelColor = ColorsManager.spaceColor,
|
||||
this.backgroundColor = ColorsManager.whiteColors,
|
||||
this.borderColor = ColorsManager.spaceColor,
|
||||
this.borderRadius = 16.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Chip(
|
||||
label: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: labelColor)
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
side: BorderSide(color: borderColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -19,12 +19,14 @@ class DefaultButton extends StatelessWidget {
|
||||
this.padding,
|
||||
this.borderColor,
|
||||
this.elevation,
|
||||
this.borderWidth = 1.0,
|
||||
});
|
||||
final void Function()? onPressed;
|
||||
final Widget child;
|
||||
final double? height;
|
||||
final bool isSecondary;
|
||||
final double? borderRadius;
|
||||
final double borderWidth;
|
||||
final bool enabled;
|
||||
final double? padding;
|
||||
final bool isDone;
|
||||
@ -66,13 +68,16 @@ class DefaultButton extends StatelessWidget {
|
||||
}),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
side: BorderSide(color: borderColor ?? Colors.transparent),
|
||||
side: BorderSide(
|
||||
color: borderColor ?? Colors.transparent,
|
||||
width: borderWidth,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 20),
|
||||
),
|
||||
),
|
||||
fixedSize: height != null
|
||||
? WidgetStateProperty.all(Size.fromHeight(height!))
|
||||
: null,
|
||||
? WidgetStateProperty.all(Size.fromHeight(height!))
|
||||
: null,
|
||||
padding: WidgetStateProperty.all(
|
||||
EdgeInsets.all(padding ?? 10),
|
||||
),
|
||||
|
@ -0,0 +1,47 @@
|
||||
class DeviceSubspace {
|
||||
final String uuid;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String subspaceName;
|
||||
final bool disabled;
|
||||
|
||||
DeviceSubspace({
|
||||
required this.uuid,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.subspaceName,
|
||||
required this.disabled,
|
||||
});
|
||||
|
||||
factory DeviceSubspace.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceSubspace(
|
||||
uuid: json['uuid'] as String,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.tryParse(json['createdAt'].toString())
|
||||
: null,
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.tryParse(json['updatedAt'].toString())
|
||||
: null,
|
||||
subspaceName: json['subspaceName'] as String,
|
||||
disabled: json['disabled'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'createdAt': createdAt?.toIso8601String(),
|
||||
'updatedAt': updatedAt?.toIso8601String(),
|
||||
'subspaceName': subspaceName,
|
||||
'disabled': disabled,
|
||||
};
|
||||
}
|
||||
|
||||
static List<DeviceSubspace> listFromJson(List<dynamic> jsonList) {
|
||||
return jsonList.map((json) => DeviceSubspace.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> listToJson(List<DeviceSubspace> subspaces) {
|
||||
return subspaces.map((subspace) => subspace.toJson()).toList();
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_community.model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_space_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_subspace.model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart';
|
||||
import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart';
|
||||
@ -47,6 +48,7 @@ class AllDevicesModel {
|
||||
*/
|
||||
|
||||
DevicesModelRoom? room;
|
||||
DeviceSubspace? subspace;
|
||||
DevicesModelUnit? unit;
|
||||
DeviceCommunityModel? community;
|
||||
String? productUuid;
|
||||
@ -77,6 +79,7 @@ class AllDevicesModel {
|
||||
|
||||
AllDevicesModel({
|
||||
this.room,
|
||||
this.subspace,
|
||||
this.unit,
|
||||
this.community,
|
||||
this.productUuid,
|
||||
@ -110,6 +113,9 @@ class AllDevicesModel {
|
||||
room = (json['room'] != null && (json['room'] is Map))
|
||||
? DevicesModelRoom.fromJson(json['room'])
|
||||
: null;
|
||||
subspace = (json['subspace'] != null && (json['subspace'] is Map))
|
||||
? DeviceSubspace.fromJson(json['subspace'])
|
||||
: null;
|
||||
unit = (json['unit'] != null && (json['unit'] is Map))
|
||||
? DevicesModelUnit.fromJson(json['unit'])
|
||||
: null;
|
||||
@ -288,6 +294,9 @@ SOS
|
||||
if (room != null) {
|
||||
data['room'] = room!.toJson();
|
||||
}
|
||||
if (subspace != null) {
|
||||
data['subspace'] = subspace!.toJson();
|
||||
}
|
||||
if (unit != null) {
|
||||
data['unit'] = unit!.toJson();
|
||||
}
|
||||
@ -330,6 +339,7 @@ SOS
|
||||
|
||||
return other is AllDevicesModel &&
|
||||
other.room == room &&
|
||||
other.subspace == subspace &&
|
||||
other.unit == unit &&
|
||||
other.productUuid == productUuid &&
|
||||
other.productType == productType &&
|
||||
@ -360,6 +370,7 @@ SOS
|
||||
@override
|
||||
int get hashCode {
|
||||
return room.hashCode ^
|
||||
subspace.hashCode ^
|
||||
unit.hashCode ^
|
||||
productUuid.hashCode ^
|
||||
productType.hashCode ^
|
||||
|
@ -95,8 +95,9 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
]),
|
||||
TableRow(
|
||||
children: [
|
||||
_buildInfoRow('Space Name:', device.unit?.name ?? 'N/A'),
|
||||
_buildInfoRow('Room:', device.room?.name ?? 'N/A'),
|
||||
_buildInfoRow('Space Name:',
|
||||
device.spaces?.firstOrNull?.spaceName ?? 'N/A'),
|
||||
_buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@ -111,9 +112,13 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Battery Level:',
|
||||
device.batteryLevel != null ? '${device.batteryLevel ?? 0}%' : "-",
|
||||
device.batteryLevel != null
|
||||
? '${device.batteryLevel ?? 0}%'
|
||||
: "-",
|
||||
statusColor: device.batteryLevel != null
|
||||
? (device.batteryLevel! < 20 ? ColorsManager.red : ColorsManager.green)
|
||||
? (device.batteryLevel! < 20
|
||||
? ColorsManager.red
|
||||
: ColorsManager.green)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/sos/bloc/sos_device_bloc.dart';
|
||||
|
||||
class SOSBatchControlView extends StatelessWidget {
|
||||
|
@ -0,0 +1,74 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
|
||||
|
||||
class AddDeviceTypeBloc extends Bloc<AddDeviceTypeEvent, AddDeviceState> {
|
||||
AddDeviceTypeBloc() : super(AddDeviceInitial()) {
|
||||
on<InitializeDevice>(_onInitializeTagModels);
|
||||
on<UpdateProductCountEvent>(_onUpdateProductCount);
|
||||
}
|
||||
|
||||
void _onInitializeTagModels(
|
||||
InitializeDevice event, Emitter<AddDeviceState> emit) {
|
||||
emit(AddDeviceLoaded(
|
||||
selectedProducts: event.addedProducts,
|
||||
initialTag: event.initialTags,
|
||||
));
|
||||
}
|
||||
|
||||
void _onUpdateProductCount(
|
||||
UpdateProductCountEvent event, Emitter<AddDeviceState> emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AddDeviceLoaded) {
|
||||
final existingProduct = currentState.selectedProducts.firstWhere(
|
||||
(p) => p.productId == event.productId,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: event.productId,
|
||||
count: 0,
|
||||
productName: event.productName,
|
||||
product: event.product,
|
||||
),
|
||||
);
|
||||
|
||||
List<SelectedProduct> updatedProducts;
|
||||
|
||||
if (event.count > 0) {
|
||||
if (!currentState.selectedProducts.contains(existingProduct)) {
|
||||
updatedProducts = [
|
||||
...currentState.selectedProducts,
|
||||
SelectedProduct(
|
||||
productId: event.productId,
|
||||
count: event.count,
|
||||
productName: event.productName,
|
||||
product: event.product,
|
||||
),
|
||||
];
|
||||
} else {
|
||||
updatedProducts = currentState.selectedProducts.map((p) {
|
||||
if (p.productId == event.productId) {
|
||||
return SelectedProduct(
|
||||
productId: p.productId,
|
||||
count: event.count,
|
||||
productName: p.productName,
|
||||
product: p.product,
|
||||
);
|
||||
}
|
||||
return p;
|
||||
}).toList();
|
||||
}
|
||||
} else {
|
||||
// Remove the product if the count is 0
|
||||
updatedProducts = currentState.selectedProducts
|
||||
.where((p) => p.productId != event.productId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Emit the updated state
|
||||
emit(AddDeviceLoaded(
|
||||
selectedProducts: updatedProducts,
|
||||
initialTag: currentState.initialTag));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
|
||||
abstract class AddDeviceState extends Equatable {
|
||||
const AddDeviceState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AddDeviceInitial extends AddDeviceState {}
|
||||
|
||||
class AddDeviceLoading extends AddDeviceState {}
|
||||
|
||||
class AddDeviceLoaded extends AddDeviceState {
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final List<Tag> initialTag;
|
||||
|
||||
const AddDeviceLoaded({
|
||||
required this.selectedProducts,
|
||||
required this.initialTag,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [selectedProducts, initialTag];
|
||||
}
|
||||
|
||||
class AddDeviceError extends AddDeviceState {
|
||||
final String errorMessage;
|
||||
|
||||
const AddDeviceError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
|
||||
abstract class AddDeviceTypeEvent extends Equatable {
|
||||
const AddDeviceTypeEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class UpdateProductCountEvent extends AddDeviceTypeEvent {
|
||||
final String productId;
|
||||
final int count;
|
||||
final String productName;
|
||||
final ProductModel product;
|
||||
|
||||
UpdateProductCountEvent(
|
||||
{required this.productId,
|
||||
required this.count,
|
||||
required this.productName,
|
||||
required this.product});
|
||||
|
||||
@override
|
||||
List<Object> get props => [productId, count];
|
||||
}
|
||||
|
||||
|
||||
class InitializeDevice extends AddDeviceTypeEvent {
|
||||
final List<Tag> initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeDevice({
|
||||
this.initialTags = const [],
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags, addedProducts];
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AddDeviceTypeWidget extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
|
||||
final List<SelectedProduct>? initialSelectedProducts;
|
||||
final List<SubspaceModel>? subspaces;
|
||||
final List<Tag>? spaceTags;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final bool isCreate;
|
||||
final Function(List<Tag>, List<SubspaceModel>?)? onSave;
|
||||
|
||||
const AddDeviceTypeWidget(
|
||||
{super.key,
|
||||
required this.isCreate,
|
||||
this.products,
|
||||
this.initialSelectedProducts,
|
||||
this.onProductsSelected,
|
||||
this.subspaces,
|
||||
this.allTags,
|
||||
this.spaceTags,
|
||||
this.onSave,
|
||||
required this.spaceName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final crossAxisCount = size.width > 1200
|
||||
? 8
|
||||
: size.width > 800
|
||||
? 5
|
||||
: 3;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => AddDeviceTypeBloc()
|
||||
..add(InitializeDevice(
|
||||
initialTags: spaceTags ?? [],
|
||||
addedProducts: initialSelectedProducts ?? [],
|
||||
)),
|
||||
child: Builder(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Add Devices'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: BlocBuilder<AddDeviceTypeBloc, AddDeviceState>(
|
||||
builder: (context, state) {
|
||||
if (state is AddDeviceLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state is AddDeviceLoaded) {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.height * 0.65,
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: ScrollableGridViewWidget(
|
||||
initialProductCounts: state.selectedProducts,
|
||||
products: products,
|
||||
isCreate: isCreate,
|
||||
crossAxisCount: crossAxisCount),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
}),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: BlocBuilder<AddDeviceTypeBloc, AddDeviceState>(
|
||||
builder: (context, state) {
|
||||
final isDisabled = state is AddDeviceLoaded &&
|
||||
state.selectedProducts.isEmpty;
|
||||
return DefaultButton(
|
||||
borderRadius: 10,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
foregroundColor: isDisabled
|
||||
? ColorsManager.whiteColorsWithOpacity
|
||||
: ColorsManager.whiteColors,
|
||||
onPressed: () async {
|
||||
if (state is AddDeviceLoaded &&
|
||||
state.selectedProducts.isNotEmpty) {
|
||||
final initialTags =
|
||||
TagHelper.generateInitialForTags(
|
||||
spaceTags: spaceTags,
|
||||
subspaces: subspaces,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final dialogTitle = initialTags.isNotEmpty
|
||||
? 'Edit Device'
|
||||
: 'Assign Tags';
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AssignTagDialog(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
addedProducts: state.selectedProducts,
|
||||
allTags: allTags,
|
||||
spaceName: spaceName,
|
||||
initialTags: initialTags,
|
||||
title: dialogTitle,
|
||||
onSave: onSave),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Next'),
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_name_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class DeviceTypeTileWidget extends StatelessWidget {
|
||||
final ProductModel product;
|
||||
final List<SelectedProduct> productCounts;
|
||||
final bool isCreate;
|
||||
|
||||
const DeviceTypeTileWidget(
|
||||
{super.key,
|
||||
required this.product,
|
||||
required this.productCounts,
|
||||
required this.isCreate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedProduct = productCounts.firstWhere(
|
||||
(p) => p.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: product.uuid,
|
||||
count: 0,
|
||||
productName: product.catName,
|
||||
product: product),
|
||||
);
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
DeviceIconWidget(icon: product.icon ?? Assets.doorSensor),
|
||||
const SizedBox(height: 4),
|
||||
DeviceNameWidget(name: product.name),
|
||||
const SizedBox(height: 4),
|
||||
CounterWidget(
|
||||
isCreate: isCreate,
|
||||
initialCount: selectedProduct.count,
|
||||
onCountChanged: (newCount) {
|
||||
context.read<AddDeviceTypeBloc>().add(
|
||||
UpdateProductCountEvent(
|
||||
productId: product.uuid,
|
||||
count: newCount,
|
||||
productName: product.catName,
|
||||
product: product),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
|
||||
class ScrollableGridViewWidget extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final int crossAxisCount;
|
||||
final List<SelectedProduct>? initialProductCounts;
|
||||
final bool isCreate;
|
||||
|
||||
const ScrollableGridViewWidget(
|
||||
{super.key,
|
||||
required this.products,
|
||||
required this.crossAxisCount,
|
||||
this.initialProductCounts,
|
||||
required this.isCreate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
|
||||
return Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: BlocBuilder<AddDeviceTypeBloc, AddDeviceState>(
|
||||
builder: (context, state) {
|
||||
final productCounts = state is AddDeviceLoaded
|
||||
? state.selectedProducts
|
||||
: <SelectedProduct>[];
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 6,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: .8,
|
||||
),
|
||||
itemCount: products?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products![index];
|
||||
final initialProductCount = _findInitialProductCount(product);
|
||||
|
||||
return DeviceTypeTileWidget(
|
||||
product: product,
|
||||
isCreate: isCreate,
|
||||
productCounts: initialProductCount != null
|
||||
? [...productCounts, initialProductCount]
|
||||
: productCounts,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SelectedProduct? _findInitialProductCount(ProductModel product) {
|
||||
if (initialProductCounts == null) return null;
|
||||
final matchingProduct = initialProductCounts!.firstWhere(
|
||||
(selectedProduct) => selectedProduct.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: '',
|
||||
count: 0,
|
||||
productName: '',
|
||||
product: null,
|
||||
),
|
||||
);
|
||||
|
||||
return matchingProduct.productId.isNotEmpty ? matchingProduct : null;
|
||||
}
|
||||
}
|
@ -1,20 +1,29 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart';
|
||||
import 'package:syncrow_web/services/product_api.dart';
|
||||
import 'package:syncrow_web/services/space_mana_api.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class SpaceManagementBloc
|
||||
extends Bloc<SpaceManagementEvent, SpaceManagementState> {
|
||||
final CommunitySpaceManagementApi _api;
|
||||
final ProductApi _productApi;
|
||||
final SpaceModelManagementApi _spaceModelApi;
|
||||
|
||||
List<ProductModel>? _cachedProducts;
|
||||
|
||||
SpaceManagementBloc(this._api, this._productApi)
|
||||
SpaceManagementBloc(this._api, this._productApi, this._spaceModelApi)
|
||||
: super(SpaceManagementInitial()) {
|
||||
on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces);
|
||||
on<UpdateSpacePositionEvent>(_onUpdateSpacePosition);
|
||||
@ -26,6 +35,8 @@ class SpaceManagementBloc
|
||||
on<FetchProductsEvent>(_onFetchProducts);
|
||||
on<SelectSpaceEvent>(_onSelectSpace);
|
||||
on<NewCommunityEvent>(_onNewCommunity);
|
||||
on<BlankStateEvent>(_onBlankState);
|
||||
on<SpaceModelLoadEvent>(_onLoadSpaceModel);
|
||||
}
|
||||
|
||||
void _onUpdateCommunity(
|
||||
@ -47,10 +58,14 @@ class SpaceManagementBloc
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: updatedCommunities,
|
||||
products: previousState.products,
|
||||
selectedCommunity: previousState.selectedCommunity,
|
||||
spaceModels: prevSpaceModels,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@ -61,6 +76,41 @@ class SpaceManagementBloc
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SpaceTemplateModel>> fetchSpaceModels(
|
||||
SpaceManagementState previousState) async {
|
||||
try {
|
||||
List<SpaceTemplateModel> allSpaces = [];
|
||||
List<SpaceTemplateModel> prevSpaceModels = [];
|
||||
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
previousState is BlankState) {
|
||||
prevSpaceModels = List<SpaceTemplateModel>.from(
|
||||
(previousState as dynamic).spaceModels ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
if (prevSpaceModels.isEmpty) {
|
||||
bool hasNext = true;
|
||||
int page = 1;
|
||||
|
||||
while (hasNext) {
|
||||
final spaces = await _spaceModelApi.listSpaceModels(page: page);
|
||||
if (spaces.isNotEmpty) {
|
||||
allSpaces.addAll(spaces);
|
||||
page++;
|
||||
} else {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
prevSpaceModels = await _spaceModelApi.listSpaceModels(page: 1);
|
||||
}
|
||||
|
||||
return allSpaces;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
void _onloadProducts() async {
|
||||
if (_cachedProducts == null) {
|
||||
final products = await _productApi.fetchProducts();
|
||||
@ -84,19 +134,66 @@ class SpaceManagementBloc
|
||||
return await _api.getSpaceHierarchy(communityUuid);
|
||||
}
|
||||
|
||||
void _onNewCommunity(
|
||||
Future<void> _onNewCommunity(
|
||||
NewCommunityEvent event,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) {
|
||||
) async {
|
||||
try {
|
||||
final previousState = state;
|
||||
|
||||
if (event.communities.isEmpty) {
|
||||
emit(const SpaceManagementError('No communities provided.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
emit(BlankState(
|
||||
communities: event.communities,
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels,
|
||||
));
|
||||
} catch (error) {
|
||||
emit(SpaceManagementError('Error loading communities: $error'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onBlankState(
|
||||
BlankStateEvent event, Emitter<SpaceManagementState> emit) async {
|
||||
try {
|
||||
final previousState = state;
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
previousState is BlankState) {
|
||||
final prevCommunities = (previousState as dynamic).communities ?? [];
|
||||
emit(BlankState(
|
||||
communities: List<CommunityModel>.from(prevCommunities),
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final communities = await _api.fetchCommunities();
|
||||
final updatedCommunities =
|
||||
await Future.wait(communities.map((community) async {
|
||||
final spaces = await _fetchSpacesForCommunity(community.uuid);
|
||||
return CommunityModel(
|
||||
uuid: community.uuid,
|
||||
createdAt: community.createdAt,
|
||||
updatedAt: community.updatedAt,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
spaces: spaces,
|
||||
region: community.region,
|
||||
);
|
||||
}));
|
||||
|
||||
emit(BlankState(
|
||||
spaceModels: prevSpaceModels,
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
));
|
||||
} catch (error) {
|
||||
emit(SpaceManagementError('Error loading communities: $error'));
|
||||
@ -107,6 +204,7 @@ class SpaceManagementBloc
|
||||
LoadCommunityAndSpacesEvent event,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) async {
|
||||
var prevState = state;
|
||||
emit(SpaceManagementLoading());
|
||||
try {
|
||||
_onloadProducts();
|
||||
@ -128,8 +226,11 @@ class SpaceManagementBloc
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
final prevSpaceModels = await fetchSpaceModels(prevState);
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: updatedCommunities, products: _cachedProducts ?? []));
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels));
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error loading communities and spaces: $e'));
|
||||
}
|
||||
@ -169,6 +270,7 @@ class SpaceManagementBloc
|
||||
try {
|
||||
CommunityModel? newCommunity =
|
||||
await _api.createCommunity(event.name, event.description);
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
if (newCommunity != null) {
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
@ -178,6 +280,7 @@ class SpaceManagementBloc
|
||||
);
|
||||
final updatedCommunities = prevCommunities..add(newCommunity);
|
||||
emit(SpaceManagementLoaded(
|
||||
spaceModels: prevSpaceModels,
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: newCommunity,
|
||||
@ -195,11 +298,15 @@ class SpaceManagementBloc
|
||||
SelectCommunityEvent event,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) async {
|
||||
_handleCommunitySpaceStateUpdate(
|
||||
emit: emit,
|
||||
selectedCommunity: event.selectedCommunity,
|
||||
selectedSpace: null,
|
||||
);
|
||||
try {
|
||||
_handleCommunitySpaceStateUpdate(
|
||||
emit: emit,
|
||||
selectedCommunity: event.selectedCommunity,
|
||||
selectedSpace: null,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error updating state: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectSpace(
|
||||
@ -223,16 +330,21 @@ class SpaceManagementBloc
|
||||
|
||||
try {
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
previousState is BlankState) {
|
||||
previousState is BlankState ||
|
||||
previousState is SpaceModelLoaded) {
|
||||
final communities = List<CommunityModel>.from(
|
||||
(previousState as dynamic).communities,
|
||||
);
|
||||
|
||||
final spaceModels = List<SpaceTemplateModel>.from(
|
||||
(previousState as dynamic).spaceModels,
|
||||
);
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
));
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
spaceModels: spaceModels));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error updating state: $e'));
|
||||
@ -255,7 +367,7 @@ class SpaceManagementBloc
|
||||
emit(SpaceCreationSuccess(spaces: updatedSpaces));
|
||||
|
||||
if (previousState is SpaceManagementLoaded) {
|
||||
_updateLoadedState(
|
||||
await _updateLoadedState(
|
||||
previousState,
|
||||
allSpaces,
|
||||
event.communityUuid,
|
||||
@ -273,23 +385,25 @@ class SpaceManagementBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _updateLoadedState(
|
||||
Future<void> _updateLoadedState(
|
||||
SpaceManagementLoaded previousState,
|
||||
List<SpaceModel> allSpaces,
|
||||
String communityUuid,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) {
|
||||
) async {
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
final communities = List<CommunityModel>.from(previousState.communities);
|
||||
|
||||
for (var community in communities) {
|
||||
if (community.uuid == communityUuid) {
|
||||
community.spaces = allSpaces;
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: community,
|
||||
selectedSpace: null,
|
||||
));
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: community,
|
||||
selectedSpace: null,
|
||||
spaceModels: prevSpaceModels));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -309,34 +423,125 @@ class SpaceManagementBloc
|
||||
await _api.deleteSpace(communityUuid, parent.uuid!);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow; // Decide whether to stop execution or continue
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
orderedSpaces.removeWhere((space) => parentsToDelete.contains(space));
|
||||
|
||||
for (var space in orderedSpaces) {
|
||||
try {
|
||||
if (space.uuid != null && space.uuid!.isNotEmpty) {
|
||||
List<TagModelUpdate> tagUpdates = [];
|
||||
|
||||
final prevSpace = await _api.getSpace(communityUuid, space.uuid!);
|
||||
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
|
||||
final List<SubspaceModel>? prevSubspaces = prevSpace?.subspaces;
|
||||
final List<SubspaceModel>? newSubspaces = space.subspaces;
|
||||
|
||||
tagUpdates = processTagUpdates(prevSpace?.tags, space.tags);
|
||||
|
||||
if (prevSubspaces != null || newSubspaces != null) {
|
||||
if (prevSubspaces != null && newSubspaces != null) {
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
final existsInNew = newSubspaces
|
||||
.any((subspace) => subspace.uuid == prevSubspace.uuid);
|
||||
if (!existsInNew) {
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete, uuid: prevSubspace.uuid));
|
||||
}
|
||||
}
|
||||
} else if (prevSubspaces != null && newSubspaces == null) {
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete, uuid: prevSubspace.uuid));
|
||||
}
|
||||
}
|
||||
|
||||
if (newSubspaces != null) {
|
||||
for (var newSubspace in newSubspaces) {
|
||||
// Tag without UUID
|
||||
if ((newSubspace.uuid == null || newSubspace.uuid!.isEmpty)) {
|
||||
final List<TagModelUpdate> tagUpdates = [];
|
||||
|
||||
if (newSubspace.tags != null) {
|
||||
for (var tag in newSubspace.tags!) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
uuid: tag.uuid == '' ? null : tag.uuid,
|
||||
tag: tag.tag,
|
||||
productUuid: tag.product?.uuid));
|
||||
}
|
||||
}
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.add,
|
||||
subspaceName: newSubspace.subspaceName,
|
||||
tags: tagUpdates));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSubspaces != null && newSubspaces != null) {
|
||||
final newSubspaceMap = {
|
||||
for (var subspace in newSubspaces) subspace.uuid: subspace
|
||||
};
|
||||
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
final newSubspace = newSubspaceMap[prevSubspace.uuid];
|
||||
|
||||
if (newSubspace != null) {
|
||||
final List<TagModelUpdate> tagSubspaceUpdates =
|
||||
processTagUpdates(prevSubspace.tags, newSubspace.tags);
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.update,
|
||||
uuid: newSubspace.uuid,
|
||||
subspaceName: newSubspace.subspaceName,
|
||||
tags: tagSubspaceUpdates));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final response = await _api.updateSpace(
|
||||
communityId: communityUuid,
|
||||
spaceId: space.uuid!,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
products: space.selectedProducts);
|
||||
communityId: communityUuid,
|
||||
spaceId: space.uuid!,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
subspaces: subspaceUpdates,
|
||||
tags: tagUpdates,
|
||||
direction: space.incomingConnection?.direction,
|
||||
);
|
||||
} else {
|
||||
// Call create if the space does not have a UUID
|
||||
final List<CreateTagBodyModel> tagBodyModels = space.tags != null
|
||||
? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList()
|
||||
: [];
|
||||
|
||||
final createSubspaceBodyModels = space.subspaces?.map((subspace) {
|
||||
final tagBodyModels = subspace.tags
|
||||
?.map((tag) => tag.toCreateTagBodyModel())
|
||||
.toList() ??
|
||||
[];
|
||||
return CreateSubspaceModel()
|
||||
..subspaceName = subspace.subspaceName
|
||||
..tags = tagBodyModels;
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
final response = await _api.createSpace(
|
||||
communityId: communityUuid,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
products: space.selectedProducts);
|
||||
communityId: communityUuid,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
spaceModelUuid: space.spaceModel?.uuid,
|
||||
tags: tagBodyModels,
|
||||
subspaces: createSubspaceBodyModels,
|
||||
);
|
||||
space.uuid = response?.uuid;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -370,4 +575,114 @@ class SpaceManagementBloc
|
||||
}
|
||||
return result.toList(); // Convert back to a list
|
||||
}
|
||||
|
||||
void _onLoadSpaceModel(
|
||||
SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
|
||||
emit(SpaceManagementLoading());
|
||||
try {
|
||||
var prevState = state;
|
||||
|
||||
List<CommunityModel> communities = await _api.fetchCommunities();
|
||||
|
||||
List<CommunityModel> updatedCommunities = await Future.wait(
|
||||
communities.map((community) async {
|
||||
List<SpaceModel> spaces =
|
||||
await _fetchSpacesForCommunity(community.uuid);
|
||||
return CommunityModel(
|
||||
uuid: community.uuid,
|
||||
createdAt: community.createdAt,
|
||||
updatedAt: community.updatedAt,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
spaces: spaces, // New spaces list
|
||||
region: community.region,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
var prevSpaceModels = await fetchSpaceModels(prevState);
|
||||
|
||||
emit(SpaceModelLoaded(
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels));
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error loading communities and spaces: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
List<TagModelUpdate> processTagUpdates(
|
||||
List<Tag>? prevTags,
|
||||
List<Tag>? newTags,
|
||||
) {
|
||||
final List<TagModelUpdate> tagUpdates = [];
|
||||
final processedTags = <String?>{};
|
||||
|
||||
if (prevTags == null && newTags != null) {
|
||||
for (var newTag in newTags) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
tag: newTag.tag,
|
||||
uuid: newTag.uuid,
|
||||
productUuid: newTag.product?.uuid,
|
||||
));
|
||||
}
|
||||
return tagUpdates;
|
||||
}
|
||||
|
||||
if (newTags != null || prevTags != null) {
|
||||
// Case 1: Tags deleted
|
||||
if (prevTags != null && newTags != null) {
|
||||
for (var prevTag in prevTags) {
|
||||
final existsInNew =
|
||||
newTags.any((newTag) => newTag.uuid == prevTag.uuid);
|
||||
if (!existsInNew) {
|
||||
tagUpdates
|
||||
.add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid));
|
||||
}
|
||||
}
|
||||
} else if (prevTags != null && newTags == null) {
|
||||
for (var prevTag in prevTags) {
|
||||
tagUpdates
|
||||
.add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid));
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Tags added
|
||||
if (newTags != null) {
|
||||
final prevTagUuids = prevTags?.map((t) => t.uuid).toSet() ?? {};
|
||||
|
||||
for (var newTag in newTags) {
|
||||
// Tag without UUID
|
||||
if ((newTag.uuid == null || !prevTagUuids.contains(newTag.uuid)) &&
|
||||
!processedTags.contains(newTag.tag)) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
tag: newTag.tag,
|
||||
uuid: newTag.uuid == '' ? null : newTag.uuid,
|
||||
productUuid: newTag.product?.uuid));
|
||||
processedTags.add(newTag.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Tags updated
|
||||
if (prevTags != null && newTags != null) {
|
||||
final newTagMap = {for (var tag in newTags) tag.uuid: tag};
|
||||
|
||||
for (var prevTag in prevTags) {
|
||||
final newTag = newTagMap[prevTag.uuid];
|
||||
if (newTag != null) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.update,
|
||||
uuid: newTag.uuid,
|
||||
tag: newTag.tag,
|
||||
));
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagUpdates;
|
||||
}
|
||||
}
|
||||
|
@ -140,3 +140,8 @@ class LoadSpaceHierarchyEvent extends SpaceManagementEvent {
|
||||
@override
|
||||
List<Object> get props => [communityId];
|
||||
}
|
||||
|
||||
|
||||
class BlankStateEvent extends SpaceManagementEvent {}
|
||||
|
||||
class SpaceModelLoadEvent extends SpaceManagementEvent {}
|
||||
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class SpaceManagementState extends Equatable {
|
||||
const SpaceManagementState();
|
||||
@ -19,22 +20,27 @@ class SpaceManagementLoaded extends SpaceManagementState {
|
||||
final List<ProductModel> products;
|
||||
CommunityModel? selectedCommunity;
|
||||
SpaceModel? selectedSpace;
|
||||
List<SpaceTemplateModel>? spaceModels;
|
||||
|
||||
SpaceManagementLoaded(
|
||||
{required this.communities,
|
||||
required this.products,
|
||||
this.selectedCommunity,
|
||||
this.selectedSpace});
|
||||
this.selectedSpace,
|
||||
this.spaceModels});
|
||||
}
|
||||
|
||||
class SpaceModelManagenetLoaded extends SpaceManagementState {
|
||||
SpaceModelManagenetLoaded();
|
||||
}
|
||||
|
||||
class BlankState extends SpaceManagementState {
|
||||
final List<CommunityModel> communities;
|
||||
final List<ProductModel> products;
|
||||
List<SpaceTemplateModel>? spaceModels;
|
||||
|
||||
BlankState({
|
||||
required this.communities,
|
||||
required this.products,
|
||||
});
|
||||
BlankState(
|
||||
{required this.communities, required this.products, this.spaceModels});
|
||||
}
|
||||
|
||||
class SpaceCreationSuccess extends SpaceManagementState {
|
||||
@ -54,3 +60,18 @@ class SpaceManagementError extends SpaceManagementState {
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
||||
|
||||
class SpaceModelLoaded extends SpaceManagementState {
|
||||
List<SpaceTemplateModel> spaceModels;
|
||||
final List<ProductModel> products;
|
||||
final List<CommunityModel> communities;
|
||||
|
||||
SpaceModelLoaded({
|
||||
required this.communities,
|
||||
required this.products,
|
||||
required this.spaceModels,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [communities, products, spaceModels];
|
||||
}
|
||||
|
26
lib/pages/spaces_management/all_spaces/model/base_tag.dart
Normal file
26
lib/pages/spaces_management/all_spaces/model/base_tag.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class BaseTag {
|
||||
String? uuid;
|
||||
String? tag;
|
||||
final ProductModel? product;
|
||||
String internalId;
|
||||
String? location;
|
||||
|
||||
BaseTag({
|
||||
this.uuid,
|
||||
required this.tag,
|
||||
this.product,
|
||||
String? internalId,
|
||||
this.location,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
BaseTag copyWith({
|
||||
String? tag,
|
||||
ProductModel? product,
|
||||
String? location,
|
||||
String? internalId,
|
||||
});
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
|
||||
|
||||
class CreateSubspaceModel {
|
||||
late String subspaceName;
|
||||
late List<CreateTagBodyModel>? tags;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((tag) => tag.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ class ProductModel {
|
||||
|
||||
// Factory method to create a Product from JSON
|
||||
factory ProductModel.fromMap(Map<String, dynamic> json) {
|
||||
String icon = _mapIconToProduct(json['prodType']);
|
||||
return ProductModel(
|
||||
uuid: json['uuid'],
|
||||
catName: json['catName'],
|
||||
@ -67,4 +66,25 @@ class ProductModel {
|
||||
String toString() {
|
||||
return 'ProductModel(uuid: $uuid, catName: $catName, prodId: $prodId, prodType: $prodType, name: $name, icon: $icon)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ProductModel &&
|
||||
runtimeType == other.runtimeType &&
|
||||
uuid == other.uuid &&
|
||||
catName == other.catName &&
|
||||
prodId == other.prodId &&
|
||||
prodType == other.prodType &&
|
||||
name == other.name &&
|
||||
icon == other.icon;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
uuid.hashCode ^
|
||||
catName.hashCode ^
|
||||
prodId.hashCode ^
|
||||
prodType.hashCode ^
|
||||
name.hashCode ^
|
||||
icon.hashCode;
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
|
||||
class SelectedProduct {
|
||||
final String productId;
|
||||
int count;
|
||||
final String productName;
|
||||
final ProductModel? product;
|
||||
|
||||
SelectedProduct({required this.productId, required this.count});
|
||||
SelectedProduct({required this.productId, required this.count, required this.productName, this.product});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'productId': productId,
|
||||
'count': count,
|
||||
'productName': productName,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import 'dart:ui';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum SpaceStatus { newSpace, modified, unchanged, deleted }
|
||||
enum SpaceStatus { newSpace, modified, unchanged, deleted, parentDeleted }
|
||||
|
||||
class SpaceModel {
|
||||
String? uuid;
|
||||
@ -20,8 +22,10 @@ class SpaceModel {
|
||||
Offset position;
|
||||
bool isHovered;
|
||||
SpaceStatus status;
|
||||
List<SelectedProduct> selectedProducts;
|
||||
String internalId;
|
||||
SpaceTemplateModel? spaceModel;
|
||||
List<Tag>? tags;
|
||||
List<SubspaceModel>? subspaces;
|
||||
|
||||
List<Connection> outgoingConnections = []; // Connections from this space
|
||||
Connection? incomingConnection; // Connections to this space
|
||||
@ -41,7 +45,9 @@ class SpaceModel {
|
||||
this.isHovered = false,
|
||||
this.incomingConnection,
|
||||
this.status = SpaceStatus.unchanged,
|
||||
this.selectedProducts = const [],
|
||||
this.spaceModel,
|
||||
this.tags,
|
||||
this.subspaces,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SpaceModel.fromJson(Map<String, dynamic> json,
|
||||
@ -60,10 +66,14 @@ class SpaceModel {
|
||||
final instance = SpaceModel(
|
||||
internalId: internalId,
|
||||
uuid: json['uuid'] ?? '',
|
||||
spaceTuyaUuid: json['spaceTuyaUuid'],
|
||||
name: json['spaceName'],
|
||||
isPrivate: json['isPrivate'] ?? false,
|
||||
invitationCode: json['invitationCode'],
|
||||
subspaces: (json['subspaces'] as List<dynamic>?)
|
||||
?.where((e) => e is Map<String, dynamic>) // Validate type
|
||||
.map((e) => SubspaceModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
parent: parentInternalId != null
|
||||
? SpaceModel(
|
||||
internalId: parentInternalId,
|
||||
@ -85,14 +95,11 @@ class SpaceModel {
|
||||
icon: json['icon'] ?? Assets.location,
|
||||
position: Offset(json['x'] ?? 0, json['y'] ?? 0),
|
||||
isHovered: false,
|
||||
selectedProducts: json['spaceProducts'] != null
|
||||
? (json['spaceProducts'] as List).map((product) {
|
||||
return SelectedProduct(
|
||||
productId: product['product']['uuid'],
|
||||
count: product['productCount'],
|
||||
);
|
||||
}).toList()
|
||||
: [],
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.where((item) => item is Map<String, dynamic>) // Validate type
|
||||
.map((item) => Tag.fromJson(item as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
|
||||
if (json['incomingConnections'] != null &&
|
||||
@ -118,6 +125,7 @@ class SpaceModel {
|
||||
'isPrivate': isPrivate,
|
||||
'invitationCode': invitationCode,
|
||||
'parent': parent?.uuid,
|
||||
'subspaces': subspaces?.map((e) => e.toJson()).toList(),
|
||||
'community': community?.toMap(),
|
||||
'children': children.map((child) => child.toMap()).toList(),
|
||||
'icon': icon,
|
||||
@ -125,6 +133,7 @@ class SpaceModel {
|
||||
'isHovered': isHovered,
|
||||
'outgoingConnections': outgoingConnections.map((c) => c.toMap()).toList(),
|
||||
'incomingConnection': incomingConnection?.toMap(),
|
||||
'tags': tags?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -132,3 +141,28 @@ class SpaceModel {
|
||||
outgoingConnections.add(connection);
|
||||
}
|
||||
}
|
||||
|
||||
extension SpaceExtensions on SpaceModel {
|
||||
List<String> listAllTagValues() {
|
||||
final List<String> tagValues = [];
|
||||
|
||||
if (tags != null) {
|
||||
tagValues.addAll(
|
||||
tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty));
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (final subspace in subspaces!) {
|
||||
if (subspace.tags != null) {
|
||||
tagValues.addAll(
|
||||
subspace.tags!
|
||||
.map((tag) => tag.tag ?? '')
|
||||
.where((tag) => tag.isNotEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagValues;
|
||||
}
|
||||
}
|
||||
|
116
lib/pages/spaces_management/all_spaces/model/subspace_model.dart
Normal file
116
lib/pages/spaces_management/all_spaces/model/subspace_model.dart
Normal file
@ -0,0 +1,116 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'tag.dart';
|
||||
|
||||
class SubspaceModel {
|
||||
final String? uuid;
|
||||
String subspaceName;
|
||||
final bool disabled;
|
||||
List<Tag>? tags;
|
||||
String internalId;
|
||||
|
||||
SubspaceModel({
|
||||
this.uuid,
|
||||
required this.subspaceName,
|
||||
required this.disabled,
|
||||
this.tags,
|
||||
String? internalId,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SubspaceModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return SubspaceModel(
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
disabled: json['disabled'] ?? false,
|
||||
internalId: internalId,
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((item) => Tag.fromJson(item))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'disabled': disabled,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateSubspaceModel {
|
||||
final String uuid;
|
||||
final Action action;
|
||||
final String? subspaceName;
|
||||
final List<UpdateTag>? tags;
|
||||
UpdateSubspaceModel({
|
||||
required this.action,
|
||||
required this.uuid,
|
||||
this.subspaceName,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
factory UpdateSubspaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return UpdateSubspaceModel(
|
||||
action: ActionExtension.fromValue(json['action']),
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
tags: (json['tags'] as List)
|
||||
.map((item) => UpdateTag.fromJson(item))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateTag {
|
||||
final Action action;
|
||||
final String? uuid;
|
||||
final String tag;
|
||||
final bool disabled;
|
||||
final ProductModel? product;
|
||||
|
||||
UpdateTag({
|
||||
required this.action,
|
||||
this.uuid,
|
||||
required this.tag,
|
||||
required this.disabled,
|
||||
this.product,
|
||||
});
|
||||
|
||||
factory UpdateTag.fromJson(Map<String, dynamic> json) {
|
||||
return UpdateTag(
|
||||
action: ActionExtension.fromValue(json['action']),
|
||||
uuid: json['uuid'] ?? '',
|
||||
tag: json['tag'] ?? '',
|
||||
disabled: json['disabled'] ?? false,
|
||||
product: json['product'] != null
|
||||
? ProductModel.fromMap(json['product'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'disabled': disabled,
|
||||
'product': product?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
73
lib/pages/spaces_management/all_spaces/model/tag.dart
Normal file
73
lib/pages/spaces_management/all_spaces/model/tag.dart
Normal file
@ -0,0 +1,73 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Tag extends BaseTag {
|
||||
Tag({
|
||||
String? uuid,
|
||||
required String? tag,
|
||||
ProductModel? product,
|
||||
String? internalId,
|
||||
String? location,
|
||||
}) : super(
|
||||
uuid: uuid,
|
||||
tag: tag,
|
||||
product: product,
|
||||
internalId: internalId,
|
||||
location: location,
|
||||
);
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return Tag(
|
||||
uuid: json['uuid'] ?? '',
|
||||
internalId: internalId,
|
||||
tag: json['tag'] ?? '',
|
||||
product: json['product'] != null
|
||||
? ProductModel.fromMap(json['product'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Tag copyWith({
|
||||
String? tag,
|
||||
ProductModel? product,
|
||||
String? location,
|
||||
String? internalId,
|
||||
}) {
|
||||
return Tag(
|
||||
uuid: uuid,
|
||||
tag: tag ?? this.tag,
|
||||
product: product ?? this.product,
|
||||
location: location ?? this.location,
|
||||
internalId: internalId ?? this.internalId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'product': product?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension TagModelExtensions on Tag {
|
||||
TagBodyModel toTagBodyModel() {
|
||||
return TagBodyModel()
|
||||
..uuid = uuid ?? ''
|
||||
..tag = tag ?? ''
|
||||
..productUuid = product?.uuid;
|
||||
}
|
||||
|
||||
CreateTagBodyModel toCreateTagBodyModel() {
|
||||
return CreateTagBodyModel()
|
||||
..tag = tag ?? ''
|
||||
..productUuid = product?.uuid;
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/view/center_body_widget.dart';
|
||||
import 'package:syncrow_web/services/product_api.dart';
|
||||
import 'package:syncrow_web/services/space_mana_api.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class SpaceManagementPage extends StatefulWidget {
|
||||
@ -22,48 +22,59 @@ class SpaceManagementPage extends StatefulWidget {
|
||||
class SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
|
||||
final ProductApi _productApi = ProductApi();
|
||||
Map<String, List<SpaceModel>> communitySpaces = {};
|
||||
List<ProductModel> products = [];
|
||||
bool isProductDataLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceManagementBloc(_api, _productApi)
|
||||
..add(LoadCommunityAndSpacesEvent()),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => SpaceManagementBloc(_api, _productApi, _spaceModelApi)
|
||||
..add(LoadCommunityAndSpacesEvent()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => CenterBodyBloc(),
|
||||
),
|
||||
],
|
||||
child: WebScaffold(
|
||||
appBarTitle: Text('Space Management',
|
||||
style: Theme.of(context).textTheme.headlineLarge),
|
||||
enableMenuSidebar: false,
|
||||
centerBody: CenterBodyWidget(),
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
scaffoldBody: BlocBuilder<SpaceManagementBloc, SpaceManagementState>(
|
||||
builder: (context, state) {
|
||||
if (state is SpaceManagementLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is BlankState) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: null,
|
||||
selectedSpace: null,
|
||||
products: state.products,
|
||||
);
|
||||
} else if (state is SpaceManagementLoaded) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: state.selectedCommunity,
|
||||
selectedSpace: state.selectedSpace,
|
||||
products: state.products,
|
||||
);
|
||||
} else if (state is SpaceManagementError) {
|
||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
builder: (context, state) {
|
||||
if (state is SpaceManagementLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is BlankState) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: null,
|
||||
selectedSpace: null,
|
||||
products: state.products,
|
||||
shouldNavigateToSpaceModelPage: false,
|
||||
);
|
||||
} else if (state is SpaceManagementLoaded) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: state.selectedCommunity,
|
||||
selectedSpace: state.selectedSpace,
|
||||
products: state.products,
|
||||
spaceModels: state.spaceModels,
|
||||
shouldNavigateToSpaceModelPage: false,
|
||||
);
|
||||
} else if (state is SpaceModelLoaded) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
products: state.products,
|
||||
spaceModels: state.spaceModels,
|
||||
shouldNavigateToSpaceModelPage: true,
|
||||
);
|
||||
} else if (state is SpaceManagementError) {
|
||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
|
||||
_buildActionButton('Cancel', ColorsManager.boxColor, ColorsManager.blackColor, () {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
_buildActionButton('Continue', ColorsManager.secondaryColor, Colors.white, () {
|
||||
_buildActionButton('Continue', ColorsManager.secondaryColor, ColorsManager.whiteColors, () {
|
||||
Navigator.of(context).pop();
|
||||
if (widget.onProductsSelected != null) {
|
||||
widget.onProductsSelected!(productCounts);
|
||||
@ -114,7 +114,7 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
|
||||
Widget _buildDeviceTypeTile(ProductModel product, Size size) {
|
||||
final selectedProduct = productCounts.firstWhere(
|
||||
(p) => p.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(productId: product.uuid, count: 0),
|
||||
orElse: () => SelectedProduct(productId: product.uuid, count: 0, productName: product.catName, product: product),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
@ -137,13 +137,14 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
|
||||
_buildDeviceName(product, size),
|
||||
const SizedBox(height: 4),
|
||||
CounterWidget(
|
||||
isCreate: false,
|
||||
initialCount: selectedProduct.count,
|
||||
onCountChanged: (newCount) {
|
||||
setState(() {
|
||||
if (newCount > 0) {
|
||||
if (!productCounts.contains(selectedProduct)) {
|
||||
productCounts
|
||||
.add(SelectedProduct(productId: product.uuid, count: newCount));
|
||||
.add(SelectedProduct(productId: product.uuid, count: newCount, productName: product.catName, product: product));
|
||||
} else {
|
||||
selectedProduct.count = newCount;
|
||||
}
|
||||
|
@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class CommunityStructureHeaderActionButtons extends StatelessWidget {
|
||||
const CommunityStructureHeaderActionButtons(
|
||||
{super.key,
|
||||
required this.theme,
|
||||
required this.isSave,
|
||||
required this.onSave,
|
||||
required this.onDelete,
|
||||
required this.selectedSpace,
|
||||
required this.onDuplicate,
|
||||
required this.onEdit});
|
||||
|
||||
final ThemeData theme;
|
||||
final bool isSave;
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onDelete;
|
||||
final VoidCallback onDuplicate;
|
||||
final VoidCallback onEdit;
|
||||
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canShowActions = selectedSpace != null &&
|
||||
selectedSpace?.status != SpaceStatus.deleted &&
|
||||
selectedSpace?.status != SpaceStatus.parentDeleted;
|
||||
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (isSave)
|
||||
CommunityStructureHeaderButton(
|
||||
label: "Save",
|
||||
icon: const Icon(Icons.save,
|
||||
size: 18, color: ColorsManager.spaceColor),
|
||||
onPressed: onSave,
|
||||
theme: theme,
|
||||
),
|
||||
if (canShowActions) ...[
|
||||
CommunityStructureHeaderButton(
|
||||
label: "Edit",
|
||||
svgAsset: Assets.editSpace,
|
||||
onPressed: onEdit,
|
||||
theme: theme,
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: "Duplicate",
|
||||
svgAsset: Assets.duplicate,
|
||||
onPressed: onDuplicate,
|
||||
theme: theme,
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: "Delete",
|
||||
svgAsset: Assets.spaceDelete,
|
||||
onPressed: onDelete,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CommunityStructureHeaderButton extends StatelessWidget {
|
||||
const CommunityStructureHeaderButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
this.svgAsset,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final Widget? icon;
|
||||
final VoidCallback onPressed;
|
||||
final String? svgAsset;
|
||||
final ThemeData theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double buttonHeight = 40;
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 130,
|
||||
minHeight: buttonHeight,
|
||||
),
|
||||
child: DefaultButton(
|
||||
onPressed: onPressed,
|
||||
borderWidth: 2,
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: ColorsManager.blackColor,
|
||||
borderRadius: 12.0,
|
||||
padding: 2.0,
|
||||
height: buttonHeight,
|
||||
elevation: 0,
|
||||
borderColor: ColorsManager.lightGrayColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) icon!,
|
||||
if (svgAsset != null)
|
||||
SvgPicture.asset(
|
||||
svgAsset!,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
@ -13,10 +14,14 @@ class CommunityStructureHeader extends StatefulWidget {
|
||||
final TextEditingController nameController;
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onDelete;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDuplicate;
|
||||
|
||||
final VoidCallback onEditName;
|
||||
final ValueChanged<String> onNameSubmitted;
|
||||
final List<CommunityModel> communities;
|
||||
final CommunityModel? community;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
const CommunityStructureHeader(
|
||||
{super.key,
|
||||
@ -29,7 +34,10 @@ class CommunityStructureHeader extends StatefulWidget {
|
||||
required this.onEditName,
|
||||
required this.onNameSubmitted,
|
||||
this.community,
|
||||
required this.communities});
|
||||
required this.communities,
|
||||
this.selectedSpace,
|
||||
required this.onDuplicate,
|
||||
required this.onEdit});
|
||||
|
||||
@override
|
||||
State<CommunityStructureHeader> createState() =>
|
||||
@ -137,65 +145,19 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.isSave) ...[
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButtons(theme),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
CommunityStructureHeaderActionButtons(
|
||||
theme: theme,
|
||||
isSave: widget.isSave,
|
||||
onSave: widget.onSave,
|
||||
onDelete: widget.onDelete,
|
||||
onDuplicate: widget.onDuplicate,
|
||||
onEdit: widget.onEdit,
|
||||
selectedSpace: widget.selectedSpace,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(ThemeData theme) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
spacing: 10,
|
||||
children: [
|
||||
_buildButton(
|
||||
label: "Save",
|
||||
icon: const Icon(Icons.save,
|
||||
size: 18, color: ColorsManager.spaceColor),
|
||||
onPressed: widget.onSave,
|
||||
theme: theme),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
{required String label,
|
||||
required Widget icon,
|
||||
required VoidCallback onPressed,
|
||||
required ThemeData theme}) {
|
||||
const double buttonHeight = 30;
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 80, minHeight: buttonHeight),
|
||||
child: DefaultButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: ColorsManager.blackColor,
|
||||
borderRadius: 8.0,
|
||||
padding: 2.0,
|
||||
height: buttonHeight,
|
||||
elevation: 0,
|
||||
borderColor: Colors.grey.shade300,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 5),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
// Syncrow project imports
|
||||
import 'package:syncrow_web/pages/common/buttons/add_space_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
@ -11,12 +13,16 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/duplicate_process_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CommunityStructureArea extends StatefulWidget {
|
||||
@ -26,6 +32,7 @@ class CommunityStructureArea extends StatefulWidget {
|
||||
final ValueChanged<SpaceModel?>? onSpaceSelected;
|
||||
final List<CommunityModel> communities;
|
||||
final List<SpaceModel> spaces;
|
||||
final List<SpaceTemplateModel>? spaceModels;
|
||||
|
||||
CommunityStructureArea({
|
||||
this.selectedCommunity,
|
||||
@ -34,6 +41,7 @@ class CommunityStructureArea extends StatefulWidget {
|
||||
this.products,
|
||||
required this.spaces,
|
||||
this.onSpaceSelected,
|
||||
this.spaceModels,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -126,7 +134,10 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
isEditingName: isEditingName,
|
||||
nameController: _nameController,
|
||||
onSave: _saveSpaces,
|
||||
selectedSpace: widget.selectedSpace,
|
||||
onDelete: _onDelete,
|
||||
onDuplicate: () => {_onDuplicate(context)},
|
||||
onEdit: () => {_showEditSpaceDialog()},
|
||||
onEditName: () {
|
||||
setState(() {
|
||||
isEditingName = !isEditingName;
|
||||
@ -171,7 +182,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
painter: CurvedLinePainter([connection])),
|
||||
),
|
||||
for (var entry in spaces.asMap().entries)
|
||||
if (entry.value.status != SpaceStatus.deleted)
|
||||
if (entry.value.status != SpaceStatus.deleted &&
|
||||
entry.value.status != SpaceStatus.parentDeleted)
|
||||
Positioned(
|
||||
left: entry.value.position.dx,
|
||||
top: entry.value.position.dy,
|
||||
@ -183,6 +195,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
screenSize,
|
||||
position:
|
||||
spaces[index].position + newPosition,
|
||||
|
||||
parentIndex: index,
|
||||
direction: direction,
|
||||
);
|
||||
@ -202,9 +215,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
opacity: isHighlighted ? 1.0 : 0.3,
|
||||
child: SpaceContainerWidget(
|
||||
index: index,
|
||||
onDoubleTap: () {
|
||||
_showEditSpaceDialog(spaces[index]);
|
||||
},
|
||||
onTap: () {
|
||||
_selectSpace(context, spaces[index]);
|
||||
},
|
||||
@ -284,12 +294,17 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
builder: (BuildContext context) {
|
||||
return CreateSpaceDialog(
|
||||
products: widget.products,
|
||||
spaceModels: widget.spaceModels,
|
||||
allTags: _getAllTagValues(spaces),
|
||||
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
|
||||
onCreateSpace: (String name, String icon,
|
||||
List<SelectedProduct> selectedProducts) {
|
||||
onCreateSpace: (String name,
|
||||
String icon,
|
||||
List<SelectedProduct> selectedProducts,
|
||||
SpaceTemplateModel? spaceModel,
|
||||
List<SubspaceModel>? subspaces,
|
||||
List<Tag>? tags) {
|
||||
setState(() {
|
||||
// Set the first space in the center or use passed position
|
||||
|
||||
Offset centerPosition =
|
||||
position ?? _getCenterPosition(screenSize);
|
||||
SpaceModel newSpace = SpaceModel(
|
||||
@ -299,7 +314,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
isPrivate: false,
|
||||
children: [],
|
||||
status: SpaceStatus.newSpace,
|
||||
selectedProducts: selectedProducts);
|
||||
spaceModel: spaceModel,
|
||||
subspaces: subspaces,
|
||||
tags: tags);
|
||||
|
||||
if (parentIndex != null && direction != null) {
|
||||
SpaceModel parentSpace = spaces[parentIndex];
|
||||
@ -315,7 +332,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
parentSpace.addOutgoingConnection(newConnection);
|
||||
parentSpace.children.add(newSpace);
|
||||
}
|
||||
|
||||
spaces.add(newSpace);
|
||||
_updateNodePosition(newSpace, newSpace.position);
|
||||
});
|
||||
@ -325,34 +341,46 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditSpaceDialog(SpaceModel space) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreateSpaceDialog(
|
||||
products: widget.products,
|
||||
name: space.name,
|
||||
icon: space.icon,
|
||||
editSpace: space,
|
||||
isEdit: true,
|
||||
selectedProducts: space.selectedProducts,
|
||||
onCreateSpace: (String name, String icon,
|
||||
List<SelectedProduct> selectedProducts) {
|
||||
setState(() {
|
||||
// Update the space's properties
|
||||
space.name = name;
|
||||
space.icon = icon;
|
||||
space.selectedProducts = selectedProducts;
|
||||
void _showEditSpaceDialog() {
|
||||
if (widget.selectedSpace != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreateSpaceDialog(
|
||||
products: widget.products,
|
||||
spaceModels: widget.spaceModels,
|
||||
name: widget.selectedSpace!.name,
|
||||
icon: widget.selectedSpace!.icon,
|
||||
editSpace: widget.selectedSpace,
|
||||
tags: widget.selectedSpace?.tags,
|
||||
subspaces: widget.selectedSpace?.subspaces,
|
||||
isEdit: true,
|
||||
allTags: _getAllTagValues(spaces),
|
||||
onCreateSpace: (String name,
|
||||
String icon,
|
||||
List<SelectedProduct> selectedProducts,
|
||||
SpaceTemplateModel? spaceModel,
|
||||
List<SubspaceModel>? subspaces,
|
||||
List<Tag>? tags) {
|
||||
setState(() {
|
||||
// Update the space's properties
|
||||
widget.selectedSpace!.name = name;
|
||||
widget.selectedSpace!.icon = icon;
|
||||
widget.selectedSpace!.spaceModel = spaceModel;
|
||||
widget.selectedSpace!.subspaces = subspaces;
|
||||
widget.selectedSpace!.tags = tags;
|
||||
|
||||
if (space.status != SpaceStatus.newSpace) {
|
||||
space.status = SpaceStatus.modified; // Mark as modified
|
||||
}
|
||||
});
|
||||
},
|
||||
key: Key(space.name),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (widget.selectedSpace!.status != SpaceStatus.newSpace) {
|
||||
widget.selectedSpace!.status =
|
||||
SpaceStatus.modified; // Mark as modified
|
||||
}
|
||||
});
|
||||
},
|
||||
key: Key(widget.selectedSpace!.name),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleHoverChanged(int index, bool isHovered) {
|
||||
@ -365,10 +393,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
List<SpaceModel> result = [];
|
||||
|
||||
void flatten(SpaceModel space) {
|
||||
if (space.status == SpaceStatus.deleted) return;
|
||||
|
||||
if (space.status == SpaceStatus.deleted ||
|
||||
space.status == SpaceStatus.parentDeleted) {
|
||||
return;
|
||||
}
|
||||
result.add(space);
|
||||
|
||||
for (var child in space.children) {
|
||||
flatten(child);
|
||||
}
|
||||
@ -436,21 +465,15 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
}
|
||||
|
||||
void _onDelete() {
|
||||
if (widget.selectedCommunity != null &&
|
||||
widget.selectedCommunity?.uuid != null &&
|
||||
widget.selectedSpace == null) {
|
||||
context.read<SpaceManagementBloc>().add(DeleteCommunityEvent(
|
||||
communityUuid: widget.selectedCommunity!.uuid,
|
||||
));
|
||||
}
|
||||
if (widget.selectedSpace != null) {
|
||||
setState(() {
|
||||
for (var space in spaces) {
|
||||
if (space.uuid == widget.selectedSpace?.uuid) {
|
||||
if (space.internalId == widget.selectedSpace?.internalId) {
|
||||
space.status = SpaceStatus.deleted;
|
||||
_markChildrenAsDeleted(space);
|
||||
}
|
||||
}
|
||||
|
||||
_removeConnectionsForDeletedSpaces();
|
||||
});
|
||||
}
|
||||
@ -458,7 +481,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
|
||||
void _markChildrenAsDeleted(SpaceModel parent) {
|
||||
for (var child in parent.children) {
|
||||
child.status = SpaceStatus.deleted;
|
||||
child.status = SpaceStatus.parentDeleted;
|
||||
|
||||
_markChildrenAsDeleted(child);
|
||||
}
|
||||
}
|
||||
@ -466,7 +490,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
void _removeConnectionsForDeletedSpaces() {
|
||||
connections.removeWhere((connection) {
|
||||
return connection.startSpace.status == SpaceStatus.deleted ||
|
||||
connection.endSpace.status == SpaceStatus.deleted;
|
||||
connection.endSpace.status == SpaceStatus.deleted ||
|
||||
connection.startSpace.status == SpaceStatus.parentDeleted ||
|
||||
connection.endSpace.status == SpaceStatus.parentDeleted;
|
||||
});
|
||||
}
|
||||
|
||||
@ -529,4 +555,206 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
space.status == SpaceStatus.modified ||
|
||||
space.status == SpaceStatus.deleted);
|
||||
}
|
||||
|
||||
void _onDuplicate(BuildContext parentContext) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
if (widget.selectedSpace != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
title: Text(
|
||||
"Duplicate Space",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: screenWidth * 0.4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Are you sure you want to duplicate?",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(height: 15),
|
||||
Text("All the child spaces will be duplicated.",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: ColorsManager.lightGrayColor)),
|
||||
const SizedBox(width: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: CancelButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: "Cancel",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: DefaultButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return DuplicateProcessDialog(
|
||||
onDuplicate: () {
|
||||
_duplicateSpace(widget.selectedSpace!);
|
||||
_deselectSpace(parentContext);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
])
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _duplicateSpace(SpaceModel space) {
|
||||
final Map<SpaceModel, SpaceModel> originalToDuplicate = {};
|
||||
const double horizontalGap = 200.0;
|
||||
const double verticalGap = 100.0;
|
||||
|
||||
final Map<String, int> nameCounters = {};
|
||||
|
||||
String _generateCopyName(String originalName) {
|
||||
final baseName = originalName.replaceAll(RegExp(r'\(\d+\)$'), '').trim();
|
||||
nameCounters[baseName] = (nameCounters[baseName] ?? 0) + 1;
|
||||
return "$baseName(${nameCounters[baseName]})";
|
||||
}
|
||||
|
||||
SpaceModel duplicateRecursive(SpaceModel original, Offset parentPosition,
|
||||
SpaceModel? duplicatedParent) {
|
||||
Offset newPosition = parentPosition + Offset(horizontalGap, 0);
|
||||
|
||||
while (spaces.any((s) =>
|
||||
(s.position - newPosition).distance < horizontalGap &&
|
||||
s.status != SpaceStatus.deleted)) {
|
||||
newPosition += Offset(horizontalGap, 0);
|
||||
}
|
||||
|
||||
final duplicatedName = _generateCopyName(original.name);
|
||||
|
||||
final duplicated = SpaceModel(
|
||||
name: duplicatedName,
|
||||
icon: original.icon,
|
||||
position: newPosition,
|
||||
isPrivate: original.isPrivate,
|
||||
children: [],
|
||||
status: SpaceStatus.newSpace,
|
||||
parent: duplicatedParent,
|
||||
spaceModel: original.spaceModel,
|
||||
subspaces: original.subspaces,
|
||||
tags: original.tags,
|
||||
);
|
||||
|
||||
originalToDuplicate[original] = duplicated;
|
||||
|
||||
setState(() {
|
||||
spaces.add(duplicated);
|
||||
_updateNodePosition(duplicated, duplicated.position);
|
||||
|
||||
if (duplicatedParent != null) {
|
||||
final newConnection = Connection(
|
||||
startSpace: duplicatedParent,
|
||||
endSpace: duplicated,
|
||||
direction: "down",
|
||||
);
|
||||
connections.add(newConnection);
|
||||
duplicated.incomingConnection = newConnection;
|
||||
duplicatedParent.addOutgoingConnection(newConnection);
|
||||
}
|
||||
|
||||
if (original.parent != null && duplicatedParent == null) {
|
||||
final originalParent = original.parent!;
|
||||
final duplicatedParent =
|
||||
originalToDuplicate[originalParent] ?? originalParent;
|
||||
|
||||
final parentConnection = Connection(
|
||||
startSpace: duplicatedParent,
|
||||
endSpace: duplicated,
|
||||
direction: original.incomingConnection?.direction ?? "down",
|
||||
);
|
||||
|
||||
connections.add(parentConnection);
|
||||
duplicated.incomingConnection = parentConnection;
|
||||
duplicatedParent.addOutgoingConnection(parentConnection);
|
||||
}
|
||||
});
|
||||
|
||||
final childrenWithDownDirection = original.children
|
||||
.where((child) =>
|
||||
child.incomingConnection?.direction == "down" &&
|
||||
child.status != SpaceStatus.deleted)
|
||||
.toList();
|
||||
|
||||
Offset childStartPosition = childrenWithDownDirection.length == 1
|
||||
? duplicated.position
|
||||
: newPosition + Offset(0, verticalGap);
|
||||
|
||||
for (final child in original.children) {
|
||||
final isDownDirection =
|
||||
child.incomingConnection?.direction == "down" ?? false;
|
||||
|
||||
if (isDownDirection && childrenWithDownDirection.length == 1) {
|
||||
// Place the only "down" child vertically aligned with the parent
|
||||
childStartPosition = duplicated.position + Offset(0, verticalGap);
|
||||
} else if (!isDownDirection) {
|
||||
// Position children with other directions horizontally
|
||||
childStartPosition = duplicated.position + Offset(horizontalGap, 0);
|
||||
}
|
||||
|
||||
final duplicatedChild =
|
||||
duplicateRecursive(child, childStartPosition, duplicated);
|
||||
duplicated.children.add(duplicatedChild);
|
||||
childStartPosition += Offset(0, verticalGap);
|
||||
}
|
||||
|
||||
return duplicated;
|
||||
}
|
||||
|
||||
if (space.parent == null) {
|
||||
duplicateRecursive(space, space.position, null);
|
||||
} else {
|
||||
final duplicatedParent =
|
||||
originalToDuplicate[space.parent!] ?? space.parent!;
|
||||
duplicateRecursive(space, space.position, duplicatedParent);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _getAllTagValues(List<SpaceModel> spaces) {
|
||||
final List<String> allTags = [];
|
||||
for (final space in spaces) {
|
||||
if (space.tags != null) {
|
||||
allTags.addAll(space.listAllTagValues());
|
||||
}
|
||||
}
|
||||
return allTags;
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
class CounterWidget extends StatefulWidget {
|
||||
final int initialCount;
|
||||
final ValueChanged<int> onCountChanged;
|
||||
final bool isCreate;
|
||||
|
||||
const CounterWidget({
|
||||
Key? key,
|
||||
this.initialCount = 0,
|
||||
required this.onCountChanged,
|
||||
}) : super(key: key);
|
||||
const CounterWidget(
|
||||
{Key? key,
|
||||
this.initialCount = 0,
|
||||
required this.onCountChanged,
|
||||
required this.isCreate})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<CounterWidget> createState() => _CounterWidgetState();
|
||||
@ -53,25 +55,26 @@ class _CounterWidgetState extends State<CounterWidget> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildCounterButton(Icons.remove, _decrementCounter),
|
||||
_buildCounterButton(Icons.remove, _decrementCounter,!widget.isCreate ),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.spaceColor),
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildCounterButton(Icons.add, _incrementCounter),
|
||||
_buildCounterButton(Icons.add, _incrementCounter, false),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterButton(IconData icon, VoidCallback onPressed) {
|
||||
Widget _buildCounterButton(IconData icon, VoidCallback onPressed, bool isDisabled) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
onTap: isDisabled? null: onPressed,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: ColorsManager.spaceColor,
|
||||
color: isDisabled? ColorsManager.spaceColor.withOpacity(0.3): ColorsManager.spaceColor,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
|
@ -1,20 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/common/edit_chip.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/constants/space_icon_const.dart';
|
||||
|
||||
class CreateSpaceDialog extends StatefulWidget {
|
||||
final Function(String, String, List<SelectedProduct> selectedProducts)
|
||||
onCreateSpace;
|
||||
final Function(
|
||||
String,
|
||||
String,
|
||||
List<SelectedProduct> selectedProducts,
|
||||
SpaceTemplateModel? spaceModel,
|
||||
List<SubspaceModel>? subspaces,
|
||||
List<Tag>? tags) onCreateSpace;
|
||||
final List<ProductModel>? products;
|
||||
final String? name;
|
||||
final String? icon;
|
||||
@ -22,6 +36,10 @@ class CreateSpaceDialog extends StatefulWidget {
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final SpaceModel? parentSpace;
|
||||
final SpaceModel? editSpace;
|
||||
final List<SpaceTemplateModel>? spaceModels;
|
||||
final List<SubspaceModel>? subspaces;
|
||||
final List<Tag>? tags;
|
||||
final List<String>? allTags;
|
||||
|
||||
const CreateSpaceDialog(
|
||||
{super.key,
|
||||
@ -32,7 +50,11 @@ class CreateSpaceDialog extends StatefulWidget {
|
||||
this.icon,
|
||||
this.isEdit = false,
|
||||
this.editSpace,
|
||||
this.selectedProducts = const []});
|
||||
this.allTags,
|
||||
this.selectedProducts = const [],
|
||||
this.spaceModels,
|
||||
this.subspaces,
|
||||
this.tags});
|
||||
|
||||
@override
|
||||
CreateSpaceDialogState createState() => CreateSpaceDialogState();
|
||||
@ -40,12 +62,15 @@ class CreateSpaceDialog extends StatefulWidget {
|
||||
|
||||
class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
String selectedIcon = Assets.location;
|
||||
SpaceTemplateModel? selectedSpaceModel;
|
||||
String enteredName = '';
|
||||
List<SelectedProduct> selectedProducts = [];
|
||||
late TextEditingController nameController;
|
||||
bool isOkButtonEnabled = false;
|
||||
bool isNameFieldInvalid = false;
|
||||
bool isNameFieldExist = false;
|
||||
List<SubspaceModel>? subspaces;
|
||||
List<Tag>? tags;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -56,198 +81,425 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
|
||||
isOkButtonEnabled =
|
||||
enteredName.isNotEmpty || nameController.text.isNotEmpty;
|
||||
tags = widget.tags ?? [];
|
||||
subspaces = widget.subspaces ?? [];
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return AlertDialog(
|
||||
title: widget.isEdit
|
||||
? const Text('Edit Space')
|
||||
: const Text('Create New Space'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SizedBox(
|
||||
width: screenWidth * 0.5, // Limit dialog width
|
||||
width: screenWidth * 0.5,
|
||||
child: SingleChildScrollView(
|
||||
// Scrollable content to prevent overflow
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: screenWidth * 0.1, // Adjusted width
|
||||
height: screenWidth * 0.1, // Adjusted height
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
SvgPicture.asset(
|
||||
selectedIcon,
|
||||
width: screenWidth * 0.04,
|
||||
height: screenWidth * 0.04,
|
||||
),
|
||||
Positioned(
|
||||
top: 6,
|
||||
right: 6,
|
||||
child: InkWell(
|
||||
onTap: _showIconSelectionDialog,
|
||||
child: Container(
|
||||
width: screenWidth * 0.020,
|
||||
height: screenWidth * 0.020,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.iconEdit,
|
||||
width: screenWidth * 0.06,
|
||||
height: screenWidth * 0.06,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
// Ensure the text field expands responsively
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
onChanged: (value) {
|
||||
enteredName = value.trim();
|
||||
setState(() {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = false;
|
||||
isNameFieldInvalid = value.isEmpty;
|
||||
|
||||
if (!isNameFieldInvalid) {
|
||||
if (_isNameConflict(value)) {
|
||||
isNameFieldExist = true;
|
||||
isOkButtonEnabled = false;
|
||||
} else {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
style: const TextStyle(color: Colors.black),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: isNameFieldInvalid || isNameFieldExist
|
||||
? ColorsManager.red
|
||||
: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
Container(
|
||||
width: screenWidth * 0.1,
|
||||
height: screenWidth * 0.1,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
SvgPicture.asset(
|
||||
selectedIcon,
|
||||
width: screenWidth * 0.04,
|
||||
height: screenWidth * 0.04,
|
||||
),
|
||||
Positioned(
|
||||
top: 20,
|
||||
right: 20,
|
||||
child: InkWell(
|
||||
onTap: _showIconSelectionDialog,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(
|
||||
color: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
child: SvgPicture.asset(
|
||||
Assets.iconEdit,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isNameFieldInvalid)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Space name should not be empty.',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
if (isNameFieldExist)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Name already exist',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (selectedProducts.isNotEmpty)
|
||||
_buildSelectedProductsButtons(widget.products ?? [])
|
||||
else
|
||||
DefaultButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceWidget(
|
||||
products: widget.products,
|
||||
onProductsSelected: (selectedProductsMap) {
|
||||
setState(() {
|
||||
selectedProducts = selectedProductsMap;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: Colors.black,
|
||||
borderColor: ColorsManager.neutralGray,
|
||||
borderRadius: 16.0,
|
||||
padding: 10.0, // Reduced padding for smaller size
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.addIcon,
|
||||
width: screenWidth *
|
||||
0.015, // Adjust icon size
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Add devices / Assign a space model',
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Prevent overflow
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: TextField(
|
||||
controller: nameController,
|
||||
onChanged: (value) {
|
||||
enteredName = value.trim();
|
||||
setState(() {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = false;
|
||||
isNameFieldInvalid = value.isEmpty;
|
||||
|
||||
if (!isNameFieldInvalid) {
|
||||
if (_isNameConflict(value)) {
|
||||
isNameFieldExist = true;
|
||||
isOkButtonEnabled = false;
|
||||
} else {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: ColorsManager.lightGrayColor),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: isNameFieldInvalid || isNameFieldExist
|
||||
? ColorsManager.red
|
||||
: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(
|
||||
color: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isNameFieldInvalid)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Space name should not be empty.',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
if (isNameFieldExist)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Name already exist',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
selectedSpaceModel == null
|
||||
? TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () {
|
||||
_showLinkSpaceModelDialog(context);
|
||||
},
|
||||
child: const ButtonContentWidget(
|
||||
svgAssets: Assets.link,
|
||||
label: 'Link a space model',
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: screenWidth * 0.25,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
Chip(
|
||||
label: Text(
|
||||
selectedSpaceModel?.modelName ?? '',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onDeleted: () => setState(() {
|
||||
this.selectedSpaceModel = null;
|
||||
})),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Divider(
|
||||
color: ColorsManager.neutralGray,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: Text(
|
||||
'OR',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const Expanded(
|
||||
child: Divider(
|
||||
color: ColorsManager.neutralGray,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
subspaces == null || subspaces!.isEmpty
|
||||
? TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
overlayColor: ColorsManager.transparentColor,
|
||||
),
|
||||
onPressed: () async {
|
||||
_showSubSpaceDialog(context, enteredName, [],
|
||||
false, widget.products, subspaces);
|
||||
},
|
||||
child: const ButtonContentWidget(
|
||||
icon: Icons.add,
|
||||
label: 'Create Sub Space',
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
if (subspaces != null)
|
||||
...subspaces!.map((subspace) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SubspaceNameDisplayWidget(
|
||||
text: subspace.subspaceName,
|
||||
validateName: (updatedName) {
|
||||
bool nameExists =
|
||||
subspaces!.any((s) {
|
||||
bool isSameId = s.internalId ==
|
||||
subspace.internalId;
|
||||
bool isSameName = s.subspaceName
|
||||
.trim()
|
||||
.toLowerCase() ==
|
||||
updatedName
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
return !isSameId && isSameName;
|
||||
});
|
||||
|
||||
return !nameExists;
|
||||
},
|
||||
onNameChanged: (updatedName) {
|
||||
setState(() {
|
||||
subspace.subspaceName =
|
||||
updatedName;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
EditChip(
|
||||
onTap: () async {
|
||||
_showSubSpaceDialog(context, enteredName,
|
||||
[], true, widget.products, subspaces);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
(tags?.isNotEmpty == true ||
|
||||
subspaces?.any((subspace) =>
|
||||
subspace.tags?.isNotEmpty == true) ==
|
||||
true)
|
||||
? SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
// Combine tags from spaceModel and subspaces
|
||||
...TagHelper.groupTags([
|
||||
...?tags,
|
||||
...?subspaces?.expand(
|
||||
(subspace) => subspace.tags ?? [])
|
||||
]).entries.map(
|
||||
(entry) => Chip(
|
||||
avatar: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: SvgPicture.asset(
|
||||
entry.key.icon ??
|
||||
'assets/icons/gateway.svg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
'x${entry.value}', // Show count
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: ColorsManager
|
||||
.spaceColor),
|
||||
),
|
||||
backgroundColor:
|
||||
ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
EditChip(onTap: () async {
|
||||
final result = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AssignTagDialog(
|
||||
products: widget.products,
|
||||
subspaces: widget.subspaces,
|
||||
addedProducts: TagHelper
|
||||
.createInitialSelectedProductsForTags(
|
||||
tags ?? [], subspaces),
|
||||
title: 'Edit Device',
|
||||
initialTags:
|
||||
TagHelper.generateInitialForTags(
|
||||
spaceTags: tags,
|
||||
subspaces: subspaces),
|
||||
spaceName: widget.name ?? '',
|
||||
onSave:
|
||||
(updatedTags, updatedSubspaces) {
|
||||
setState(() {
|
||||
tags = updatedTags;
|
||||
subspaces = updatedSubspaces;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: () {
|
||||
_showTagCreateDialog(
|
||||
context,
|
||||
enteredName,
|
||||
widget.isEdit,
|
||||
widget.products,
|
||||
subspaces,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
child: const ButtonContentWidget(
|
||||
icon: Icons.add,
|
||||
label: 'Add Devices',
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -278,7 +530,12 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
: (widget.name ?? '');
|
||||
if (newName.isNotEmpty) {
|
||||
widget.onCreateSpace(
|
||||
newName, selectedIcon, selectedProducts);
|
||||
newName,
|
||||
selectedIcon,
|
||||
selectedProducts,
|
||||
selectedSpaceModel,
|
||||
subspaces,
|
||||
tags);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@ -313,74 +570,6 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedProductsButtons(List<ProductModel> products) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
width: screenWidth * 0.6,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (var i = 0; i < selectedProducts.length; i++) ...[
|
||||
HoverableButton(
|
||||
iconPath:
|
||||
_mapIconToProduct(selectedProducts[i].productId, products),
|
||||
text: 'x${selectedProducts[i].count}',
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedProducts.remove(selectedProducts[i]);
|
||||
});
|
||||
// Handle button tap
|
||||
},
|
||||
),
|
||||
if (i < selectedProducts.length - 1)
|
||||
const SizedBox(
|
||||
width: 2), // Add space except after the last button
|
||||
],
|
||||
const SizedBox(width: 2),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceWidget(
|
||||
products: widget.products,
|
||||
initialSelectedProducts: selectedProducts,
|
||||
onProductsSelected: (selectedProductsMap) {
|
||||
setState(() {
|
||||
selectedProducts = selectedProductsMap;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.spaceColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isNameConflict(String value) {
|
||||
return (widget.parentSpace?.children.any((child) => child.name == value) ??
|
||||
false) ||
|
||||
@ -390,20 +579,125 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
false);
|
||||
}
|
||||
|
||||
String _mapIconToProduct(String uuid, List<ProductModel> products) {
|
||||
// Find the product with the matching UUID
|
||||
final product = products.firstWhere(
|
||||
(product) => product.uuid == uuid,
|
||||
orElse: () => ProductModel(
|
||||
uuid: '',
|
||||
catName: '',
|
||||
prodId: '',
|
||||
prodType: '',
|
||||
name: '',
|
||||
icon: Assets.presenceSensor,
|
||||
),
|
||||
void _showLinkSpaceModelDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return LinkSpaceModelDialog(
|
||||
spaceModels: widget.spaceModels ?? [],
|
||||
onSave: (selectedModel) {
|
||||
if (selectedModel != null) {
|
||||
setState(() {
|
||||
selectedSpaceModel = selectedModel;
|
||||
subspaces = null;
|
||||
tags = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return product.icon ?? Assets.presenceSensor;
|
||||
void _showSubSpaceDialog(
|
||||
BuildContext context,
|
||||
String name,
|
||||
final List<Tag>? spaceTags,
|
||||
bool isEdit,
|
||||
List<ProductModel>? products,
|
||||
final List<SubspaceModel>? existingSubSpaces) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreateSubSpaceDialog(
|
||||
spaceName: name,
|
||||
dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space',
|
||||
spaceTags: spaceTags,
|
||||
isEdit: isEdit,
|
||||
products: products,
|
||||
existingSubSpaces: existingSubSpaces,
|
||||
onSave: (slectedSubspaces) {
|
||||
if (slectedSubspaces != null) {
|
||||
setState(() {
|
||||
subspaces = slectedSubspaces;
|
||||
selectedSpaceModel = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showTagCreateDialog(BuildContext context, String name, bool isEdit,
|
||||
List<ProductModel>? products, List<SubspaceModel>? subspaces) {
|
||||
isEdit
|
||||
? showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AssignTagDialog(
|
||||
title: 'Edit Device',
|
||||
addedProducts: TagHelper.createInitialSelectedProductsForTags(
|
||||
tags, subspaces),
|
||||
spaceName: name,
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
allTags: widget.allTags,
|
||||
onSave: (selectedSpaceTags, selectedSubspaces) {
|
||||
setState(() {
|
||||
tags = selectedSpaceTags;
|
||||
selectedSpaceModel = null;
|
||||
|
||||
if (selectedSubspaces != null) {
|
||||
if (subspaces != null) {
|
||||
for (final subspace in subspaces!) {
|
||||
for (final selectedSubspace in selectedSubspaces) {
|
||||
if (subspace.subspaceName ==
|
||||
selectedSubspace.subspaceName) {
|
||||
subspace.tags = selectedSubspace.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
: showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AddDeviceTypeWidget(
|
||||
spaceName: name,
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
spaceTags: tags,
|
||||
isCreate: true,
|
||||
allTags: widget.allTags,
|
||||
initialSelectedProducts:
|
||||
TagHelper.createInitialSelectedProductsForTags(
|
||||
tags, subspaces),
|
||||
onSave: (selectedSpaceTags, selectedSubspaces) {
|
||||
setState(() {
|
||||
tags = selectedSpaceTags;
|
||||
selectedSpaceModel = null;
|
||||
|
||||
if (selectedSubspaces != null) {
|
||||
if (subspaces != null) {
|
||||
for (final subspace in subspaces!) {
|
||||
for (final selectedSubspace in selectedSubspaces) {
|
||||
if (subspace.subspaceName ==
|
||||
selectedSubspace.subspaceName) {
|
||||
subspace.tags = selectedSubspace.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ void showDeleteConfirmationDialog(BuildContext context, VoidCallback onConfirm,
|
||||
Navigator.of(context).pop(); // Close the first dialog
|
||||
showProcessingPopup(context, isSpace, onConfirm);
|
||||
},
|
||||
style: _dialogButtonStyle(Colors.blue),
|
||||
child: const Text('Continue', style: TextStyle(color: Colors.white)),
|
||||
style: _dialogButtonStyle(ColorsManager.spaceColor),
|
||||
child: const Text('Continue', style: TextStyle(color: ColorsManager.whiteColors)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -83,7 +83,7 @@ void showProcessingPopup(BuildContext context, bool isSpace, VoidCallback onDele
|
||||
ElevatedButton(
|
||||
onPressed: onDelete,
|
||||
style: _dialogButtonStyle(ColorsManager.warningRed),
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.white)),
|
||||
child: const Text('Delete', style: TextStyle(color: ColorsManager.whiteColors)),
|
||||
),
|
||||
CancelButton(
|
||||
label: 'Cancel',
|
||||
@ -108,7 +108,7 @@ Widget _buildWarningIcon() {
|
||||
color: ColorsManager.warningRed,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white, size: 40),
|
||||
child: const Icon(Icons.close, color: ColorsManager.whiteColors, size: 40),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DuplicateProcessDialog extends StatefulWidget {
|
||||
final VoidCallback onDuplicate;
|
||||
|
||||
const DuplicateProcessDialog({required this.onDuplicate, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_DuplicateProcessDialogState createState() => _DuplicateProcessDialogState();
|
||||
}
|
||||
|
||||
class _DuplicateProcessDialogState extends State<DuplicateProcessDialog> {
|
||||
bool isDuplicating = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startDuplication();
|
||||
}
|
||||
|
||||
void _startDuplication() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onDuplicate();
|
||||
});
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
setState(() {
|
||||
isDuplicating = false;
|
||||
});
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: screenWidth * 0.4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isDuplicating) ...[
|
||||
const CircularProgressIndicator(
|
||||
color: ColorsManager.vividBlue,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
"Duplicating in progress",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: ColorsManager.primaryColor),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
] else ...[
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: ColorsManager.vividBlue,
|
||||
size: 50,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
"Duplicating successful",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: ColorsManager.primaryColor),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ class IconSelectionDialog extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2), // Shadow color
|
||||
color: ColorsManager.blackColor.withOpacity(0.2), // Shadow color
|
||||
blurRadius: 20, // Spread of the blur
|
||||
offset: const Offset(0, 8), // Offset of the shadow
|
||||
),
|
||||
|
@ -1,16 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/gradient_canvas_border_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/view/space_model_page.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
|
||||
class LoadedSpaceView extends StatefulWidget {
|
||||
class LoadedSpaceView extends StatelessWidget {
|
||||
final List<CommunityModel> communities;
|
||||
final CommunityModel? selectedCommunity;
|
||||
final SpaceModel? selectedSpace;
|
||||
final List<ProductModel>? products;
|
||||
final List<SpaceTemplateModel>? spaceModels;
|
||||
final bool shouldNavigateToSpaceModelPage;
|
||||
|
||||
const LoadedSpaceView({
|
||||
super.key,
|
||||
@ -18,33 +25,43 @@ class LoadedSpaceView extends StatefulWidget {
|
||||
this.selectedCommunity,
|
||||
this.selectedSpace,
|
||||
this.products,
|
||||
this.spaceModels,
|
||||
required this.shouldNavigateToSpaceModelPage
|
||||
});
|
||||
|
||||
@override
|
||||
_LoadedStateViewState createState() => _LoadedStateViewState();
|
||||
}
|
||||
|
||||
class _LoadedStateViewState extends State<LoadedSpaceView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SidebarWidget(
|
||||
communities: widget.communities,
|
||||
selectedSpaceUuid: widget.selectedSpace?.uuid ??
|
||||
widget.selectedCommunity?.uuid ??
|
||||
'',
|
||||
),
|
||||
CommunityStructureArea(
|
||||
selectedCommunity: widget.selectedCommunity,
|
||||
selectedSpace: widget.selectedSpace,
|
||||
spaces: widget.selectedCommunity?.spaces ?? [],
|
||||
products: widget.products,
|
||||
communities: widget.communities,
|
||||
communities: communities,
|
||||
selectedSpaceUuid:
|
||||
selectedSpace?.uuid ?? selectedCommunity?.uuid ?? '',
|
||||
),
|
||||
shouldNavigateToSpaceModelPage
|
||||
? Expanded(
|
||||
child: BlocProvider(
|
||||
create: (context) => SpaceModelBloc(
|
||||
api: SpaceModelManagementApi(),
|
||||
initialSpaceModels: spaceModels ?? [],
|
||||
),
|
||||
child: SpaceModelPage(
|
||||
products: products,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CommunityStructureArea(
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
spaces: selectedCommunity?.spaces ?? [],
|
||||
products: products,
|
||||
communities: communities,
|
||||
spaceModels: spaceModels,
|
||||
),
|
||||
],
|
||||
),
|
||||
const GradientCanvasBorderWidget(),
|
||||
|
@ -45,7 +45,7 @@ class PlusButtonWidget extends StatelessWidget {
|
||||
color: ColorsManager.spaceColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.add, color: Colors.white, size: 20),
|
||||
child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class SelectedProductsButtons extends StatelessWidget {
|
||||
final List<ProductModel> products;
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final Function(List<SelectedProduct>) onProductsUpdated;
|
||||
final BuildContext context;
|
||||
|
||||
const SelectedProductsButtons({
|
||||
Key? key,
|
||||
required this.products,
|
||||
required this.selectedProducts,
|
||||
required this.onProductsUpdated,
|
||||
required this.context,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
width: screenWidth * 0.6,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
..._buildSelectedProductButtons(),
|
||||
_buildAddButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSelectedProductButtons() {
|
||||
return [
|
||||
for (var i = 0; i < selectedProducts.length; i++) ...[
|
||||
HoverableButton(
|
||||
iconPath: _mapIconToProduct(selectedProducts[i].productId, products),
|
||||
text: 'x${selectedProducts[i].count}',
|
||||
onTap: () {
|
||||
_removeProduct(i);
|
||||
},
|
||||
),
|
||||
if (i < selectedProducts.length - 1)
|
||||
const SizedBox(width: 2), // Add space except after the last button
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildAddButton() {
|
||||
return GestureDetector(
|
||||
onTap: _showAddDeviceDialog,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.spaceColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddDeviceDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceWidget(
|
||||
products: products,
|
||||
initialSelectedProducts: selectedProducts,
|
||||
onProductsSelected: (selectedProductsMap) {
|
||||
onProductsUpdated(selectedProductsMap);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _removeProduct(int index) {
|
||||
final updatedProducts = [...selectedProducts];
|
||||
updatedProducts.removeAt(index);
|
||||
onProductsUpdated(updatedProducts);
|
||||
}
|
||||
|
||||
String _mapIconToProduct(String uuid, List<ProductModel> products) {
|
||||
// Find the product with the matching UUID
|
||||
final product = products.firstWhere(
|
||||
(product) => product.uuid == uuid,
|
||||
orElse: () => ProductModel(
|
||||
uuid: '',
|
||||
catName: '',
|
||||
prodId: '',
|
||||
prodType: '',
|
||||
name: '',
|
||||
icon: Assets.presenceSensor,
|
||||
),
|
||||
);
|
||||
|
||||
return product.icon ?? Assets.presenceSensor;
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_m
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
@ -118,7 +120,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
||||
children: [
|
||||
Text('Communities',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.black,
|
||||
color: ColorsManager.blackColor,
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () => _navigateToBlank(context),
|
||||
@ -186,6 +188,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
||||
_selectedSpaceUuid = null; // Update the selected community
|
||||
});
|
||||
|
||||
context.read<CenterBodyBloc>().add(CommunitySelectedEvent());
|
||||
|
||||
context.read<SpaceManagementBloc>().add(
|
||||
SelectCommunityEvent(selectedCommunity: community),
|
||||
);
|
||||
@ -195,9 +199,11 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
||||
},
|
||||
children: hasChildren
|
||||
? community.spaces
|
||||
.where((space) => (space.status != SpaceStatus.deleted ||
|
||||
space.status != SpaceStatus.parentDeleted))
|
||||
.map((space) => _buildSpaceTile(space, community))
|
||||
.toList()
|
||||
: null, // Render spaces within the community
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class SpaceContainerWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: ColorsManager.lightGrayColor.withOpacity(0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3), // Shadow position
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceWidget extends StatelessWidget {
|
||||
final String name;
|
||||
@ -19,15 +20,14 @@ class SpaceWidget extends StatelessWidget {
|
||||
top: position.dy,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child:
|
||||
Container(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: ColorsManager.lightGrayColor.withOpacity(0.5),
|
||||
spreadRadius: 5,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
@ -36,13 +36,12 @@ class SpaceWidget extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, color: Colors.blue),
|
||||
const Icon(Icons.location_on, color: ColorsManager.spaceColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(name, style: const TextStyle(fontSize: 16)),
|
||||
Text(name, style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
|
146
lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart
Normal file
146
lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart
Normal file
@ -0,0 +1,146 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
|
||||
|
||||
class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
|
||||
AssignTagBloc() : super(AssignTagInitial()) {
|
||||
on<InitializeTags>((event, emit) {
|
||||
final initialTags = event.initialTags ?? [];
|
||||
|
||||
final existingTagCounts = <String, int>{};
|
||||
for (var tag in initialTags) {
|
||||
if (tag.product != null) {
|
||||
existingTagCounts[tag.product!.uuid] =
|
||||
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final allTags = <Tag>[];
|
||||
|
||||
for (var selectedProduct in event.addedProducts) {
|
||||
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
|
||||
|
||||
if (selectedProduct.count == 0 ||
|
||||
selectedProduct.count <= existingCount) {
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
continue;
|
||||
}
|
||||
|
||||
final missingCount = selectedProduct.count - existingCount;
|
||||
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
|
||||
if (missingCount > 0) {
|
||||
allTags.addAll(List.generate(
|
||||
missingCount,
|
||||
(index) => Tag(
|
||||
tag: '',
|
||||
product: selectedProduct.product,
|
||||
location: 'Main Space',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: allTags,
|
||||
isSaveEnabled: _validateTags(allTags),
|
||||
errorMessage: ''));
|
||||
});
|
||||
|
||||
on<UpdateTagEvent>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final tags = List<Tag>.from(currentState.tags);
|
||||
tags[event.index].tag = event.tag;
|
||||
emit(AssignTagLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<UpdateLocation>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final tags = List<Tag>.from(currentState.tags);
|
||||
|
||||
// Use copyWith for immutability
|
||||
tags[event.index] =
|
||||
tags[event.index].copyWith(location: event.location);
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<ValidateTags>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final tags = List<Tag>.from(currentState.tags);
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<DeleteTag>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final updatedTags = List<Tag>.from(currentState.tags)
|
||||
..remove(event.tagToDelete);
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: updatedTags,
|
||||
isSaveEnabled: _validateTags(updatedTags),
|
||||
));
|
||||
} else {
|
||||
emit(const AssignTagLoaded(
|
||||
tags: [],
|
||||
isSaveEnabled: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _validateTags(List<Tag> tags) {
|
||||
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
final isValid = uniqueTags.length == tags.length && !hasEmptyTag;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
String? _getValidationError(List<Tag> tags) {
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
if (hasEmptyTag) return 'Tags cannot be empty.';
|
||||
final duplicateTags = tags
|
||||
.map((tag) => tag.tag?.trim() ?? '')
|
||||
.fold<Map<String, int>>({}, (map, tag) {
|
||||
map[tag] = (map[tag] ?? 0) + 1;
|
||||
return map;
|
||||
})
|
||||
.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
if (duplicateTags.isNotEmpty) {
|
||||
return 'Duplicate tags found: ${duplicateTags.join(', ')}';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
|
||||
abstract class AssignTagEvent extends Equatable {
|
||||
const AssignTagEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitializeTags extends AssignTagEvent {
|
||||
final List<Tag>? initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeTags({
|
||||
required this.initialTags,
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags ?? [], addedProducts];
|
||||
}
|
||||
|
||||
class UpdateTagEvent extends AssignTagEvent {
|
||||
final int index;
|
||||
final String tag;
|
||||
|
||||
const UpdateTagEvent({required this.index, required this.tag});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, tag];
|
||||
}
|
||||
|
||||
class UpdateLocation extends AssignTagEvent {
|
||||
final int index;
|
||||
final String location;
|
||||
|
||||
const UpdateLocation({required this.index, required this.location});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, location];
|
||||
}
|
||||
|
||||
class ValidateTags extends AssignTagEvent {}
|
||||
|
||||
class DeleteTag extends AssignTagEvent {
|
||||
final Tag tagToDelete;
|
||||
final List<Tag> tags;
|
||||
|
||||
const DeleteTag({required this.tagToDelete, required this.tags});
|
||||
|
||||
@override
|
||||
List<Object> get props => [tagToDelete, tags];
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AssignTagState extends Equatable {
|
||||
const AssignTagState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AssignTagInitial extends AssignTagState {}
|
||||
|
||||
class AssignTagLoading extends AssignTagState {}
|
||||
|
||||
class AssignTagLoaded extends AssignTagState {
|
||||
final List<Tag> tags;
|
||||
final bool isSaveEnabled;
|
||||
final String? errorMessage;
|
||||
|
||||
const AssignTagLoaded({
|
||||
required this.tags,
|
||||
required this.isSaveEnabled,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [tags, isSaveEnabled];
|
||||
}
|
||||
|
||||
class AssignTagError extends AssignTagState {
|
||||
final String errorMessage;
|
||||
|
||||
const AssignTagError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,343 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/common/dialog_dropdown.dart';
|
||||
import 'package:syncrow_web/common/dialog_textfield_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AssignTagDialog extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<SubspaceModel>? subspaces;
|
||||
final List<Tag>? initialTags;
|
||||
final ValueChanged<List<Tag>>? onTagsAssigned;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final String title;
|
||||
final Function(List<Tag>, List<SubspaceModel>?)? onSave;
|
||||
|
||||
const AssignTagDialog(
|
||||
{Key? key,
|
||||
required this.products,
|
||||
required this.subspaces,
|
||||
required this.addedProducts,
|
||||
this.initialTags,
|
||||
this.onTagsAssigned,
|
||||
this.allTags,
|
||||
required this.spaceName,
|
||||
required this.title,
|
||||
this.onSave})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> locations = (subspaces ?? [])
|
||||
.map((subspace) => subspace.subspaceName)
|
||||
.toList()
|
||||
..add('Main Space');
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => AssignTagBloc()
|
||||
..add(InitializeTags(
|
||||
initialTags: initialTags,
|
||||
addedProducts: addedProducts,
|
||||
)),
|
||||
child: BlocBuilder<AssignTagBloc, AssignTagState>(
|
||||
builder: (context, state) {
|
||||
if (state is AssignTagLoaded) {
|
||||
final controllers = List.generate(
|
||||
state.tags.length,
|
||||
(index) => TextEditingController(text: state.tags[index].tag),
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
ColorsManager.dataHeaderGrey),
|
||||
border: TableBorder.all(
|
||||
color: ColorsManager.dataHeaderGrey,
|
||||
width: 1,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text('#',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Device',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
DataColumn(
|
||||
numeric: false,
|
||||
label: Text('Tag',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Location',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
],
|
||||
rows: state.tags.isEmpty
|
||||
? [
|
||||
DataRow(cells: [
|
||||
DataCell(
|
||||
Center(
|
||||
child: Text('No Data Available',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager
|
||||
.lightGrayColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
])
|
||||
]
|
||||
: List.generate(state.tags.length, (index) {
|
||||
final tag = state.tags[index];
|
||||
final controller = controllers[index];
|
||||
final availableTags = getAvailableTags(
|
||||
allTags ?? [], state.tags, tag);
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text((index + 1).toString())),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tag.product?.name ?? 'Unknown',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager
|
||||
.lightGrayColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager
|
||||
.lightGreyColor,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AssignTagBloc>()
|
||||
.add(DeleteTag(
|
||||
tagToDelete: tag,
|
||||
tags: state.tags));
|
||||
},
|
||||
tooltip: 'Delete Tag',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
alignment: Alignment
|
||||
.centerLeft, // Align cell content to the left
|
||||
child: SizedBox(
|
||||
width: double
|
||||
.infinity, // Ensure full width for dropdown
|
||||
child: DialogTextfieldDropdown(
|
||||
items: availableTags,
|
||||
initialValue: tag.tag,
|
||||
onSelected: (value) {
|
||||
controller.text = value;
|
||||
context
|
||||
.read<AssignTagBloc>()
|
||||
.add(UpdateTagEvent(
|
||||
index: index,
|
||||
tag: value.trim(),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DialogDropdown(
|
||||
items: locations,
|
||||
selectedValue:
|
||||
tag.location ?? 'Main Space',
|
||||
onSelected: (value) {
|
||||
context
|
||||
.read<AssignTagBloc>()
|
||||
.add(UpdateLocation(
|
||||
index: index,
|
||||
location: value,
|
||||
));
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (state.errorMessage != null)
|
||||
Text(state.errorMessage!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.warningRed)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (buttonContext) => CancelButton(
|
||||
label: 'Add New Device',
|
||||
onPressed: () async {
|
||||
final updatedTags = List<Tag>.from(state.tags);
|
||||
final result = processTags(updatedTags, subspaces);
|
||||
|
||||
final processedTags =
|
||||
result['updatedTags'] as List<Tag>;
|
||||
final processedSubspaces = List<SubspaceModel>.from(
|
||||
result['subspaces'] as List<dynamic>);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceTypeWidget(
|
||||
products: products,
|
||||
subspaces: processedSubspaces,
|
||||
initialSelectedProducts: TagHelper
|
||||
.createInitialSelectedProductsForTags(
|
||||
processedTags, processedSubspaces),
|
||||
spaceName: spaceName,
|
||||
spaceTags: processedTags,
|
||||
isCreate: false,
|
||||
onSave: onSave,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
borderRadius: 10,
|
||||
backgroundColor: state.isSaveEnabled
|
||||
? ColorsManager.secondaryColor
|
||||
: ColorsManager.grayColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
onPressed: state.isSaveEnabled
|
||||
? () async {
|
||||
final updatedTags = List<Tag>.from(state.tags);
|
||||
final result =
|
||||
processTags(updatedTags, subspaces);
|
||||
|
||||
final processedTags =
|
||||
result['updatedTags'] as List<Tag>;
|
||||
final processedSubspaces =
|
||||
List<SubspaceModel>.from(
|
||||
result['subspaces'] as List<dynamic>);
|
||||
onSave?.call(processedTags, processedSubspaces);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is AssignTagLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return const Center(child: Text('Something went wrong.'));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> getAvailableTags(
|
||||
List<String> allTags, List<Tag> currentTags, Tag currentTag) {
|
||||
List<String> availableTagsForTagModel = TagHelper.getAvailableTags<Tag>(
|
||||
allTags: allTags,
|
||||
currentTags: currentTags,
|
||||
currentTag: currentTag,
|
||||
getTag: (tag) => tag.tag ?? '',
|
||||
);
|
||||
return availableTagsForTagModel;
|
||||
}
|
||||
|
||||
Map<String, dynamic> processTags(
|
||||
List<Tag> updatedTags, List<SubspaceModel>? subspaces) {
|
||||
return TagHelper.updateTags<Tag>(
|
||||
updatedTags: updatedTags,
|
||||
subspaces: subspaces,
|
||||
getInternalId: (tag) => tag.internalId,
|
||||
getLocation: (tag) => tag.location,
|
||||
setLocation: (tag, location) => tag.location = location,
|
||||
getSubspaceName: (subspace) => subspace.subspaceName,
|
||||
getSubspaceTags: (subspace) => subspace.tags,
|
||||
setSubspaceTags: (subspace, tags) => subspace.tags = tags,
|
||||
checkTagExistInSubspace: checkTagExistInSubspace,
|
||||
);
|
||||
}
|
||||
|
||||
int? checkTagExistInSubspace(Tag tag, List<dynamic>? subspaces) {
|
||||
if (subspaces == null) return null;
|
||||
for (int i = 0; i < subspaces.length; i++) {
|
||||
final subspace = subspaces[i];
|
||||
if (subspace.tags == null) continue;
|
||||
for (var t in subspace.tags!) {
|
||||
if (tag.internalId == t.internalId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
class AssignTagModelBloc
|
||||
extends Bloc<AssignTagModelEvent, AssignTagModelState> {
|
||||
AssignTagModelBloc() : super(AssignTagModelInitial()) {
|
||||
on<InitializeTagModels>((event, emit) {
|
||||
final initialTags = event.initialTags ?? [];
|
||||
|
||||
final existingTagCounts = <String, int>{};
|
||||
for (var tag in initialTags) {
|
||||
if (tag.product != null) {
|
||||
existingTagCounts[tag.product!.uuid] =
|
||||
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final allTags = <TagModel>[];
|
||||
|
||||
for (var selectedProduct in event.addedProducts) {
|
||||
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
|
||||
|
||||
if (selectedProduct.count == 0 ||
|
||||
selectedProduct.count <= existingCount) {
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
continue;
|
||||
}
|
||||
|
||||
final missingCount = selectedProduct.count - existingCount;
|
||||
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
|
||||
if (missingCount > 0) {
|
||||
allTags.addAll(List.generate(
|
||||
missingCount,
|
||||
(index) => TagModel(
|
||||
tag: '',
|
||||
product: selectedProduct.product,
|
||||
location: 'Main Space',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: allTags,
|
||||
isSaveEnabled: _validateTags(allTags),
|
||||
errorMessage: ''));
|
||||
});
|
||||
|
||||
on<UpdateTag>((event, emit) {
|
||||
final currentState = state;
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final tags = List<TagModel>.from(currentState.tags);
|
||||
tags[event.index].tag = event.tag;
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<UpdateLocation>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final tags = List<TagModel>.from(currentState.tags);
|
||||
|
||||
// Use copyWith for immutability
|
||||
tags[event.index] =
|
||||
tags[event.index].copyWith(location: event.location);
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<ValidateTagModels>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final tags = List<TagModel>.from(currentState.tags);
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<DeleteTagModel>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final updatedTags = List<TagModel>.from(currentState.tags)
|
||||
..remove(event.tagToDelete);
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: updatedTags,
|
||||
isSaveEnabled: _validateTags(updatedTags),
|
||||
));
|
||||
} else {
|
||||
emit(const AssignTagModelLoaded(
|
||||
tags: [],
|
||||
isSaveEnabled: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _validateTags(List<TagModel> tags) {
|
||||
|
||||
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
final isValid = uniqueTags.length == tags.length && !hasEmptyTag;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
String? _getValidationError(List<TagModel> tags) {
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
if (hasEmptyTag) {
|
||||
return 'Tags cannot be empty.';
|
||||
}
|
||||
|
||||
// Check for duplicate tags
|
||||
final duplicateTags = tags
|
||||
.map((tag) => tag.tag?.trim() ?? '')
|
||||
.fold<Map<String, int>>({}, (map, tag) {
|
||||
map[tag] = (map[tag] ?? 0) + 1;
|
||||
return map;
|
||||
})
|
||||
.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
if (duplicateTags.isNotEmpty) {
|
||||
return 'Duplicate tags found: ${duplicateTags.join(', ')}';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
|
||||
abstract class AssignTagModelEvent extends Equatable {
|
||||
const AssignTagModelEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitializeTagModels extends AssignTagModelEvent {
|
||||
final List<TagModel> initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeTagModels({
|
||||
this.initialTags = const [],
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags, addedProducts];
|
||||
}
|
||||
|
||||
class UpdateTag extends AssignTagModelEvent {
|
||||
final int index;
|
||||
final String tag;
|
||||
|
||||
const UpdateTag({required this.index, required this.tag});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, tag];
|
||||
}
|
||||
|
||||
class UpdateLocation extends AssignTagModelEvent {
|
||||
final int index;
|
||||
final String location;
|
||||
|
||||
const UpdateLocation({required this.index, required this.location});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, location];
|
||||
}
|
||||
|
||||
class ValidateTagModels extends AssignTagModelEvent {}
|
||||
|
||||
class DeleteTagModel extends AssignTagModelEvent {
|
||||
final TagModel tagToDelete;
|
||||
final List<TagModel> tags;
|
||||
|
||||
const DeleteTagModel({required this.tagToDelete, required this.tags});
|
||||
|
||||
@override
|
||||
List<Object> get props => [tagToDelete, tags];
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AssignTagModelState extends Equatable {
|
||||
const AssignTagModelState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class AssignTagModelInitial extends AssignTagModelState {}
|
||||
|
||||
class AssignTagModelLoading extends AssignTagModelState {}
|
||||
|
||||
class AssignTagModelLoaded extends AssignTagModelState {
|
||||
final List<TagModel> tags;
|
||||
final bool isSaveEnabled;
|
||||
final String? errorMessage;
|
||||
|
||||
const AssignTagModelLoaded({
|
||||
required this.tags,
|
||||
required this.isSaveEnabled,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tags, isSaveEnabled, errorMessage];
|
||||
}
|
||||
|
||||
class AssignTagModelError extends AssignTagModelState {
|
||||
final String errorMessage;
|
||||
|
||||
const AssignTagModelError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,367 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/common/dialog_dropdown.dart';
|
||||
import 'package:syncrow_web/common/dialog_textfield_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
|
||||
|
||||
class AssignTagModelsDialog extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<SubspaceTemplateModel>? subspaces;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
|
||||
final List<TagModel> initialTags;
|
||||
final ValueChanged<List<TagModel>>? onTagsAssigned;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final String title;
|
||||
final BuildContext? pageContext;
|
||||
final List<String>? otherSpaceModels;
|
||||
final List<SpaceTemplateModel>? allSpaceModels;
|
||||
|
||||
const AssignTagModelsDialog(
|
||||
{Key? key,
|
||||
required this.products,
|
||||
required this.subspaces,
|
||||
required this.addedProducts,
|
||||
required this.initialTags,
|
||||
this.onTagsAssigned,
|
||||
this.allTags,
|
||||
required this.spaceName,
|
||||
required this.title,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels,
|
||||
this.spaceModel,
|
||||
this.allSpaceModels})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> locations = (subspaces ?? [])
|
||||
.map((subspace) => subspace.subspaceName)
|
||||
.toList()
|
||||
..add('Main Space');
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => AssignTagModelBloc()
|
||||
..add(InitializeTagModels(
|
||||
initialTags: initialTags,
|
||||
addedProducts: addedProducts,
|
||||
)),
|
||||
child: BlocListener<AssignTagModelBloc, AssignTagModelState>(
|
||||
listener: (context, state) {},
|
||||
child: BlocBuilder<AssignTagModelBloc, AssignTagModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is AssignTagModelLoaded) {
|
||||
final controllers = List.generate(
|
||||
state.tags.length,
|
||||
(index) => TextEditingController(text: state.tags[index].tag),
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
ColorsManager.dataHeaderGrey),
|
||||
border: TableBorder.all(
|
||||
color: ColorsManager.dataHeaderGrey,
|
||||
width: 1,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text('#',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Device',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
DataColumn(
|
||||
numeric: false,
|
||||
label: Text('Tag',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Location',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
],
|
||||
rows: state.tags.isEmpty
|
||||
? [
|
||||
DataRow(cells: [
|
||||
DataCell(
|
||||
Center(
|
||||
child: Text('No Devices Available',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager
|
||||
.lightGrayColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
])
|
||||
]
|
||||
: List.generate(state.tags.length, (index) {
|
||||
final tag = state.tags[index];
|
||||
final controller = controllers[index];
|
||||
final availableTags =
|
||||
TagHelper.getAvailableTagModels(
|
||||
allTags ?? [], state.tags, tag);
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text((index + 1).toString())),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tag.product?.name ?? 'Unknown',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager
|
||||
.lightGrayColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager
|
||||
.lightGreyColor,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<
|
||||
AssignTagModelBloc>()
|
||||
.add(DeleteTagModel(
|
||||
tagToDelete: tag,
|
||||
tags: state.tags));
|
||||
},
|
||||
tooltip: 'Delete Tag',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
alignment: Alignment
|
||||
.centerLeft, // Align cell content to the left
|
||||
child: SizedBox(
|
||||
width: double
|
||||
.infinity, // Ensure full width for dropdown
|
||||
child: DialogTextfieldDropdown(
|
||||
items: availableTags,
|
||||
initialValue: tag.tag,
|
||||
onSelected: (value) {
|
||||
controller.text = value;
|
||||
context
|
||||
.read<
|
||||
AssignTagModelBloc>()
|
||||
.add(UpdateTag(
|
||||
index: index,
|
||||
tag: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DialogDropdown(
|
||||
items: locations,
|
||||
selectedValue: tag.location ??
|
||||
'Main Space',
|
||||
onSelected: (value) {
|
||||
context
|
||||
.read<
|
||||
AssignTagModelBloc>()
|
||||
.add(UpdateLocation(
|
||||
index: index,
|
||||
location: value,
|
||||
));
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (state.errorMessage != null)
|
||||
Text(state.errorMessage!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.warningRed)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (buttonContext) => CancelButton(
|
||||
label: 'Add New Device',
|
||||
onPressed: () async {
|
||||
final updatedTags =
|
||||
List<TagModel>.from(state.tags);
|
||||
final result =
|
||||
TagHelper.updateSubspaceTagModels(
|
||||
updatedTags, subspaces);
|
||||
|
||||
final processedTags =
|
||||
result['updatedTags'] as List<TagModel>;
|
||||
final processedSubspaces =
|
||||
List<SubspaceTemplateModel>.from(
|
||||
result['subspaces'] as List<dynamic>);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (dialogContext) =>
|
||||
AddDeviceTypeModelWidget(
|
||||
products: products,
|
||||
subspaces: processedSubspaces,
|
||||
isCreate: false,
|
||||
initialSelectedProducts: TagHelper
|
||||
.createInitialSelectedProducts(
|
||||
processedTags,
|
||||
processedSubspaces),
|
||||
allTags: allTags,
|
||||
spaceName: spaceName,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
spaceTagModels: processedTags,
|
||||
pageContext: pageContext,
|
||||
spaceModel: SpaceTemplateModel(
|
||||
modelName: spaceName,
|
||||
tags: updatedTags,
|
||||
uuid: spaceModel?.uuid,
|
||||
internalId:
|
||||
spaceModel?.internalId,
|
||||
subspaceModels:
|
||||
processedSubspaces)),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
borderRadius: 10,
|
||||
backgroundColor: state.isSaveEnabled
|
||||
? ColorsManager.secondaryColor
|
||||
: ColorsManager.grayColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
onPressed: state.isSaveEnabled
|
||||
? () async {
|
||||
final updatedTags =
|
||||
List<TagModel>.from(state.tags);
|
||||
|
||||
final result =
|
||||
TagHelper.updateSubspaceTagModels(
|
||||
updatedTags, subspaces);
|
||||
|
||||
final processedTags =
|
||||
result['updatedTags'] as List<TagModel>;
|
||||
final processedSubspaces =
|
||||
List<SubspaceTemplateModel>.from(
|
||||
result['subspaces']
|
||||
as List<dynamic>);
|
||||
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allSpaceModels: allSpaceModels,
|
||||
allTags: allTags,
|
||||
pageContext: pageContext,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
spaceModel: SpaceTemplateModel(
|
||||
modelName: spaceName,
|
||||
tags: processedTags,
|
||||
uuid: spaceModel?.uuid,
|
||||
internalId:
|
||||
spaceModel?.internalId,
|
||||
subspaceModels:
|
||||
processedSubspaces),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is AssignTagModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return const Center(child: Text('Something went wrong.'));
|
||||
}
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
@ -77,9 +77,7 @@ class CreateCommunityDialog extends StatelessWidget {
|
||||
.read<CommunityDialogBloc>()
|
||||
.add(ValidateCommunityNameEvent(value));
|
||||
},
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the community name',
|
||||
filled: true,
|
||||
|
@ -0,0 +1,110 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
import 'subspace_event.dart';
|
||||
import 'subspace_state.dart';
|
||||
|
||||
class SubSpaceBloc extends Bloc<SubSpaceEvent, SubSpaceState> {
|
||||
SubSpaceBloc() : super(SubSpaceState([], [], '', {})) {
|
||||
on<AddSubSpace>((event, emit) {
|
||||
final existingNames = state.subSpaces.map((e) => e.subspaceName).toSet();
|
||||
|
||||
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
|
||||
final updatedDuplicates = Set<String>.from(state.duplicates)
|
||||
..add(event.subSpace.subspaceName.toLowerCase());
|
||||
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'*Duplicated sub-space name',
|
||||
updatedDuplicates,
|
||||
));
|
||||
} else {
|
||||
// Add subspace if no duplicate exists
|
||||
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
|
||||
if (state.duplicates.isNotEmpty) {
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'*Duplicated sub-space name',
|
||||
state.duplicates,
|
||||
));
|
||||
} else {
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'',
|
||||
state.duplicates,
|
||||
// Clear error message
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle RemoveSubSpace Event
|
||||
on<RemoveSubSpace>((event, emit) {
|
||||
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
|
||||
..remove(event.subSpace);
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
|
||||
if (event.subSpace.uuid?.isNotEmpty ?? false) {
|
||||
updatedSubspaceModels.add(UpdateSubspaceModel(
|
||||
action: Action.delete,
|
||||
uuid: event.subSpace.uuid!,
|
||||
));
|
||||
}
|
||||
|
||||
final nameOccurrences = <String, int>{};
|
||||
for (final subSpace in updatedSubSpaces) {
|
||||
final lowerName = subSpace.subspaceName.toLowerCase();
|
||||
nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
final updatedDuplicates = nameOccurrences.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toSet();
|
||||
final errorMessage =
|
||||
updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : '';
|
||||
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
errorMessage,
|
||||
updatedDuplicates,
|
||||
));
|
||||
});
|
||||
|
||||
on<UpdateSubSpace>((event, emit) {
|
||||
final updatedSubSpaces = state.subSpaces.map((subSpace) {
|
||||
if (subSpace.uuid == event.updatedSubSpace.uuid) {
|
||||
return event.updatedSubSpace;
|
||||
}
|
||||
return subSpace;
|
||||
}).toList();
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
|
||||
updatedSubspaceModels.add(UpdateSubspaceModel(
|
||||
action: Action.update,
|
||||
uuid: event.updatedSubSpace.uuid!,
|
||||
));
|
||||
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
'',
|
||||
state.duplicates,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
|
||||
abstract class SubSpaceEvent {}
|
||||
|
||||
class AddSubSpace extends SubSpaceEvent {
|
||||
final SubspaceModel subSpace;
|
||||
AddSubSpace(this.subSpace);
|
||||
}
|
||||
|
||||
class RemoveSubSpace extends SubSpaceEvent {
|
||||
final SubspaceModel subSpace;
|
||||
RemoveSubSpace(this.subSpace);
|
||||
}
|
||||
|
||||
class UpdateSubSpace extends SubSpaceEvent {
|
||||
final SubspaceModel updatedSubSpace;
|
||||
UpdateSubSpace(this.updatedSubSpace);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
|
||||
class SubSpaceState {
|
||||
final List<SubspaceModel> subSpaces;
|
||||
final List<UpdateSubspaceModel> updatedSubSpaceModels;
|
||||
final String errorMessage;
|
||||
final Set<String> duplicates;
|
||||
|
||||
SubSpaceState(
|
||||
this.subSpaces,
|
||||
this.updatedSubSpaceModels,
|
||||
this.errorMessage,
|
||||
this.duplicates,
|
||||
);
|
||||
|
||||
SubSpaceState copyWith({
|
||||
List<SubspaceModel>? subSpaces,
|
||||
List<UpdateSubspaceModel>? updatedSubSpaceModels,
|
||||
String? errorMessage,
|
||||
Set<String>? duplicates,
|
||||
}) {
|
||||
return SubSpaceState(
|
||||
subSpaces ?? this.subSpaces,
|
||||
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
|
||||
errorMessage ?? this.errorMessage,
|
||||
duplicates ?? this.duplicates,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSubSpaceDialog extends StatelessWidget {
|
||||
final bool isEdit;
|
||||
final String dialogTitle;
|
||||
final List<SubspaceModel>? existingSubSpaces;
|
||||
final String? spaceName;
|
||||
final List<Tag>? spaceTags;
|
||||
final List<ProductModel>? products;
|
||||
final Function(List<SubspaceModel>?)? onSave;
|
||||
|
||||
const CreateSubSpaceDialog(
|
||||
{Key? key,
|
||||
required this.isEdit,
|
||||
required this.dialogTitle,
|
||||
this.existingSubSpaces,
|
||||
required this.spaceName,
|
||||
required this.spaceTags,
|
||||
required this.products,
|
||||
required this.onSave})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final textController = TextEditingController();
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = SubSpaceBloc();
|
||||
if (existingSubSpaces != null) {
|
||||
for (var subSpace in existingSubSpaces!) {
|
||||
bloc.add(AddSubSpace(subSpace));
|
||||
}
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<SubSpaceBloc, SubSpaceState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: screenWidth * 0.35,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dialogTitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: screenWidth * 0.35,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...state.subSpaces.asMap().entries.map(
|
||||
(entry) {
|
||||
final index = entry.key;
|
||||
final subSpace = entry.value;
|
||||
|
||||
final lowerName =
|
||||
subSpace.subspaceName.toLowerCase();
|
||||
|
||||
final duplicateIndices = state.subSpaces
|
||||
.asMap()
|
||||
.entries
|
||||
.where((e) =>
|
||||
e.value.subspaceName.toLowerCase() ==
|
||||
lowerName)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
final isDuplicate =
|
||||
duplicateIndices.length > 1 &&
|
||||
duplicateIndices.indexOf(index) != 0;
|
||||
|
||||
return Chip(
|
||||
label: Text(subSpace.subspaceName,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color:
|
||||
ColorsManager.spaceColor)),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(
|
||||
color: isDuplicate
|
||||
? ColorsManager.red
|
||||
: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onDeleted: () => context
|
||||
.read<SubSpaceBloc>()
|
||||
.add(RemoveSubSpace(subSpace)),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: state.subSpaces.isEmpty
|
||||
? 'Please enter the name'
|
||||
: null,
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: ColorsManager
|
||||
.lightGrayColor)),
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
context.read<SubSpaceBloc>().add(
|
||||
AddSubSpace(SubspaceModel(
|
||||
subspaceName: value.trim(),
|
||||
disabled: false)));
|
||||
textController.clear();
|
||||
}
|
||||
},
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.errorMessage.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(state.errorMessage,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: ColorsManager.warningRed,
|
||||
)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
onPressed: (state.errorMessage.isNotEmpty)
|
||||
? null
|
||||
: () async {
|
||||
final subSpaces = context
|
||||
.read<SubSpaceBloc>()
|
||||
.state
|
||||
.subSpaces;
|
||||
onSave!(subSpaces);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: state.errorMessage.isNotEmpty
|
||||
? ColorsManager.whiteColorsWithOpacity
|
||||
: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
|
||||
SubSpaceModelBloc() : super(SubSpaceModelState([], [], '', {})) {
|
||||
// Handle AddSubSpaceModel Event
|
||||
on<AddSubSpaceModel>((event, emit) {
|
||||
final existingNames =
|
||||
state.subSpaces.map((e) => e.subspaceName.toLowerCase()).toSet();
|
||||
|
||||
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
|
||||
final updatedDuplicates = Set<String>.from(state.duplicates)
|
||||
..add(event.subSpace.subspaceName.toLowerCase());
|
||||
final updatedSubSpaces =
|
||||
List<SubspaceTemplateModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'*Duplicated sub-space name',
|
||||
updatedDuplicates,
|
||||
));
|
||||
} else {
|
||||
final updatedSubSpaces =
|
||||
List<SubspaceTemplateModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
|
||||
if (state.duplicates.isNotEmpty) {
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'*Duplicated sub-space name',
|
||||
state.duplicates,
|
||||
));
|
||||
} else {
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'',
|
||||
state.duplicates,
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle RemoveSubSpaceModel Event
|
||||
|
||||
on<RemoveSubSpaceModel>((event, emit) {
|
||||
final updatedSubSpaces = List<SubspaceTemplateModel>.from(state.subSpaces)
|
||||
..remove(event.subSpace);
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
|
||||
if (event.subSpace.uuid?.isNotEmpty ?? false) {
|
||||
updatedSubspaceModels.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete,
|
||||
uuid: event.subSpace.uuid!,
|
||||
));
|
||||
}
|
||||
|
||||
// Count occurrences of sub-space names to identify duplicates
|
||||
final nameOccurrences = <String, int>{};
|
||||
for (final subSpace in updatedSubSpaces) {
|
||||
final lowerName = subSpace.subspaceName.toLowerCase();
|
||||
nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Identify duplicate names
|
||||
final updatedDuplicates = nameOccurrences.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toSet();
|
||||
|
||||
// Determine the error message
|
||||
final errorMessage =
|
||||
updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : '';
|
||||
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
errorMessage,
|
||||
updatedDuplicates,
|
||||
));
|
||||
});
|
||||
|
||||
// Handle UpdateSubSpaceModel Event
|
||||
on<UpdateSubSpaceModel>((event, emit) {
|
||||
final updatedSubSpaces = state.subSpaces.map((subSpace) {
|
||||
if (subSpace.uuid == event.updatedSubSpace.uuid) {
|
||||
return event.updatedSubSpace;
|
||||
}
|
||||
return subSpace;
|
||||
}).toList();
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
|
||||
updatedSubspaceModels.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.update,
|
||||
uuid: event.updatedSubSpace.uuid!,
|
||||
));
|
||||
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
'',
|
||||
state.duplicates,
|
||||
// Clear error message
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
|
||||
abstract class SubSpaceModelEvent {}
|
||||
|
||||
class AddSubSpaceModel extends SubSpaceModelEvent {
|
||||
final SubspaceTemplateModel subSpace;
|
||||
AddSubSpaceModel(this.subSpace);
|
||||
}
|
||||
|
||||
class RemoveSubSpaceModel extends SubSpaceModelEvent {
|
||||
final SubspaceTemplateModel subSpace;
|
||||
RemoveSubSpaceModel(this.subSpace);
|
||||
}
|
||||
|
||||
class UpdateSubSpaceModel extends SubSpaceModelEvent {
|
||||
final SubspaceTemplateModel updatedSubSpace;
|
||||
UpdateSubSpaceModel(this.updatedSubSpace);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
|
||||
class SubSpaceModelState {
|
||||
final List<SubspaceTemplateModel> subSpaces;
|
||||
final List<UpdateSubspaceTemplateModel> updatedSubSpaceModels;
|
||||
final String errorMessage;
|
||||
final Set<String> duplicates;
|
||||
|
||||
SubSpaceModelState(
|
||||
this.subSpaces,
|
||||
this.updatedSubSpaceModels,
|
||||
this.errorMessage,
|
||||
this.duplicates,
|
||||
);
|
||||
|
||||
SubSpaceModelState copyWith({
|
||||
List<SubspaceTemplateModel>? subSpaces,
|
||||
List<UpdateSubspaceTemplateModel>? updatedSubSpaceModels,
|
||||
String? errorMessage,
|
||||
Set<String>? duplicates,
|
||||
}) {
|
||||
return SubSpaceModelState(
|
||||
subSpaces ?? this.subSpaces,
|
||||
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
|
||||
errorMessage ?? this.errorMessage,
|
||||
duplicates ?? this.duplicates,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSubSpaceModelDialog extends StatelessWidget {
|
||||
final bool isEdit;
|
||||
final String dialogTitle;
|
||||
final List<SubspaceTemplateModel>? existingSubSpaces;
|
||||
final void Function(List<SubspaceTemplateModel> newSubspaces)? onUpdate;
|
||||
|
||||
const CreateSubSpaceModelDialog(
|
||||
{Key? key,
|
||||
required this.isEdit,
|
||||
required this.dialogTitle,
|
||||
this.existingSubSpaces,
|
||||
this.onUpdate})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final textController = TextEditingController();
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = SubSpaceModelBloc();
|
||||
if (existingSubSpaces != null) {
|
||||
for (var subSpace in existingSubSpaces!) {
|
||||
bloc.add(AddSubSpaceModel(subSpace));
|
||||
}
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<SubSpaceModelBloc, SubSpaceModelState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: screenWidth * 0.3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dialogTitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: screenWidth * 0.35,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...state.subSpaces.asMap().entries.map(
|
||||
(entry) {
|
||||
final index = entry.key;
|
||||
final subSpace = entry.value;
|
||||
|
||||
final lowerName =
|
||||
subSpace.subspaceName.toLowerCase();
|
||||
|
||||
final duplicateIndices = state.subSpaces
|
||||
.asMap()
|
||||
.entries
|
||||
.where((e) =>
|
||||
e.value.subspaceName.toLowerCase() ==
|
||||
lowerName)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
final isDuplicate =
|
||||
duplicateIndices.length > 1 &&
|
||||
duplicateIndices.indexOf(index) != 0;
|
||||
|
||||
return Chip(
|
||||
label: Text(subSpace.subspaceName,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: ColorsManager.spaceColor,
|
||||
)),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(
|
||||
color: isDuplicate
|
||||
? ColorsManager.red
|
||||
: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onDeleted: () => context
|
||||
.read<SubSpaceModelBloc>()
|
||||
.add(RemoveSubSpaceModel(subSpace)),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: state.subSpaces.isEmpty
|
||||
? 'Please enter the name'
|
||||
: null,
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
color: ColorsManager
|
||||
.lightGrayColor)),
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
context.read<SubSpaceModelBloc>().add(
|
||||
AddSubSpaceModel(
|
||||
SubspaceTemplateModel(
|
||||
subspaceName: value.trim(),
|
||||
disabled: false)));
|
||||
textController.clear();
|
||||
}
|
||||
},
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager.blackColor)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.errorMessage.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(state.errorMessage,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: ColorsManager.red,
|
||||
)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
onPressed: (state.errorMessage.isNotEmpty)
|
||||
? null
|
||||
: () async {
|
||||
final subSpaces = context
|
||||
.read<SubSpaceModelBloc>()
|
||||
.state
|
||||
.subSpaces;
|
||||
Navigator.of(context).pop();
|
||||
if (onUpdate != null) {
|
||||
onUpdate!(subSpaces);
|
||||
}
|
||||
},
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: state.errorMessage.isNotEmpty
|
||||
? ColorsManager.whiteColorsWithOpacity
|
||||
: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
311
lib/pages/spaces_management/helper/tag_helper.dart
Normal file
311
lib/pages/spaces_management/helper/tag_helper.dart
Normal file
@ -0,0 +1,311 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
class TagHelper {
|
||||
static Map<String, dynamic> updateTags<T>({
|
||||
required List<T> updatedTags,
|
||||
required List<dynamic>? subspaces,
|
||||
required String Function(T) getInternalId,
|
||||
required String? Function(T) getLocation,
|
||||
required void Function(T, String) setLocation,
|
||||
required String Function(dynamic) getSubspaceName,
|
||||
required List<T>? Function(dynamic) getSubspaceTags,
|
||||
required void Function(dynamic, List<T>?) setSubspaceTags,
|
||||
required int? Function(T, List<dynamic>) checkTagExistInSubspace,
|
||||
}) {
|
||||
final modifiedTags = List<T>.from(updatedTags);
|
||||
final modifiedSubspaces = List<dynamic>.from(subspaces ?? []);
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
getSubspaceTags(subspace)?.removeWhere(
|
||||
(tag) => !modifiedTags.any(
|
||||
(updatedTag) => getInternalId(updatedTag) == getInternalId(tag)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (var tag in modifiedTags.toList()) {
|
||||
if (modifiedSubspaces.isEmpty) continue;
|
||||
|
||||
final prevIndice = checkTagExistInSubspace(tag, modifiedSubspaces);
|
||||
final tagLocation = getLocation(tag);
|
||||
|
||||
if ((tagLocation == 'Main Space' || tagLocation == null) &&
|
||||
(prevIndice == null ||
|
||||
getSubspaceName(modifiedSubspaces[prevIndice]) == 'Main Space')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tagLocation == 'Main Space' || tagLocation == null) &&
|
||||
prevIndice != null) {
|
||||
getSubspaceTags(modifiedSubspaces[prevIndice])
|
||||
?.removeWhere((t) => getInternalId(t) == getInternalId(tag));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tagLocation != 'Main Space' && tagLocation != null) &&
|
||||
prevIndice == null) {
|
||||
final newIndex = modifiedSubspaces
|
||||
.indexWhere((subspace) => getSubspaceName(subspace) == tagLocation);
|
||||
|
||||
if (newIndex != -1) {
|
||||
if (getSubspaceTags(modifiedSubspaces[newIndex])
|
||||
?.any((t) => getInternalId(t) == getInternalId(tag)) !=
|
||||
true) {
|
||||
setLocation(tag, getSubspaceName(modifiedSubspaces[newIndex]));
|
||||
final subspaceTags =
|
||||
getSubspaceTags(modifiedSubspaces[newIndex]) ?? [];
|
||||
subspaceTags.add(tag);
|
||||
setSubspaceTags(modifiedSubspaces[newIndex], subspaceTags);
|
||||
}
|
||||
}
|
||||
|
||||
modifiedTags.removeWhere((t) => getInternalId(t) == getInternalId(tag));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tagLocation != 'Main Space' && tagLocation != null) &&
|
||||
tagLocation != getSubspaceName(modifiedSubspaces[prevIndice!])) {
|
||||
getSubspaceTags(modifiedSubspaces[prevIndice])
|
||||
?.removeWhere((t) => getInternalId(t) == getInternalId(tag));
|
||||
|
||||
final newIndex = modifiedSubspaces
|
||||
.indexWhere((subspace) => getSubspaceName(subspace) == tagLocation);
|
||||
|
||||
if (newIndex != -1) {
|
||||
if (getSubspaceTags(modifiedSubspaces[newIndex])
|
||||
?.any((t) => getInternalId(t) == getInternalId(tag)) !=
|
||||
true) {
|
||||
setLocation(tag, getSubspaceName(modifiedSubspaces[newIndex]));
|
||||
final subspaceTags =
|
||||
getSubspaceTags(modifiedSubspaces[newIndex]) ?? [];
|
||||
subspaceTags.add(tag);
|
||||
setSubspaceTags(modifiedSubspaces[newIndex], subspaceTags);
|
||||
}
|
||||
}
|
||||
|
||||
modifiedTags.removeWhere((t) => getInternalId(t) == getInternalId(tag));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tagLocation != 'Main Space' && tagLocation != null) &&
|
||||
tagLocation == getSubspaceName(modifiedSubspaces[prevIndice!])) {
|
||||
modifiedTags.removeWhere((t) => getInternalId(t) == getInternalId(tag));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tagLocation == 'Main Space' || tagLocation == null) &&
|
||||
prevIndice != null) {
|
||||
getSubspaceTags(modifiedSubspaces[prevIndice])
|
||||
?.removeWhere((t) => getInternalId(t) == getInternalId(tag));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'updatedTags': modifiedTags,
|
||||
'subspaces': modifiedSubspaces,
|
||||
};
|
||||
}
|
||||
|
||||
static List<String> getAvailableTags<T>({
|
||||
required List<String> allTags,
|
||||
required List<T> currentTags,
|
||||
required T currentTag,
|
||||
required String? Function(T) getTag, // Allow nullable return type
|
||||
}) {
|
||||
return allTags
|
||||
.where((tagValue) => !currentTags
|
||||
.where((e) => e != currentTag) // Exclude the current row
|
||||
.map((e) => getTag(e) ?? '') // Handle null values gracefully
|
||||
.contains(tagValue))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<String> getAvailableTagModels(
|
||||
List<String> allTags, List<TagModel> currentTags, TagModel currentTag) {
|
||||
List<String> availableTagsForTagModel =
|
||||
TagHelper.getAvailableTags<TagModel>(
|
||||
allTags: allTags,
|
||||
currentTags: currentTags,
|
||||
currentTag: currentTag,
|
||||
getTag: (tag) => tag.tag ?? '',
|
||||
);
|
||||
return availableTagsForTagModel;
|
||||
}
|
||||
|
||||
static List<TagModel> generateInitialTags({
|
||||
List<TagModel>? spaceTagModels,
|
||||
List<SubspaceTemplateModel>? subspaces,
|
||||
}) {
|
||||
final List<TagModel> initialTags = [];
|
||||
|
||||
if (spaceTagModels != null) {
|
||||
initialTags.addAll(spaceTagModels);
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
initialTags.addAll(
|
||||
subspace.tags!.map(
|
||||
(tag) => tag.copyWith(
|
||||
location: subspace.subspaceName,
|
||||
internalId: tag.internalId,
|
||||
tag: tag.tag,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialTags;
|
||||
}
|
||||
|
||||
static List<Tag> generateInitialForTags({
|
||||
List<Tag>? spaceTags,
|
||||
List<SubspaceModel>? subspaces,
|
||||
}) {
|
||||
final List<Tag> initialTags = [];
|
||||
|
||||
if (spaceTags != null) {
|
||||
initialTags.addAll(spaceTags);
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
initialTags.addAll(
|
||||
subspace.tags!.map(
|
||||
(tag) => tag.copyWith(
|
||||
location: subspace.subspaceName,
|
||||
internalId: tag.internalId,
|
||||
tag: tag.tag,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialTags;
|
||||
}
|
||||
|
||||
static Map<ProductModel, int> groupTags(List<BaseTag> tags) {
|
||||
final Map<ProductModel, int> groupedTags = {};
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
final product = tag.product!;
|
||||
groupedTags[product] = (groupedTags[product] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return groupedTags;
|
||||
}
|
||||
|
||||
static List<SelectedProduct> createInitialSelectedProducts(
|
||||
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces) {
|
||||
final Map<ProductModel, int> productCounts = {};
|
||||
|
||||
if (tags != null) {
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
for (var tag in subspace.tags!) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] =
|
||||
(productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return productCounts.entries
|
||||
.map((entry) => SelectedProduct(
|
||||
productId: entry.key.uuid,
|
||||
count: entry.value,
|
||||
productName: entry.key.name ?? 'Unnamed',
|
||||
product: entry.key,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<SelectedProduct> createInitialSelectedProductsForTags(
|
||||
List<Tag>? tags, List<SubspaceModel>? subspaces) {
|
||||
final Map<ProductModel, int> productCounts = {};
|
||||
|
||||
if (tags != null) {
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
for (var tag in subspace.tags!) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] =
|
||||
(productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return productCounts.entries
|
||||
.map((entry) => SelectedProduct(
|
||||
productId: entry.key.uuid,
|
||||
count: entry.value,
|
||||
productName: entry.key.name ?? 'Unnamed',
|
||||
product: entry.key,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static int? checkTagExistInSubspaceModels(TagModel tag, List<dynamic>? subspaces) {
|
||||
if (subspaces == null) return null;
|
||||
|
||||
for (int i = 0; i < subspaces.length; i++) {
|
||||
final subspace = subspaces[i] as SubspaceTemplateModel; // Explicit cast
|
||||
if (subspace.tags == null) continue;
|
||||
for (var t in subspace.tags!) {
|
||||
if (tag.internalId == t.internalId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> updateSubspaceTagModels(
|
||||
List<TagModel> updatedTags, List<SubspaceTemplateModel>? subspaces) {
|
||||
return TagHelper.updateTags<TagModel>(
|
||||
updatedTags: updatedTags,
|
||||
subspaces: subspaces,
|
||||
getInternalId: (tag) => tag.internalId,
|
||||
getLocation: (tag) => tag.location,
|
||||
setLocation: (tag, location) => tag.location = location,
|
||||
getSubspaceName: (subspace) => subspace.subspaceName,
|
||||
getSubspaceTags: (subspace) => subspace.tags,
|
||||
setSubspaceTags: (subspace, tags) => subspace.tags = tags,
|
||||
checkTagExistInSubspace: checkTagExistInSubspaceModels,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart';
|
||||
|
||||
|
||||
class SpaceModelBloc extends Bloc<SpaceModelEvent, SpaceModelState> {
|
||||
SpaceModelBloc() : super(SpaceModelInitial()) {
|
||||
on<SpaceModelSelectedEvent>((event, emit) {
|
||||
emit(SpaceModelSelectedState(event.selectedIndex));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
abstract class SpaceModelEvent {}
|
||||
|
||||
class SpaceModelSelectedEvent extends SpaceModelEvent {
|
||||
final int selectedIndex;
|
||||
|
||||
SpaceModelSelectedEvent(this.selectedIndex);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
abstract class SpaceModelState {}
|
||||
|
||||
class SpaceModelInitial extends SpaceModelState {}
|
||||
|
||||
class SpaceModelSelectedState extends SpaceModelState {
|
||||
final int selectedIndex;
|
||||
|
||||
SpaceModelSelectedState(this.selectedIndex);
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class LinkSpaceModelDialog extends StatelessWidget {
|
||||
final Function(SpaceTemplateModel?)? onSave;
|
||||
final int? initialSelectedIndex;
|
||||
final List<SpaceTemplateModel> spaceModels;
|
||||
|
||||
const LinkSpaceModelDialog({
|
||||
Key? key,
|
||||
this.onSave,
|
||||
this.initialSelectedIndex,
|
||||
required this.spaceModels,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceModelBloc()
|
||||
..add(
|
||||
SpaceModelSelectedEvent(initialSelectedIndex ?? -1),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final bloc = context.read<SpaceModelBloc>();
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
title: const Text('Link a space model'),
|
||||
content: spaceModels.isNotEmpty
|
||||
? Container(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: BlocBuilder<SpaceModelBloc, SpaceModelState>(
|
||||
builder: (context, state) {
|
||||
int selectedIndex = -1;
|
||||
if (state is SpaceModelSelectedState) {
|
||||
selectedIndex = state.selectedIndex;
|
||||
}
|
||||
return GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
childAspectRatio: 3,
|
||||
),
|
||||
itemCount: spaceModels.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final model = spaceModels[index];
|
||||
final isSelected = selectedIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
bloc.add(SpaceModelSelectedEvent(index));
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? ColorsManager.spaceColor
|
||||
: Colors.transparent,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: SpaceModelCardWidget(model: model,),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const Text('No space models available.'),
|
||||
actions: [
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CancelButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: 'Cancel',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
BlocBuilder<SpaceModelBloc, SpaceModelState>(
|
||||
builder: (context, state) {
|
||||
final isEnabled = state is SpaceModelSelectedState &&
|
||||
state.selectedIndex >= 0;
|
||||
return SizedBox(
|
||||
width: 140,
|
||||
child: DefaultButton(
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
onPressed: isEnabled
|
||||
? () {
|
||||
if (onSave != null) {
|
||||
final selectedModel =
|
||||
spaceModels[state.selectedIndex];
|
||||
|
||||
onSave!(selectedModel);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,386 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class CreateSpaceModelBloc
|
||||
extends Bloc<CreateSpaceModelEvent, CreateSpaceModelState> {
|
||||
SpaceTemplateModel? _space;
|
||||
|
||||
final SpaceModelManagementApi _api;
|
||||
|
||||
CreateSpaceModelBloc(this._api) : super(CreateSpaceModelInitial()) {
|
||||
on<CreateSpaceTemplate>((event, emit) async {
|
||||
try {
|
||||
late SpaceTemplateModel spaceTemplate = event.spaceTemplate;
|
||||
|
||||
final tagBodyModels =
|
||||
spaceTemplate.tags?.map((tag) => tag.toTagBodyModel()).toList() ??
|
||||
[];
|
||||
|
||||
final subspaceTemplateBodyModels =
|
||||
spaceTemplate.subspaceModels?.map((subspaceModel) {
|
||||
final tagsubspaceBodyModels = subspaceModel.tags
|
||||
?.map((tag) => tag.toTagBodyModel())
|
||||
.toList() ??
|
||||
[];
|
||||
return CreateSubspaceTemplateModel()
|
||||
..subspaceName = subspaceModel.subspaceName
|
||||
..tags = tagsubspaceBodyModels;
|
||||
}).toList() ??
|
||||
[];
|
||||
final spaceModelBody = CreateSpaceTemplateBodyModel(
|
||||
modelName: spaceTemplate.modelName,
|
||||
tags: tagBodyModels,
|
||||
subspaceModels: subspaceTemplateBodyModels);
|
||||
|
||||
final newSpaceTemplate = await _api.createSpaceModel(spaceModelBody);
|
||||
spaceTemplate.uuid = newSpaceTemplate?.uuid ?? '';
|
||||
|
||||
if (newSpaceTemplate != null) {
|
||||
emit(CreateSpaceModelLoaded(spaceTemplate));
|
||||
|
||||
if (event.onCreate != null) {
|
||||
event.onCreate!(spaceTemplate);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
emit(CreateSpaceModelError('Error creating space model'));
|
||||
}
|
||||
});
|
||||
|
||||
on<LoadSpaceTemplate>((event, emit) {
|
||||
emit(CreateSpaceModelLoading());
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (_space != null) {
|
||||
emit(CreateSpaceModelLoaded(_space!));
|
||||
} else {
|
||||
emit(CreateSpaceModelError("No space template found"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
on<UpdateSpaceTemplate>((event, emit) {
|
||||
_space = event.spaceTemplate;
|
||||
final String? errorMessage = _checkDuplicateModelName(
|
||||
event.allModels ?? [], event.spaceTemplate.modelName);
|
||||
emit(CreateSpaceModelLoaded(_space!, errorMessage: errorMessage));
|
||||
});
|
||||
|
||||
on<AddSubspacesToSpaceTemplate>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is CreateSpaceModelLoaded) {
|
||||
final eventSubspaceIds =
|
||||
event.subspaces.map((e) => e.internalId).toSet();
|
||||
|
||||
// Update or retain subspaces
|
||||
final updatedSubspaces = currentState.space.subspaceModels
|
||||
?.where((subspace) =>
|
||||
eventSubspaceIds.contains(subspace.internalId))
|
||||
.map((subspace) {
|
||||
final matchingEventSubspace = event.subspaces.firstWhere(
|
||||
(e) => e.internalId == subspace.internalId,
|
||||
orElse: () => subspace,
|
||||
);
|
||||
|
||||
// Update the subspace's tags
|
||||
final eventTagIds = matchingEventSubspace.tags
|
||||
?.map((e) => e.internalId)
|
||||
.toSet() ??
|
||||
{};
|
||||
|
||||
final updatedTags = [
|
||||
...?subspace.tags?.map<TagModel>((tag) {
|
||||
final matchingTag =
|
||||
matchingEventSubspace.tags?.firstWhere(
|
||||
(e) => e.internalId == tag.internalId,
|
||||
orElse: () => tag,
|
||||
);
|
||||
final isUpdated = matchingTag != tag;
|
||||
return isUpdated
|
||||
? tag.copyWith(tag: matchingTag?.tag)
|
||||
: tag;
|
||||
}) ??
|
||||
<TagModel>[],
|
||||
...?matchingEventSubspace.tags?.where(
|
||||
(e) =>
|
||||
subspace.tags
|
||||
?.every((t) => t.internalId != e.internalId) ??
|
||||
true,
|
||||
) ??
|
||||
<TagModel>[],
|
||||
];
|
||||
return subspace.copyWith(
|
||||
subspaceName: matchingEventSubspace.subspaceName,
|
||||
tags: updatedTags,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
// Add new subspaces
|
||||
event.subspaces
|
||||
.where((e) =>
|
||||
updatedSubspaces.every((s) => s.internalId != e.internalId))
|
||||
.forEach((newSubspace) {
|
||||
updatedSubspaces.add(newSubspace);
|
||||
});
|
||||
|
||||
final updatedSpace =
|
||||
currentState.space.copyWith(subspaceModels: updatedSubspaces);
|
||||
|
||||
emit(CreateSpaceModelLoaded(updatedSpace,
|
||||
errorMessage: currentState.errorMessage));
|
||||
} else {
|
||||
emit(CreateSpaceModelError("Space template not initialized"));
|
||||
}
|
||||
});
|
||||
|
||||
on<AddTagsToSpaceTemplate>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is CreateSpaceModelLoaded) {
|
||||
final eventTagIds = event.tags.map((e) => e.internalId).toSet();
|
||||
|
||||
final updatedTags = currentState.space.tags
|
||||
?.where((tag) => eventTagIds.contains(tag.internalId))
|
||||
.map((tag) {
|
||||
final matchingEventTag = event.tags.firstWhere(
|
||||
(e) => e.internalId == tag.internalId,
|
||||
orElse: () => tag,
|
||||
);
|
||||
return matchingEventTag != tag
|
||||
? tag.copyWith(tag: matchingEventTag.tag)
|
||||
: tag;
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
event.tags
|
||||
.where(
|
||||
(e) => updatedTags.every((t) => t.internalId != e.internalId))
|
||||
.forEach((e) {
|
||||
updatedTags.add(e);
|
||||
});
|
||||
|
||||
emit(CreateSpaceModelLoaded(
|
||||
currentState.space.copyWith(tags: updatedTags)));
|
||||
} else {
|
||||
emit(CreateSpaceModelError("Space template not initialized"));
|
||||
}
|
||||
});
|
||||
|
||||
on<UpdateSpaceTemplateName>((event, emit) {
|
||||
final currentState = state;
|
||||
if (currentState is CreateSpaceModelLoaded) {
|
||||
if (event.allModels.contains(event.name) == true) {
|
||||
emit(CreateSpaceModelLoaded(
|
||||
currentState.space,
|
||||
errorMessage: "Duplicate Model name",
|
||||
));
|
||||
} else if (event.name.trim().isEmpty) {
|
||||
emit(CreateSpaceModelLoaded(
|
||||
currentState.space,
|
||||
errorMessage: "Model name cannot be empty",
|
||||
));
|
||||
} else {
|
||||
final updatedSpaceModel =
|
||||
currentState.space.copyWith(modelName: event.name);
|
||||
|
||||
emit(CreateSpaceModelLoaded(updatedSpaceModel));
|
||||
}
|
||||
} else {
|
||||
emit(CreateSpaceModelError("Space template not initialized"));
|
||||
}
|
||||
});
|
||||
|
||||
on<ModifySpaceTemplate>((event, emit) async {
|
||||
try {
|
||||
if (event.spaceTemplate.uuid != null) {
|
||||
final prevSpaceModel =
|
||||
await _api.getSpaceModel(event.spaceTemplate.uuid ?? '');
|
||||
|
||||
final newSpaceModel = event.updatedSpaceTemplate;
|
||||
String? spaceModelName;
|
||||
if (prevSpaceModel?.modelName != newSpaceModel.modelName) {
|
||||
spaceModelName = newSpaceModel.modelName;
|
||||
}
|
||||
List<TagModelUpdate> tagUpdates = [];
|
||||
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
|
||||
final List<SubspaceTemplateModel>? prevSubspaces =
|
||||
prevSpaceModel?.subspaceModels;
|
||||
final List<SubspaceTemplateModel>? newSubspaces =
|
||||
newSpaceModel.subspaceModels;
|
||||
|
||||
tagUpdates =
|
||||
processTagUpdates(prevSpaceModel?.tags, newSpaceModel.tags);
|
||||
|
||||
if (prevSubspaces != null || newSubspaces != null) {
|
||||
if (prevSubspaces != null && newSubspaces != null) {
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
final existsInNew = newSubspaces
|
||||
.any((subspace) => subspace.uuid == prevSubspace.uuid);
|
||||
if (!existsInNew) {
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete, uuid: prevSubspace.uuid));
|
||||
}
|
||||
}
|
||||
} else if (prevSubspaces != null && newSubspaces == null) {
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete, uuid: prevSubspace.uuid));
|
||||
}
|
||||
}
|
||||
|
||||
if (newSubspaces != null) {
|
||||
for (var newSubspace in newSubspaces!) {
|
||||
// Tag without UUID
|
||||
if ((newSubspace.uuid == null || newSubspace.uuid!.isEmpty)) {
|
||||
final List<TagModelUpdate> tagUpdates = [];
|
||||
|
||||
if (newSubspace.tags != null) {
|
||||
for (var tag in newSubspace.tags!) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
uuid: tag.uuid == '' ? null : tag.uuid,
|
||||
tag: tag.tag,
|
||||
productUuid: tag.product?.uuid));
|
||||
}
|
||||
}
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.add,
|
||||
subspaceName: newSubspace.subspaceName,
|
||||
tags: tagUpdates));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSubspaces != null && newSubspaces != null) {
|
||||
final newSubspaceMap = {
|
||||
for (var subspace in newSubspaces!) subspace.uuid: subspace
|
||||
};
|
||||
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
final newSubspace = newSubspaceMap[prevSubspace.uuid];
|
||||
|
||||
if (newSubspace != null) {
|
||||
final List<TagModelUpdate> tagSubspaceUpdates =
|
||||
processTagUpdates(prevSubspace.tags, newSubspace.tags);
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.update,
|
||||
uuid: newSubspace.uuid,
|
||||
subspaceName: newSubspace.subspaceName,
|
||||
tags: tagSubspaceUpdates));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final spaceModelBody = CreateSpaceTemplateBodyModel(
|
||||
modelName: spaceModelName,
|
||||
tags: tagUpdates,
|
||||
subspaceModels: subspaceUpdates);
|
||||
|
||||
final res = await _api.updateSpaceModel(
|
||||
spaceModelBody, prevSpaceModel?.uuid ?? '');
|
||||
|
||||
if (res != null) {
|
||||
emit(CreateSpaceModelLoaded(newSpaceModel));
|
||||
if (event.onUpdate != null) {
|
||||
event.onUpdate!(event.updatedSpaceTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
emit(CreateSpaceModelError('Error creating space model'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<TagModelUpdate> processTagUpdates(
|
||||
List<TagModel>? prevTags,
|
||||
List<TagModel>? newTags,
|
||||
) {
|
||||
final List<TagModelUpdate> tagUpdates = [];
|
||||
final processedTags = <String?>{};
|
||||
|
||||
if (prevTags == null && newTags != null) {
|
||||
for (var newTag in newTags) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
tag: newTag.tag,
|
||||
uuid: newTag.uuid,
|
||||
productUuid: newTag.product?.uuid,
|
||||
));
|
||||
}
|
||||
return tagUpdates;
|
||||
}
|
||||
|
||||
if (newTags != null || prevTags != null) {
|
||||
// Case 1: Tags deleted
|
||||
if (prevTags != null && newTags != null) {
|
||||
for (var prevTag in prevTags) {
|
||||
final existsInNew =
|
||||
newTags!.any((newTag) => newTag.uuid == prevTag.uuid);
|
||||
if (!existsInNew) {
|
||||
tagUpdates
|
||||
.add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid));
|
||||
}
|
||||
}
|
||||
} else if (prevTags != null && newTags == null) {
|
||||
for (var prevTag in prevTags) {
|
||||
tagUpdates
|
||||
.add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid));
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Tags added
|
||||
if (newTags != null) {
|
||||
final prevTagUuids = prevTags?.map((t) => t.uuid).toSet() ?? {};
|
||||
|
||||
for (var newTag in newTags!) {
|
||||
// Tag without UUID
|
||||
if ((newTag.uuid == null || !prevTagUuids.contains(newTag.uuid)) &&
|
||||
!processedTags.contains(newTag.tag)) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
tag: newTag.tag,
|
||||
uuid: newTag.uuid == '' ? null : newTag.uuid,
|
||||
productUuid: newTag.product?.uuid));
|
||||
processedTags.add(newTag.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Tags updated
|
||||
if (prevTags != null && newTags != null) {
|
||||
final newTagMap = {for (var tag in newTags!) tag.uuid: tag};
|
||||
|
||||
for (var prevTag in prevTags!) {
|
||||
final newTag = newTagMap[prevTag.uuid];
|
||||
if (newTag != null) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.update,
|
||||
uuid: newTag.uuid,
|
||||
tag: newTag.tag,
|
||||
));
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagUpdates;
|
||||
}
|
||||
|
||||
String? _checkDuplicateModelName(List<String> allModels, String name) {
|
||||
if (allModels.contains(name)) {
|
||||
return "Duplicate Model name";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class CreateSpaceModelEvent extends Equatable {
|
||||
const CreateSpaceModelEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadSpaceTemplate extends CreateSpaceModelEvent {}
|
||||
|
||||
class UpdateSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final SpaceTemplateModel spaceTemplate;
|
||||
List<String>? allModels;
|
||||
|
||||
UpdateSpaceTemplate(this.spaceTemplate,this.allModels);
|
||||
}
|
||||
|
||||
class CreateSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final SpaceTemplateModel spaceTemplate;
|
||||
final Function(SpaceTemplateModel)? onCreate;
|
||||
|
||||
const CreateSpaceTemplate({
|
||||
required this.spaceTemplate,
|
||||
this.onCreate,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [spaceTemplate];
|
||||
}
|
||||
|
||||
class UpdateSpaceTemplateName extends CreateSpaceModelEvent {
|
||||
final String name;
|
||||
final List<String> allModels;
|
||||
|
||||
UpdateSpaceTemplateName({required this.name, required this.allModels});
|
||||
|
||||
@override
|
||||
List<Object> get props => [name, allModels];
|
||||
}
|
||||
|
||||
class AddSubspacesToSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final List<SubspaceTemplateModel> subspaces;
|
||||
|
||||
AddSubspacesToSpaceTemplate(this.subspaces);
|
||||
}
|
||||
|
||||
class AddTagsToSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final List<TagModel> tags;
|
||||
|
||||
AddTagsToSpaceTemplate(this.tags);
|
||||
}
|
||||
|
||||
class ValidateSpaceTemplateName extends CreateSpaceModelEvent {
|
||||
final String name;
|
||||
|
||||
ValidateSpaceTemplateName({required this.name});
|
||||
}
|
||||
|
||||
class ModifySpaceTemplate extends CreateSpaceModelEvent {
|
||||
final SpaceTemplateModel spaceTemplate;
|
||||
final SpaceTemplateModel updatedSpaceTemplate;
|
||||
final Function(SpaceTemplateModel)? onUpdate;
|
||||
|
||||
ModifySpaceTemplate(
|
||||
{required this.spaceTemplate,
|
||||
required this.updatedSpaceTemplate,
|
||||
this.onUpdate});
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class CreateSpaceModelState extends Equatable {
|
||||
const CreateSpaceModelState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class CreateSpaceModelInitial extends CreateSpaceModelState {}
|
||||
|
||||
class CreateSpaceModelLoading extends CreateSpaceModelState {}
|
||||
|
||||
class CreateSpaceModelLoaded extends CreateSpaceModelState {
|
||||
final SpaceTemplateModel space;
|
||||
final String? errorMessage;
|
||||
|
||||
CreateSpaceModelLoaded(this.space, {this.errorMessage});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [space, errorMessage];
|
||||
}
|
||||
|
||||
class CreateSpaceModelError extends CreateSpaceModelState {
|
||||
final String message;
|
||||
|
||||
CreateSpaceModelError(this.message);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
|
||||
class SpaceModelBloc extends Bloc<SpaceModelEvent, SpaceModelState> {
|
||||
final SpaceModelManagementApi api;
|
||||
|
||||
SpaceModelBloc({
|
||||
required this.api,
|
||||
required List<SpaceTemplateModel> initialSpaceModels,
|
||||
}) : super(SpaceModelLoaded(spaceModels: initialSpaceModels)) {
|
||||
on<CreateSpaceModel>(_onCreateSpaceModel);
|
||||
on<UpdateSpaceModel>(_onUpdateSpaceModel);
|
||||
}
|
||||
|
||||
Future<void> _onCreateSpaceModel(
|
||||
CreateSpaceModel event, Emitter<SpaceModelState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is SpaceModelLoaded) {
|
||||
try {
|
||||
final newSpaceModel =
|
||||
await api.getSpaceModel(event.newSpaceModel.uuid ?? '');
|
||||
|
||||
if (newSpaceModel != null) {
|
||||
final updatedSpaceModels =
|
||||
List<SpaceTemplateModel>.from(currentState.spaceModels)
|
||||
..add(newSpaceModel);
|
||||
emit(SpaceModelLoaded(spaceModels: updatedSpaceModels));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SpaceModelError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdateSpaceModel(
|
||||
UpdateSpaceModel event, Emitter<SpaceModelState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is SpaceModelLoaded) {
|
||||
try {
|
||||
final newSpaceModel =
|
||||
await api.getSpaceModel(event.spaceModelUuid ?? '');
|
||||
if (newSpaceModel != null) {
|
||||
final updatedSpaceModels = currentState.spaceModels.map((model) {
|
||||
return model.uuid == event.spaceModelUuid ? newSpaceModel : model;
|
||||
}).toList();
|
||||
emit(SpaceModelLoaded(spaceModels: updatedSpaceModels));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SpaceModelError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class SpaceModelEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadSpaceModels extends SpaceModelEvent {}
|
||||
|
||||
class CreateSpaceModel extends SpaceModelEvent {
|
||||
final SpaceTemplateModel newSpaceModel;
|
||||
|
||||
CreateSpaceModel({required this.newSpaceModel});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [newSpaceModel];
|
||||
}
|
||||
|
||||
class GetSpaceModel extends SpaceModelEvent {
|
||||
final String spaceModelUuid;
|
||||
|
||||
GetSpaceModel({required this.spaceModelUuid});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spaceModelUuid];
|
||||
}
|
||||
|
||||
class UpdateSpaceModel extends SpaceModelEvent {
|
||||
final String spaceModelUuid;
|
||||
|
||||
UpdateSpaceModel({required this.spaceModelUuid});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spaceModelUuid];
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class SpaceModelState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class SpaceModelInitial extends SpaceModelState {}
|
||||
|
||||
class SpaceModelLoading extends SpaceModelState {}
|
||||
|
||||
class SpaceModelLoaded extends SpaceModelState {
|
||||
final List<SpaceTemplateModel> spaceModels;
|
||||
|
||||
SpaceModelLoaded({required this.spaceModels});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spaceModels];
|
||||
}
|
||||
|
||||
class SpaceModelError extends SpaceModelState {
|
||||
final String message;
|
||||
|
||||
SpaceModelError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
class TagBodyModel {
|
||||
late String? uuid;
|
||||
late String tag;
|
||||
late final String? productUuid;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'productUuid': productUuid,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toJson().toString();
|
||||
}
|
||||
}
|
||||
|
||||
class CreateSubspaceTemplateModel {
|
||||
late String subspaceName;
|
||||
late List<TagBodyModel>? tags;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((tag) => tag.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CreateSpaceTemplateBodyModel {
|
||||
final String? modelName;
|
||||
final List<dynamic>? tags;
|
||||
final List<dynamic>? subspaceModels;
|
||||
|
||||
CreateSpaceTemplateBodyModel({
|
||||
this.modelName,
|
||||
this.tags,
|
||||
this.subspaceModels,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'modelName': modelName,
|
||||
'tags': tags,
|
||||
'subspaceModels': subspaceModels,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toJson().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SpaceTemplateModel extends Equatable {
|
||||
String? uuid;
|
||||
String modelName;
|
||||
List<SubspaceTemplateModel>? subspaceModels;
|
||||
final List<TagModel>? tags;
|
||||
String internalId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [modelName, subspaceModels, tags];
|
||||
|
||||
SpaceTemplateModel({
|
||||
this.uuid,
|
||||
String? internalId,
|
||||
required this.modelName,
|
||||
this.subspaceModels,
|
||||
this.tags,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SpaceTemplateModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return SpaceTemplateModel(
|
||||
uuid: json['uuid'] ?? '',
|
||||
internalId: internalId,
|
||||
modelName: json['modelName'] ?? '',
|
||||
subspaceModels: (json['subspaceModels'] as List<dynamic>?)
|
||||
?.where((e) => e is Map<String, dynamic>) // Validate type
|
||||
.map((e) =>
|
||||
SubspaceTemplateModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.where((item) => item is Map<String, dynamic>) // Validate type
|
||||
.map((item) => TagModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
SpaceTemplateModel copyWith({
|
||||
String? uuid,
|
||||
String? modelName,
|
||||
List<SubspaceTemplateModel>? subspaceModels,
|
||||
List<TagModel>? tags,
|
||||
String? internalId,
|
||||
}) {
|
||||
return SpaceTemplateModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
modelName: modelName ?? this.modelName,
|
||||
subspaceModels: subspaceModels ?? this.subspaceModels,
|
||||
tags: tags ?? this.tags,
|
||||
internalId: internalId ?? this.internalId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'modelName': modelName,
|
||||
'subspaceModels': subspaceModels?.map((e) => e.toJson()).toList(),
|
||||
'tags': tags?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateSubspaceTemplateModel {
|
||||
final String? uuid;
|
||||
final Action action;
|
||||
final String? subspaceName;
|
||||
final List<TagModelUpdate>? tags;
|
||||
|
||||
UpdateSubspaceTemplateModel({
|
||||
required this.action,
|
||||
this.uuid,
|
||||
this.subspaceName,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
factory UpdateSubspaceTemplateModel.fromJson(Map<String, dynamic> json) {
|
||||
return UpdateSubspaceTemplateModel(
|
||||
action: ActionExtension.fromValue(json['action']),
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
tags: (json['tags'] as List)
|
||||
.map((item) => TagModelUpdate.fromJson(item))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension SpaceTemplateExtensions on SpaceTemplateModel {
|
||||
List<String> listAllTagValues() {
|
||||
final List<String> tagValues = [];
|
||||
|
||||
if (tags != null) {
|
||||
tagValues.addAll(
|
||||
tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty));
|
||||
}
|
||||
|
||||
if (subspaceModels != null) {
|
||||
for (final subspace in subspaceModels!) {
|
||||
if (subspace.tags != null) {
|
||||
tagValues.addAll(
|
||||
subspace.tags!
|
||||
.map((tag) => tag.tag ?? '')
|
||||
.where((tag) => tag.isNotEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagValues;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SubspaceTemplateModel {
|
||||
final String? uuid;
|
||||
String subspaceName;
|
||||
final bool disabled;
|
||||
List<TagModel>? tags;
|
||||
String internalId;
|
||||
|
||||
SubspaceTemplateModel({
|
||||
this.uuid,
|
||||
required this.subspaceName,
|
||||
required this.disabled,
|
||||
this.tags,
|
||||
String? internalId,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SubspaceTemplateModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return SubspaceTemplateModel(
|
||||
uuid: json['uuid'],
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
internalId: internalId,
|
||||
disabled: json['disabled'] ?? false,
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((item) => TagModel.fromJson(item))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'disabled': disabled,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
SubspaceTemplateModel copyWith({
|
||||
String? uuid,
|
||||
String? subspaceName,
|
||||
bool? disabled,
|
||||
List<TagModel>? tags,
|
||||
String? internalId,
|
||||
}) {
|
||||
return SubspaceTemplateModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
subspaceName: subspaceName ?? this.subspaceName,
|
||||
disabled: disabled ?? this.disabled,
|
||||
tags: tags ?? this.tags,
|
||||
internalId: internalId ?? this.internalId,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
class CreateTagBodyModel {
|
||||
late String tag;
|
||||
late final String? productUuid;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tag': tag,
|
||||
'productUuid': productUuid,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toJson().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class TagModel extends BaseTag {
|
||||
TagModel({
|
||||
String? uuid,
|
||||
required String? tag,
|
||||
ProductModel? product,
|
||||
String? internalId,
|
||||
String? location,
|
||||
}) : super(
|
||||
uuid: uuid,
|
||||
tag: tag,
|
||||
product: product,
|
||||
internalId: internalId,
|
||||
location: location,
|
||||
);
|
||||
factory TagModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return TagModel(
|
||||
uuid: json['uuid'] ,
|
||||
internalId: internalId,
|
||||
tag: json['tag'] ?? '',
|
||||
product: json['product'] != null
|
||||
? ProductModel.fromMap(json['product'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TagModel copyWith(
|
||||
{String? tag,
|
||||
ProductModel? product,
|
||||
String? uuid,
|
||||
String? location,
|
||||
String? internalId}) {
|
||||
return TagModel(
|
||||
tag: tag ?? this.tag,
|
||||
product: product ?? this.product,
|
||||
location: location ?? this.location,
|
||||
internalId: internalId ?? this.internalId,
|
||||
uuid:uuid?? this.uuid
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'product': product?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension TagModelExtensions on TagModel {
|
||||
TagBodyModel toTagBodyModel() {
|
||||
return TagBodyModel()
|
||||
..uuid = uuid
|
||||
..tag = tag ?? ''
|
||||
..productUuid = product?.uuid;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class TagModelUpdate {
|
||||
final Action action;
|
||||
final String? uuid;
|
||||
final String? tag;
|
||||
final String? productUuid;
|
||||
|
||||
TagModelUpdate({
|
||||
required this.action,
|
||||
this.uuid,
|
||||
this.tag,
|
||||
this.productUuid,
|
||||
});
|
||||
|
||||
factory TagModelUpdate.fromJson(Map<String, dynamic> json) {
|
||||
return TagModelUpdate(
|
||||
action: json['action'],
|
||||
uuid: json['uuid'],
|
||||
tag: json['tag'],
|
||||
productUuid: json['productUuid'],
|
||||
);
|
||||
}
|
||||
|
||||
// Method to convert an instance to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid, // Nullable field
|
||||
'tag': tag,
|
||||
'productUuid': productUuid,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/add_space_model_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceModelPage extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
|
||||
const SpaceModelPage({Key? key, this.products}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SpaceModelBloc, SpaceModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is SpaceModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is SpaceModelLoaded) {
|
||||
final spaceModels = state.spaceModels;
|
||||
final allTagValues = _getAllTagValues(spaceModels);
|
||||
final allSpaceModelNames = _getAllSpaceModelName(spaceModels);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20.0, 16.0, 16.0, 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
childAspectRatio: _calculateChildAspectRatio(context),
|
||||
),
|
||||
itemCount: spaceModels.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == spaceModels.length) {
|
||||
// Add Button
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allTags: allTagValues,
|
||||
pageContext: context,
|
||||
otherSpaceModels: allSpaceModelNames,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const AddSpaceModelWidget(),
|
||||
);
|
||||
}
|
||||
// Render existing space model
|
||||
final model = spaceModels[index];
|
||||
final otherModel =
|
||||
List<String>.from(allSpaceModelNames);
|
||||
otherModel.remove(model.modelName);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allTags: allTagValues,
|
||||
spaceModel: model,
|
||||
otherSpaceModels: otherModel,
|
||||
pageContext: context,
|
||||
allSpaceModels: spaceModels,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: SpaceModelCardWidget(model: model),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is SpaceModelError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Error: ${state.message}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.warningRed),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(child: Text('Initializing...'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateChildAspectRatio(BuildContext context) {
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth > 1600) {
|
||||
return 1.5; // Decrease to make cards taller
|
||||
}
|
||||
if (screenWidth > 1200) {
|
||||
return 2.0;
|
||||
} else if (screenWidth > 800) {
|
||||
return 2.5;
|
||||
} else {
|
||||
return 3.0;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _getAllTagValues(List<SpaceTemplateModel> spaceModels) {
|
||||
final List<String> allTags = [];
|
||||
for (final spaceModel in spaceModels) {
|
||||
if (spaceModel.tags != null) {
|
||||
allTags.addAll(spaceModel.listAllTagValues());
|
||||
}
|
||||
}
|
||||
return allTags;
|
||||
}
|
||||
|
||||
List<String> _getAllSpaceModelName(List<SpaceTemplateModel> spaceModels) {
|
||||
final List<String> names = [];
|
||||
for (final spaceModel in spaceModels) {
|
||||
names.add(spaceModel.modelName);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AddSpaceModelWidget extends StatelessWidget {
|
||||
const AddSpaceModelWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.semiTransparentBlackColor,
|
||||
blurRadius: 15,
|
||||
offset: Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: ColorsManager.semiTransparentBlackColor,
|
||||
blurRadius: 25,
|
||||
offset: Offset(0, 15),
|
||||
spreadRadius: -5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60, // Set a proper height here
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: ColorsManager.neutralGray,
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
size: 40,
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ButtonContentWidget extends StatelessWidget {
|
||||
final IconData? icon;
|
||||
final String label;
|
||||
final String? svgAssets;
|
||||
|
||||
const ButtonContentWidget(
|
||||
{Key? key, this.icon, required this.label, this.svgAssets})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 3.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (icon != null)
|
||||
Icon(
|
||||
icon,
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
if (svgAssets != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
svgAssets!,
|
||||
width: screenWidth * 0.015, // Adjust icon size
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,283 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_event.dart';
|
||||
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceModelDialog extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<String>? allTags;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
final BuildContext? pageContext;
|
||||
final List<String>? otherSpaceModels;
|
||||
final List<SpaceTemplateModel>? allSpaceModels;
|
||||
|
||||
const CreateSpaceModelDialog(
|
||||
{Key? key,
|
||||
this.products,
|
||||
this.allTags,
|
||||
this.spaceModel,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels,
|
||||
this.allSpaceModels})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi();
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final TextEditingController spaceNameController = TextEditingController(
|
||||
text: spaceModel?.modelName ?? '',
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SizedBox(
|
||||
width: screenWidth * 0.3,
|
||||
child: BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = CreateSpaceModelBloc(_spaceModelApi);
|
||||
if (spaceModel != null) {
|
||||
bloc.add(UpdateSpaceTemplate(spaceModel!, otherSpaceModels));
|
||||
} else {
|
||||
bloc.add(UpdateSpaceTemplate(
|
||||
SpaceTemplateModel(
|
||||
modelName: '',
|
||||
subspaceModels: const [],
|
||||
),
|
||||
otherSpaceModels));
|
||||
}
|
||||
|
||||
spaceNameController.addListener(() {
|
||||
bloc.add(UpdateSpaceTemplateName(
|
||||
name: spaceNameController.text,
|
||||
allModels: otherSpaceModels ?? []));
|
||||
});
|
||||
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<CreateSpaceModelBloc, CreateSpaceModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is CreateSpaceModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is CreateSpaceModelLoaded) {
|
||||
final updatedSpaceModel = state.space;
|
||||
final subspaces = updatedSpaceModel.subspaceModels ?? [];
|
||||
final isNameValid = spaceNameController.text.trim().isNotEmpty;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
spaceModel?.uuid == null
|
||||
? 'Create New Space Model'
|
||||
: 'Edit Space Model',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: TextField(
|
||||
controller: spaceNameController,
|
||||
onChanged: (value) {
|
||||
context.read<CreateSpaceModelBloc>().add(
|
||||
UpdateSpaceTemplateName(
|
||||
name: value,
|
||||
allModels: otherSpaceModels ?? []));
|
||||
},
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: ColorsManager.blackColor),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: ColorsManager.textFieldGreyColor,
|
||||
hintText: 'Please enter the name',
|
||||
errorText: state.errorMessage,
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: ColorsManager.lightGrayColor),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SubspaceModelCreate(
|
||||
subspaces: state.space.subspaceModels ?? [],
|
||||
tags: state.space.tags ?? [],
|
||||
onSpaceModelUpdate: (updatedSubspaces,updatedTags) {
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(AddSubspacesToSpaceTemplate(updatedSubspaces));
|
||||
if(updatedTags!=null){
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(AddTagsToSpaceTemplate(updatedTags));
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TagChipDisplay(
|
||||
context,
|
||||
screenWidth: screenWidth,
|
||||
spaceModel: updatedSpaceModel,
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
allTags: allTags,
|
||||
spaceNameController: spaceNameController,
|
||||
pageContext: pageContext,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
allSpaceModels: allSpaceModels,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
onPressed: ((state.errorMessage != null &&
|
||||
state.errorMessage != '') ||
|
||||
!isNameValid)
|
||||
? null
|
||||
: () {
|
||||
final updatedSpaceTemplate =
|
||||
updatedSpaceModel.copyWith(
|
||||
modelName:
|
||||
spaceNameController.text.trim(),
|
||||
);
|
||||
if (updatedSpaceModel.uuid == null) {
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(
|
||||
CreateSpaceTemplate(
|
||||
spaceTemplate:
|
||||
updatedSpaceTemplate,
|
||||
onCreate: (newModel) {
|
||||
if (pageContext != null) {
|
||||
pageContext!
|
||||
.read<SpaceModelBloc>()
|
||||
.add(CreateSpaceModel(
|
||||
newSpaceModel:
|
||||
newModel));
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop(); // Close the dialog
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (pageContext != null) {
|
||||
final currentState = pageContext!
|
||||
.read<SpaceModelBloc>()
|
||||
.state;
|
||||
if (currentState
|
||||
is SpaceModelLoaded) {
|
||||
final spaceModels =
|
||||
List<SpaceTemplateModel>.from(
|
||||
currentState.spaceModels);
|
||||
|
||||
final SpaceTemplateModel?
|
||||
currentSpaceModel = spaceModels
|
||||
.cast<SpaceTemplateModel?>()
|
||||
.firstWhere(
|
||||
(sm) =>
|
||||
sm?.uuid ==
|
||||
updatedSpaceModel
|
||||
.uuid,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (currentSpaceModel != null) {
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(ModifySpaceTemplate(
|
||||
spaceTemplate:
|
||||
currentSpaceModel,
|
||||
updatedSpaceTemplate:
|
||||
updatedSpaceTemplate,
|
||||
onUpdate: (newModel) {
|
||||
if (pageContext !=
|
||||
null) {
|
||||
pageContext!
|
||||
.read<
|
||||
SpaceModelBloc>()
|
||||
.add(UpdateSpaceModel(
|
||||
spaceModelUuid:
|
||||
newModel.uuid ??
|
||||
''));
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: ((state.errorMessage != null &&
|
||||
state.errorMessage != '') ||
|
||||
!isNameValid)
|
||||
? ColorsManager.whiteColorsWithOpacity
|
||||
: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is CreateSpaceModelError) {
|
||||
return Text(
|
||||
'Error: ${state.message}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: ColorsManager.warningRed),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: Text('Initializing...'));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/flexible_item_widget.dart';
|
||||
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DynamicProductWidget extends StatelessWidget {
|
||||
final Map<String, int> productTagCount;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
|
||||
const DynamicProductWidget({
|
||||
Key? key,
|
||||
required this.productTagCount,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double itemSpacing = 8.0;
|
||||
const double lineSpacing = 8.0;
|
||||
const double textPadding = 16.0;
|
||||
const double itemHeight = 40.0;
|
||||
|
||||
List<Widget> productWidgets = [];
|
||||
double currentLineWidth = 0.0;
|
||||
double currentHeight = itemHeight;
|
||||
|
||||
for (final product in productTagCount.entries) {
|
||||
final String prodType = product.key;
|
||||
final int count = product.value;
|
||||
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: 'x$count',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final double itemWidth = textPainter.width + textPadding + 20;
|
||||
|
||||
if (currentLineWidth + itemWidth + itemSpacing > maxWidth) {
|
||||
currentHeight += itemHeight + lineSpacing;
|
||||
if (currentHeight > maxHeight) {
|
||||
productWidgets.add(const EllipsisItemWidget());
|
||||
break;
|
||||
}
|
||||
currentLineWidth = 0.0;
|
||||
}
|
||||
|
||||
productWidgets.add(FlexibleItemWidget(prodType: prodType, count: count));
|
||||
currentLineWidth += itemWidth + itemSpacing;
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: itemSpacing,
|
||||
runSpacing: lineSpacing,
|
||||
children: productWidgets,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/room_name_widget.dart';
|
||||
|
||||
class DynamicRoomWidget extends StatelessWidget {
|
||||
final List<SubspaceTemplateModel>? subspaceModels;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
|
||||
const DynamicRoomWidget({
|
||||
Key? key,
|
||||
required this.subspaceModels,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double itemSpacing = 8.0;
|
||||
const double lineSpacing = 8.0;
|
||||
const double textPadding = 16.0;
|
||||
const double itemHeight = 30.0;
|
||||
|
||||
List<Widget> roomWidgets = [];
|
||||
double currentLineWidth = 0.0;
|
||||
double currentHeight = itemHeight;
|
||||
|
||||
for (final subspace in subspaceModels!) {
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: subspace.subspaceName,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final double itemWidth = textPainter.width + textPadding;
|
||||
|
||||
if (currentLineWidth + itemWidth + itemSpacing > maxWidth) {
|
||||
currentHeight += itemHeight + lineSpacing;
|
||||
if (currentHeight > maxHeight) {
|
||||
roomWidgets.add(const RoomNameWidget(name: "..."));
|
||||
break;
|
||||
}
|
||||
currentLineWidth = 0.0;
|
||||
}
|
||||
|
||||
roomWidgets.add(RoomNameWidget(name: subspace.subspaceName));
|
||||
currentLineWidth += itemWidth + itemSpacing;
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: itemSpacing,
|
||||
runSpacing: lineSpacing,
|
||||
children: roomWidgets,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class EllipsisItemWidget extends StatelessWidget {
|
||||
const EllipsisItemWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Text(
|
||||
"...",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class FlexibleItemWidget extends StatelessWidget {
|
||||
final String prodType;
|
||||
final int count;
|
||||
|
||||
const FlexibleItemWidget({
|
||||
Key? key,
|
||||
required this.prodType,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
prodType,
|
||||
width: 15,
|
||||
height: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'x$count',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RoomNameWidget extends StatelessWidget {
|
||||
final String name;
|
||||
|
||||
const RoomNameWidget({Key? key, required this.name}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceModelCardWidget extends StatelessWidget {
|
||||
final SpaceTemplateModel model;
|
||||
|
||||
const SpaceModelCardWidget({Key? key, required this.model}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, int> productTagCount = {};
|
||||
|
||||
if (model.tags != null) {
|
||||
for (var tag in model.tags!) {
|
||||
final prodIcon = tag.product?.icon ?? 'Unknown';
|
||||
productTagCount[prodIcon] = (productTagCount[prodIcon] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (model.subspaceModels != null) {
|
||||
for (var subspace in model.subspaceModels!) {
|
||||
if (subspace.tags != null) {
|
||||
for (var tag in subspace.tags!) {
|
||||
final prodIcon = tag.product?.icon ?? 'Unknown';
|
||||
productTagCount[prodIcon] = (productTagCount[prodIcon] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
bool showOnlyName = constraints.maxWidth < 250;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
model.modelName,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!showOnlyName) ...[
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Container
|
||||
Expanded(
|
||||
flex: 1, // Distribute space proportionally
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: DynamicRoomWidget(
|
||||
subspaceModels: model.subspaceModels,
|
||||
maxWidth: constraints.maxWidth,
|
||||
maxHeight: constraints.maxHeight,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (productTagCount.isNotEmpty &&
|
||||
model.subspaceModels != null)
|
||||
Container(
|
||||
width: 1.0,
|
||||
color: ColorsManager.softGray,
|
||||
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1, // Distribute space proportionally
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: DynamicProductWidget(
|
||||
productTagCount: productTagCount,
|
||||
maxWidth: constraints.maxWidth,
|
||||
maxHeight: constraints.maxHeight));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SubspaceChipWidget extends StatelessWidget {
|
||||
final String subspace;
|
||||
|
||||
const SubspaceChipWidget({
|
||||
Key? key,
|
||||
required this.subspace,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
subspace,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/common/edit_chip.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SubspaceModelCreate extends StatefulWidget {
|
||||
final List<SubspaceTemplateModel> subspaces;
|
||||
final void Function(
|
||||
List<SubspaceTemplateModel> newSubspaces, List<TagModel>? tags)?
|
||||
onSpaceModelUpdate;
|
||||
final List<TagModel> tags;
|
||||
|
||||
const SubspaceModelCreate({
|
||||
Key? key,
|
||||
required this.subspaces,
|
||||
this.onSpaceModelUpdate,
|
||||
required this.tags,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SubspaceModelCreateState createState() => _SubspaceModelCreateState();
|
||||
}
|
||||
|
||||
class _SubspaceModelCreateState extends State<SubspaceModelCreate> {
|
||||
late List<SubspaceTemplateModel> _subspaces;
|
||||
String? errorSubspaceId;
|
||||
late List<TagModel> _tags;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_subspaces = List.from(widget.subspaces);
|
||||
_tags = List.from(widget.tags);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Container(
|
||||
child: _subspaces.isEmpty
|
||||
? TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
overlayColor: ColorsManager.transparentColor,
|
||||
),
|
||||
onPressed: () async {
|
||||
await _openDialog(context, 'Create Sub-space');
|
||||
},
|
||||
child: const ButtonContentWidget(
|
||||
icon: Icons.add,
|
||||
label: 'Create Sub Space',
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
..._subspaces.map((subspace) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SubspaceNameDisplayWidget(
|
||||
text: subspace.subspaceName,
|
||||
validateName: (updatedName) {
|
||||
return !_subspaces.any((s) =>
|
||||
s != subspace &&
|
||||
s.subspaceName == updatedName);
|
||||
},
|
||||
onNameChanged: (updatedName) {
|
||||
setState(() {
|
||||
subspace.subspaceName = updatedName;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
EditChip(
|
||||
onTap: () async {
|
||||
await _openDialog(context, 'Edit Sub-space');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openDialog(BuildContext context, String dialogTitle) async {
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreateSubSpaceModelDialog(
|
||||
isEdit: true,
|
||||
dialogTitle: dialogTitle,
|
||||
existingSubSpaces: _subspaces,
|
||||
onUpdate: (subspaceModels) {
|
||||
final updatedIds = subspaceModels.map((s) => s.internalId).toSet();
|
||||
final deletedSubspaces = _subspaces
|
||||
.where((s) => !updatedIds.contains(s.internalId))
|
||||
.toList();
|
||||
|
||||
final List<TagModel> tagsToAppendToSpace = [];
|
||||
|
||||
for (var s in deletedSubspaces) {
|
||||
if (s.tags != null) {
|
||||
tagsToAppendToSpace.addAll(s.tags!);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_subspaces = subspaceModels;
|
||||
_tags.addAll(tagsToAppendToSpace);
|
||||
});
|
||||
if (widget.onSpaceModelUpdate != null) {
|
||||
widget.onSpaceModelUpdate!(subspaceModels, _tags);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SubspaceNameDisplayWidget extends StatefulWidget {
|
||||
final String text;
|
||||
final TextStyle? textStyle;
|
||||
final Color backgroundColor;
|
||||
final Color borderColor;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final void Function(String updatedName) onNameChanged;
|
||||
final bool Function(String updatedName) validateName;
|
||||
|
||||
const SubspaceNameDisplayWidget({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.textStyle,
|
||||
this.backgroundColor = ColorsManager.whiteColors,
|
||||
this.borderColor = ColorsManager.transparentColor,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(10)),
|
||||
required this.onNameChanged,
|
||||
required this.validateName,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SubspaceNameDisplayWidgetState createState() =>
|
||||
_SubspaceNameDisplayWidgetState();
|
||||
}
|
||||
|
||||
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
|
||||
bool isEditing = false;
|
||||
late TextEditingController _controller;
|
||||
late FocusNode _focusNode;
|
||||
late String previousName;
|
||||
String? errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.text);
|
||||
_focusNode = FocusNode();
|
||||
previousName = widget.text;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleValidationAndSave() {
|
||||
final updatedName = _controller.text;
|
||||
|
||||
if (updatedName.isEmpty) {
|
||||
setState(() {
|
||||
errorText = 'Subspace name cannot be empty.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (widget.validateName(updatedName)) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
isEditing = false;
|
||||
previousName = updatedName;
|
||||
widget.onNameChanged(updatedName);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
errorText = 'Subspace name already exists.';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isEditing = true;
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor,
|
||||
borderRadius: widget.borderRadius,
|
||||
border: Border.all(color: widget.borderColor),
|
||||
),
|
||||
child: isEditing
|
||||
? TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
style: widget.textStyle ??
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
_handleValidationAndSave();
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
widget.text,
|
||||
style: widget.textStyle ??
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
errorText!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: ColorsManager.warningRed),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/common/edit_chip.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class TagChipDisplay extends StatelessWidget {
|
||||
final double screenWidth;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
final List<ProductModel>? products;
|
||||
final List<SubspaceTemplateModel>? subspaces;
|
||||
final List<String>? allTags;
|
||||
final TextEditingController spaceNameController;
|
||||
final BuildContext? pageContext;
|
||||
final List<String>? otherSpaceModels;
|
||||
final List<SpaceTemplateModel>? allSpaceModels;
|
||||
|
||||
const TagChipDisplay(BuildContext context,
|
||||
{Key? key,
|
||||
required this.screenWidth,
|
||||
required this.spaceModel,
|
||||
required this.products,
|
||||
required this.subspaces,
|
||||
required this.allTags,
|
||||
required this.spaceNameController,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels,
|
||||
this.allSpaceModels})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return (spaceModel?.tags?.isNotEmpty == true ||
|
||||
spaceModel?.subspaceModels
|
||||
?.any((subspace) => subspace.tags?.isNotEmpty == true) ==
|
||||
true)
|
||||
? SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
// Combine tags from spaceModel and subspaces
|
||||
...TagHelper.groupTags([
|
||||
...?spaceModel?.tags,
|
||||
...?spaceModel?.subspaceModels
|
||||
?.expand((subspace) => subspace.tags ?? [])
|
||||
]).entries.map(
|
||||
(entry) => Chip(
|
||||
avatar: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: SvgPicture.asset(
|
||||
entry.key.icon ?? 'assets/icons/gateway.svg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
'x${entry.value}', // Show count
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
color:
|
||||
ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
EditChip(onTap: () async {
|
||||
// Use the Navigator's context for showDialog
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AssignTagModelsDialog(
|
||||
products: products,
|
||||
allSpaceModels: allSpaceModels,
|
||||
subspaces: subspaces,
|
||||
pageContext: pageContext,
|
||||
allTags: allTags,
|
||||
spaceModel: spaceModel,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
initialTags: TagHelper.generateInitialTags(
|
||||
subspaces: subspaces,
|
||||
spaceTagModels: spaceModel?.tags ?? []),
|
||||
title: 'Edit Device',
|
||||
addedProducts:
|
||||
TagHelper.createInitialSelectedProducts(
|
||||
spaceModel?.tags ?? [], subspaces),
|
||||
spaceName: spaceModel?.modelName ?? '',
|
||||
));
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AddDeviceTypeModelWidget(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
allTags: allTags,
|
||||
spaceName: spaceNameController.text,
|
||||
pageContext: pageContext,
|
||||
isCreate: true,
|
||||
spaceModel: spaceModel,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
child: const ButtonContentWidget(
|
||||
icon: Icons.add,
|
||||
label: 'Add Devices',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_state.dart';
|
||||
|
||||
// Bloc Implementation
|
||||
class CenterBodyBloc extends Bloc<CenterBodyEvent, CenterBodyState> {
|
||||
CenterBodyBloc() : super(InitialState()) {
|
||||
on<CommunityStructureSelectedEvent>((event, emit) {
|
||||
emit(CommunityStructureState());
|
||||
});
|
||||
|
||||
on<SpaceModelSelectedEvent>((event, emit) {
|
||||
emit(SpaceModelState());
|
||||
});
|
||||
|
||||
on<CommunitySelectedEvent>((event, emit) {
|
||||
emit(CommunitySelectedState());
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// Define Events
|
||||
abstract class CenterBodyEvent {}
|
||||
|
||||
class CommunityStructureSelectedEvent extends CenterBodyEvent {}
|
||||
|
||||
class SpaceModelSelectedEvent extends CenterBodyEvent {}
|
||||
|
||||
class CommunitySelectedEvent extends CenterBodyEvent {}
|
@ -0,0 +1,9 @@
|
||||
abstract class CenterBodyState {}
|
||||
|
||||
class InitialState extends CenterBodyState {}
|
||||
|
||||
class CommunityStructureState extends CenterBodyState {}
|
||||
|
||||
class SpaceModelState extends CenterBodyState {}
|
||||
|
||||
class CommunitySelectedState extends CenterBodyState {}
|
@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_state.dart';
|
||||
import '../bloc/center_body_bloc.dart';
|
||||
|
||||
class CenterBodyWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CenterBodyBloc, CenterBodyState>(
|
||||
builder: (context, state) {
|
||||
if (state is InitialState) {
|
||||
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
|
||||
}
|
||||
if (state is CommunityStructureState) {
|
||||
context.read<SpaceManagementBloc>().add(BlankStateEvent());
|
||||
}
|
||||
|
||||
if (state is SpaceModelState) {
|
||||
context.read<SpaceManagementBloc>().add(SpaceModelLoadEvent());
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
|
||||
},
|
||||
child: Text(
|
||||
'Community Structure',
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontWeight: state is CommunityStructureState || state is CommunitySelectedState
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: state is CommunityStructureState || state is CommunitySelectedState
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
|
||||
},
|
||||
child: Text(
|
||||
'Space Model',
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontWeight: state is SpaceModelState
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: state is SpaceModelState
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart';
|
||||
|
||||
class AddDeviceTypeModelBloc
|
||||
extends Bloc<AddDeviceTypeModelEvent, AddDeviceModelState> {
|
||||
AddDeviceTypeModelBloc() : super(AddDeviceModelInitial()) {
|
||||
on<InitializeDeviceTypeModel>(_onInitializeTagModels);
|
||||
on<UpdateProductCountEvent>(_onUpdateProductCount);
|
||||
}
|
||||
|
||||
void _onInitializeTagModels(
|
||||
InitializeDeviceTypeModel event, Emitter<AddDeviceModelState> emit) {
|
||||
emit(AddDeviceModelLoaded(
|
||||
selectedProducts: event.addedProducts,
|
||||
initialTag: event.initialTags,
|
||||
));
|
||||
}
|
||||
|
||||
void _onUpdateProductCount(
|
||||
UpdateProductCountEvent event, Emitter<AddDeviceModelState> emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AddDeviceModelLoaded) {
|
||||
final existingProduct = currentState.selectedProducts.firstWhere(
|
||||
(p) => p.productId == event.productId,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: event.productId,
|
||||
count: 0,
|
||||
productName: event.productName,
|
||||
product: event.product,
|
||||
),
|
||||
);
|
||||
|
||||
List<SelectedProduct> updatedProducts;
|
||||
|
||||
if (event.count > 0) {
|
||||
if (!currentState.selectedProducts.contains(existingProduct)) {
|
||||
updatedProducts = [
|
||||
...currentState.selectedProducts,
|
||||
SelectedProduct(
|
||||
productId: event.productId,
|
||||
count: event.count,
|
||||
productName: event.productName,
|
||||
product: event.product,
|
||||
),
|
||||
];
|
||||
} else {
|
||||
updatedProducts = currentState.selectedProducts.map((p) {
|
||||
if (p.productId == event.productId) {
|
||||
return SelectedProduct(
|
||||
productId: p.productId,
|
||||
count: event.count,
|
||||
productName: p.productName,
|
||||
product: p.product,
|
||||
);
|
||||
}
|
||||
return p;
|
||||
}).toList();
|
||||
}
|
||||
} else {
|
||||
// Remove the product if the count is 0
|
||||
updatedProducts = currentState.selectedProducts
|
||||
.where((p) => p.productId != event.productId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Emit the updated state
|
||||
emit(AddDeviceModelLoaded(
|
||||
selectedProducts: updatedProducts,
|
||||
initialTag: currentState.initialTag));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AddDeviceModelState extends Equatable {
|
||||
const AddDeviceModelState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AddDeviceModelInitial extends AddDeviceModelState {}
|
||||
|
||||
class AddDeviceModelLoading extends AddDeviceModelState {}
|
||||
|
||||
class AddDeviceModelLoaded extends AddDeviceModelState {
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final List<TagModel> initialTag;
|
||||
|
||||
const AddDeviceModelLoaded({
|
||||
required this.selectedProducts,
|
||||
required this.initialTag,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [selectedProducts, initialTag];
|
||||
}
|
||||
|
||||
class AddDeviceModelError extends AddDeviceModelState {
|
||||
final String errorMessage;
|
||||
|
||||
const AddDeviceModelError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AddDeviceTypeModelEvent extends Equatable {
|
||||
const AddDeviceTypeModelEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
|
||||
class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
|
||||
final String productId;
|
||||
final int count;
|
||||
final String productName;
|
||||
final ProductModel product;
|
||||
|
||||
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
|
||||
|
||||
@override
|
||||
List<Object> get props => [productId, count];
|
||||
}
|
||||
|
||||
|
||||
class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent {
|
||||
final List<TagModel> initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeDeviceTypeModel({
|
||||
this.initialTags = const [],
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags, addedProducts];
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user