Merge remote-tracking branch 'origin/master' into changes

# Conflicts:
#	assets/langs/ar-SA.json
pull/237/head
tahaalam 2 days ago
commit befcee7c65

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7501 1.97257C12.7352 1.56461 12.3967 1.24352 11.9885 1.2501C11.5804 1.25668 11.2524 1.58851 11.2506 1.99674C11.2485 2.4854 10.9063 3.79916 9.52097 5.66527C9.37283 5.86481 9.44668 6.1691 9.68599 6.23616C9.86992 6.28771 10.1456 6.22765 10.2355 6.05914C10.5779 5.4179 11.2584 4.98778 12.0331 5.00026C13.1216 5.01781 13.9961 5.9027 14.0008 6.99131C14.0014 7.00025 14.0076 7.09332 14.0573 7.27852C14.1079 7.46728 14.1966 7.72504 14.3438 8.04628C14.6377 8.68804 15.1562 9.56385 16.038 10.6233C16.0555 10.6443 16.0726 10.6657 16.0892 10.6873C17.2456 12.1985 18.0009 14.0834 18.0009 16C18.0009 16.0825 17.9995 16.1645 17.9969 16.246C17.9813 16.7261 17.9735 16.9661 18.1166 17.0581C18.2597 17.1502 18.4513 17.0552 18.8347 16.8653C21.1457 15.7203 22.7506 13.565 22.7506 11C22.7506 9.40543 22.1171 7.78106 21.0962 6.44698C21.09 6.43884 21.0836 6.43082 21.0771 6.42294C19.19 4.15586 18.753 2.54913 18.7506 1.99674C18.7488 1.58851 18.4209 1.25668 18.0127 1.2501C17.6045 1.24352 17.2661 1.56461 17.2511 1.97257C17.233 2.46736 16.9266 2.82542 16.5831 2.95502C16.2811 3.06897 15.9181 3.02413 15.6247 2.58398C15.4856 2.37533 15.2514 2.25 15.0006 2.25C14.7499 2.25 14.5157 2.37533 14.3766 2.58398C14.0831 3.02413 13.7201 3.06897 13.4181 2.95502C13.0746 2.82542 12.7682 2.46736 12.7501 1.97257ZM16.1637 5.82934C15.7932 6.01459 15.643 6.46509 15.8283 6.83557L16.3283 7.83557C16.5135 8.20606 16.964 8.35623 17.3345 8.17098C17.705 7.98574 17.8552 7.53524 17.6699 7.16475L17.1699 6.16475C16.9847 5.79427 16.5342 5.6441 16.1637 5.82934Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.98718 6.2501C6.39536 6.24352 6.73384 6.56461 6.74877 6.97257C6.76687 7.46736 7.07325 7.82542 7.41675 7.95502C7.71875 8.06897 8.08179 8.02413 8.37523 7.58398C8.51433 7.37533 8.7485 7.25 8.99927 7.25C9.25003 7.25 9.48421 7.37533 9.62331 7.58398C9.91674 8.02413 10.2798 8.06897 10.5818 7.95502C10.9253 7.82542 11.2317 7.46736 11.2498 6.97257C11.2647 6.56461 11.6032 6.24352 12.0114 6.2501C12.4195 6.25668 12.7475 6.58851 12.7493 6.99674C12.7517 7.54913 13.1886 9.15586 15.0757 11.4229C15.0823 11.4308 15.0887 11.4388 15.0949 11.447C16.1157 12.7811 16.7493 14.4054 16.7493 16C16.7493 19.8322 13.1668 22.75 8.99927 22.75C4.83169 22.75 1.24927 19.8322 1.24927 16C1.24927 14.4054 1.88281 12.7811 2.90364 11.447C2.90987 11.4388 2.91627 11.4308 2.92283 11.4229C4.8099 9.15586 5.24687 7.54913 5.24927 6.99674C5.25105 6.58851 5.579 6.25668 5.98718 6.2501ZM7.83484 10.8292C8.20533 11.0144 8.35549 11.4649 8.17025 11.8354L7.67025 12.8354C7.48501 13.2059 7.03451 13.356 6.66402 13.1708C6.29354 12.9856 6.14337 12.5351 6.32861 12.1646L6.82861 11.1646C7.01385 10.7941 7.46436 10.6439 7.83484 10.8292ZM10.164 10.8292C10.5345 10.6439 10.985 10.7941 11.1703 11.1646L11.6703 12.1646C11.8555 12.5351 11.7053 12.9856 11.3348 13.1708C10.9644 13.356 10.5139 13.2059 10.3286 12.8354L9.82861 11.8354C9.64337 11.4649 9.79354 11.0144 10.164 10.8292Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6464 4.85841C12.9445 4.48673 13.1271 4.01976 13.1271 3.5C13.1271 2.20288 12.0081 1.25 10.7543 1.25H5.24524C3.99144 1.25 2.87244 2.20288 2.87244 3.5C2.87244 4.01976 3.055 4.48673 3.35316 4.85841C3.52079 5.06738 3.6046 5.17187 3.61356 5.26012C3.62251 5.34837 3.57237 5.44614 3.47209 5.64169C2.15246 8.21494 1.24963 10.6245 1.24963 13.7771V18C1.24963 19.6296 1.41033 20.9586 2.31169 21.7918C2.76397 22.2098 3.3378 22.4427 3.99127 22.5754C4.63937 22.707 5.41717 22.75 6.32711 22.75H9.67242C10.0971 22.75 10.536 22.7311 10.9492 22.6976C11.458 22.6563 11.7124 22.6356 11.7825 22.466C11.8525 22.2965 11.6681 22.0801 11.2993 21.6473C10.355 20.5392 9.74963 19.1021 9.74963 17.3845C9.74963 15.5039 10.3683 13.8652 11.2997 12.6615C11.9146 11.867 12.7887 11.1221 13.8271 10.7559C14.1207 10.6524 14.2674 10.6007 14.3223 10.4975C14.3772 10.3943 14.343 10.2627 14.2747 9.9996C13.8827 8.49067 13.2713 7.09213 12.5274 5.64158C12.4271 5.44611 12.377 5.34837 12.386 5.26012C12.3949 5.17187 12.4787 5.06738 12.6464 4.85841Z" fill="white"/>
<path d="M20.7264 10C20.7264 9.44771 20.2787 9 19.7264 9C18.4312 9 17.4699 9.67339 16.8679 10.5139C16.515 11.0066 16.2718 11.5713 16.1343 12.1362C15.845 12.0691 15.5182 12.0195 15.1816 12.0195C14.1838 12.0195 13.1925 12.667 12.4862 13.5798C11.7591 14.5195 11.2498 15.8349 11.2498 17.3848C11.2498 20.6682 14.1829 22.7503 16.9998 22.7503C19.8167 22.7503 22.7498 20.6682 22.7498 17.3848C22.7498 15.8349 22.2406 14.5195 21.5134 13.5798C20.8071 12.667 19.8158 12.0195 18.818 12.0196C18.6312 12.0195 18.4473 12.0348 18.2712 12.0599C18.3347 11.9256 18.409 11.7969 18.4939 11.6785C18.801 11.2497 19.2033 11 19.7264 11C20.2787 11 20.7264 10.5523 20.7264 10Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6439 1.25L13.3561 1.25C14.259 1.24997 15.0211 1.24994 15.6249 1.32042C16.255 1.39396 16.8707 1.55847 17.3706 2.00915C17.5958 2.21222 17.7873 2.44899 17.9376 2.71113C18.2326 3.22573 18.2812 3.77334 18.2347 4.31609C19.2103 4.4287 20.0196 4.68604 20.6521 5.31966C21.2538 5.92238 21.5125 6.68113 21.6335 7.58241C21.75 8.4507 21.75 9.55477 21.75 10.9249V16.043C21.75 17.4131 21.75 18.5172 21.6335 19.3855C21.5125 20.2867 21.2538 21.0455 20.6521 21.6482C20.0502 22.2511 19.2923 22.5105 18.3919 22.6318C17.5249 22.7486 16.4225 22.7486 15.055 22.7485L8.94504 22.7485C7.57752 22.7486 6.47513 22.7486 5.60808 22.6318C4.70774 22.5105 3.94976 22.2511 3.3479 21.6482C2.74621 21.0455 2.48747 20.2867 2.3665 19.3855C2.24997 18.5172 2.24998 17.4131 2.25 16.0431L2.25 10.9248C2.24998 9.55478 2.24997 8.45069 2.3665 7.58241C2.48747 6.68113 2.74621 5.92238 3.3479 5.31966C3.98043 4.68604 4.78975 4.4287 5.76534 4.31608C5.71879 3.77334 5.76736 3.22573 6.06239 2.71113C6.21268 2.44899 6.40422 2.21222 6.62944 2.00915C7.1293 1.55847 7.74503 1.39396 8.37506 1.32042C8.97893 1.24994 9.74097 1.24997 10.6439 1.25ZM8.60067 3.2073C8.13641 3.26149 7.99252 3.35241 7.92575 3.4126C7.8493 3.48154 7.78534 3.56095 7.73568 3.64756C7.69637 3.71613 7.64027 3.8608 7.69746 4.30137C7.75673 4.75797 7.91092 5.36081 8.14823 6.27796C8.33725 7.00848 8.46246 7.48783 8.59555 7.84684C8.72171 8.18716 8.82647 8.33584 8.9287 8.43308C9.00978 8.51021 9.10016 8.57847 9.1982 8.63631C9.32461 8.71091 9.5056 8.77465 9.88389 8.81094C10.1378 8.83531 10.4406 8.84448 10.8301 8.84793L11.5514 6.68404C11.726 6.1601 12.2924 5.87694 12.8163 6.05159C13.3402 6.22624 13.6234 6.79256 13.4488 7.3165L12.9378 8.84935C13.4424 8.84743 13.8138 8.83994 14.1161 8.81094C14.4944 8.77465 14.6754 8.71091 14.8018 8.63631C14.8998 8.57847 14.9902 8.51021 15.0713 8.43308C15.1735 8.33584 15.2783 8.18716 15.4045 7.84684C15.5375 7.48783 15.6628 7.00848 15.8518 6.27796C16.0891 5.36081 16.2433 4.75797 16.3025 4.30137C16.3597 3.8608 16.3036 3.71613 16.2643 3.64756C16.2147 3.56095 16.1507 3.48153 16.0742 3.4126C16.0075 3.35241 15.8636 3.26149 15.3993 3.2073C14.9218 3.15157 14.2747 3.15 13.2979 3.15L10.7021 3.15C9.72532 3.15 9.07816 3.15157 8.60067 3.2073ZM10 17C9.44772 17 9 17.4477 9 18C9 18.5523 9.44772 19 10 19H14C14.5523 19 15 18.5523 15 18C15 17.4477 14.5523 17 14 17L10 17Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25 8C9.25 7.58579 9.58579 7.25 10 7.25H10.0081C10.4223 7.25 10.7581 7.58579 10.7581 8C10.7581 8.41421 10.4223 8.75 10.0081 8.75H10C9.58579 8.75 9.25 8.41421 9.25 8Z" fill="white"/>
<path d="M13.25 8C13.25 7.58579 13.5858 7.25 14 7.25H14.0081C14.4223 7.25 14.7581 7.58579 14.7581 8C14.7581 8.41421 14.4223 8.75 14.0081 8.75H14C13.5858 8.75 13.25 8.41421 13.25 8Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0039 1.25C8.27598 1.25 5.25391 4.27208 5.25391 8C5.25391 9.38824 5.67299 10.6786 6.39155 11.7515C5.6694 12.8779 5.25 14.2322 5.25 15.6847C5.25 19.5503 8.23629 22.7505 12 22.7505C14.6194 22.7505 16.868 21.1942 17.9876 18.9496C16.4713 18.291 14.8272 17.3477 13.3318 16.1767C12.767 15.7344 12.2184 15.2558 11.7036 14.7434C11.8031 14.7478 11.9033 14.75 12.0039 14.75C12.5875 14.75 13.1538 14.6759 13.694 14.5367C13.8779 14.6931 14.0656 14.8462 14.2566 14.9957C15.6278 16.0694 17.1339 16.9362 18.5141 17.5427C18.668 16.9498 18.75 16.3267 18.75 15.6847C18.75 14.2348 18.3321 12.8827 17.6123 11.7574C18.3333 10.6834 18.7539 9.39081 18.7539 8C18.7539 4.27208 15.7318 1.25 12.0039 1.25ZM9.30582 4.0036C8.02465 4.87025 7.18248 6.33678 7.18248 8C7.18248 10.6628 9.3411 12.8214 12.0039 12.8214C14.6667 12.8214 16.8253 10.6628 16.8253 8C16.8253 5.3372 14.6667 3.17857 12.0039 3.17857C11.5521 3.17857 11.2325 3.5278 11.2325 3.90179C11.2325 4.27578 11.5521 4.625 12.0039 4.625C12.2101 4.625 12.3921 4.55043 12.528 4.43216C12.9298 4.08256 13.5389 4.12485 13.8885 4.5266C14.2381 4.92836 14.1958 5.53744 13.794 5.88704C13.3158 6.30318 12.6868 6.55357 12.0039 6.55357C10.5721 6.55357 9.36055 5.44435 9.30582 4.0036Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,9 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00439 1.5C9.41861 1.5 9.75439 1.83579 9.75439 2.25V4.50003L10.1913 4.50001C10.323 4.49992 10.4848 4.4998 10.6171 4.51636L10.6196 4.51667C10.7144 4.5285 11.1465 4.58237 11.3522 5.00657C11.5585 5.43169 11.332 5.80679 11.283 5.88795L11.0287 6.23932C10.8077 6.52119 10.5034 6.90703 10.2181 7.20028C10.0757 7.34661 9.91277 7.49754 9.73959 7.61668C9.58565 7.72258 9.323 7.87503 9 7.87503C8.677 7.87503 8.41436 7.72258 8.26041 7.61668C8.08723 7.49754 7.92428 7.34661 7.78191 7.20028C7.49659 6.90703 7.19231 6.52118 6.97134 6.23931L6.71698 5.88795C6.66799 5.80679 6.44154 5.43169 6.64776 5.00657C6.85353 4.58237 7.28556 4.5285 7.38039 4.51667L7.3829 4.51636C7.51523 4.49981 7.67697 4.49992 7.8087 4.50001L8.25439 4.50003V2.25C8.25439 1.83579 8.59018 1.5 9.00439 1.5Z" fill="#2E3039"/>
<path d="M14.3028 8.50787C14.5957 8.21498 14.5957 7.7401 14.3028 7.44721C14.0099 7.15432 13.535 7.15432 13.2421 7.44721L12.4466 8.2427C12.1537 8.5356 12.1537 9.01047 12.4466 9.30336C12.7395 9.59626 13.2144 9.59626 13.5073 9.30336L14.3028 8.50787Z" fill="#2E3039"/>
<path d="M4.75778 7.44767C4.46489 7.15478 3.99002 7.15478 3.69712 7.44767C3.40423 7.74057 3.40423 8.21544 3.69712 8.50833L4.49262 9.30383C4.78551 9.59672 5.26039 9.59672 5.55328 9.30383C5.84617 9.01094 5.84617 8.53606 5.55328 8.24317L4.75778 7.44767Z" fill="#2E3039"/>
<path d="M2.25 12.0005C1.83579 12.0005 1.5 12.3363 1.5 12.7505C1.5 13.1648 1.83579 13.5005 2.25 13.5005H3.375C3.78921 13.5005 4.125 13.1648 4.125 12.7505C4.125 12.3363 3.78921 12.0005 3.375 12.0005H2.25Z" fill="#2E3039"/>
<path d="M14.625 12.0005C14.2108 12.0005 13.875 12.3363 13.875 12.7505C13.875 13.1648 14.2108 13.5005 14.625 13.5005H15.75C16.1642 13.5005 16.5 13.1648 16.5 12.7505C16.5 12.3363 16.1642 12.0005 15.75 12.0005H14.625Z" fill="#2E3039"/>
<path d="M16.5 15.75C16.5 16.1642 16.1642 16.5 15.75 16.5L2.25 16.5C1.83579 16.5 1.5 16.1642 1.5 15.75C1.5 15.3358 1.83579 15 2.25 15L15.75 15C16.1642 15 16.5 15.3358 16.5 15.75Z" fill="#2E3039"/>
<path d="M9 9C6.92893 9 5.25 10.6789 5.25 12.75C5.25 13.1642 5.58579 13.5 6 13.5H12C12.4142 13.5 12.75 13.1642 12.75 12.75C12.75 10.6789 11.0711 9 9 9Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.22103 1.7795C9.81474 1.35942 10.537 1.15927 11.4457 1.41276L13.8325 2.07854C13.8415 2.08106 13.8506 2.08358 13.8596 2.08609C14.1283 2.16063 14.401 2.23626 14.6357 2.39226C14.867 2.54596 15.0425 2.76342 15.2153 2.9775C15.2248 2.98922 15.2343 3.00093 15.2437 3.01262C17.8745 6.25803 19.615 8.80922 21.0951 12.0611L21.1211 12.1182C21.683 13.3527 22.1426 14.3625 22.3793 15.2434C22.6308 16.1796 22.6491 17.0304 22.2891 17.9326C21.9294 18.8342 21.3483 19.4157 20.5396 19.8663C19.7934 20.2819 18.8182 20.603 17.6511 20.9872L17.5852 21.0089C15.2201 21.7876 13.0748 21.9411 10.669 21.1702C10.5551 21.1337 10.4982 21.1155 10.4464 21.1264C10.3946 21.1373 10.3485 21.178 10.2564 21.2596C9.06151 22.3173 7.45354 22.815 5.89016 22.8005C4.19123 22.7847 2.45139 22.163 1.26272 20.8727C1.13515 20.7342 1.06433 20.5528 1.06433 20.3646V12.9848C1.06433 12.7866 1.14282 12.5964 1.28264 12.4558C2.54568 11.1863 4.38273 10.5793 6.17069 10.5601C7.07426 10.5504 7.99402 10.6905 8.85052 10.9896C9.09931 11.0765 9.22371 11.1199 9.25049 11.2234C9.27728 11.3268 9.18109 11.4347 8.98871 11.6505C8.81551 11.8448 8.65167 12.0526 8.50126 12.2661C8.0296 12.9357 7.62672 13.7512 7.4766 14.5559C7.40064 14.9631 7.66916 15.3548 8.07634 15.4308C8.48353 15.5067 8.8752 15.2382 8.95116 14.831C9.0507 14.2974 9.33974 13.6805 9.72756 13.1299C9.99274 12.7535 10.2755 12.4484 10.5286 12.2379C10.6156 12.1656 10.6592 12.1294 10.7767 12.0747C10.8942 12.02 10.9653 12.0072 11.1074 11.9817C11.9593 11.8286 12.7692 11.8715 13.5198 12.0602C13.6644 12.0966 13.7367 12.1147 13.7763 12.0789C13.8159 12.0432 13.8053 11.9704 13.7841 11.8248C13.7289 11.4458 13.6909 11.0636 13.6531 10.6817C13.626 10.409 13.599 10.1364 13.5656 9.86524C13.5521 9.75498 13.5275 9.58286 13.5086 9.45334C13.4917 9.33746 13.4511 9.22589 13.3827 9.13085C13.2693 8.97333 13.1507 8.81947 13.0322 8.6657C13.0045 8.62974 12.9768 8.59379 12.9492 8.5578C12.877 8.46331 12.8062 8.36863 12.7357 8.27426C12.5251 7.9923 12.3165 7.7131 12.0765 7.45006C12.0344 7.40383 11.9726 7.38127 11.9104 7.38829C11.6833 7.41395 11.119 7.47937 10.8177 7.52912C10.2109 7.62929 9.32812 7.58686 8.55022 7.34492C7.81147 7.11515 6.86667 6.60136 6.68264 5.54605C6.59164 5.02417 6.73496 4.58765 6.98353 4.19587C7.19707 3.8593 7.52123 3.50757 7.86706 3.13234C7.95848 3.03313 8.04835 2.93256 8.13781 2.83158C8.44695 2.48267 8.81614 2.06598 9.22103 1.7795Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,9 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00439 7.87503C9.41861 7.87503 9.75439 7.53924 9.75439 7.12503L9.75439 4.875L10.1913 4.87501C10.323 4.87511 10.4848 4.87522 10.6171 4.85867L10.6196 4.85836C10.7144 4.84653 11.1465 4.79266 11.3522 4.36846C11.5585 3.94334 11.332 3.56824 11.283 3.48708L11.0287 3.13571C10.8077 2.85384 10.5034 2.468 10.2181 2.17474C10.0757 2.02841 9.91277 1.87749 9.73959 1.75835C9.58565 1.65245 9.323 1.5 9 1.5C8.677 1.5 8.41436 1.65245 8.26041 1.75835C8.08723 1.87749 7.92428 2.02841 7.78191 2.17474C7.49658 2.468 7.19231 2.85385 6.97134 3.13572L6.71698 3.48708C6.66799 3.56823 6.44154 3.94334 6.64776 4.36846C6.85353 4.79265 7.28556 4.84653 7.38039 4.85836L7.38291 4.85867C7.51523 4.87522 7.67697 4.87511 7.8087 4.87501L8.25439 4.875L8.25439 7.12503C8.25439 7.53924 8.59018 7.87503 9.00439 7.87503Z" fill="#2E3039"/>
<path d="M14.3028 8.5079C14.5957 8.215 14.5957 7.74013 14.3028 7.44724C14.0099 7.15434 13.535 7.15434 13.2421 7.44724L12.4466 8.24273C12.1537 8.53563 12.1537 9.0105 12.4466 9.30339C12.7395 9.59629 13.2144 9.59629 13.5073 9.30339L14.3028 8.5079Z" fill="#2E3039"/>
<path d="M4.75778 7.4477C4.46489 7.15481 3.99002 7.15481 3.69712 7.4477C3.40423 7.74059 3.40423 8.21547 3.69712 8.50836L4.49262 9.30386C4.78551 9.59675 5.26039 9.59675 5.55328 9.30386C5.84617 9.01096 5.84617 8.53609 5.55328 8.2432L4.75778 7.4477Z" fill="#2E3039"/>
<path d="M2.25 12.0006C1.83579 12.0006 1.5 12.3364 1.5 12.7506C1.5 13.1648 1.83579 13.5006 2.25 13.5006H3.375C3.78921 13.5006 4.125 13.1648 4.125 12.7506C4.125 12.3364 3.78921 12.0006 3.375 12.0006H2.25Z" fill="#2E3039"/>
<path d="M14.625 12.0006C14.2108 12.0006 13.875 12.3364 13.875 12.7506C13.875 13.1648 14.2108 13.5006 14.625 13.5006H15.75C16.1642 13.5006 16.5 13.1648 16.5 12.7506C16.5 12.3364 16.1642 12.0006 15.75 12.0006H14.625Z" fill="#2E3039"/>
<path d="M16.5 15.75C16.5 16.1642 16.1642 16.5 15.75 16.5L2.25 16.5C1.83579 16.5 1.5 16.1642 1.5 15.75C1.5 15.3358 1.83579 15 2.25 15L15.75 15C16.1642 15 16.5 15.3358 16.5 15.75Z" fill="#2E3039"/>
<path d="M9 9.00003C6.92893 9.00003 5.25 10.679 5.25 12.75C5.25 13.1642 5.58579 13.5 6 13.5H12C12.4142 13.5 12.75 13.1642 12.75 12.75C12.75 10.679 11.0711 9.00003 9 9.00003Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 11.75C7.30964 11.75 6.75 12.3096 6.75 13C6.75 13.6904 7.30964 14.25 8 14.25H8.00897C8.69933 14.25 9.25897 13.6904 9.25897 13C9.25897 12.3096 8.69933 11.75 8.00897 11.75H8Z" fill="white"/>
<path d="M11.9955 11.75C11.3052 11.75 10.7455 12.3096 10.7455 13C10.7455 13.6904 11.3052 14.25 11.9955 14.25H12.0045C12.6948 14.25 13.2545 13.6904 13.2545 13C13.2545 12.3096 12.6948 11.75 12.0045 11.75H11.9955Z" fill="white"/>
<path d="M15.991 11.75C15.3007 11.75 14.741 12.3096 14.741 13C14.741 13.6904 15.3007 14.25 15.991 14.25H16C16.6904 14.25 17.25 13.6904 17.25 13C17.25 12.3096 16.6904 11.75 16 11.75H15.991Z" fill="white"/>
<path d="M8 15.75C7.30964 15.75 6.75 16.3096 6.75 17C6.75 17.6904 7.30964 18.25 8 18.25H8.00897C8.69933 18.25 9.25897 17.6904 9.25897 17C9.25897 16.3096 8.69933 15.75 8.00897 15.75H8Z" fill="white"/>
<path d="M11.9955 15.75C11.3052 15.75 10.7455 16.3096 10.7455 17C10.7455 17.6904 11.3052 18.25 11.9955 18.25H12.0045C12.6948 18.25 13.2545 17.6904 13.2545 17C13.2545 16.3096 12.6948 15.75 12.0045 15.75H11.9955Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 2C7 1.44772 6.55229 1 6 1C5.44772 1 5 1.44772 5 2V2.44885C4.23882 2.6903 3.57734 3.06994 3.01802 3.6746C2.18949 4.57031 1.83279 5.69272 1.66416 7.04866C1.49997 8.36894 1.49998 10.0541 1.5 12.1739L1.5 12.8261C1.49998 14.9459 1.49997 16.6311 1.66416 17.9513C1.83279 19.3073 2.18949 20.4297 3.01802 21.3254C3.8568 22.2322 4.92535 22.6329 6.21533 22.8204C7.45141 23.0001 9.02291 23 10.9712 23H13.0288C14.977 23 16.5486 23.0001 17.7847 22.8204C19.0747 22.6329 20.1432 22.2322 20.982 21.3254C21.8105 20.4297 22.1672 19.3073 22.3358 17.9513C22.5 16.6311 22.5 14.9459 22.5 12.8261V12.1739C22.5 10.0541 22.5 8.36895 22.3358 7.04866C22.1672 5.69272 21.8105 4.57031 20.982 3.6746C20.4227 3.06993 19.7612 2.6903 19 2.44885V2C19 1.44772 18.5523 1 18 1C17.4477 1 17 1.44772 17 2V2.09173C15.903 1.99995 14.5863 1.99998 13.0288 2L10.9712 2C9.41376 1.99998 8.09704 1.99995 7 2.09173V2ZM4.49783 9C4.03921 9 3.8099 9 3.66385 9.14417C3.51781 9.28833 3.51487 9.51472 3.509 9.96751C3.50027 10.6407 3.5 11.3942 3.5 12.2432V12.7568C3.5 14.9616 3.50182 16.5221 3.64887 17.7045C3.79327 18.8656 4.06263 19.5094 4.48622 19.9673C4.89956 20.4142 5.4647 20.6903 6.503 20.8412C7.57858 20.9975 9.00425 21 11.05 21H12.95C14.9957 21 16.4214 20.9975 17.497 20.8412C18.5353 20.6903 19.1004 20.4142 19.5138 19.9673C19.9374 19.5094 20.2067 18.8656 20.3511 17.7045C20.4982 16.5221 20.5 14.9616 20.5 12.7568V12.2432C20.5 11.3942 20.4997 10.6407 20.491 9.96751C20.4851 9.51472 20.4822 9.28833 20.3362 9.14417C20.1901 9 19.9608 9 19.5022 9L4.49783 9Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 11.75C7.30964 11.75 6.75 12.3096 6.75 13C6.75 13.6904 7.30964 14.25 8 14.25H8.00897C8.69933 14.25 9.25897 13.6904 9.25897 13C9.25897 12.3096 8.69933 11.75 8.00897 11.75H8Z" fill="white"/>
<path d="M11.9955 11.75C11.3052 11.75 10.7455 12.3096 10.7455 13C10.7455 13.6904 11.3052 14.25 11.9955 14.25H12.0045C12.6948 14.25 13.2545 13.6904 13.2545 13C13.2545 12.3096 12.6948 11.75 12.0045 11.75H11.9955Z" fill="white"/>
<path d="M15.991 11.75C15.3007 11.75 14.741 12.3096 14.741 13C14.741 13.6904 15.3007 14.25 15.991 14.25H16C16.6904 14.25 17.25 13.6904 17.25 13C17.25 12.3096 16.6904 11.75 16 11.75H15.991Z" fill="white"/>
<path d="M8 15.75C7.30964 15.75 6.75 16.3096 6.75 17C6.75 17.6904 7.30964 18.25 8 18.25H8.00897C8.69933 18.25 9.25897 17.6904 9.25897 17C9.25897 16.3096 8.69933 15.75 8.00897 15.75H8Z" fill="white"/>
<path d="M11.9955 15.75C11.3052 15.75 10.7455 16.3096 10.7455 17C10.7455 17.6904 11.3052 18.25 11.9955 18.25H12.0045C12.6948 18.25 13.2545 17.6904 13.2545 17C13.2545 16.3096 12.6948 15.75 12.0045 15.75H11.9955Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 2C7 1.44772 6.55229 1 6 1C5.44772 1 5 1.44772 5 2V2.44885C4.23882 2.6903 3.57734 3.06994 3.01802 3.6746C2.18949 4.57031 1.83279 5.69272 1.66416 7.04866C1.49997 8.36894 1.49998 10.0541 1.5 12.1739L1.5 12.8261C1.49998 14.9459 1.49997 16.6311 1.66416 17.9513C1.83279 19.3073 2.18949 20.4297 3.01802 21.3254C3.8568 22.2322 4.92535 22.6329 6.21533 22.8204C7.45141 23.0001 9.02291 23 10.9712 23H13.0288C14.977 23 16.5486 23.0001 17.7847 22.8204C19.0747 22.6329 20.1432 22.2322 20.982 21.3254C21.8105 20.4297 22.1672 19.3073 22.3358 17.9513C22.5 16.6311 22.5 14.9459 22.5 12.8261V12.1739C22.5 10.0541 22.5 8.36895 22.3358 7.04866C22.1672 5.69272 21.8105 4.57031 20.982 3.6746C20.4227 3.06993 19.7612 2.6903 19 2.44885V2C19 1.44772 18.5523 1 18 1C17.4477 1 17 1.44772 17 2V2.09173C15.903 1.99995 14.5863 1.99998 13.0288 2L10.9712 2C9.41376 1.99998 8.09704 1.99995 7 2.09173V2ZM4.49783 9C4.03921 9 3.8099 9 3.66385 9.14417C3.51781 9.28833 3.51487 9.51472 3.509 9.96751C3.50027 10.6407 3.5 11.3942 3.5 12.2432V12.7568C3.5 14.9616 3.50182 16.5221 3.64887 17.7045C3.79327 18.8656 4.06263 19.5094 4.48622 19.9673C4.89956 20.4142 5.4647 20.6903 6.503 20.8412C7.57858 20.9975 9.00425 21 11.05 21H12.95C14.9957 21 16.4214 20.9975 17.497 20.8412C18.5353 20.6903 19.1004 20.4142 19.5138 19.9673C19.9374 19.5094 20.2067 18.8656 20.3511 17.7045C20.4982 16.5221 20.5 14.9616 20.5 12.7568V12.2432C20.5 11.3942 20.4997 10.6407 20.491 9.96751C20.4851 9.51472 20.4822 9.28833 20.3362 9.14417C20.1901 9 19.9608 9 19.5022 9L4.49783 9Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -1186,6 +1186,14 @@
"carbsProteinFat": "الكربوهيدرات\nالبروتين والدهون",
"ovulationPeriod": "فترة\nالإباضة",
"deliveryDueDate": "تاريخ الولادة\nالمتوقع",
"bmiCalculatorDesc": "احسب مؤشر كتلة جسمك لفهم نطاق وزنك الصحي.",
"caloriesCalculatorDesc": "اكتشف عدد السعرات الحرارية التي تحتاجها يومياً للحفاظ على الوزن أو فقدانه.",
"bmrCalculatorDesc": "حدد معدل الأيض الأساسي ومتطلباتك اليومية من الطاقة.",
"idealBodyWeightDesc": "اكتشف وزنك المثالي بناءً على الطول والعمر وبنية الجسم.",
"bodyFatCalculatorDesc": "قدّر نسبة دهون جسمك لتتبع أهداف اللياقة والصحة.",
"carbsProteinFatDesc": "وازن بين العناصر الغذائية مع النسب الصحيحة من الكربوهيدرات والبروتين والدهون.",
"ovulationPeriodDesc": "تتبع فترة خصوبتك وخطط للحمل بدقة.",
"deliveryDueDateDesc": "احسب تاريخ ولادتك المتوقع وراقب مراحل الحمل.",
"low": "منخفض",
"preDiabetic": "ما قبل السكري",
"high": "مرتفع",

@ -1169,14 +1169,22 @@
"monitorCholesterolLevels": "Monitor cholesterol levels, assess heart health risks, and take proactive steps for well-being.",
"triglyceridesFatBlood": "Triglycerides Fat Blood",
"understandTriglyceridesImpact": "Understand triglycerides' impact on heart health with personalized insights and expert recommendations.",
"bmiCalculator": "BMI\nCalculator",
"caloriesCalculator": "Calories\nCalculator",
"bmrCalculator": "BMR\nCalculator",
"idealBodyWeight": "Ideal Body\nWeight",
"bodyFatCalculator": "Body Fat\nCalculator",
"carbsProteinFat": "Carbs\nProtein & Fat",
"ovulationPeriod": "Ovulation\nPeriod",
"deliveryDueDate": "Delivery\nDue Date",
"bmiCalculator": "BMI Calculator",
"bmiCalculatorDesc": "Calculate your body mass index to understand your healthy weight range.",
"caloriesCalculator": "Calories Calculator",
"caloriesCalculatorDesc": "Find out how many calories you need daily to maintain or lose weight.",
"bmrCalculator": "BMR Calculator",
"bmrCalculatorDesc": "Determine your basal metabolic rate and daily energy requirements.",
"idealBodyWeight": "Ideal Body Weight",
"idealBodyWeightDesc": "Discover your ideal weight based on height, age, and body structure.",
"bodyFatCalculator": "Body Fat Calculator",
"bodyFatCalculatorDesc": "Estimate your body fat percentage to track fitness and health goals.",
"carbsProteinFat": "Carbs Protein & Fat",
"carbsProteinFatDesc": "Balance your macros with the right carbs, protein, and fat ratios.",
"ovulationPeriod": "Ovulation Period",
"ovulationPeriodDesc": "Track your fertile window and plan for conception with accuracy.",
"deliveryDueDate": "Delivery Due Date",
"deliveryDueDateDesc": "Calculate your expected delivery date and monitor pregnancy milestones.",
"low": "Low",
"preDiabetic": "Pre-diabetic",
"high": "High",

@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.preProd;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT

@ -363,6 +363,14 @@ class AppAssets {
static const String files = '$svgBasePath/files.svg';
static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg';
//Health Calculators
static const String bodyfatcalc = '$svgBasePath/bfc.svg';
static const String duedatecalc = '$svgBasePath/ddc.svg';
static const String idealbodyweightcacl = '$svgBasePath/ibwc.svg';
static const String ovluationperiod = '$svgBasePath/op.svg';
static const String bmicalc = '$svgBasePath/bmi1.svg';
static const String bmrcalc = '$svgBasePath/bmr1.svg';
// PNGS //
static const String hmgLogo = '$pngBasePath/hmg_logo.png';
static const String liveCareService = '$pngBasePath/livecare_service.png';

@ -64,6 +64,8 @@ import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_repo.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
@ -164,6 +166,7 @@ class AppDependencies {
getIt.registerLazySingleton<SymptomsCheckerRepo>(() => SymptomsCheckerRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<BloodDonationRepo>(() => BloodDonationRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<WaterMonitorRepo>(() => WaterMonitorRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<WeatherRepo>(() => WeatherRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<MyInvoicesRepo>(() => MyInvoicesRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<HealthTrackersRepo>(() => HealthTrackersRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<MonthlyReportRepo>(() => MonthlyReportRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
@ -299,6 +302,8 @@ class AppDependencies {
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<WeatherMonitorViewModel>(() => WeatherMonitorViewModel(weatherRepo: getIt(), errorHandlerService: getIt(), locationUtils: getIt()));
//commenting this because its already define there was on run time error because of this.
getIt.registerLazySingleton<MyInvoicesViewModel>(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));

@ -52,13 +52,7 @@ class CalenderUtilsNew {
});
}
FutureOr<bool> createOrUpdateEvent(
{required String title,
required String description,
required String location,
DateTime? scheduleDateTime,
String? eventId,
int? reminderMinutes}) async {
FutureOr<bool> createOrUpdateEvent({required String title, required String description, required String location, DateTime? scheduleDateTime, String? eventId, int? reminderMinutes}) async {
if (writableCalender.isEmpty) {
await getCalenders();
}
@ -93,19 +87,19 @@ class CalenderUtilsNew {
// return eventId.isNotEmpty;
}
FutureOr<bool> createMultipleEvents(
{required int reminderMinutes,
int? frequencyNumber,
required int days,
required String orderDate,
required String itemDescriptionN,
required String route,
Function(String)? onFailure,
String? prescriptionNumber,
DateTime? scheduleDateTime,
}) async {
FutureOr<bool> createMultipleEvents({
required int reminderMinutes,
int? frequencyNumber,
required int days,
required String orderDate,
required String itemDescriptionN,
required String route,
Function(String)? onFailure,
String? prescriptionNumber,
DateTime? scheduleDateTime,
}) async {
DateTime currentDay = DateTime.now();
DateTime actualDate = scheduleDateTime??DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0);
DateTime actualDate = scheduleDateTime ?? DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0);
print("the frequency is $frequencyNumber");
frequencyNumber ??= 2; //Some time frequency number is null so by default will be 2
int interval = calculateIntervalAsPerFrequency(frequencyNumber);

@ -322,8 +322,10 @@ extension EmailValidator on String {
Widget toText32({bool isEnglishOnly = false, FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal),
height: getIt.get<AppState>().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,),
);
Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text(

@ -41,6 +41,7 @@ class MedicalFileViewModel extends ChangeNotifier {
List<SickLeaveList> patientSickLeavesViewList = [];
bool isSickLeavesSortByClinic = true;
// bool isSickLeavesDataNeedsReloading = true;
List<GetAllergiesResponseModel> patientAllergiesList = [];
@ -165,13 +166,13 @@ class MedicalFileViewModel extends ChangeNotifier {
setIsPatientSickLeaveListLoading(bool val) {
// if (val && isSickLeavesDataNeedsReloading) {
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
patientSickLeavesByHospital.clear();
patientSickLeavesViewList.clear();
patientSickLeavePDFBase64 = "";
isSickLeavesSortByClinic = true;
isPatientSickLeaveListLoading = val;
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
patientSickLeavesByHospital.clear();
patientSickLeavesViewList.clear();
patientSickLeavePDFBase64 = "";
isSickLeavesSortByClinic = true;
isPatientSickLeaveListLoading = val;
// }
notifyListeners();
}
@ -456,6 +457,7 @@ class MedicalFileViewModel extends ChangeNotifier {
patientIdenficationNumber: currentUser.patientIdentificationNo,
emaiLAddress: currentUser.emailAddress,
genderDescription: currentUser.genderDescription,
patientImageData: _appState.getProfileImageData, // Set main user's profile image
);
// Clear and start fresh with current user
@ -499,6 +501,7 @@ class MedicalFileViewModel extends ChangeNotifier {
genderDescription: element.genderDescription,
genderImage: element.genderImage,
emaiLAddress: element.emaiLAddress,
patientImageData: element.patientImageData, // Include profile image data
);
if (isPending) {
@ -774,4 +777,41 @@ class MedicalFileViewModel extends ChangeNotifier {
},
);
}
/// Update family member's profile image in cached family files
void updateFamilyMemberProfileImage(int patientID, String imageData) {
try {
bool updated = false;
// Update in patientFamilyFiles list
for (var i = 0; i < patientFamilyFiles.length; i++) {
if (patientFamilyFiles[i].patientId == patientID ||
patientFamilyFiles[i].responseId == patientID) {
patientFamilyFiles[i].patientImageData = imageData;
updated = true;
print("✅ Updated profile image for family member in patientFamilyFiles (index: $i, patientID: $patientID)");
break;
}
}
// Update in pendingFamilyFiles list if exists
for (var i = 0; i < pendingFamilyFiles.length; i++) {
if (pendingFamilyFiles[i].patientId == patientID ||
pendingFamilyFiles[i].responseId == patientID) {
pendingFamilyFiles[i].patientImageData = imageData;
print("✅ Updated profile image for pending family member (index: $i, patientID: $patientID)");
break;
}
}
if (updated) {
// Notify listeners to update UI
notifyListeners();
} else {
print("⚠️ Family member not found in cache for patientID: $patientID");
}
} catch (e) {
print("❌ Error updating family member profile image in cache: $e");
}
}
}

@ -25,6 +25,7 @@ class FamilyFileResponseModelLists {
String? statusDescription;
bool? isSuperUser = false;
bool? isRequestFromMySide;
String? patientImageData;
FamilyFileResponseModelLists(
{this.id,
@ -50,7 +51,8 @@ class FamilyFileResponseModelLists {
this.patientName,
this.statusDescription,
this.isSuperUser,
this.isRequestFromMySide});
this.isRequestFromMySide,
this.patientImageData});
factory FamilyFileResponseModelLists.fromRawJson(String str) => FamilyFileResponseModelLists.fromJson(json.decode(str));
@ -81,6 +83,7 @@ class FamilyFileResponseModelLists {
statusDescription: json["StatusDescription"],
isSuperUser: json["isSuperUser"] ?? false,
isRequestFromMySide: json["isRequestFromMySide"] ?? false,
patientImageData: json["PatientImageData"],
);
Map<String, dynamic> toJson() => {
@ -108,5 +111,6 @@ class FamilyFileResponseModelLists {
"StatusDescription": statusDescription,
"isSuperUser": isSuperUser,
"isRequestFromMySide": isRequestFromMySide,
"PatientImageData": patientImageData,
};
}

@ -0,0 +1,50 @@
/// Enum to define different types of reminders
enum ReminderType {
/// Reminder for appointment schedule
appointment,
/// Reminder for payment due
payment,
/// Reminder for check-in
checkIn,
}
extension ReminderTypeExtension on ReminderType {
/// Get display name for the reminder type
String get displayName {
switch (this) {
case ReminderType.appointment:
return 'Appointment Reminder';
case ReminderType.payment:
return 'Payment Reminder';
case ReminderType.checkIn:
return 'Check-In Reminder';
}
}
/// Get description for the reminder type
String get description {
switch (this) {
case ReminderType.appointment:
return 'Appointment scheduled reminder';
case ReminderType.payment:
return 'Payment due reminder';
case ReminderType.checkIn:
return 'Check-in reminder';
}
}
/// Get event ID prefix for calendar events
String get eventIdPrefix {
switch (this) {
case ReminderType.appointment:
return 'APPT';
case ReminderType.payment:
return 'PAY';
case ReminderType.checkIn:
return 'CHKIN';
}
}
}

@ -2,11 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/reminder_type.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_rated_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart';
@ -19,6 +21,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
@ -1109,4 +1112,409 @@ class MyAppointmentsViewModel extends ChangeNotifier {
},
);
}
/// Creates a reminder for the appointment
/// Shows a bottom sheet to select reminder time and creates calendar event
/// [reminderType] specifies the type of reminder (appointment, payment, or check-in)
/// For payment and check-in reminders, shows only 1, 2, 3 hours before appointment
/// Automatically removes previous reminders when creating a new one
Future<void> createReminder({
required BuildContext context,
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
ReminderType reminderType = ReminderType.appointment,
Function(bool)? onSuccess,
Function(String)? onError,
}) async {
try {
CalenderUtilsNew calender = CalenderUtilsNew.instance;
DateTime endDate = DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate);
// Generate title and description based on reminder type
String title;
String description;
String eventIdPrefix = reminderType.eventIdPrefix;
switch (reminderType) {
case ReminderType.appointment:
title = "Appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj}";
description = "${patientAppointmentHistoryResponseModel.doctorNameObj} will be having an appointment on ${patientAppointmentHistoryResponseModel.appointmentDate}";
break;
case ReminderType.payment:
title = "Payment Due for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
description = "Payment reminder for appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.checkIn:
title = "Check-In Reminder for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
description = "Don't forget to check in for your appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj}";
break;
}
// For payment and check-in, remove existing reminders first
if (reminderType == ReminderType.payment || reminderType == ReminderType.checkIn) {
// Remove all existing reminder types
await _removeAllRemindersForAppointment(patientAppointmentHistoryResponseModel);
}
// For payment and check-in reminders, use custom hour-based options
if (reminderType == ReminderType.payment || reminderType == ReminderType.checkIn) {
_showHourBasedReminderSheet(
context: context,
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
title: title,
description: description,
eventIdPrefix: eventIdPrefix,
endDate: endDate,
onSuccess: onSuccess,
onError: onError,
);
} else {
// For appointment reminders, use the standard reminder sheet with all options
BottomSheetUtils().showReminderBottomSheet(
context,
endDate,
patientAppointmentHistoryResponseModel.doctorNameObj ?? "",
"${patientAppointmentHistoryResponseModel.appointmentNo}",
"",
"",
title: title,
description: description,
onSuccess: () {
setAppointmentReminder(true, patientAppointmentHistoryResponseModel);
if (onSuccess != null) {
onSuccess(true);
}
},
isMultiAllowed: true,
onMultiDateSuccess: (int selectedIndex) async {
String eventTitle;
String eventDescription;
switch (reminderType) {
case ReminderType.appointment:
eventTitle = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventDescription = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} in ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.payment:
eventTitle = "Payment Due Reminder for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo} with ${patientAppointmentHistoryResponseModel.doctorNameObj}";
eventDescription = "Payment reminder for ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.checkIn:
eventTitle = "Check-In Reminder for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo} with ${patientAppointmentHistoryResponseModel.doctorNameObj}";
eventDescription = "Check-in reminder for appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}";
break;
}
bool isEventAddedOrRemoved = await calender.createOrUpdateEvent(
title: eventTitle,
description: eventDescription,
scheduleDateTime: DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate),
eventId: "${eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}",
location: patientAppointmentHistoryResponseModel.projectName ?? '',
reminderMinutes: selectedIndex,
);
setAppointmentReminder(isEventAddedOrRemoved, patientAppointmentHistoryResponseModel);
if (onSuccess != null) {
onSuccess(isEventAddedOrRemoved);
}
},
);
}
} catch (e) {
if (onError != null) {
onError(e.toString());
}
}
}
/// Removes all reminder types for an appointment (internal helper)
Future<void> _removeAllRemindersForAppointment(PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel) async {
CalenderUtilsNew calender = CalenderUtilsNew.instance;
// Remove all possible reminder types
for (var type in ReminderType.values) {
try {
await calender.checkAndRemove(
id: "${type.eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}",
);
} catch (e) {
// Ignore errors if reminder doesn't exist
debugPrint("No existing ${type.displayName} to remove");
}
}
}
/// Shows hour-based reminder selection for payment and check-in reminders
void _showHourBasedReminderSheet({
required BuildContext context,
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
required ReminderType reminderType,
required String title,
required String description,
required String eventIdPrefix,
required DateTime endDate,
Function(bool)? onSuccess,
Function(String)? onError,
}) {
showCommonBottomSheetWithoutHeight(
context,
title: "Select Reminder Time",
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
"When would you like to be reminded?",
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 24.h),
_buildHourOption(
context: context,
hours: 1,
icon: Icons.alarm,
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
title: title,
description: description,
eventIdPrefix: eventIdPrefix,
endDate: endDate,
onSuccess: onSuccess,
onError: onError,
),
SizedBox(height: 12.h),
_buildHourOption(
context: context,
hours: 2,
icon: Icons.alarm,
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
title: title,
description: description,
eventIdPrefix: eventIdPrefix,
endDate: endDate,
onSuccess: onSuccess,
onError: onError,
),
SizedBox(height: 12.h),
_buildHourOption(
context: context,
hours: 3,
icon: Icons.alarm,
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
title: title,
description: description,
eventIdPrefix: eventIdPrefix,
endDate: endDate,
onSuccess: onSuccess,
onError: onError,
),
],
),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
/// Builds an hour option button for payment/check-in reminders
Widget _buildHourOption({
required BuildContext context,
required int hours,
required IconData icon,
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
required ReminderType reminderType,
required String title,
required String description,
required String eventIdPrefix,
required DateTime endDate,
Function(bool)? onSuccess,
Function(String)? onError,
}) {
return GestureDetector(
onTap: () async {
Navigator.of(context).pop(); // Close the selection sheet
// Create reminder with selected hours (converted to minutes)
await _createReminderWithHours(
hours: hours,
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
title: title,
description: description,
eventIdPrefix: eventIdPrefix,
endDate: endDate,
onSuccess: onSuccess,
onError: onError,
);
},
child: Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
border: Border.all(color: AppColors.borderOnlyColor),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: AppColors.primaryRedColor.withAlpha(26),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
icon,
color: AppColors.primaryRedColor,
size: 24.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: Text(
"$hours ${hours == 1 ? 'Hour' : 'Hours'} Before Appointment",
style: TextStyle(
fontSize: 16.f,
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.h,
color: AppColors.greyTextColor,
),
],
),
),
);
}
/// Creates reminder with specific hours before appointment
Future<void> _createReminderWithHours({
required int hours,
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
required ReminderType reminderType,
required String title,
required String description,
required String eventIdPrefix,
required DateTime endDate,
Function(bool)? onSuccess,
Function(String)? onError,
}) async {
try {
CalenderUtilsNew calender = CalenderUtilsNew.instance;
// Convert hours to minutes
int reminderMinutes = hours * 60;
String eventTitle;
String eventDescription;
switch (reminderType) {
case ReminderType.appointment:
eventTitle = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventDescription = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} in ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.payment:
eventTitle = "Payment Due - $hours ${hours == 1 ? 'hour' : 'hours'} before Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventDescription = "Payment reminder for ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.checkIn:
eventTitle = "Check-In Now - $hours ${hours == 1 ? 'hour' : 'hours'} before Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventDescription = "Check-in reminder for appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}";
break;
}
bool isEventAddedOrRemoved = await calender.createOrUpdateEvent(
title: eventTitle,
description: eventDescription,
scheduleDateTime: DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate),
eventId: "${eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}",
location: patientAppointmentHistoryResponseModel.projectName ?? '',
reminderMinutes: reminderMinutes,
);
setAppointmentReminder(isEventAddedOrRemoved, patientAppointmentHistoryResponseModel);
if (onSuccess != null) {
onSuccess(isEventAddedOrRemoved);
}
} catch (e) {
if (onError != null) {
onError(e.toString());
}
}
}
/// Deletes the reminder for the appointment
/// Removes the calendar event associated with the appointment
/// [reminderType] specifies the type of reminder to delete (appointment, payment, or check-in)
Future<void> deleteReminder({
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
ReminderType reminderType = ReminderType.appointment,
Function(bool)? onSuccess,
Function(String)? onError,
}) async {
try {
CalenderUtilsNew calender = CalenderUtilsNew.instance;
String eventIdPrefix = reminderType.eventIdPrefix;
bool isEventRemoved = await calender.checkAndRemove(
id: "${eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}",
);
// Update the reminder status (inverted because we're removing)
setAppointmentReminder(!isEventRemoved, patientAppointmentHistoryResponseModel);
if (onSuccess != null) {
onSuccess(isEventRemoved);
}
} catch (e) {
if (onError != null) {
onError(e.toString());
}
}
}
/// Toggle reminder for an appointment
/// Creates or deletes reminder based on the current state
/// [reminderType] specifies the type of reminder (appointment, payment, or check-in)
Future<void> toggleReminder({
required BuildContext context,
required bool shouldCreateReminder,
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
required ReminderType reminderType ,
Function(bool)? onSuccess,
Function(String)? onError,
}) async {
if (shouldCreateReminder) {
await createReminder(
context: context,
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
onSuccess: onSuccess,
onError: onError,
);
} else {
await deleteReminder(
patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel,
reminderType: reminderType,
onSuccess: onSuccess,
onError: onError,
);
}
}
}

@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/models/get_patient_info_response_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
@ -219,9 +220,10 @@ class ProfileSettingsViewModel extends ChangeNotifier {
if (response.data != null && response.data['Patient_GetProfileImageDataList'] != null) {
var imageList = response.data['Patient_GetProfileImageDataList'];
if (imageList is List && imageList.isNotEmpty) {
profileImageData = imageList[0]['PatientsProfileImageData'];
profileImageData = imageList[0]['ImageData'];
// Store in AppState for global access
GetIt.instance<AppState>().setProfileImageData = profileImageData;
} else {
profileImageData = null;
GetIt.instance<AppState>().setProfileImageData = null;
@ -286,6 +288,16 @@ class ProfileSettingsViewModel extends ChangeNotifier {
profileImageData = imageData;
// Store in AppState for global access
GetIt.instance<AppState>().setProfileImageData = imageData;
// Update the family files cache with the new profile image
try {
final medicalFileViewModel = GetIt.instance.get<MedicalFileViewModel>();
medicalFileViewModel.updateFamilyMemberProfileImage(patientID, imageData);
print("✅ Updated profile image in family files cache for patient: $patientID");
} catch (e) {
print("⚠️ Could not update family files cache: $e");
}
notifyListeners();
onSuccess?.call(response.data);
},

@ -0,0 +1,257 @@
import 'dart:convert';
class GetCityInfoList {
CategoriesNames? categoriesNames;
String? category;
String? categoryValue;
int? cityId;
String? cityName;
String? cityNameN;
ColorName? colorName;
String? createdOn;
String? id;
int? iniceId;
bool? isOrderEmpty;
bool? isValuesReversed;
bool? language;
double? latitude;
double? longitude;
String? name;
int? orderNum;
double? temperature;
String? value;
GetCityInfoList({
this.categoriesNames,
this.category,
this.categoryValue,
this.cityId,
this.cityName,
this.cityNameN,
this.colorName,
this.createdOn,
this.id,
this.iniceId,
this.isOrderEmpty,
this.isValuesReversed,
this.language,
this.latitude,
this.longitude,
this.name,
this.orderNum,
this.temperature,
this.value,
});
factory GetCityInfoList.fromRawJson(String str) => GetCityInfoList.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GetCityInfoList.fromJson(Map<String, dynamic> json) {
ColorName? parseColorName(dynamic value) {
if (value == null) return null;
try {
return colorNameValues.map[value.toString()];
} catch (e) {
print("Unknown color name: $value");
return null;
}
}
CategoriesNames? parseCategoriesNames(dynamic value) {
if (value == null) return null;
try {
return CategoriesNames.fromJson(value as Map<String, dynamic>);
} catch (e) {
print("Error parsing CategoriesNames: $e");
return null;
}
}
return GetCityInfoList(
categoriesNames: parseCategoriesNames(json["CategoriesNames"]),
category: json["Category"],
categoryValue: json["CategoryValue"],
cityId: json["CityID"],
cityName: json["CityName"],
cityNameN: json["CityNameN"],
colorName: parseColorName(json["ColorName"]),
createdOn: json["CreatedOn"],
id: json["ID"],
iniceId: json["IniceID"],
isOrderEmpty: json["IsOrderEmpty"],
isValuesReversed: json["IsValuesReversed"],
language: json["Language"],
latitude: json["Latitude"]?.toDouble(),
longitude: json["Longitude"]?.toDouble(),
name: json["Name"],
orderNum: json["OrderNum"],
temperature: json["Temperature"]?.toDouble(),
value: json["Value"],
);
}
Map<String, dynamic> toJson() => {
"CategoriesNames": categoriesNames?.toJson(),
"Category": category,
"CategoryValue": categoryValue,
"CityID": cityId,
"CityName": cityName,
"CityNameN": cityNameN,
"ColorName": colorNameValues.reverse[colorName],
"CreatedOn": createdOn,
"ID": id,
"IniceID": iniceId,
"IsOrderEmpty": isOrderEmpty,
"IsValuesReversed": isValuesReversed,
"Language": language,
"Latitude": latitude,
"Longitude": longitude,
"Name": name,
"OrderNum": orderNum,
"Temperature": temperature,
"Value": value,
};
}
class CategoriesNames {
Category1? category1;
Category2? category2;
Category3? category3;
Category4? category4;
Category5? category5;
CategoriesNames({
this.category1,
this.category2,
this.category3,
this.category4,
this.category5,
});
factory CategoriesNames.fromRawJson(String str) => CategoriesNames.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory CategoriesNames.fromJson(Map<String, dynamic> json) {
T? parseEnum<T>(dynamic value, EnumValues<T> enumValues, String enumName) {
if (value == null) return null;
try {
return enumValues.map[value.toString()];
} catch (e) {
print("Unknown $enumName value: $value");
return null;
}
}
return CategoriesNames(
category1: parseEnum(json["Category1"], category1Values, "Category1"),
category2: parseEnum(json["Category2"], category2Values, "Category2"),
category3: parseEnum(json["Category3"], category3Values, "Category3"),
category4: parseEnum(json["Category4"], category4Values, "Category4"),
category5: parseEnum(json["Category5"], category5Values, "Category5"),
);
}
Map<String, dynamic> toJson() => {
"Category1": category1Values.reverse[category1],
"Category2": category2Values.reverse[category2],
"Category3": category3Values.reverse[category3],
"Category4": category4Values.reverse[category4],
"Category5": category5Values.reverse[category5],
};
}
enum Category1 {
AT_EXTREME_RISK,
CATEGORY1_EXTREME,
EXTREME,
POOR
}
final category1Values = EnumValues({
"At Extreme Risk": Category1.AT_EXTREME_RISK,
"Extreme\u000d\n": Category1.CATEGORY1_EXTREME,
"Extreme": Category1.EXTREME,
"Poor": Category1.POOR
});
enum Category2 {
AT_HIGH_RISK,
CATEGORY2_VERY_HIGH,
FAIR,
VERY_HIGH
}
final category2Values = EnumValues({
"At High Risk": Category2.AT_HIGH_RISK,
"Very High\u000d\n": Category2.CATEGORY2_VERY_HIGH,
"Fair": Category2.FAIR,
"Very High": Category2.VERY_HIGH
});
enum Category3 {
AT_RISK,
CATEGORY3_HIGH,
GOOD,
HIGH
}
final category3Values = EnumValues({
"At Risk": Category3.AT_RISK,
"High\u000d\n": Category3.CATEGORY3_HIGH,
"Good": Category3.GOOD,
"High": Category3.HIGH
});
enum Category4 {
CATEGORY4_MODERATE,
MODERATE,
NEUTRAL,
VERY_GOOD
}
final category4Values = EnumValues({
"Moderate\u000d\n": Category4.CATEGORY4_MODERATE,
"Moderate": Category4.MODERATE,
"Neutral": Category4.NEUTRAL,
"Very Good": Category4.VERY_GOOD
});
enum Category5 {
BENEFICIAL,
CATEGORY5_LOW,
EXCELLENT,
LOW
}
final category5Values = EnumValues({
"Beneficial": Category5.BENEFICIAL,
"Low\u000d\n": Category5.CATEGORY5_LOW,
"Excellent": Category5.EXCELLENT,
"Low": Category5.LOW
});
enum ColorName {
GREEN,
ORANGE,
RED
}
final colorNameValues = EnumValues({
"green": ColorName.GREEN,
"orange": ColorName.ORANGE,
"red": ColorName.RED
});
class EnumValues<T> {
Map<String, T> map;
late Map<T, String> reverseMap;
EnumValues(this.map);
Map<T, String> get reverse {
reverseMap = map.map((k, v) => MapEntry(v, k));
return reverseMap;
}
}

@ -0,0 +1,72 @@
import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/core/location_util.dart';
import 'package:hmg_patient_app_new/features/weather/models/waether_cities_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class WeatherRepo {
Future<Either<Failure, GenericApiModel<List<GetCityInfoList>>>> getCityInfo();
}
class WeatherRepoImp implements WeatherRepo {
final ApiClient apiClient;
final LoggerService loggerService;
WeatherRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<List<GetCityInfoList>>>> getCityInfo() async {
Map<String, dynamic> request = {};
try {
GenericApiModel<List<GetCityInfoList>>? apiResponse;
Failure? failure;
await apiClient.post(
WEATHER_INDICATOR,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
loggerService.logInfo("Weather API Response: $response");
final list = response['GetCityInfo_List'];
if (list == null || list.isEmpty) {
loggerService.logInfo("Weather API returned empty or null list");
// Return empty list if no city info
apiResponse = GenericApiModel<List<GetCityInfoList>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: [],
);
return;
}
loggerService.logInfo("Parsing ${list.length} city info items");
final cityInfoList = list.map((item) => GetCityInfoList.fromJson(item as Map<String, dynamic>)).toList().cast<GetCityInfoList>();
apiResponse = GenericApiModel<List<GetCityInfoList>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: cityInfoList,
);
} catch (e) {
loggerService.logError("Error parsing weather data: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}

@ -0,0 +1,55 @@
import 'package:flutter/cupertino.dart';
import 'package:hmg_patient_app_new/core/location_util.dart';
import 'package:hmg_patient_app_new/features/weather/models/waether_cities_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class WeatherMonitorViewModel extends ChangeNotifier {
WeatherRepo weatherRepo;
ErrorHandlerService errorHandlerService;
LocationUtils locationUtils;
WeatherMonitorViewModel({required this.weatherRepo, required this.errorHandlerService, required this.locationUtils});
final List<GetCityInfoList> _cityInfoList = [];
bool isLoading = false;
List<GetCityInfoList> get cityInfoList => _cityInfoList;
Future<void> fetchCityInfoList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isLoading = true;
_cityInfoList.clear();
locationUtils.getLocation(
isShowConfirmDialog: true,
onSuccess: (position) async {
final result = await weatherRepo.getCityInfo();
result.fold(
(failure) async {
isLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isLoading = false;
if (apiResponse.messageStatus == 2) {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Unknown error");
}
} else if (apiResponse.messageStatus == 1) {
final cities = apiResponse.data ?? [];
_cityInfoList.addAll(cities);
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
});
}
}

@ -2893,14 +2893,14 @@ static const Map<String,dynamic> _en_US = {
"monitorCholesterolLevels": "Monitor cholesterol levels, assess heart health risks, and take proactive steps for well-being.",
"triglyceridesFatBlood": "Triglycerides Fat Blood",
"understandTriglyceridesImpact": "Understand triglycerides' impact on heart health with personalized insights and expert recommendations.",
"bmiCalculator": "BMI\nCalculator",
"caloriesCalculator": "Calories\nCalculator",
"bmrCalculator": "BMR\nCalculator",
"idealBodyWeight": "Ideal Body\nWeight",
"bodyFatCalculator": "Body Fat\nCalculator",
"carbsProteinFat": "Carbs\nProtein & Fat",
"ovulationPeriod": "Ovulation\nPeriod",
"deliveryDueDate": "Delivery\nDue Date",
"bmiCalculator": "BMI Calculator",
"caloriesCalculator": "Calories Calculator",
"bmrCalculator": "BMR Calculator",
"idealBodyWeight": "Ideal Body Weight",
"bodyFatCalculator": "Body Fat Calculator",
"carbsProteinFat": "Carbs Protein & Fat",
"ovulationPeriod": "Ovulation Period",
"deliveryDueDate": "Delivery Due Date",
"low": "Low",
"preDiabetic": "Pre-diabetic",
"high": "High",

@ -1177,13 +1177,21 @@ abstract class LocaleKeys {
static const triglyceridesFatBlood = 'triglyceridesFatBlood';
static const understandTriglyceridesImpact = 'understandTriglyceridesImpact';
static const bmiCalculator = 'bmiCalculator';
static const bmiCalculatorDesc = 'bmiCalculatorDesc';
static const caloriesCalculator = 'caloriesCalculator';
static const caloriesCalculatorDesc = 'caloriesCalculatorDesc';
static const bmrCalculator = 'bmrCalculator';
static const bmrCalculatorDesc = 'bmrCalculatorDesc';
static const idealBodyWeight = 'idealBodyWeight';
static const idealBodyWeightDesc = 'idealBodyWeightDesc';
static const bodyFatCalculator = 'bodyFatCalculator';
static const bodyFatCalculatorDesc = 'bodyFatCalculatorDesc';
static const carbsProteinFat = 'carbsProteinFat';
static const carbsProteinFatDesc = 'carbsProteinFatDesc';
static const ovulationPeriod = 'ovulationPeriod';
static const ovulationPeriodDesc = 'ovulationPeriodDesc';
static const deliveryDueDate = 'deliveryDueDate';
static const deliveryDueDateDesc = 'deliveryDueDateDesc';
static const low = 'low';
static const preDiabetic = 'preDiabetic';
static const high = 'high';

@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
@ -242,6 +243,9 @@ void main() async {
),
ChangeNotifierProvider<ServicesPriceListViewModel>(
create: (_) => getIt.get<ServicesPriceListViewModel>(),
) ,
ChangeNotifierProvider<WeatherMonitorViewModel>(
create: (_) => getIt.get<WeatherMonitorViewModel>(),
)
], child: MyApp()),
), // Wrap your app

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -33,13 +34,11 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointmen
import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_rating_widget.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart';
import 'package:hmg_patient_app_new/presentation/ask_doctor/ask_doctor_page.dart';
import 'package:hmg_patient_app_new/presentation/ask_doctor/doctor_response_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/bell_switch.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -47,6 +46,7 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:lottie/lottie.dart';
import 'package:map_launcher/map_launcher.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
@ -96,18 +96,54 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
} else {
isAppointmentWithin4Hours = true;
}
// Check if reminder exists in calendar and update the flag
await _checkAndUpdateReminderStatus();
}
});
// Initialize countdown timer for payment
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 ||
widget.patientAppointmentHistoryResponseModel.nextAction == 20) {
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20) {
_startCountdownTimer();
}
super.initState();
}
/// Checks if a reminder exists in the calendar for this appointment
/// and updates the hasReminder flag accordingly
Future<void> _checkAndUpdateReminderStatus() async {
try {
CalenderUtilsNew calender = CalenderUtilsNew.instance;
// Check if reminder exists for this appointment
bool reminderExists = await calender.checkIfEventExist(
widget.patientAppointmentHistoryResponseModel.appointmentNo.toString() ?? "",
);
// Update the flag if it's different from current state
if (mounted && widget.patientAppointmentHistoryResponseModel.hasReminder != reminderExists) {
setState(() {
widget.patientAppointmentHistoryResponseModel.hasReminder = reminderExists;
});
// Also update in view model to keep data consistent
myAppointmentsViewModel.setAppointmentReminder(reminderExists, widget.patientAppointmentHistoryResponseModel);
}
debugPrint("Reminder status for appointment ${widget.patientAppointmentHistoryResponseModel.appointmentNo}: $reminderExists");
setState(() {});
} catch (e) {
debugPrint("Error checking reminder status: $e");
// If there's an error, default to false
if (mounted) {
setState(() {
widget.patientAppointmentHistoryResponseModel.hasReminder = false;
});
}
}
}
void _startCountdownTimer() {
final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate);
_updateTimeRemaining();
@ -307,11 +343,13 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
showCommonBottomSheetWithoutHeight(context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
title: "", isCloseButtonVisible: true, isDismissible: false, isFullScreen: false);
title: "",
isCloseButtonVisible: true,
isDismissible: false,
isFullScreen: false);
});
}),
callBackFunc: () {},
@ -326,7 +364,58 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
onRescheduleTap: () async {
openDoctorScheduleCalendar();
},
timerWidget: Column(
children: [
SizedBox(
height: 16.w,
),
Divider(
height: 1,
color: AppColors.dividerColor,
),
SizedBox(
height: 16.w,
),
Directionality(
textDirection: ui.TextDirection.ltr,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00',
LocaleKeys.days.tr(context: context),
),
),
_buildTimeSeparator(),
Expanded(
child: _buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00',
LocaleKeys.hours.tr(context: context),
),
),
_buildTimeSeparator(),
Expanded(
child: _buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.minutes.tr(context: context),
),
),
_buildTimeSeparator(),
Expanded(
child: _buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.seconds.tr(context: context),
),
),
],
),
),
],
),
),
SizedBox(height: 16.h),
!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? Column(
@ -470,22 +559,24 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
],
),
SizedBox(height: 8.h),
bookAppointmentsViewModel.appointmentNearestGateResponseModel != null ? Wrap(
direction: Axis.horizontal,
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: bookAppointmentsVM.isAppointmentNearestGateLoading
? "Floor: Ground Floor"
bookAppointmentsViewModel.appointmentNearestGateResponseModel != null
? Wrap(
direction: Axis.horizontal,
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: bookAppointmentsVM.isAppointmentNearestGateLoading
? "Floor: Ground Floor"
: "${LocaleKeys.floor.tr(context: context)}: ${getIt.get<AppState>().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}",
).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
AppCustomChipWidget(
labelText:
AppCustomChipWidget(
labelText:
"${LocaleKeys.nearestGate.tr(context: context)}: ${getIt.get<AppState>().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}")
.toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
],
) : SizedBox.shrink(),
],
)
: SizedBox.shrink(),
],
),
),
@ -511,22 +602,27 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
],
),
const Spacer(),
Switch(
activeThumbColor: AppColors.successColor,
// activeTrackColor: AppColors.successColor.withValues(alpha: .15),
value: widget.patientAppointmentHistoryResponseModel.hasReminder!,
BellAnimatedSwitch(
initialValue: widget.patientAppointmentHistoryResponseModel.hasReminder ?? false,
activeColor: AppColors.successColor.withOpacity(0.2),
inactiveColor: AppColors.lightGrayBGColor,
activeCircleColor: AppColors.successColor,
inactiveCircleColor: AppColors.greyTextColor,
activeIconColor: AppColors.bgGreenColor,
inactiveIconColor: AppColors.greyTextColor,
activeIcon: Utils.buildSvgWithAssets(icon: AppAssets.bell, iconColor: AppColors.whiteColor, width: 15.w, height: 15.h),
inactiveIcon: Utils.buildSvgWithAssets(icon: AppAssets.bell, iconColor: AppColors.whiteColor, width: 15.w, height: 15.h),
onChanged: (newValue) async {
CalenderUtilsNew calender = CalenderUtilsNew.instance;
CalenderUtilsNew calender = CalenderUtilsNew.instance;
bool isEventAddedOrRemoved = false;
if(newValue == true){
if (newValue == true) {
DateTime startDate = DateTime.now();
DateTime endDate = DateUtil.convertStringToDate(widget
.patientAppointmentHistoryResponseModel.appointmentDate);
DateTime endDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate);
BottomSheetUtils().showReminderBottomSheet(
context,
endDate,
widget.patientAppointmentHistoryResponseModel.doctorNameObj??"",
"${widget.patientAppointmentHistoryResponseModel.appointmentNo}"??"",
widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "",
"${widget.patientAppointmentHistoryResponseModel.appointmentNo}" ?? "",
"",
"",
title: "Appointment with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}",
@ -544,27 +640,80 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
"Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
description:
"Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} in ${widget.patientAppointmentHistoryResponseModel.projectName}",
scheduleDateTime: DateUtil.convertStringToDate(widget
.patientAppointmentHistoryResponseModel.appointmentDate),
eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
location: '',
reminderMinutes: selectedIndex
);
scheduleDateTime: DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate),
eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
location: '',
reminderMinutes: selectedIndex);
setState(() {
myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel);
});
},
);
}else {
isEventAddedOrRemoved = await calender.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", );
} else {
isEventAddedOrRemoved = await calender.checkAndRemove(
id: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
);
setState(() {
myAppointmentsViewModel.setAppointmentReminder(!isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel);
});
}
},
),
)
// Switch(
// activeThumbColor: AppColors.successColor,
// // activeTrackColor: AppColors.successColor.withValues(alpha: .15),
// value: widget.patientAppointmentHistoryResponseModel.hasReminder!,
// onChanged: (newValue) async {
// CalenderUtilsNew calender = CalenderUtilsNew.instance;
// bool isEventAddedOrRemoved = false;
// if(newValue == true){
// DateTime startDate = DateTime.now();
// DateTime endDate = DateUtil.convertStringToDate(widget
// .patientAppointmentHistoryResponseModel.appointmentDate);
// BottomSheetUtils().showReminderBottomSheet(
// context,
// endDate,
// widget.patientAppointmentHistoryResponseModel.doctorNameObj??"",
// "${widget.patientAppointmentHistoryResponseModel.appointmentNo}"??"",
// "",
// "",
// title: "Appointment with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}",
// description:
// "${widget.patientAppointmentHistoryResponseModel.doctorNameObj} will be having an appointment on ${widget.patientAppointmentHistoryResponseModel.appointmentDate}",
// onSuccess: () {
// setState(() {
// myAppointmentsViewModel.setAppointmentReminder(newValue, widget.patientAppointmentHistoryResponseModel);
// });
// },
// isMultiAllowed: true,
// onMultiDateSuccess: (int selectedIndex) async {
// isEventAddedOrRemoved = await calender.createOrUpdateEvent(
// title:
// "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
// description:
// "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} in ${widget.patientAppointmentHistoryResponseModel.projectName}",
// scheduleDateTime: DateUtil.convertStringToDate(widget
// .patientAppointmentHistoryResponseModel.appointmentDate),
// eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
// location: '',
// reminderMinutes: selectedIndex
// );
// setState(() {
// myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel);
// });
// },
// );
// }else {
// isEventAddedOrRemoved = await calender.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", );
// setState(() {
// myAppointmentsViewModel.setAppointmentReminder(!isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel);
// });
// }
//
//
// },
// ),
],
).paddingSymmetrical(16.h, 16.h),
),
@ -582,106 +731,116 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
),
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
MedicalFileCard(
label: LocaleKeys.labResults.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.lab_result_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingLabResults.tr(context: context));
await labViewModel.getLabResultsByAppointmentNo(
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID,
doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj!,
clinicName: widget.patientAppointmentHistoryResponseModel.clinicName!,
projectName: widget.patientAppointmentHistoryResponseModel.projectName!,
appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
}),
MedicalFileCard(
label: "${LocaleKeys.radiology.tr(context: context)} ${LocaleKeys.radiologySubtitle.tr(context: context)}",
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.radiology_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingRadiologyResults.tr(context: context));
await radiologyViewModel.getPatientRadiologyOrdersByAppointment(
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
}),
MedicalFileCard(
label: LocaleKeys.prescriptions.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.prescription_item_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingAppointmentPrescriptions.tr(context: context));
await prescriptionsViewModel.getPrescriptionDetails(
getPrescriptionRequestModel(),
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
if (val.data.isNotEmpty) {
PatientPrescriptionsResponseModel patientPrescriptionsResponseModel = PatientPrescriptionsResponseModel(
doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL,
doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj,
appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate,
clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName,
decimalDoctorRate: widget.patientAppointmentHistoryResponseModel.decimalDoctorRate,
name: widget.patientAppointmentHistoryResponseModel.projectName,
isHomeMedicineDeliverySupported: val.data.first.isHomeMedicineDeliverySupported ?? false,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID,
setupID: widget.patientAppointmentHistoryResponseModel.setupID,
isInOutPatient: widget.patientAppointmentHistoryResponseModel.isInOutPatient,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo,
episodeID: widget.patientAppointmentHistoryResponseModel.episodeID,
dischargeNo: 0,
shrinkWrap: true,
children: [
MedicalFileCard(
label: LocaleKeys.labResults.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.lab_result_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingLabResults.tr(context: context));
await labViewModel.getLabResultsByAppointmentNo(
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID,
doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj!,
clinicName: widget.patientAppointmentHistoryResponseModel.clinicName!,
projectName: widget.patientAppointmentHistoryResponseModel.projectName!,
appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
Navigator.of(context).push(
CustomPageRoute(
page: PrescriptionDetailPage(
isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel),
),
},
);
}),
MedicalFileCard(
label: "${LocaleKeys.radiology.tr(context: context)} ${LocaleKeys.radiologySubtitle.tr(context: context)}",
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.radiology_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingRadiologyResults.tr(context: context));
await radiologyViewModel.getPatientRadiologyOrdersByAppointment(
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else {
},
);
}),
MedicalFileCard(
label: LocaleKeys.prescriptions.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.prescription_item_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingAppointmentPrescriptions.tr(context: context));
await prescriptionsViewModel.getPrescriptionDetails(
getPrescriptionRequestModel(),
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
if (val.data.isNotEmpty) {
PatientPrescriptionsResponseModel patientPrescriptionsResponseModel = PatientPrescriptionsResponseModel(
doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL,
doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj,
appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate,
clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName,
decimalDoctorRate: widget.patientAppointmentHistoryResponseModel.decimalDoctorRate,
name: widget.patientAppointmentHistoryResponseModel.projectName,
isHomeMedicineDeliverySupported: val.data.first.isHomeMedicineDeliverySupported ?? false,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID,
setupID: widget.patientAppointmentHistoryResponseModel.setupID,
isInOutPatient: widget.patientAppointmentHistoryResponseModel.isInOutPatient,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo,
episodeID: widget.patientAppointmentHistoryResponseModel.episodeID,
dischargeNo: 0,
);
Navigator.of(context).push(
CustomPageRoute(
page: PrescriptionDetailPage(isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel),
),
);
} else {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.noPrescriptionsForAppointment.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.noPrescriptionsForAppointment.tr(context: context)),
@ -689,40 +848,29 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
isFullScreen: false,
isCloseButtonVisible: true,
);
}
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.noPrescriptionsForAppointment.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
// Navigator.of(context).push(
// CustomPageRoute(
// page: VaccineListPage(),
// ),
// );
}),
MedicalFileCard(
label: LocaleKeys.doctorResponses.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.ask_doctor_medical_file_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() {
getIt.get<AskDoctorViewModel>().initAskDoctorViewModel();
getIt.get<AskDoctorViewModel>().getDoctorResponses();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorResponsePage(),
),
);
},
);
// Navigator.of(context).push(
// CustomPageRoute(
// page: VaccineListPage(),
// ),
// );
}),
MedicalFileCard(
label: LocaleKeys.doctorResponses.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.ask_doctor_medical_file_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() {
getIt.get<AskDoctorViewModel>().initAskDoctorViewModel();
getIt.get<AskDoctorViewModel>().getDoctorResponses();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorResponsePage(),
),
);
}),
myAppointmentsVM.isAppointmentRatedResponseLoading
? MedicalFileCard(
@ -993,8 +1141,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 ||
widget.patientAppointmentHistoryResponseModel.nextAction == 20)
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1002,10 +1149,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.amountBeforeTax.tr(context: context).toText18(isBold: true),
Utils.getPaymentAmountWithSymbol(
widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true),
AppColors.blackColor,
13,
Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13,
isSaudiCurrency: true),
],
),
@ -1013,9 +1157,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: LocaleKeys.upcomingPaymentNow
.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor)),
Expanded(child: LocaleKeys.upcomingPaymentNow.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor)),
"VAT 15%(${widget.patientAppointmentHistoryResponseModel.patientTaxAmount})".toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -0.64),
],
),
@ -1030,10 +1172,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Utils.getPaymentAmountWithSymbol(
widget.patientAppointmentHistoryResponseModel.patientShareWithTax!.toString().toText24(isBold: true),
AppColors.blackColor,
17,
Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShareWithTax!.toString().toText24(isBold: true), AppColors.blackColor, 17,
isSaudiCurrency: true),
],
),
@ -1057,11 +1196,11 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
borderColor: AppColors.primaryRedColor,
textColor: Colors.white,
fontSize: 16.f,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 50.h,
icon: AppAssets.add_icon,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 50.h,
icon: AppAssets.add_icon,
iconColor: Colors.white,
iconSize: 18.h,
).paddingSymmetrical(16.h, 24.h)
@ -1072,7 +1211,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
},
backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction),
borderColor:
AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01),
AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white,
fontSize: 16.f,
fontWeight: FontWeight.w600,
@ -1233,8 +1372,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
Navigator.of(context).pop();
}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isAutoDismiss: true, isCloseButtonVisible: true, isDismissible: false, isFullScreen: false,
isSuccessDialog: true);
}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isAutoDismiss: true, isCloseButtonVisible: true, isDismissible: false, isFullScreen: false, isSuccessDialog: true);
});
// LoaderBottomSheet.hideLoader();
case 15:
@ -1254,43 +1392,45 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Message text
LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(
color: AppColors.textColor,
isCenter: true,
),
SizedBox(height: 24.h),
// Countdown Timer - DD : HH : MM : SS format with labels
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Days
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00',
LocaleKeys.days.tr(context: context),
),
_buildTimeSeparator(),
// Hours
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00',
LocaleKeys.hours.tr(context: context),
),
_buildTimeSeparator(),
// Minutes
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.minutes.tr(context: context),
),
_buildTimeSeparator(),
// Seconds
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.seconds.tr(context: context),
),
],
),
Lottie.asset(AppAnimations.warningAnimation,
repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 12,),
LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(color: AppColors.textColor, isCenter: true),
SizedBox(height: 24.h),
// // Countdown Timer - DD : HH : MM : SS format with labels - Always LTR
// Directionality(
// textDirection: ui.TextDirection.ltr,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Days
// _buildTimeUnit(
// _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00',
// LocaleKeys.days.tr(context: context),
// ),
// _buildTimeSeparator(),
// // Hours
// _buildTimeUnit(
// _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00',
// LocaleKeys.hours.tr(context: context),
// ),
// _buildTimeSeparator(),
// // Minutes
// _buildTimeUnit(
// _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00',
// LocaleKeys.minutes.tr(context: context),
// ),
// _buildTimeSeparator(),
// // Seconds
// _buildTimeUnit(
// _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00',
// LocaleKeys.seconds.tr(context: context),
// ),
// ],
// ),
// ),
// SizedBox(height: 24.h),
// Green Acknowledge button with checkmark icon
CustomButton(
text: LocaleKeys.acknowledged.tr(context: context),

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -17,7 +18,6 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie
import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/eye_measurement_details_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@ -27,8 +27,9 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'dart:ui' as ui;
import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart';
class AppointmentCard extends StatelessWidget {
class AppointmentCard extends StatefulWidget {
final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel;
final MyAppointmentsViewModel myAppointmentsViewModel;
final bool isLoading;
@ -56,6 +57,97 @@ class AppointmentCard extends StatelessWidget {
this.contactUsViewModel,
this.isForRate = false});
@override
State<AppointmentCard> createState() => _AppointmentCardState();
}
class _AppointmentCardState extends State<AppointmentCard> {
Timer? _countdownTimer;
Duration? _timeRemaining;
Function(void Function())? _modalSetState;
@override
void initState() {
super.initState();
// Start countdown timer if appointment requires payment
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15) {
_startCountdownTimer();
}
}
@override
void dispose() {
_countdownTimer?.cancel();
super.dispose();
}
void _startCountdownTimer() {
final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate!);
final expiryDate = appointmentDate.subtract(const Duration(hours: 4));
setState(() {
_timeRemaining = expiryDate.difference(DateTime.now());
});
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final newTimeRemaining = expiryDate.difference(DateTime.now());
if (newTimeRemaining.isNegative) {
timer.cancel();
setState(() {
_timeRemaining = Duration.zero;
});
_modalSetState?.call(() {
_timeRemaining = Duration.zero;
});
} else {
setState(() {
_timeRemaining = newTimeRemaining;
});
_modalSetState?.call(() {
_timeRemaining = newTimeRemaining;
});
}
});
}
// Helper method to build each time unit (number + label)
Widget _buildTimeUnit(String value, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
child: value.toText32(
isBold: true,
color: AppColors.blackColor,
isEnglishOnly: true,
),
),
SizedBox(height: 4.h),
Container(
alignment: Alignment.center,
child: label.toText12(
color: AppColors.greyTextColor,
),
),
],
);
}
// Helper method to build time separator (:)
Widget _buildTimeSeparator() {
return Padding(
padding: EdgeInsets.only(bottom: 16.h, left: 6.w, right: 6.w),
child: ':'.toText32(
isBold: true,
color: AppColors.blackColor,
),
);
}
@override
Widget build(BuildContext context) {
final appState = getIt.get<AppState>();
@ -66,11 +158,11 @@ class AppointmentCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
isForRate ? SizedBox() : _buildHeader(context, appState),
widget.isForRate ? SizedBox() : _buildHeader(context, appState),
SizedBox(height: 16.h),
_buildDoctorRow(context),
SizedBox(height: 16.h),
isForRate ? SizedBox() : _buildActionArea(context, appState),
widget.isForRate ? SizedBox() : _buildActionArea(context, appState),
],
),
),
@ -87,7 +179,7 @@ class AppointmentCard extends StatelessWidget {
}
Widget _buildChips(BuildContext context, AppState appState) {
final isLiveCare = !isLoading && patientAppointmentHistoryResponseModel.isLiveCareAppointment!;
final isLiveCare = !widget.isLoading && widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!;
return Wrap(
alignment: WrapAlignment.start,
@ -96,25 +188,25 @@ class AppointmentCard extends StatelessWidget {
runSpacing: 6.h,
children: [
AppCustomChipWidget(
icon: isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon),
iconColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor),
labelText: isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)),
backgroundColor: isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor),
textColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor),
).toShimmer2(isShow: isLoading),
icon: widget.isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon),
iconColor: widget.isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor),
labelText: widget.isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)),
backgroundColor: widget.isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor),
textColor: widget.isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor),
).toShimmer2(isShow: widget.isLoading),
AppCustomChipWidget(
labelText:
isLoading ? 'OutPatient' : (appState.isArabic() ? patientAppointmentHistoryResponseModel.isInOutPatientDescriptionN! : patientAppointmentHistoryResponseModel.isInOutPatientDescription!),
widget.isLoading ? 'OutPatient' : (appState.isArabic() ? widget.patientAppointmentHistoryResponseModel.isInOutPatientDescriptionN! : widget.patientAppointmentHistoryResponseModel.isInOutPatientDescription!),
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1),
textColor: AppColors.warningColorYellow,
).toShimmer2(isShow: isLoading),
).toShimmer2(isShow: widget.isLoading),
AppCustomChipWidget(
labelText: isLoading ? 'Booked' : AppointmentType.getAppointmentStatusType(patientAppointmentHistoryResponseModel.patientStatusType!),
labelText: widget.isLoading ? 'Booked' : AppointmentType.getAppointmentStatusType(widget.patientAppointmentHistoryResponseModel.patientStatusType!),
backgroundColor: AppColors.successColor.withValues(alpha: 0.1),
textColor: AppColors.successColor,
).toShimmer2(isShow: isLoading),
).toShimmer2(isShow: widget.isLoading),
],
).toShimmer2(isShow: isLoading);
).toShimmer2(isShow: widget.isLoading);
}
Widget _buildDoctorRow(BuildContext context) {
@ -125,11 +217,11 @@ class AppointmentCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!,
widget.isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : widget.patientAppointmentHistoryResponseModel.doctorImageURL!,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
).circle(100.r).toShimmer2(isShow: isLoading),
).circle(100.r).toShimmer2(isShow: widget.isLoading),
Transform.translate(
offset: Offset(0.0, -20.h),
child: Container(
@ -149,11 +241,11 @@ class AppointmentCard extends StatelessWidget {
Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow),
SizedBox(height: 2.h),
(isFoldable || isTablet)
? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true)
: "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
? "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true)
: "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
],
),
).circle(100).toShimmer2(isShow: isLoading),
).circle(100).toShimmer2(isShow: widget.isLoading),
),
],
),
@ -164,20 +256,20 @@ class AppointmentCard extends StatelessWidget {
children: [
Row(
children: [
(isLoading ? 'Dr' : "${patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1),
(isLoading ? 'John Doe' : " ${patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}")
.toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
(widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1),
(widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}")
.toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
SizedBox(width: 12.w),
(patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty)
(widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty)
? Image.network(
patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
)
: SizedBox.shrink(),
],
).toShimmer2(isShow: isLoading),
).toShimmer2(isShow: widget.isLoading),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
@ -185,29 +277,29 @@ class AppointmentCard extends StatelessWidget {
runSpacing: 4.h,
children: [
AppCustomChipWidget(
labelText: isLoading
labelText: widget.isLoading
? 'Cardiology'
: (patientAppointmentHistoryResponseModel.clinicName!.length > 20
? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...'
: patientAppointmentHistoryResponseModel.clinicName!),
).toShimmer2(isShow: isLoading),
: (widget.patientAppointmentHistoryResponseModel.clinicName!.length > 20
? '${widget.patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...'
: widget.patientAppointmentHistoryResponseModel.clinicName!),
).toShimmer2(isShow: widget.isLoading),
AppCustomChipWidget(
labelText: isLoading
labelText: widget.isLoading
? 'Olaya'
: (patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").length > 15
? '${(patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...'
: patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital")
.toShimmer2(isShow: isLoading),
: (widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").length > 15
? '${(widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...'
: widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital")
.toShimmer2(isShow: widget.isLoading),
Directionality(
textDirection: ui.TextDirection.ltr,
child: AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w),
icon: AppAssets.appointment_calendar_icon,
richText: isLoading
? 'Cardiology'.toText10().toShimmer2(isShow: isLoading)
: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}"
richText: widget.isLoading
? 'Cardiology'.toText10().toShimmer2(isShow: widget.isLoading)
: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)}"
.toText10(isEnglishOnly: true, isBold: true),
).toShimmer2(isShow: isLoading),
).toShimmer2(isShow: widget.isLoading),
),
// AppCustomChipWidget(
@ -240,10 +332,10 @@ class AppointmentCard extends StatelessWidget {
}
Widget _buildActionArea(BuildContext context, AppState appState) {
if ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) {
if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) {
// if (((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) &&
// DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15)) {
// return Row(
@ -325,25 +417,25 @@ class AppointmentCard extends StatelessWidget {
// );
// } else {
return CustomButton(
text: isFromMedicalReport ? LocaleKeys.selectAppointment.tr(context: context) : LocaleKeys.viewDetails.tr(context: context),
text: widget.isFromMedicalReport ? LocaleKeys.selectAppointment.tr(context: context) : LocaleKeys.viewDetails.tr(context: context),
onPressed: () {
if (isFromMedicalReport) {
if (isForFeedback) {
contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel);
if (widget.isFromMedicalReport) {
if (widget.isForFeedback) {
widget.contactUsViewModel!.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel);
} else {
medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel);
widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel);
}
Navigator.pop(context, false);
} else {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((_) {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
widget.myAppointmentsViewModel.initAppointmentsViewModel();
widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
}
},
@ -356,23 +448,23 @@ class AppointmentCard extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 10.w),
// height: isTablet || isFoldable ? 46.h : 40.h,
height: 40.h,
icon: isFromMedicalReport ? AppAssets.checkmark_icon : null,
icon: widget.isFromMedicalReport ? AppAssets.checkmark_icon : null,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
);
// }
} else {
if (isFromMedicalReport) {
if (isForEyeMeasurements) {
if (widget.isFromMedicalReport) {
if (widget.isForEyeMeasurements) {
return SizedBox.shrink();
} else {
return CustomButton(
text: LocaleKeys.selectAppointment.tr(context: context),
onPressed: () {
if (isForFeedback) {
contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel);
if (widget.isForFeedback) {
widget.contactUsViewModel!.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel);
} else {
medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel);
widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel);
}
Navigator.pop(context, false);
},
@ -391,33 +483,33 @@ class AppointmentCard extends StatelessWidget {
);
}
}
return (patientAppointmentHistoryResponseModel.isActiveDoctor ?? true)
? (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) &&
patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false)
return (widget.patientAppointmentHistoryResponseModel.isActiveDoctor ?? true)
? (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) &&
widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false)
? // Show only View Details button without arrow when rebooking not allowed
_getArrivedButton(context)
: Row(
children: [
Expanded(
flex: 6,
child: (AppointmentType.isArrived(patientAppointmentHistoryResponseModel)
child: (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? _getArrivedButton(context)
: CustomButton(
text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction),
onPressed: () => _goToDetails(context),
backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15),
borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction),
text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction),
onPressed: () => handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context),
backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15),
borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
fontSize: (isFoldable || isTablet) ? 12.f : 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction),
iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction),
icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction),
iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
iconSize: 15.h,
))
.toShimmer2(isShow: isLoading),
.toShimmer2(isShow: widget.isLoading),
),
SizedBox(width: 8.h),
Expanded(
@ -442,7 +534,7 @@ class AppointmentCard extends StatelessWidget {
fit: BoxFit.contain,
),
),
).toShimmer2(isShow: isLoading).onPress(() {
).toShimmer2(isShow: widget.isLoading).onPress(() {
_goToDetails(context);
}),
),
@ -454,12 +546,12 @@ class AppointmentCard extends StatelessWidget {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((_) {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
widget.myAppointmentsViewModel.initAppointmentsViewModel();
widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
},
backgroundColor: AppColors.secondaryLightRedColor,
@ -471,7 +563,7 @@ class AppointmentCard extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 10.w),
// height: isTablet || isFoldable ? 46.h : 40.h,
height: 40.h,
icon: isFromMedicalReport ? AppAssets.checkmark_icon : null,
icon: widget.isFromMedicalReport ? AppAssets.checkmark_icon : null,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
);
@ -479,7 +571,7 @@ class AppointmentCard extends StatelessWidget {
}
Widget _getArrivedButton(BuildContext context) {
if (patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) {
if (widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) {
return CustomButton(
text: LocaleKeys.viewDetails.tr(context: context),
onPressed: () => _goToDetails(context),
@ -518,21 +610,21 @@ class AppointmentCard extends StatelessWidget {
}
void _goToDetails(BuildContext context) {
if (isFromMedicalReport) return;
if (isForEyeMeasurements) {
if (widget.isFromMedicalReport) return;
if (widget.isForEyeMeasurements) {
Navigator.of(context).push(
CustomPageRoute(
page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
);
} else {
if (!AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) {
bookAppointmentsViewModel.getAppointmentNearestGate(projectID: patientAppointmentHistoryResponseModel.projectID, clinicID: patientAppointmentHistoryResponseModel.clinicID);
if (!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) {
widget.bookAppointmentsViewModel.getAppointmentNearestGate(projectID: widget.patientAppointmentHistoryResponseModel.projectID, clinicID: widget.patientAppointmentHistoryResponseModel.clinicID);
}
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((_) {
@ -544,22 +636,22 @@ class AppointmentCard extends StatelessWidget {
void openDoctorScheduleCalendar(BuildContext context) async {
final doctor = DoctorsListResponseModel(
clinicID: patientAppointmentHistoryResponseModel.clinicID,
projectID: patientAppointmentHistoryResponseModel.projectID,
doctorID: patientAppointmentHistoryResponseModel.doctorID,
doctorImageURL: patientAppointmentHistoryResponseModel.doctorImageURL,
doctorTitle: patientAppointmentHistoryResponseModel.doctorTitle,
name: patientAppointmentHistoryResponseModel.doctorNameObj,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID,
doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL,
doctorTitle: widget.patientAppointmentHistoryResponseModel.doctorTitle,
name: widget.patientAppointmentHistoryResponseModel.doctorNameObj,
nationalityFlagURL: '',
speciality: [],
clinicName: patientAppointmentHistoryResponseModel.clinicName,
projectName: patientAppointmentHistoryResponseModel.projectName,
clinicName: widget.patientAppointmentHistoryResponseModel.clinicName,
projectName: widget.patientAppointmentHistoryResponseModel.projectName,
);
bookAppointmentsViewModel.setSelectedDoctor(doctor);
widget.bookAppointmentsViewModel.setSelectedDoctor(doctor);
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorFreeSlots(
await widget.bookAppointmentsViewModel.getDoctorFreeSlots(
isBookingForLiveCare: false,
onSuccess: (respData) async {
LoaderBottomSheet.hideLoader();
@ -584,4 +676,121 @@ class AppointmentCard extends StatelessWidget {
},
);
}
Future<void> handleAppointmentNextAction(nextAction, BuildContext context) async {
switch (nextAction) {
case 0:
// No action needed
_goToDetails(context);
break;
case 10:
// Confirm appointment - go to details
_goToDetails(context);
break;
case 15:
// Pending payment - show waiting modal
// Ensure timer is running before showing modal
if (_countdownTimer == null || !_countdownTimer!.isActive) {
_startCountdownTimer();
}
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.notice.tr(),
child: StatefulBuilder(
builder: (context, setModalState) {
// Store the modal setState callback
_modalSetState = setModalState;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Message text
LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(
color: AppColors.textColor,
isCenter: true,
),
SizedBox(height: 24.h),
// Countdown Timer - DD : HH : MM : SS format with labels
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Days
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00',
LocaleKeys.days.tr(context: context),
),
_buildTimeSeparator(),
// Hours
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00',
LocaleKeys.hours.tr(context: context),
),
_buildTimeSeparator(),
// Minutes
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.minutes.tr(context: context),
),
_buildTimeSeparator(),
// Seconds
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.seconds.tr(context: context),
),
],
),
SizedBox(height: 24.h),
// Green Acknowledge button with checkmark icon
CustomButton(
text: LocaleKeys.acknowledged.tr(context: context),
onPressed: () {
_modalSetState = null; // Clear callback when closing
Navigator.of(context).pop();
},
backgroundColor: AppColors.successColor,
borderColor: AppColors.successColor,
textColor: Colors.white,
fontSize: 16.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
height: 50.h,
icon: AppAssets.checkmark_icon,
iconColor: Colors.white,
),
],
);
},
),
callBackFunc: () {
_modalSetState = null; // Clear callback when closing
},
isFullScreen: false,
isCloseButtonVisible: true,
);
break;
case 20:
// Pay now - navigate to payment page
widget.myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true);
Navigator.of(context).push(
CustomPageRoute(
page: AppointmentPaymentPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
);
break;
case 50:
// Confirm livecare - go to details
_goToDetails(context);
break;
case 90:
// Check-in - go to details
_goToDetails(context);
break;
default:
// Default - go to details
_goToDetails(context);
}
}
}

@ -24,7 +24,13 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
class AppointmentDoctorCard extends StatelessWidget {
const AppointmentDoctorCard(
{super.key, required this.patientAppointmentHistoryResponseModel, required this.onRescheduleTap, required this.onCancelTap, required this.onAskDoctorTap, this.renderWidgetForERDisplay = false});
{super.key,
required this.patientAppointmentHistoryResponseModel,
required this.onRescheduleTap,
required this.onCancelTap,
required this.onAskDoctorTap,
this.renderWidgetForERDisplay = false,
this.timerWidget});
final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel;
final VoidCallback onRescheduleTap;
@ -32,6 +38,8 @@ class AppointmentDoctorCard extends StatelessWidget {
final VoidCallback onAskDoctorTap;
final bool renderWidgetForERDisplay;
final Widget? timerWidget;
@override
Widget build(BuildContext context) {
return Container(
@ -142,43 +150,43 @@ class AppointmentDoctorCard extends StatelessWidget {
],
),
),
patientAppointmentHistoryResponseModel.isLiveCareAppointment!
patientAppointmentHistoryResponseModel.isLiveCareAppointment! || patientAppointmentHistoryResponseModel.isClinicReBookingAllowed! ==false
? SizedBox.shrink()
: Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).onPress(() async {
DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel();
selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID;
selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL;
selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj;
selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle;
selectedDoctor.nationalityFlagURL = "";
selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality;
selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName;
selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName;
selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID;
selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID;
selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID;
selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL;
selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj;
selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle;
selectedDoctor.nationalityFlagURL = "";
selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality;
selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName;
selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName;
selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID;
selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID;
getIt.get<BookAppointmentsViewModel>().setSelectedDoctor(selectedDoctor);
LoaderBottomSheet.showLoader();
await getIt.get<BookAppointmentsViewModel>().getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(
isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false),
),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
})
getIt.get<BookAppointmentsViewModel>().setSelectedDoctor(selectedDoctor);
LoaderBottomSheet.showLoader();
await getIt.get<BookAppointmentsViewModel>().getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(
isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false),
),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
})
],
),
SizedBox(height: 8.h),
@ -188,6 +196,8 @@ class AppointmentDoctorCard extends StatelessWidget {
AppointmentType.isArrived(patientAppointmentHistoryResponseModel),
),
),
if (timerWidget != null)
timerWidget ?? SizedBox()
],
),
),
@ -215,26 +225,26 @@ class AppointmentDoctorCard extends StatelessWidget {
iconColor: AppColors.primaryRedColor,
)
:
// !patientAppointmentHistoryResponseModel.isLiveCareAppointment!
// ? CustomButton(
// text: LocaleKeys.rebookSameDoctor.tr(),
// onPressed: () {
// onRescheduleTap();
// },
// backgroundColor: AppColors.greyColor,
// borderColor: AppColors.greyColor,
// textColor: AppColors.blackColor,
// fontSize: 12.f,
// isBold: true,
// borderRadius: 12.r,
// padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0),
// height: 40.h,
// icon: AppAssets.rebook_appointment_icon,
// iconColor: AppColors.blackColor,
// iconSize: 14.h,
// )
// :
SizedBox.shrink();
// !patientAppointmentHistoryResponseModel.isLiveCareAppointment!
// ? CustomButton(
// text: LocaleKeys.rebookSameDoctor.tr(),
// onPressed: () {
// onRescheduleTap();
// },
// backgroundColor: AppColors.greyColor,
// borderColor: AppColors.greyColor,
// textColor: AppColors.blackColor,
// fontSize: 12.f,
// isBold: true,
// borderRadius: 12.r,
// padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0),
// height: 40.h,
// icon: AppAssets.rebook_appointment_icon,
// iconColor: AppColors.blackColor,
// iconSize: 14.h,
// )
// :
SizedBox.shrink();
} else {
return patientAppointmentHistoryResponseModel.isLiveCareAppointment!
? CustomButton(

@ -71,7 +71,7 @@ class LoginScreenState extends State<LoginScreen> {
children: [
Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 200.h, height: 200.h, repeat: true, fit: BoxFit.cover),
SizedBox(height: 130.h), // Adjusted to sizer unit
LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor),
LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor ),
SizedBox(height: 32.h),
Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget
TextInputWidget(

@ -1,6 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -59,7 +61,7 @@ class DoctorsFilters extends StatelessWidget{
Text(
LocaleKeys.filters.tr(),
style:TextStyle(
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w600,
fontSize: 27.f,
color: AppColors.textColor,
@ -69,7 +71,7 @@ class DoctorsFilters extends StatelessWidget{
Text(
LocaleKeys.clearAllFilters.tr(),
style:TextStyle(
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w600,
fontSize: 14.f,
color: AppColors.errorColor
@ -199,7 +201,7 @@ class DoctorsFilters extends StatelessWidget{
Text(
title,
style:TextStyle(
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w600,
fontSize: 16.f,
color: AppColors.textColor,

@ -90,12 +90,14 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
.toString()
.toText16(isBold: true, maxlines: 1),
SizedBox(width: 12.w),
(bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null && bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty )? Image.network(
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
) : SizedBox.shrink(),
(bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null && bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty)
? Image.network(
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
)
: SizedBox.shrink(),
],
),
SizedBox(height: 2.h),
@ -162,7 +164,9 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} ${LocaleKeys.yearsOld.tr(context: context)}"),
AppCustomChipWidget(labelText: "${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"),
AppCustomChipWidget(
labelText:
"${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"),
],
),
],
@ -191,19 +195,19 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
bookAppointmentsViewModel.appointmentNearestGateResponseModel != null
? Wrap(
direction: Axis.horizontal,
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: bookAppointmentsVM.isAppointmentNearestGateLoading
? "Floor: Ground Floor"
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: bookAppointmentsVM.isAppointmentNearestGateLoading
? "Floor: Ground Floor"
: "${LocaleKeys.floor.tr(context: context)} ${getIt.get<AppState>().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}",
).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
AppCustomChipWidget(
labelText:
AppCustomChipWidget(
labelText:
"${LocaleKeys.nearestGate.tr(context: context)} ${getIt.get<AppState>().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}")
.toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
],
],
)
: SizedBox.shrink(),
],
@ -317,8 +321,8 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
void initiateBookAppointment() async {
// LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context));
// LoaderBottomSheet.showLoader(loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context));
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
LoaderBottomSheet.showLoader(
loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context));
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
if (bookAppointmentsViewModel.isLiveCareSchedule) {
@ -329,23 +333,16 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
LoaderBottomSheet.hideLoader();
await Future.delayed(Duration(milliseconds: 50)).then((value) async {
// LoaderBottomSheet.showLoader(loadingText: LocaleKeys.appointmentSuccess.tr());
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()),
callBackFunc: () {
bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false);
bookAppointmentsViewModel.setIsLiveCareSchedule(false);
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
},
isFullScreen: false,
isCloseButtonVisible: false,
isAutoDismiss: true
);
showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()), callBackFunc: () {
bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false);
bookAppointmentsViewModel.setIsLiveCareSchedule(false);
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
}, isFullScreen: false, isCloseButtonVisible: false, isAutoDismiss: true);
});
});
} else {
@ -368,23 +365,11 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
}, onSuccess: (apiResp) async {
LoaderBottomSheet.hideLoader();
await Future.delayed(Duration(milliseconds: 50)).then((value) async {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h),
callBackFunc: () {
bookAppointmentsViewModel.setIsLiveCareSchedule(false);
bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false);
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
},
isFullScreen: false,
isCloseButtonVisible: false,
isAutoDismiss: true
);
showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () {
bookAppointmentsViewModel.setIsLiveCareSchedule(false);
bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false);
Navigator.pushAndRemoveUntil(context, CustomPageRoute(page: LandingNavigation()), (r) => false);
}, isFullScreen: false, isCloseButtonVisible: false, isAutoDismiss: true);
});
});
}

@ -0,0 +1,441 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/reminder_type.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
enum ReminderBottomSheetStep {
success,
reminderConfirmation,
reminderTypeSelection,
creatingReminder,
reminderSuccess,
reminderError,
}
class AppointmentSuccessReminderBottomSheet extends StatefulWidget {
final PatientAppointmentHistoryResponseModel appointmentModel;
final MyAppointmentsViewModel myAppointmentsViewModel;
final VoidCallback onComplete;
const AppointmentSuccessReminderBottomSheet({
Key? key,
required this.appointmentModel,
required this.myAppointmentsViewModel,
required this.onComplete,
}) : super(key: key);
@override
State<AppointmentSuccessReminderBottomSheet> createState() => _AppointmentSuccessReminderBottomSheetState();
}
class _AppointmentSuccessReminderBottomSheetState extends State<AppointmentSuccessReminderBottomSheet> {
ReminderBottomSheetStep currentStep = ReminderBottomSheetStep.success;
ReminderType? selectedReminderType;
String errorMessage = '';
bool isProcessing = false;
@override
void initState() {
super.initState();
// Auto-progress to reminder confirmation after showing success
Future.delayed(Duration(milliseconds: 2000), () {
if (mounted) {
setState(() {
currentStep = ReminderBottomSheetStep.reminderConfirmation;
});
}
});
}
void _handleNoReminder() {
Navigator.of(context).pop();
widget.onComplete();
}
void _handleYesReminder() {
setState(() {
currentStep = ReminderBottomSheetStep.reminderTypeSelection;
});
}
void _selectReminderType(ReminderType type) {
setState(() {
selectedReminderType = type;
});
}
Future<void> _createReminder() async {
if (selectedReminderType == null) return;
setState(() {
currentStep = ReminderBottomSheetStep.creatingReminder;
isProcessing = true;
});
await widget.myAppointmentsViewModel.toggleReminder(
context: context,
shouldCreateReminder: true,
reminderType: selectedReminderType!,
patientAppointmentHistoryResponseModel: widget.appointmentModel,
onSuccess: (result) {
if (mounted) {
setState(() {
currentStep = ReminderBottomSheetStep.reminderSuccess;
isProcessing = false;
});
// Auto-close after showing success
Future.delayed(Duration(milliseconds: 2000), () {
if (mounted) {
Navigator.of(context).pop();
widget.onComplete();
}
});
}
},
onError: (error) {
if (mounted) {
setState(() {
currentStep = ReminderBottomSheetStep.reminderError;
errorMessage = error;
isProcessing = false;
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)),
),
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: _buildStepContent(),
),
);
}
Widget _buildStepContent() {
switch (currentStep) {
case ReminderBottomSheetStep.success:
return _buildSuccessStep();
case ReminderBottomSheetStep.reminderConfirmation:
return _buildReminderConfirmationStep();
case ReminderBottomSheetStep.reminderTypeSelection:
return _buildReminderTypeSelectionStep();
case ReminderBottomSheetStep.creatingReminder:
return _buildCreatingReminderStep();
case ReminderBottomSheetStep.reminderSuccess:
return _buildReminderSuccessStep();
case ReminderBottomSheetStep.reminderError:
return _buildReminderErrorStep();
}
}
Widget _buildSuccessStep() {
return Column(
key: ValueKey('success'),
mainAxisSize: MainAxisSize.min,
children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr(context: context)),
SizedBox(height: 16.h),
Center(
child: "Your appointment has been booked successfully!".toText14(
color: AppColors.greyTextColor,
),
),
],
);
}
Widget _buildReminderConfirmationStep() {
return Column(
key: ValueKey('confirmation'),
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.notifications_active_outlined,
size: 56.h,
color: AppColors.primaryRedColor,
),
SizedBox(height: 16.h),
Center(child: "Add Reminder?".toText20(isBold: true)),
SizedBox(height: 12.h),
Center(
child: "Would you like to add a reminder for this appointment?".toText14(
color: AppColors.greyTextColor,
),
),
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
text: LocaleKeys.cancel.tr(context: context),
onPressed: _handleNoReminder,
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.greyTextColor,
textColor: AppColors.greyTextColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12,
height: 48.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: LocaleKeys.confirm.tr(context: context),
onPressed: _handleYesReminder,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: Colors.white,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12,
height: 48.h,
),
),
],
),
],
);
}
Widget _buildReminderTypeSelectionStep() {
return Column(
key: ValueKey('type_selection'),
mainAxisSize: MainAxisSize.min,
children: [
Center(child: "Select Reminder Type".toText20(isBold: true)),
SizedBox(height: 8.h),
Center(
child: "Choose what type of reminder you want to set".toText14(
color: AppColors.greyTextColor,
),
),
SizedBox(height: 24.h),
_buildReminderTypeCard(
type: ReminderType.appointment,
icon: Icons.event_outlined,
title: "Appointment Reminder",
description: "Get reminded about your upcoming appointment",
),
SizedBox(height: 12.h),
_buildReminderTypeCard(
type: ReminderType.payment,
icon: Icons.payment_outlined,
title: "Payment Reminder",
description: "Get reminded about payment for this appointment",
),
SizedBox(height: 12.h),
_buildReminderTypeCard(
type: ReminderType.checkIn,
icon: Icons.check_circle_outline,
title: "Check-In Reminder",
description: "Get reminded to check in for your appointment",
),
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
text: LocaleKeys.cancel.tr(context: context),
onPressed: () {
setState(() {
currentStep = ReminderBottomSheetStep.reminderConfirmation;
selectedReminderType = null;
});
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.greyTextColor,
textColor: AppColors.greyTextColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12,
height: 48.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: "Continue",
onPressed: selectedReminderType != null ? _createReminder : null,
backgroundColor: selectedReminderType != null ? AppColors.primaryRedColor : AppColors.greyTextColor.withAlpha(76),
borderColor: selectedReminderType != null ? AppColors.primaryRedColor : AppColors.greyTextColor.withAlpha(76),
textColor: Colors.white,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12,
height: 48.h,
),
),
],
),
],
);
}
Widget _buildReminderTypeCard({
required ReminderType type,
required IconData icon,
required String title,
required String description,
}) {
final isSelected = selectedReminderType == type;
return GestureDetector(
onTap: () => _selectReminderType(type),
child: Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor.withAlpha(26) : AppColors.scaffoldBgColor,
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.borderOnlyColor,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor : AppColors.greyTextColor.withAlpha(26),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
icon,
color: isSelected ? Colors.white : AppColors.greyTextColor,
size: 24.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title.toText16(
isBold: true,
color: isSelected ? AppColors.primaryRedColor : AppColors.textColor,
),
SizedBox(height: 4.h),
description.toText12(
color: AppColors.greyTextColor,
),
],
),
),
if (isSelected)
Icon(
Icons.check_circle,
color: AppColors.primaryRedColor,
size: 24.h,
),
],
),
),
);
}
Widget _buildCreatingReminderStep() {
return Column(
key: ValueKey('creating'),
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: AppColors.primaryRedColor),
SizedBox(height: 24.h),
Center(
child: "Creating your reminder...".toText16(
isBold: true,
),
),
SizedBox(height: 8.h),
Center(
child: "Please wait while we set up your reminder".toText14(
color: AppColors.greyTextColor,
),
),
],
);
}
Widget _buildReminderSuccessStep() {
return Column(
key: ValueKey('reminder_success'),
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 64.h,
color: AppColors.successColor,
),
SizedBox(height: 16.h),
Center(
child: LocaleKeys.reminderSuccess.tr(context: context).toText20(
isBold: true,
),
),
SizedBox(height: 8.h),
Center(
child: "Your ${selectedReminderType?.displayName ?? 'reminder'} has been set successfully!".toText14(
color: AppColors.greyTextColor,
),
),
],
);
}
Widget _buildReminderErrorStep() {
return Column(
key: ValueKey('error'),
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 64.h,
color: AppColors.primaryRedColor,
),
SizedBox(height: 16.h),
Center(
child: "Error".toText20(
isBold: true,
color: AppColors.primaryRedColor,
),
),
SizedBox(height: 8.h),
Center(
child: errorMessage.toText14(
color: AppColors.greyTextColor,
),
),
SizedBox(height: 24.h),
CustomButton(
text: "Close",
onPressed: () {
Navigator.of(context).pop();
widget.onComplete();
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: Colors.white,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12,
height: 48.h,
),
],
);
}
}

@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
class BellAnimatedSwitch extends StatefulWidget {
final bool initialValue;
final ValueChanged<bool>? onChanged;
final Color? activeColor;
final Color? inactiveColor;
final Color? activeIconColor;
final Color? inactiveIconColor;
final Color? activeCircleColor;
final Color? inactiveCircleColor;
final Widget? activeIcon;
final Widget? inactiveIcon;
final double width;
final double height;
final Duration animationDuration;
const BellAnimatedSwitch({
super.key,
this.initialValue = false,
this.onChanged,
this.activeColor,
this.inactiveColor,
this.activeIconColor,
this.inactiveIconColor,
this.activeCircleColor,
this.inactiveCircleColor,
this.activeIcon,
this.inactiveIcon,
this.width = 70,
this.height = 40,
this.animationDuration = const Duration(milliseconds: 300),
});
@override
State<BellAnimatedSwitch> createState() => _BellAnimatedSwitchState();
}
class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTickerProviderStateMixin {
late bool _isActive;
late AnimationController _animationController;
late Animation<Alignment> _circleAlignment;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_isActive = widget.initialValue;
_animationController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
// Animation for the circle position (left/right)
_circleAlignment = AlignmentTween(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
// Scale animation for tap feedback
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.9).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
),
);
if (_isActive) {
_animationController.value = 1.0;
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_isActive = !_isActive;
if (_isActive) {
_animationController.forward();
} else {
_animationController.reverse();
}
widget.onChanged?.call(_isActive);
});
}
Color _getBackgroundColor() {
if (_isActive) {
return widget.activeColor ?? Colors.green.shade400;
}
return widget.inactiveColor ?? Colors.grey.shade300;
}
Color _getCircleColor() {
if (_isActive) {
return widget.activeCircleColor ?? Colors.white;
}
return widget.inactiveCircleColor ?? Colors.white;
}
Widget _getIcon() {
if (_isActive) {
return widget.activeIcon ?? Icon(
Icons.check,
size: 18,
color: widget.activeIconColor ?? Colors.green.shade400,
);
}
return widget.inactiveIcon ?? Icon(
Icons.close,
size: 18,
color: widget.inactiveIconColor ?? Colors.grey.shade600,
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _toggle,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: _getBackgroundColor(),
borderRadius: BorderRadius.circular(widget.height / 2),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withAlpha(26),
// blurRadius: 4,
// offset: const Offset(0, 2),
// ),
// ],
),
child: Stack(
children: [
// Animated circle with icon inside
Align(
alignment: _circleAlignment.value,
child: Container(
margin: const EdgeInsets.all(4),
width: widget.height - 8,
height: widget.height - 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getCircleColor(),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(51),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Center(
child: _getIcon(),
),
),
),
],
),
),
);
},
),
);
}
}

@ -475,12 +475,12 @@ class FeedbackPage extends StatelessWidget {
),
SizedBox(height: 8.h),
"${contactUsViewModel.cocItemsList[index].cOCTitle}"
.toText16(weight: FontWeight.bold, fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].cOCTitle ?? "") ? "GESSTwo" : "Poppins"),
.toText16(weight: FontWeight.bold, fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].cOCTitle ?? "") ? "CairoArabic" : "Poppins"),
SizedBox(height: 8.h),
"${contactUsViewModel.cocItemsList[index].detail}".toText14(
weight: FontWeight.w600,
color: AppColors.textColorLight,
fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].detail ?? "") ? "GESSTwo" : "Poppins"),
fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].detail ?? "") ? "CairoArabic" : "Poppins"),
],
),
).paddingOnly(bottom: 16.h),

@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
class HealthCalculatorsPage extends StatefulWidget {
@ -37,73 +38,89 @@ class _HealthCalculatorsPageState extends State<HealthCalculatorsPage> {
return CollapsingListView(
title: widget.type == HealthCalConEnum.calculator ? LocaleKeys.healthCalculators.tr(context: context) : LocaleKeys.healthConverters.tr(),
child: widget.type == HealthCalConEnum.calculator
? Column(
children: [
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r),
child: Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
LocaleKeys.generalHealth.tr().toText16(isBold: true),
LocaleKeys.relatedToBMICalories.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))
],
),
),
SizedBox(
width: 12.w,
),
Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
],
).paddingAll(16.w))
.onPress(() {
dialogService.showFamilyBottomSheetWithoutHWithChild(
label: LocaleKeys.selectCalculator.tr(),
message: "",
child: showCalculatorsItems(type: HealthCalculatorEnum.general),
onOkPressed: () {},
);
}),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r),
child: Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
LocaleKeys.womensHealth.tr().toText16(isBold: true),
LocaleKeys.relatedToPeriodsOvulation.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))
],
),
),
SizedBox(width: 12.w),
Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
],
).paddingAll(16.w))
.onPress(() {
dialogService.showFamilyBottomSheetWithoutHWithChild(
label: LocaleKeys.selectCalculator.tr(),
message: "",
child: showCalculatorsItems(type: HealthCalculatorEnum.women),
onOkPressed: () {},
);
}),
? CustomExpandableList(
expansionMode: ExpansionMode.exactlyOne,
dividerColor: AppColors.dividerColor,
itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h),
items: [
ExpandableListItem(
title: LocaleKeys.womensHealth.tr(context: context).toText18(isBold: true),
expandedBackgroundColor: Colors.transparent,
initiallyExpanded: true,
children: [showCalculatorsItems(type: HealthCalculatorEnum.women)],
),
ExpandableListItem(
title: LocaleKeys.generalHealth.tr(context: context).toText18(isBold: true),
expandedBackgroundColor: Colors.transparent,
initiallyExpanded: true,
children: [showCalculatorsItems(type: HealthCalculatorEnum.general)],
),
],
).paddingSymmetrical(20.w, 24.h)
theme: ExpandableListTheme.custom(
defaultTrailingIcon: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w, iconColor: AppColors.textColor),
),
).paddingSymmetrical(16.w, 0.0)
// ? Column(
// children: [
// Container(
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r),
// child: Row(
// children: [
// Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false),
// SizedBox(width: 12.w),
// Flexible(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [LocaleKeys.generalHealth.tr().toText16(isBold: true), LocaleKeys.relatedToBMICalories.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))],
// ),
// ),
// SizedBox(
// width: 12.w,
// ),
// Transform.flip(
// flipX: getIt.get<AppState>().isArabic(),
// child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
// ],
// ).paddingAll(16.w))
// .onPress(() {
// dialogService.showFamilyBottomSheetWithoutHWithChild(
// label: LocaleKeys.selectCalculator.tr(),
// message: "",
// child: showCalculatorsItems(type: HealthCalculatorEnum.general),
// onOkPressed: () {},
// );
// }),
// SizedBox(height: 16.h),
// Container(
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r),
// child: Row(
// children: [
// Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false),
// SizedBox(width: 12.w),
// Flexible(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [LocaleKeys.womensHealth.tr().toText16(isBold: true), LocaleKeys.relatedToPeriodsOvulation.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))],
// ),
// ),
// SizedBox(width: 12.w),
// Transform.flip(
// flipX: getIt.get<AppState>().isArabic(),
// child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
// ],
// ).paddingAll(16.w))
// .onPress(() {
// dialogService.showFamilyBottomSheetWithoutHWithChild(
// label: LocaleKeys.selectCalculator.tr(),
// message: "",
// child: showCalculatorsItems(type: HealthCalculatorEnum.women),
// onOkPressed: () {},
// );
// }),
// ],
// ).paddingSymmetrical(20.w, 24.h)
: Column(
children: [
Container(
@ -151,11 +168,7 @@ class _HealthCalculatorsPageState extends State<HealthCalculatorsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
LocaleKeys.bloodCholesterol.tr().toText16(isBold: true),
LocaleKeys.monitorCholesterolLevels.tr()
.toText12(isBold: true, color: Color(0xFF8F9AA3))
],
children: [LocaleKeys.bloodCholesterol.tr().toText16(isBold: true), LocaleKeys.monitorCholesterolLevels.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))],
),
),
SizedBox(width: 12.w),
@ -183,8 +196,7 @@ class _HealthCalculatorsPageState extends State<HealthCalculatorsPage> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
LocaleKeys.triglyceridesFatBlood.tr().toText16(isBold: true),
LocaleKeys.understandTriglyceridesImpact.tr()
.toText12(isBold: true, color: Color(0xFF8F9AA3))
LocaleKeys.understandTriglyceridesImpact.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))
],
),
),
@ -204,49 +216,104 @@ class _HealthCalculatorsPageState extends State<HealthCalculatorsPage> {
}
Widget showCalculatorsItems({required HealthCalculatorEnum type}) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 4 icons per row
crossAxisSpacing: 16.w,
mainAxisSpacing: 16.w,
childAspectRatio: 0.85),
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: type == HealthCalculatorEnum.general ? generalHealthServices.length : womenHealthServices.length,
padding: EdgeInsets.zero,
itemBuilder: (BuildContext context, int index) {
return HealthCard(
icon: type == HealthCalculatorEnum.general ? generalHealthServices[index].icon : womenHealthServices[index].icon,
labelText: type == HealthCalculatorEnum.general ? generalHealthServices[index].title : womenHealthServices[index].title,
onTap: () {
Navigator.pop(context);
Navigator.of(context).push(
CustomPageRoute(
page: HealthCalculatorDetailedPage(
calculatorType: type == HealthCalculatorEnum.general ? generalHealthServices[index].type : womenHealthServices[index].type,
clinicID: type == HealthCalculatorEnum.general ? generalHealthServices[index].clinicID : womenHealthServices[index].clinicID,
calculationID: type == HealthCalculatorEnum.general ? generalHealthServices[index].calculationID : womenHealthServices[index].calculationID,
return ListView.separated(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: type == HealthCalculatorEnum.general ? generalHealthServices.length : womenHealthServices.length,
padding: EdgeInsets.zero,
itemBuilder: (BuildContext context, int index) {
return HealthCard(
icon: type == HealthCalculatorEnum.general ? generalHealthServices[index].icon : womenHealthServices[index].icon,
labelText: type == HealthCalculatorEnum.general ? generalHealthServices[index].title : womenHealthServices[index].title,
subtitle: type == HealthCalculatorEnum.general ? generalHealthServices[index].subTitle ?? "" : womenHealthServices[index].subTitle ?? "",
iconColor: type == HealthCalculatorEnum.general ? generalHealthServices[index].iconColor : womenHealthServices[index].iconColor,
iconBgColor: type == HealthCalculatorEnum.general ? generalHealthServices[index].bgColor : womenHealthServices[index].bgColor,
onTap: () {
Navigator.of(context).push(
CustomPageRoute(
page: HealthCalculatorDetailedPage(
calculatorType: type == HealthCalculatorEnum.general ? generalHealthServices[index].type : womenHealthServices[index].type,
clinicID: type == HealthCalculatorEnum.general ? generalHealthServices[index].clinicID : womenHealthServices[index].clinicID,
calculationID: type == HealthCalculatorEnum.general ? generalHealthServices[index].calculationID : womenHealthServices[index].calculationID,
),
),
),
);
},
);
},
);
);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(height: 16.h);
});
}
final List<HealthComponentModel> generalHealthServices = [
HealthComponentModel(title: LocaleKeys.bmiCalculator.tr(), icon: AppAssets.bmi, type: HealthCalculatorsTypeEnum.bmi, clinicID: 108, calculationID: null),
HealthComponentModel(title: LocaleKeys.caloriesCalculator.tr(), icon: AppAssets.calories, type: HealthCalculatorsTypeEnum.calories, clinicID: null, calculationID: 2),
HealthComponentModel(title: LocaleKeys.bmrCalculator.tr(), icon: AppAssets.bmr, type: HealthCalculatorsTypeEnum.bmr, clinicID: null, calculationID: 3),
HealthComponentModel(title: LocaleKeys.idealBodyWeight.tr(), icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.idealBodyWeight, clinicID: null, calculationID: 4),
HealthComponentModel(title: LocaleKeys.bodyFatCalculator.tr(), icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.bodyFat, clinicID: null, calculationID: 5),
HealthComponentModel(title: LocaleKeys.carbsProteinFat.tr(), icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.crabsProteinFat, clinicID: null, calculationID: 11),
HealthComponentModel(
title: LocaleKeys.bmiCalculator.tr(),
subTitle: LocaleKeys.bmiCalculatorDesc.tr(),
icon: AppAssets.bmicalc,
type: HealthCalculatorsTypeEnum.bmi,
clinicID: 108,
bgColor: AppColors.bmiCalculatorColor,
calculationID: null),
HealthComponentModel(
title: LocaleKeys.caloriesCalculator.tr(),
subTitle: LocaleKeys.caloriesCalculatorDesc.tr(),
icon: AppAssets.bmicalc,
type: HealthCalculatorsTypeEnum.calories,
clinicID: null,
bgColor: AppColors.caloriesCalculatorColor,
calculationID: 2),
HealthComponentModel(
title: LocaleKeys.bmrCalculator.tr(),
subTitle: LocaleKeys.bmrCalculatorDesc.tr(),
icon: AppAssets.bmrcalc,
type: HealthCalculatorsTypeEnum.bmr,
clinicID: null,
bgColor: AppColors.bmrCalculatorColor,
calculationID: 3),
HealthComponentModel(
title: LocaleKeys.idealBodyWeight.tr(),
subTitle: LocaleKeys.idealBodyWeightDesc.tr(),
icon: AppAssets.idealbodyweightcacl,
type: HealthCalculatorsTypeEnum.idealBodyWeight,
bgColor: AppColors.idealBodyWeightColor,
clinicID: null,
calculationID: 4),
HealthComponentModel(
title: LocaleKeys.bodyFatCalculator.tr(),
subTitle: LocaleKeys.bodyFatCalculatorDesc.tr(),
icon: AppAssets.bodyfatcalc,
type: HealthCalculatorsTypeEnum.bodyFat,
clinicID: null,
bgColor: AppColors.bodyFatCalculatorColor,
calculationID: 5),
HealthComponentModel(
title: LocaleKeys.carbsProteinFat.tr(),
subTitle: LocaleKeys.carbsProteinFatDesc.tr(),
icon: AppAssets.bmicalc,
type: HealthCalculatorsTypeEnum.crabsProteinFat,
clinicID: null,
bgColor: AppColors.carbsProteinFatColor,
calculationID: 11),
];
final List<HealthComponentModel> womenHealthServices = [
HealthComponentModel(title: LocaleKeys.ovulationPeriod.tr(), icon: AppAssets.locate_me, type: HealthCalculatorsTypeEnum.ovulation, clinicID: null, calculationID: 6 ),
HealthComponentModel(title: LocaleKeys.deliveryDueDate.tr(), icon: AppAssets.activeCheck, type: HealthCalculatorsTypeEnum.deliveryDueDate, clinicID: null, calculationID: 6),
HealthComponentModel(
title: LocaleKeys.ovulationPeriod.tr(),
subTitle: LocaleKeys.ovulationPeriodDesc.tr(),
icon: AppAssets.ovluationperiod,
type: HealthCalculatorsTypeEnum.ovulation,
clinicID: null,
calculationID: 6,
bgColor: AppColors.ovulationPeriodColor),
HealthComponentModel(
title: LocaleKeys.deliveryDueDate.tr(),
subTitle: LocaleKeys.deliveryDueDateDesc.tr(),
icon: AppAssets.duedatecalc,
type: HealthCalculatorsTypeEnum.deliveryDueDate,
clinicID: null,
bgColor: AppColors.deliveryDueDateColor,
calculationID: 6),
];
}

@ -1,4 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -9,41 +12,55 @@ class HealthCard extends StatelessWidget {
const HealthCard({
super.key,
required this.icon,
this.iconColor,
this.iconBgColor,
required this.labelText,
required this.subtitle,
this.onTap,
});
final String icon;
final Color? iconColor;
final Color? iconBgColor;
final String labelText;
final String subtitle;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: false),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(
icon: icon,
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: iconBgColor ?? Colors.transparent, borderRadius: 10.r),
height: 40.w,
width: 40.w,
height: 40.h,
fit: BoxFit.cover,
).toShimmer2(isShow: false, radius: 12.r),
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: labelText.toText12(
isBold: true,
maxLine: 2,
)),
],
child: Utils.buildSvgWithAssets(icon: icon, fit: BoxFit.none, height: 22.w, width: 22.w, iconColor: iconColor),
),
SizedBox(width: 12.h),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
labelText.toText16(weight: FontWeight.w600),
],
),
SizedBox(height: 4.h),
subtitle.toText12(maxLine: 2, color: AppColors.greyTextColor),
],
),
),
SizedBox(width: 2.h),
Transform.flip(
flipX: appState.getLanguageCode() == "ar",
child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.cover, iconColor: AppColors.textColor),
),
],
).paddingAll(16.w).onPress(() {

@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/location_util.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -19,6 +20,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_s
import 'package:hmg_patient_app_new/features/hospital/hospital_selection_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart';
@ -47,47 +49,46 @@ import '../../core/dependencies.dart' show getIt;
import '../../features/qr_parking/qr_parking_view_model.dart';
import '../emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart';
class ServicesPage extends StatelessWidget {
class ServicesPage extends StatefulWidget {
bool showBackIcon;
ServicesPage({super.key, this.showBackIcon = true});
@override
State<ServicesPage> createState() => _ServicesPageState();
}
class _ServicesPageState extends State<ServicesPage> {
late BloodDonationViewModel bloodDonationViewModel;
late MedicalFileViewModel medicalFileViewModel;
late WeatherMonitorViewModel weatherVM;
late final List<HmgServicesComponentModel> hmgServices = [
HmgServicesComponentModel(11, LocaleKeys.emergencyServices.tr(), "", AppAssets.emergency_services_icon, bgColor: AppColors.primaryRedColor, true, route: null, onTap: () async {
// if (getIt.get<AppState>().isAuthenticated) {
// getIt.get<EmergencyServicesViewModel>().flushData();
// getIt.get<EmergencyServicesViewModel>().getTransportationOrders(
// showLoader: false,
// );
// getIt.get<EmergencyServicesViewModel>().getRRTOrders(
// showLoader: false,
// );
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: EmergencyServicesPage(),
settings: const RouteSettings(name: '/EmergencyServicesPage'),
),
);
// getIt.get<EmergencyServicesViewModel>().flushData();
// getIt.get<EmergencyServicesViewModel>().getTransportationOrders(
// showLoader: false,
// );
// getIt.get<EmergencyServicesViewModel>().getRRTOrders(
// showLoader: false,
// );
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: EmergencyServicesPage(),
settings: const RouteSettings(name: '/EmergencyServicesPage'),
),
);
// } else {
// await getIt.get<AuthenticationViewModel>().onLoginPressed();
// }
}),
HmgServicesComponentModel(
11,
LocaleKeys.bookAppointment.tr(),
"",
AppAssets.appointment_calendar_icon,
bgColor: AppColors.bookAppointment,
true,
route: null,
onTap: () {
getIt.get<BookAppointmentsViewModel>().onTabChanged(0);
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(CustomPageRoute(page: BookAppointmentPage()));
}
),
HmgServicesComponentModel(11, LocaleKeys.bookAppointment.tr(), "", AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, route: null, onTap: () {
getIt.get<BookAppointmentsViewModel>().onTabChanged(0);
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(CustomPageRoute(page: BookAppointmentPage()));
}),
HmgServicesComponentModel(5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async {
if (getIt.get<AppState>().isAuthenticated) {
getIt.get<NavigationService>().pushPageRoute(AppRoutes.comprehensiveCheckupPage);
@ -164,33 +165,31 @@ class ServicesPage extends StatelessWidget {
}
}),
HmgServicesComponentModel(
3,
LocaleKeys.bloodDonation.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
"".needTranslation,
AppAssets.blood_donation_icon,
bgColor: AppColors.bloodDonationCardColor,
true,
route: null,
onTap: () {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: BloodDonationPage(),
),
);
} else {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: ServiceInfoPage(
serviceName: LocaleKeys.bloodDonation.tr(),
serviceHeader: LocaleKeys.bloodDonationServiceHeader.tr(),
serviceDescription: LocaleKeys.bloodDonationServiceDescription.tr(),
serviceImage: AppAssets.bloodDonationService),
),
);
}
3,
LocaleKeys.bloodDonation.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
"".needTranslation,
AppAssets.blood_donation_icon,
bgColor: AppColors.bloodDonationCardColor,
true,
route: null, onTap: () {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: BloodDonationPage(),
),
);
} else {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: ServiceInfoPage(
serviceName: LocaleKeys.bloodDonation.tr(),
serviceHeader: LocaleKeys.bloodDonationServiceHeader.tr(),
serviceDescription: LocaleKeys.bloodDonationServiceDescription.tr(),
serviceImage: AppAssets.bloodDonationService),
),
);
}
),
}),
HmgServicesComponentModel(
113,
LocaleKeys.servicePriceList.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
@ -375,21 +374,100 @@ class ServicesPage extends StatelessWidget {
),
];
@override
void initState() {
// TODO: implement initState
super.initState();
weatherVM = getIt<WeatherMonitorViewModel>();
weatherVM.fetchCityInfoList();
}
@override
Widget build(BuildContext context) {
bloodDonationViewModel = Provider.of<BloodDonationViewModel>(context);
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context);
hmgServices.removeWhere((element) => Utils.havePrivilege(element.action) == false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.exploreServices.tr(),
isLeading: showBackIcon,
isLeading: widget.showBackIcon,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Container(
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
child: Column(
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: "assets/images/svg/cloudyweather.svg", width: 64.w, height: 64.w).onPress(() {
// locationUtils?.getCurrentLocation();
print(getIt.get<AppState>().userLat);
print(getIt.get<AppState>().userLong);
}),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"August 19, 2025 - Tuesday, 11:25 ".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor),
SizedBox(
height: 4.w,
),
"Cloudy 38oC".toText16(isBold: true, weight: FontWeight.w600),
SizedBox(
height: 4.w,
),
"Health tips based on current weather".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500)
],
),
],
),
SizedBox(height: 16.w),
Container(
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.lightGrayBGColor,
borderRadius: 10.r,
hasShadow: false,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: "assets/images/svg/morning.svg", width: 24.w, height: 24.h),
SizedBox(width: 4.w),
DateTime.now().toLocal().toString().substring(11, 16).toText14(isBold: true),
],
),
SizedBox(width: 25.w),
DateTime.now().toLocal().toString().substring(11, 16).toText14(isBold: true),
SizedBox(width: 25.w),
Row(
children: [
Utils.buildSvgWithAssets(icon: "assets/images/svg/evening.svg", width: 24.w, height: 24.h),
SizedBox(width: 4.w),
DateTime.now().toLocal().toString().substring(11, 16).toText14(isBold: true),
],
),
],
).expanded,
).paddingSymmetrical(0.w, 8.w)
],
),
),
)
],
).paddingSymmetrical(24.w, 0.w),
SizedBox(height: 16.h),
LocaleKeys.medicalAndCareServices.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0),
SizedBox(height: 16.h),
@ -440,173 +518,176 @@ class ServicesPage extends StatelessWidget {
),
),
SizedBox(height: 24.h),
getIt.get<AppState>().isAuthenticated ? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.personalServices.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: Container(
height: 183.h,
width: 183.h,
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 8.w,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false),
LocaleKeys.habibWallet.tr().toText14(isBold: true, maxlines: 2).expanded,
Utils.buildSvgWithAssets(icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward),
],
),
Spacer(),
getIt.get<AppState>().isAuthenticated
? Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), isExpanded: false, letterSpacing: -1)
.toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h);
})
: LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2),
Spacer(),
getIt.get<AppState>().isAuthenticated
? CustomButton(
height: 40.h,
icon: AppAssets.recharge_icon,
iconSize: 24.w,
iconColor: AppColors.infoColor,
textColor: AppColors.infoColor,
text: LocaleKeys.recharge.tr(),
borderWidth: 0.w,
isBold: true,
borderColor: Colors.transparent,
backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w),
fontSize: 14.f,
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage()));
},
)
: SizedBox.shrink(),
],
).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage()));
} else {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
}
}),
),
),
SizedBox(width: 16.w),
Expanded(
child: Container(
height: 183.h,
width: 183.h,
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 8.w,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false),
LocaleKeys.medicalFile.tr().toText16(isBold: true, maxlines: 2).expanded,
Utils.buildSvgWithAssets(icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward),
],
getIt.get<AppState>().isAuthenticated
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.personalServices.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: Container(
height: 183.h,
width: 183.h,
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 8.w,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false),
LocaleKeys.habibWallet.tr().toText14(isBold: true, maxlines: 2).expanded,
Utils.buildSvgWithAssets(icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward),
],
),
Spacer(),
getIt.get<AppState>().isAuthenticated
? Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)),
isExpanded: false, letterSpacing: -1)
.toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h);
})
: LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2),
Spacer(),
getIt.get<AppState>().isAuthenticated
? CustomButton(
height: 40.h,
icon: AppAssets.recharge_icon,
iconSize: 24.w,
iconColor: AppColors.infoColor,
textColor: AppColors.infoColor,
text: LocaleKeys.recharge.tr(),
borderWidth: 0.w,
isBold: true,
borderColor: Colors.transparent,
backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w),
fontSize: 14.f,
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage()));
},
)
: SizedBox.shrink(),
],
).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage()));
} else {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
}
}),
),
Spacer(),
getIt.get<AppState>().isAuthenticated
? Wrap(
spacing: -12.h,
// runSpacing: 0.h,
children: [
Utils.buildImgWithAssets(
icon: AppAssets.babyGirlImg,
height: 32.h,
width: 32.w,
border: 1,
fit: BoxFit.contain,
borderRadius: 50.r,
),
Utils.buildImgWithAssets(
icon: AppAssets.femaleImg,
height: 32.h,
width: 32.w,
border: 1,
borderRadius: 50.r,
fit: BoxFit.contain,
),
Utils.buildImgWithAssets(
icon: AppAssets.maleImg,
height: 32.h,
width: 32.w,
border: 1,
borderRadius: 50.r,
fit: BoxFit.contain,
),
],
)
: LocaleKeys.loginToViewMedicalFile.tr().toText12(isBold: true, maxLine: 2),
Spacer(),
getIt.get<AppState>().isAuthenticated
? CustomButton(
height: 40.h,
icon: AppAssets.add_icon,
iconSize: 24.h,
iconColor: AppColors.primaryRedColor,
textColor: AppColors.primaryRedColor,
text: LocaleKeys.addMember.tr(),
borderWidth: 0.w,
isBold: true,
borderColor: Colors.transparent,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w),
fontSize: 14.f,
onPressed: () {
DialogService dialogService = getIt.get<DialogService>();
medicalFileViewModel.clearAuthValues();
dialogService.showAddFamilyFileSheet(
label: LocaleKeys.addFamilyMember.tr(),
message: LocaleKeys.pleaseFillBelowFieldToAddNewFamilyMember.tr(),
onVerificationPress: () {
medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms);
});
},
)
: SizedBox.shrink(),
],
).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(
CustomPageRoute(
page: MedicalFilePage(),
),
SizedBox(width: 16.w),
Expanded(
child: Container(
height: 183.h,
width: 183.h,
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
);
} else {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
}
}),
),
),
],
).paddingSymmetrical(24.w, 0),
],
) : SizedBox(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 8.w,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false),
LocaleKeys.medicalFile.tr().toText16(isBold: true, maxlines: 2).expanded,
Utils.buildSvgWithAssets(icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward),
],
),
Spacer(),
getIt.get<AppState>().isAuthenticated
? Wrap(
spacing: -12.h,
// runSpacing: 0.h,
children: [
Utils.buildImgWithAssets(
icon: AppAssets.babyGirlImg,
height: 32.h,
width: 32.w,
border: 1,
fit: BoxFit.contain,
borderRadius: 50.r,
),
Utils.buildImgWithAssets(
icon: AppAssets.femaleImg,
height: 32.h,
width: 32.w,
border: 1,
borderRadius: 50.r,
fit: BoxFit.contain,
),
Utils.buildImgWithAssets(
icon: AppAssets.maleImg,
height: 32.h,
width: 32.w,
border: 1,
borderRadius: 50.r,
fit: BoxFit.contain,
),
],
)
: LocaleKeys.loginToViewMedicalFile.tr().toText12(isBold: true, maxLine: 2),
Spacer(),
getIt.get<AppState>().isAuthenticated
? CustomButton(
height: 40.h,
icon: AppAssets.add_icon,
iconSize: 24.h,
iconColor: AppColors.primaryRedColor,
textColor: AppColors.primaryRedColor,
text: LocaleKeys.addMember.tr(),
borderWidth: 0.w,
isBold: true,
borderColor: Colors.transparent,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w),
fontSize: 14.f,
onPressed: () {
DialogService dialogService = getIt.get<DialogService>();
medicalFileViewModel.clearAuthValues();
dialogService.showAddFamilyFileSheet(
label: LocaleKeys.addFamilyMember.tr(),
message: LocaleKeys.pleaseFillBelowFieldToAddNewFamilyMember.tr(),
onVerificationPress: () {
medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms);
});
},
)
: SizedBox.shrink(),
],
).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(
CustomPageRoute(
page: MedicalFilePage(),
),
);
} else {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
}
}),
),
),
],
).paddingSymmetrical(24.w, 0),
],
)
: SizedBox(),
SizedBox(height: 24.h),
LocaleKeys.healthTools.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0),
SizedBox(height: 16.h),

@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -58,7 +60,7 @@ class LabOrderResultItem extends StatelessWidget {
style: TextStyle(
fontSize: 24.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: tests!.checkIfGraphShouldBeDisplayed()
? context.read<LabViewModel>().getColor(
tests?.calculatedResultFlag ?? "",
@ -77,7 +79,7 @@ class LabOrderResultItem extends StatelessWidget {
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,

@ -286,7 +286,7 @@ class _LabResultDetailsState extends State<LabResultDetails> {
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
@ -364,7 +364,7 @@ class _LabResultDetailsState extends State<LabResultDetails> {
value,
style: TextStyle(
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontSize: 8.f,
color: AppColors.textColor,
),
@ -376,7 +376,7 @@ class _LabResultDetailsState extends State<LabResultDetails> {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
label,
style: TextStyle(fontSize: 8.f, fontFamily: 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor),
style: TextStyle(fontSize: 8.f, fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor),
),
);
}

@ -1,5 +1,7 @@
import 'package:flutter/material.dart' ;
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@ -25,7 +27,7 @@ class LabHistoryItem extends StatelessWidget{
style: TextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.labelTextColor
),
),
@ -34,7 +36,7 @@ class LabHistoryItem extends StatelessWidget{
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.textColor
),
)

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -13,14 +14,16 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie
import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart';
import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'dart:ui' as ui;
class MedicalFileAppointmentCard extends StatelessWidget {
class MedicalFileAppointmentCard extends StatefulWidget {
final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel;
final MyAppointmentsViewModel myAppointmentsViewModel;
final Function onRescheduleTap;
@ -34,6 +37,97 @@ class MedicalFileAppointmentCard extends StatelessWidget {
required this.onAskDoctorTap,
});
@override
State<MedicalFileAppointmentCard> createState() => _MedicalFileAppointmentCardState();
}
class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard> {
Timer? _countdownTimer;
Duration? _timeRemaining;
Function(void Function())? _modalSetState;
@override
void initState() {
super.initState();
// Start countdown timer if appointment requires payment
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15) {
_startCountdownTimer();
}
}
@override
void dispose() {
_countdownTimer?.cancel();
super.dispose();
}
void _startCountdownTimer() {
final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate!);
final expiryDate = appointmentDate.subtract(const Duration(hours: 4));
setState(() {
_timeRemaining = expiryDate.difference(DateTime.now());
});
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final newTimeRemaining = expiryDate.difference(DateTime.now());
if (newTimeRemaining.isNegative) {
timer.cancel();
setState(() {
_timeRemaining = Duration.zero;
});
_modalSetState?.call(() {
_timeRemaining = Duration.zero;
});
} else {
setState(() {
_timeRemaining = newTimeRemaining;
});
_modalSetState?.call(() {
_timeRemaining = newTimeRemaining;
});
}
});
}
// Helper method to build each time unit (number + label)
Widget _buildTimeUnit(String value, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
child: value.toText32(
isBold: true,
color: AppColors.blackColor,
isEnglishOnly: true,
),
),
SizedBox(height: 4.h),
Container(
alignment: Alignment.center,
child: label.toText12(
color: AppColors.greyTextColor,
),
),
],
);
}
// Helper method to build time separator (:)
Widget _buildTimeSeparator() {
return Padding(
padding: EdgeInsets.only(bottom: 16.h, left: 6.w, right: 6.w),
child: ':'.toText32(
isBold: true,
color: AppColors.blackColor,
),
);
}
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
@ -43,17 +137,17 @@ class MedicalFileAppointmentCard extends StatelessWidget {
AppCustomChipWidget(
richText: Directionality(
textDirection: ui.TextDirection.ltr,
child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)
.toText12(color: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, isBold: true, isEnglishOnly: true)
child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)
.toText12(color: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, isBold: true, isEnglishOnly: true)
.paddingSymmetrical(8.w, 0),
),
icon: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppAssets.appointment_calendar_icon : AppAssets.alarm_clock_icon,
iconColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor,
icon: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppAssets.appointment_calendar_icon : AppAssets.alarm_clock_icon,
iconColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor,
iconSize: 16.w,
backgroundColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.greyColor : AppColors.secondaryLightRedColor,
textColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor,
backgroundColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.greyColor : AppColors.secondaryLightRedColor,
textColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor,
padding: EdgeInsets.only(top: 12.h, bottom: 12.h, left: 8.w, right: 8.w),
).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading),
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
@ -64,20 +158,20 @@ class MedicalFileAppointmentCard extends StatelessWidget {
Row(
children: [
Image.network(
patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
widget.patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 25.w,
height: 27.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading),
).circle(100).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText14(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "")).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading),
(patientAppointmentHistoryResponseModel.clinicName ?? "")
(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText14(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "")).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
(widget.patientAppointmentHistoryResponseModel.clinicName ?? "")
.toText12(maxLine: 1, isBold: true, color: AppColors.greyTextColor)
.toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading),
.toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
],
),
),
@ -86,42 +180,35 @@ class MedicalFileAppointmentCard extends StatelessWidget {
SizedBox(height: 12.h),
Row(
children: [
myAppointmentsViewModel.isMyAppointmentsLoading
widget.myAppointmentsViewModel.isMyAppointmentsLoading
? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r)
: Expanded(
flex: 7,
child: AppointmentType.isArrived(patientAppointmentHistoryResponseModel)
? getArrivedAppointmentButton(context).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading)
child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading)
: CustomButton(
text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction),
text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction),
onPressed: () {
Navigator.of(context)
.push(CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
))
.then((val) {
// widget.myAppointmentsViewModel.initAppointmentsViewModel();
// widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context);
},
backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.15),
borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01),
textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction),
backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15),
borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction),
iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction),
icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction),
iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
iconSize: 14.h,
).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading),
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
),
SizedBox(width: 8.w),
((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(patientAppointmentHistoryResponseModel))
((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel))
? SizedBox.shrink()
: Expanded(
flex: 2,
@ -145,11 +232,11 @@ class MedicalFileAppointmentCard extends StatelessWidget {
),
),
),
).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() {
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((val) {
@ -168,22 +255,22 @@ class MedicalFileAppointmentCard extends StatelessWidget {
}
Widget getArrivedAppointmentButton(BuildContext context) {
if ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) {
if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) {
return CustomButton(
text: LocaleKeys.viewDetails.tr(context: context),
onPressed: () {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel),
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((_) {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
widget.myAppointmentsViewModel.initAppointmentsViewModel();
widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
},
backgroundColor: AppColors.secondaryLightRedColor,
@ -200,30 +287,10 @@ class MedicalFileAppointmentCard extends StatelessWidget {
iconSize: 16.h,
);
} else {
return
// DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15
// ? CustomButton(
// text: LocaleKeys.askDoctor.tr(context: context),
// onPressed: () {
// onAskDoctorTap();
// },
// backgroundColor: AppColors.secondaryLightRedColor,
// borderColor: AppColors.secondaryLightRedColor,
// textColor: AppColors.primaryRedColor,
// fontSize: 14.f,
// fontWeight: FontWeight.w600,
// borderRadius: 12.r,
// padding: EdgeInsets.symmetric(horizontal: 10.w),
// height: 40.h,
// icon: AppAssets.ask_doctor_icon,
// iconColor: AppColors.primaryRedColor,
// iconSize: 16.h,
// )
// :
CustomButton(
return CustomButton(
text: LocaleKeys.rebook.tr(context: context),
onPressed: () {
onRescheduleTap();
widget.onRescheduleTap();
},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
@ -239,4 +306,141 @@ class MedicalFileAppointmentCard extends StatelessWidget {
);
}
}
Future<void> handleAppointmentNextAction(nextAction, BuildContext context) async {
switch (nextAction) {
case 0:
// No action needed - go to details
Navigator.of(context)
.push(CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
))
.then((val) {});
break;
case 10:
// Confirm appointment - go to details
Navigator.of(context)
.push(CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
))
.then((val) {});
break;
case 15:
// Pending payment - show waiting modal
// Ensure timer is running before showing modal
if (_countdownTimer == null || !_countdownTimer!.isActive) {
_startCountdownTimer();
}
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.notice.tr(),
child: StatefulBuilder(
builder: (context, setModalState) {
// Store the modal setState callback
_modalSetState = setModalState;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Message text
LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(
color: AppColors.textColor,
isCenter: true,
),
SizedBox(height: 24.h),
// Countdown Timer - DD : HH : MM : SS format with labels
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Days
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00',
LocaleKeys.days.tr(context: context),
),
_buildTimeSeparator(),
// Hours
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00',
LocaleKeys.hours.tr(context: context),
),
_buildTimeSeparator(),
// Minutes
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.minutes.tr(context: context),
),
_buildTimeSeparator(),
// Seconds
_buildTimeUnit(
_timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00',
LocaleKeys.seconds.tr(context: context),
),
],
),
SizedBox(height: 24.h),
// Green Acknowledge button with checkmark icon
CustomButton(
text: LocaleKeys.acknowledged.tr(context: context),
onPressed: () {
_modalSetState = null; // Clear callback when closing
Navigator.of(context).pop();
},
backgroundColor: AppColors.successColor,
borderColor: AppColors.successColor,
textColor: Colors.white,
fontSize: 16.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
height: 50.h,
icon: AppAssets.checkmark_icon,
iconColor: Colors.white,
),
],
);
},
),
callBackFunc: () {
_modalSetState = null; // Clear callback when closing
},
isFullScreen: false,
isCloseButtonVisible: true,
);
break;
case 20:
// Pay now - navigate to payment page
widget.myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true);
Navigator.of(context).push(
CustomPageRoute(
page: AppointmentPaymentPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
);
break;
case 50:
// Confirm livecare - go to details
Navigator.of(context)
.push(CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
))
.then((val) {});
break;
case 90:
// Check-in - go to details
Navigator.of(context)
.push(CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
))
.then((val) {});
break;
default:
// Default - go to details
Navigator.of(context)
.push(CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
))
.then((val) {});
}
}
}

@ -398,14 +398,18 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader();
await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) {
await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) async{
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.yourMedicalReportRequestSubmittedSuccessfully.tr(context: context)),
callBackFunc: () {
callBackFunc: () async{
medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
medicalFileViewModel.onMedicalReportTabChange(0);
medicalFileViewModel.getPatientMedicalReportList();
});
await Future.delayed(Duration(milliseconds: 2000)).then((value) {
if (mounted) Navigator.pop(context);
});
}, onError: (err) {
LoaderBottomSheet.hideLoader();

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -9,11 +10,13 @@ import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/core/utils/image_compression_helper.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
@ -24,6 +27,8 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart';
import 'package:hmg_patient_app_new/widgets/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class FamilyCards extends StatefulWidget {
@ -58,6 +63,9 @@ class FamilyCards extends StatefulWidget {
class _FamilyCardsState extends State<FamilyCards> {
AppState appState = getIt<AppState>();
late InsuranceViewModel insuranceViewModel;
late ProfileSettingsViewModel profileSettingsViewModel;
File? _selectedImage;
bool _isUploadingImage = false;
@override
void initState() {
@ -67,6 +75,321 @@ class _FamilyCardsState extends State<FamilyCards> {
super.initState();
}
void _pickImage() {
// Show image picker options without checking permissions first
ImageOptions.showImageOptionsNew(
context,
false, // Don't show files option, only camera and gallery
(base64String, file) async {
try {
print('=== Starting image processing ===');
print('File path: ${file.path}');
print('File exists: ${await file.exists()}');
print('Original file size: ${await file.length() / 1024} KB');
// Compress and resize the image
print('Calling compressAndResizeImage...');
final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file);
File finalFile;
String finalBase64;
if (compressedFile == null) {
print('⚠️ Compression failed - using original file as fallback');
// Fallback: use original image if compression fails
final originalSize = await file.length();
final maxSize = 1048576; // 1MB
if (originalSize > maxSize) {
print('❌ Original file is too large: ${originalSize / 1024} KB');
if (mounted) {
Utils.showToast(
LocaleKeys.imageSizeTooLarge.tr(context: context),
);
}
return;
}
print('✅ Using original file (${originalSize / 1024} KB)');
finalFile = file;
var bytes = await file.readAsBytes();
finalBase64 = base64.encode(bytes);
} else {
// Check compressed file size
final fileSize = await compressedFile.length();
final maxSize = 1048576; // 1MB
print('✅ Compression successful: ${fileSize / 1024} KB');
if (fileSize > maxSize) {
print('❌ Compressed file still too large');
if (mounted) {
Utils.showToast(
LocaleKeys.imageSizeTooLarge.tr(context: context),
);
}
return;
}
finalFile = compressedFile;
var bytes = await compressedFile.readAsBytes();
finalBase64 = base64.encode(bytes);
}
print('Converting to base64... Length: ${finalBase64.length}');
if (mounted) {
setState(() {
_selectedImage = finalFile;
});
print('📤 Starting upload...');
// Upload the image
_uploadImage(finalBase64);
}
print('=== Image processing complete ===');
} catch (e, stackTrace) {
print('❌ Error in _pickImage: $e');
print('Stack trace: $stackTrace');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToProcessImage.tr(context: context),
);
}
}
},
checkCameraPermission: _checkCameraPermission,
checkGalleryPermission: _checkGalleryPermission,
);
}
Future<bool> _checkCameraPermission() async {
try {
print('=== Checking camera permission ===');
// First check current status
PermissionStatus currentStatus = await Permission.camera.status;
print('Current camera permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted) {
print('✅ Camera permission already granted');
return true;
}
// If denied or permanently denied, show settings dialog
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Still denied - show settings dialog
print('⚠️ Camera permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Camera permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking camera permission: $e');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
Future<bool> _checkGalleryPermission() async {
try {
print('=== Checking gallery permission ===');
// Determine which permission to check based on platform and Android version
Permission galleryPermission;
if (Platform.isIOS) {
galleryPermission = Permission.photos;
} else {
// Android: use photos permission which handles API level differences automatically
galleryPermission = Permission.photos;
}
// First check current status
PermissionStatus currentStatus = await galleryPermission.status;
print('Current gallery permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted || currentStatus.isLimited) {
print('✅ Gallery permission already granted');
return true;
}
// If denied or permanently denied, request permission first
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
}
// Still denied - show settings dialog
print('⚠️ Gallery permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Gallery permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking gallery permission: $e');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
void _uploadImage(String base64String) {
final patientID = appState.getAuthenticatedUser()?.patientId;
if (patientID != null) {
setState(() {
_isUploadingImage = true;
});
profileSettingsViewModel.uploadProfileImage(
patientID: patientID,
imageData: base64String,
onSuccess: (data) {
if (mounted) {
setState(() {
_selectedImage = null; // Clear selected image after successful upload
_isUploadingImage = false;
});
Utils.showToast(
LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context),
);
}
},
onError: (error) {
if (mounted) {
setState(() {
_isUploadingImage = false;
});
}
Utils.showToast(error);
},
);
}
}
double _calculateAspectRatio(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
@ -96,6 +419,7 @@ class _FamilyCardsState extends State<FamilyCards> {
widget.profileViewList!.addAll(widget.profiles);
widget.profileViewList!.removeWhere((element) => element.responseId == appState.getAuthenticatedUser()?.patientId);
insuranceViewModel = Provider.of<InsuranceViewModel>(context, listen: false);
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
DialogService dialogService = getIt.get<DialogService>();
if (widget.isRequestDesign) {
return Column(
@ -192,17 +516,61 @@ class _FamilyCardsState extends State<FamilyCards> {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UserAvatarWidget(
width: 56.w,
height: 56.h,
fit: BoxFit.cover,
isCircular: true,
Stack(
children: [
UserAvatarWidget(
width: 56.w,
height: 56.h,
fit: BoxFit.cover,
isCircular: true,
),
// Camera/Edit button
Positioned(
right: 0,
bottom: 0,
child: GestureDetector(
onTap: () {
if (!_isUploadingImage) {
_pickImage();
}
},
child: Container(
width: 20.w,
height: 20.h,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.whiteColor,
width: 1.5.w,
),
),
child: _isUploadingImage
? SizedBox(
width: 10.w,
height: 10.h,
child: CircularProgressIndicator(
strokeWidth: 1.5.w,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.whiteColor,
),
).paddingAll(4.w),
)
: Icon(
Icons.camera_alt,
color: AppColors.whiteColor,
size: 10.w,
),
),
),
),
],
),
SizedBox(width: 8.w),
Column(
@ -311,10 +679,10 @@ class _FamilyCardsState extends State<FamilyCards> {
}
},
backgroundColor: insuranceVM.isInsuranceExpired
? AppColors.primaryRedColor.withOpacity(0.1)
? AppColors.primaryRedColor.withValues(alpha: 0.1)
: insuranceVM.isInsuranceActive
? AppColors.successColor.withOpacity(0.1)
: AppColors.warningColorYellow.withOpacity(0.1),
? AppColors.successColor.withValues(alpha: 0.1)
: AppColors.warningColorYellow.withValues(alpha: 0.1),
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w),
).toShimmer2(isShow: insuranceVM.isInsuranceLoading);
}),
@ -360,8 +728,9 @@ class _FamilyCardsState extends State<FamilyCards> {
gender: profile.gender,
age: profile.age,
fit: BoxFit.cover,
customProfileImageData: "", // Prevent showing logged-in user's image for family members
customProfileImageData: profile.patientImageData,
isCircular: true,
isFamilyMember: true, // Prevent AppState fallback
),
SizedBox(height: 8.h),
(profile.patientName ?? "Unknown").toText14(isBold: true, isCenter: true, maxlines: 1),

@ -422,7 +422,7 @@ class ProfileSettingsState extends State<ProfileSettings> {
}
}
class FamilyCardWidget extends StatelessWidget {
class FamilyCardWidget extends StatefulWidget {
final Function() onAddFamilyMemberPress;
final Function(FamilyFileResponseModelLists member) onFamilySwitchPress;
final FamilyFileResponseModelLists profile;
@ -434,12 +434,62 @@ class FamilyCardWidget extends StatelessWidget {
required this.onFamilySwitchPress(FamilyFileResponseModelLists member),
});
@override
State<FamilyCardWidget> createState() => _FamilyCardWidgetState();
}
class _FamilyCardWidgetState extends State<FamilyCardWidget> {
@override
void initState() {
super.initState();
// Sync ProfileSettingsViewModel with AppState when widget is created
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_syncProfileImageFromAppState();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Also sync when dependencies change (e.g., after user switch)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_syncProfileImageFromAppState();
}
});
}
void _syncProfileImageFromAppState() {
try {
final profileVm = context.read<ProfileSettingsViewModel>();
final appState = getIt.get<AppState>();
final appStateImageData = appState.getProfileImageData;
// Sync profile image from AppState if it's different
if (appStateImageData != null && appStateImageData.isNotEmpty) {
if (profileVm.profileImageData != appStateImageData) {
print("🔄 Syncing profile image from AppState to ProfileSettingsViewModel");
profileVm.syncProfileImageFromAppState();
}
} else if (profileVm.profileImageData != null) {
// AppState is null but ViewModel has data - clear it
print("🧹 Clearing ProfileSettingsViewModel image data (AppState is null)");
profileVm.clearProfileImageCache();
}
} catch (e) {
print("⚠️ Error syncing profile image: $e");
}
}
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId);
final isActive = (widget.profile.responseId == appState.getAuthenticatedUser()?.patientId);
// final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
// final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID);
// final canSwitch = isParentUser || (!isParentUser && widget.profile.responseId == appState.getSuperUserID);
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
@ -453,19 +503,44 @@ class FamilyCardWidget extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UserAvatarWidget(
width: 56.w,
height: 56.h,
gender: profile.gender,
age: profile.age,
fit: BoxFit.cover,
isCircular: true,
// Wrap in Consumer to rebuild when ProfileSettingsViewModel changes
Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
final currentUserId = appState.getAuthenticatedUser()?.patientId;
final isCurrentUser = widget.profile.responseId == currentUserId;
if (isCurrentUser) {
// Current user - use AppState directly (like home screen profile icon)
return UserAvatarWidget(
width: 56.w,
height: 56.h,
gender: widget.profile.gender,
age: widget.profile.age,
fit: BoxFit.cover,
// Don't pass customProfileImageData - let it use AppState
isCircular: true,
// Don't pass isFamilyMember - allow AppState fallback
);
} else {
// Family member - use their specific image data
return UserAvatarWidget(
width: 56.w,
height: 56.h,
gender: widget.profile.gender,
age: widget.profile.age,
fit: BoxFit.cover,
customProfileImageData: widget.profile.patientImageData,
isCircular: true,
isFamilyMember: true, // Prevent AppState fallback
);
}
},
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
"${profile.patientName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: true),
"${widget.profile.patientName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: true),
Wrap(
direction: Axis.horizontal,
spacing: 4.w,
@ -474,7 +549,7 @@ class FamilyCardWidget extends StatelessWidget {
AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w),
icon: AppAssets.file_icon,
labelText: "${LocaleKeys.fileno.tr(context: context)}: ${profile.responseId}",
labelText: "${LocaleKeys.fileno.tr(context: context)}: ${widget.profile.responseId}",
iconSize: 12.w,
),
isActive ? AppCustomChipWidget(
@ -488,7 +563,7 @@ class FamilyCardWidget extends StatelessWidget {
],
).expanded,
// Icon(Icons.qr_code, size: 56.h)
Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w)
Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${widget.profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w)
],
),
SizedBox(height: 4.h),
@ -499,7 +574,7 @@ class FamilyCardWidget extends StatelessWidget {
runSpacing: 4.h,
children: [
AppCustomChipWidget(
labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': profile.age.toString(), 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}),
labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': widget.profile.age.toString(), 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}),
),
// isActive && appState.getAuthenticatedUser()!.bloodGroup != null
// ?
@ -598,12 +673,12 @@ class FamilyCardWidget extends StatelessWidget {
}
Widget _buildParentUserButton(int? currentUserId) {
final canSwitch = profile.responseId != currentUserId;
final canSwitch = widget.profile.responseId != currentUserId;
return CustomButton(
icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family,
text: canSwitch ? LocaleKeys.switchAccount.tr() : LocaleKeys.addANewFamilyMember.tr(),
onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress,
onPressed: canSwitch ? () => widget.onFamilySwitchPress(widget.profile) : widget.onAddFamilyMemberPress,
backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor,
borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor,
textColor: canSwitch ? AppColors.primaryRedColor : Colors.white,
@ -614,7 +689,7 @@ class FamilyCardWidget extends StatelessWidget {
}
Widget _buildNonParentUserButton(int? superUserId) {
final canSwitchBack = superUserId != null && superUserId == profile.responseId;
final canSwitchBack = superUserId != null && superUserId == widget.profile.responseId;
return CustomButton(
icon: AppAssets.switch_user,
@ -623,7 +698,7 @@ class FamilyCardWidget extends StatelessWidget {
borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200,
textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor,
iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor,
onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {},
onPressed: canSwitchBack ? () => widget.onFamilySwitchPress(widget.profile) : () {},
height: isFoldable ? 50.h : 40.h,
fontSize: 14.f,
).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h);

@ -56,8 +56,9 @@ class FamilyCardWidget extends StatelessWidget {
gender: profile.gender,
age: profile.age,
fit: BoxFit.cover,
customProfileImageData: "",
isCircular: true,// Prevent showing logged-in user's image for family members
customProfileImageData: profile.patientImageData,
isCircular: true,
isFamilyMember: true, // Prevent AppState fallback
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

@ -28,34 +28,100 @@ class ProfilePictureWidget extends StatefulWidget {
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
final AppState _appState = getIt.get<AppState>();
File? _selectedImage;
int? _currentPatientId;
@override
void initState() {
super.initState();
_currentPatientId = _appState.getAuthenticatedUser()?.patientId;
print('🎬 ProfilePictureWidget initState - patient: $_currentPatientId');
// Use addPostFrameCallback to ensure widget is built before loading
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final profileVm = context.read<ProfileSettingsViewModel>();
final patientID = _appState.getAuthenticatedUser()?.patientId;
// Only sync if AppState has data and ViewModel doesn't
if (_appState.getProfileImageData != null &&
_appState.getProfileImageData!.isNotEmpty &&
(profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) {
profileVm.syncProfileImageFromAppState();
if (patientID == null) {
print('⚠️ No authenticated user found');
return;
}
// Only load if no data exists in both AppState and ViewModel
if ((_appState.getProfileImageData == null || _appState.getProfileImageData!.isEmpty) &&
(profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) {
_loadProfileImage();
// Check if we have data in AppState that matches current user
final appStateImageData = _appState.getProfileImageData;
if (appStateImageData != null && appStateImageData.isNotEmpty) {
// Sync to ViewModel if it doesn't have data
if (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty) {
print('🔄 Syncing AppState data to ViewModel');
profileVm.syncProfileImageFromAppState();
}
} else {
// No cached data - load from API
print('📥 No cached data - loading from API for patient: $patientID');
_loadProfileImage(forceRefresh: false);
}
});
}
void _loadProfileImage() {
// Check if profile image is already loaded in AppState
if (_appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_checkAndUpdateUserImage();
}
@override
void didUpdateWidget(ProfilePictureWidget oldWidget) {
super.didUpdateWidget(oldWidget);
_checkAndUpdateUserImage();
}
void _checkAndUpdateUserImage() {
// Check if the authenticated user has changed (family member switch)
final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
if (currentPatientId != null && currentPatientId != _currentPatientId) {
print('🔄 User switched detected: $_currentPatientId -> $currentPatientId');
// Update patient ID IMMEDIATELY before any other operations
final oldPatientId = _currentPatientId;
_currentPatientId = currentPatientId;
// Clear the old profile image data
try {
final profileVm = context.read<ProfileSettingsViewModel>();
print('🧹 Clearing cache for old user: $oldPatientId');
profileVm.clearProfileImageCache();
print('📥 Loading profile image for new user: $currentPatientId');
// Load the new user's profile image immediately
profileVm.getProfileImage(
patientID: currentPatientId,
forceRefresh: true,
onSuccess: (data) {
print('✅ Profile image loaded successfully for user: $currentPatientId');
if (mounted) {
setState(() {}); // Force rebuild to show new data
}
},
onError: (error) {
print('❌ Error loading profile image: $error');
if (mounted) {
setState(() {}); // Force rebuild to show default avatar
}
},
);
} catch (e) {
print('❌ Error in _checkAndUpdateUserImage: $e');
}
}
}
void _loadProfileImage({bool forceRefresh = false}) {
// Check if profile image is already loaded in AppState (skip if forcing refresh)
if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
// Image already loaded, no need to call API
return;
}
@ -64,12 +130,16 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID != null) {
print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)');
profileVm.getProfileImage(
patientID: patientID,
forceRefresh: forceRefresh,
onSuccess: (data) {
print('✅ Profile image loaded successfully');
// Image loaded successfully
},
onError: (error) {
print('❌ Error loading profile image: $error');
// Error loading image
},
);
@ -383,8 +453,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
}
Widget _buildProfileImage(ProfileSettingsViewModel profileVm) {
final gender = _appState.getAuthenticatedUser()?.gender ?? 1;
final age = _appState.getAuthenticatedUser()?.age ?? 0;
// Always get fresh user data
final currentUser = _appState.getAuthenticatedUser();
final gender = currentUser?.gender ?? 1;
final age = currentUser?.age ?? 0;
// Determine the default image based on gender and age
final String defaultImage;
@ -396,7 +468,7 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
defaultImage = age < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg;
}
// Show selected image if available
// Show selected image if available (only during upload)
if (_selectedImage != null) {
return ClipOval(
child: Image.file(
@ -408,9 +480,12 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
);
}
// Check both ViewModel and AppState for profile image
// Use ViewModel data if available, otherwise fall back to AppState
// This ensures we show the current logged-in user's image (same as homepage profile icon)
final String? imageData = profileVm.profileImageData ?? _appState.getProfileImageData;
print('🖼️ Building profile image - has data: ${imageData != null && imageData.isNotEmpty}, patient: ${currentUser?.patientId}');
// Show uploaded image if available
if (imageData != null && imageData.isNotEmpty) {
try {
@ -424,7 +499,7 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
),
);
} catch (e) {
print('Error decoding profile image: $e');
print('Error decoding profile image: $e');
// If decoding fails, show default image
return Image.asset(
defaultImage,
@ -434,7 +509,8 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
}
}
// Show default image
// Show default image (no image data or user has no uploaded image)
print('📷 Showing default avatar for user ${currentUser?.patientId}');
return Image.asset(
defaultImage,
width: 136.w,
@ -444,6 +520,13 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
@override
Widget build(BuildContext context) {
// Check for user change on every build (handles navigation scenarios)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_checkAndUpdateUserImage();
}
});
return Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
return Center(

@ -21,6 +21,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -439,12 +440,25 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
SizedBox(height: 16.h),
CustomButton(
text: LocaleKeys.viewReport.tr(),
onPressed: () {
model.navigationService.push(
CustomPageRoute(
page: RadiologyResultPage(patientRadiologyResponseModel: group),
),
);
onPressed: () async {
// Check if reportData is empty or null
if (group.reportData == null || group.reportData!.trim().isEmpty) {
// Open image directly
await model.getRadiologyImage(patientRadiologyResponseModel: group);
if (model.radiologyImageURL.isNotEmpty) {
Uri uri = Uri.parse(model.radiologyImageURL);
await launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: "");
} else {
Utils.showToast(LocaleKeys.noDataAvailable.tr());
}
} else {
// Navigate to details page
model.navigationService.push(
CustomPageRoute(
page: RadiologyResultPage(patientRadiologyResponseModel: group),
),
);
}
},
backgroundColor: AppColors.infoColor.withAlpha(20),
borderColor: AppColors.infoColor.withAlpha(0),

@ -125,7 +125,7 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
fontSize: 13.f,
fontWeight: FontWeight.w600,
color: AppColors.greyInfoTextColor,
fontFamily: isArabic ? 'GESSTwo' : 'Poppins'
fontFamily: isArabic ? 'CairoArabic' : 'Poppins'
),
children: [
TextSpan(

@ -468,14 +468,14 @@ class _TriagePageState extends State<TriagePage> {
text: TextSpan(
text: LocaleKeys.possibleSymptom.tr(context: context),
style: TextStyle(
color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'GESSTwo' : 'Poppins'),
color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'),
children: [
TextSpan(
text: suggestedCondition,
style: TextStyle(
color: AppColors.textColor,
fontWeight: FontWeight.w600,
fontSize: 14.f, fontFamily: isArabic ? 'GESSTwo' : 'Poppins'),
fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'),
),
],
),
@ -491,7 +491,7 @@ class _TriagePageState extends State<TriagePage> {
text: TextSpan(
text: "${probability.toStringAsFixed(1)}% ",
style: TextStyle(
fontFamily: 'Poppins',
fontFamily: isArabic ? 'CairoArabic' : 'Poppins',
color: AppColors.primaryRedColor,
fontWeight: FontWeight.w600,
fontSize: 14.f,
@ -503,7 +503,7 @@ class _TriagePageState extends State<TriagePage> {
color: AppColors.textColor,
fontWeight: FontWeight.w600,
fontSize: 13.f,
fontFamily: isArabic ? 'GESSTwo' : 'Poppins',
fontFamily: isArabic ? 'CairoArabic' : 'Poppins',
),
),
],

@ -196,7 +196,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
@ -335,7 +335,6 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
Widget _buildHistoryGraph(List<DataPoint> history, {List<DataPoint>? secondaryHistory}) {
final minY = _minY(history, secondaryHistory: secondaryHistory);
final maxY = _maxY(history, secondaryHistory: secondaryHistory);
final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title);
final isRTL = getIt.get<AppState>().isArabic();
return CustomGraph(
@ -344,44 +343,43 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
makeGraphBasedOnActualValue: true,
leftLabelReservedSize: 40,
showGridLines: true,
showShadow: true,
showShadow: false,
isRTL: isRTL,
leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory),
maxY: maxY,
minY: minY,
maxX: history.length.toDouble() - .75,
horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory),
horizontalInterval: .01,
leftLabelFormatter: (value) {
// Show only numeric labels at regular intervals
return _axisLabel(value.toStringAsFixed(0));
// Format value to 2 decimal places for consistency
value = double.parse(value.toStringAsFixed(2));
return _axisLabel(value.toStringAsFixed(2));
},
getDrawingHorizontalLine: (value) {
// Draw reference lines for high/low bounds
value = double.parse(value.toStringAsFixed(2));
// Draw solid reference lines for high/low bounds (matching lab result style)
if (args.high != null && (value - args.high!).abs() < 0.1) {
return FlLine(
color: AppColors.bgGreenColor.withOpacity(0.2),
color: AppColors.bgGreenColor.withValues(alpha: 0.6),
strokeWidth: 1,
dashArray: [5, 5],
);
}
if (args.low != null && (value - args.low!).abs() < 0.1) {
return FlLine(
color: AppColors.bgGreenColor.withOpacity(0.2),
color: AppColors.bgGreenColor.withValues(alpha: 0.6),
strokeWidth: 1,
dashArray: [5, 5],
);
}
// Draw grid lines at intervals
// Hide other grid lines
return FlLine(
color: AppColors.bgGreenColor.withOpacity(0.2),
color: Colors.transparent,
strokeWidth: 1,
dashArray: [5, 5],
);
},
graphColor: AppColors.bgGreenColor,
secondaryGraphColor: AppColors.blueColor,
graphShadowColor: AppColors.lightGreenColor.withOpacity(.4),
graphGridColor: scheme.iconFg,
graphColor: AppColors.textColor.withValues(alpha: 90 / 255),
secondaryGraphColor: AppColors.textColor.withValues(alpha: 90 / 255),
graphShadowColor: AppColors.greyColor.withValues(alpha: 0.5),
graphGridColor: AppColors.textColor.withValues(alpha: 0.4),
bottomLabelFormatter: (value, data) {
if (data.isEmpty) return const SizedBox.shrink();
if (value == 0) return _bottomLabel(data[value.toInt()].label);
@ -497,32 +495,35 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
final List<HorizontalRangeAnnotation> ranges = [];
// Below low reference (transparent - matching lab result style)
if (args.low != null) {
ranges.add(
HorizontalRangeAnnotation(
y1: minY,
y2: args.low!,
color: AppColors.highAndLow.withOpacity(0.05),
color: Colors.transparent,
),
);
}
// Normal range (light green shading - matching lab result style)
if (args.low != null && args.high != null) {
ranges.add(
HorizontalRangeAnnotation(
y1: args.low!,
y2: args.high!,
color: AppColors.bgGreenColor.withOpacity(0.05),
color: AppColors.bgGreenColor.withValues(alpha: 0.05),
),
);
}
// Above high reference (transparent - matching lab result style)
if (args.high != null) {
ranges.add(
HorizontalRangeAnnotation(
y1: args.high!,
y2: maxY,
color: AppColors.criticalLowAndHigh.withOpacity(0.05),
color: Colors.transparent,
),
);
}
@ -755,7 +756,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
value,
style: TextStyle(
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontSize: 8.f,
color: AppColors.textColor,
),

@ -90,6 +90,16 @@ class AppColors {
static Color get myChildVaccineCardColor => isDarkMode ? dark.myChildVaccineCardColor : const Color(0xFFFF2D78);
static Color get bookAppointment => isDarkMode ? dark.bookAppointment : const Color(0xFF415364);
// Health Calculators Colors
static Color get bmiCalculatorColor => isDarkMode ? dark.bmiCalculatorColor : const Color(0xFFF43333);
static Color get caloriesCalculatorColor => isDarkMode ? dark.caloriesCalculatorColor : const Color(0xFF0B85F7);
static Color get bmrCalculatorColor => isDarkMode ? dark.bmrCalculatorColor : const Color(0xFF18C273);
static Color get idealBodyWeightColor => isDarkMode ? dark.idealBodyWeightColor : const Color(0xFFFF8012);
static Color get bodyFatCalculatorColor => isDarkMode ? dark.bodyFatCalculatorColor : const Color(0xFFFF5662);
static Color get carbsProteinFatColor => isDarkMode ? dark.carbsProteinFatColor : const Color(0xFF18C273);
static Color get ovulationPeriodColor => isDarkMode ? dark.ovulationPeriodColor : const Color(0xFFFF47A9);
static Color get deliveryDueDateColor => isDarkMode ? dark.deliveryDueDateColor : const Color(0xFFFF2D78);
// Water Monitor
static Color get blueColor => isDarkMode ? dark.blueColor : const Color(0xFF4EB5FF);
static Color get blueGradientColorOne => isDarkMode ? dark.blueGradientColorOne : const Color(0xFFF1F7FD);
@ -217,6 +227,16 @@ class AppColorsDark {
Color get myChildVaccineCardColor => const Color(0xFFFF2D78);
Color get bookAppointment => const Color(0xFF415364);
// Health Calculators
Color get bmiCalculatorColor => const Color(0xFFF43333);
Color get caloriesCalculatorColor => const Color(0xFF0B85F7);
Color get bmrCalculatorColor => const Color(0xFF18C273);
Color get idealBodyWeightColor => const Color(0xFFFF8012);
Color get bodyFatCalculatorColor => const Color(0xFFFF5662);
Color get carbsProteinFatColor => const Color(0xFF18C273);
Color get ovulationPeriodColor => const Color(0xFFFF47A9);
Color get deliveryDueDateColor => const Color(0xFFFF2D78);
// Water Monitor
Color get blueColor => const Color(0xFF4EB5FF);
Color get blueGradientColorOne => const Color(0xFF101C2A);

@ -20,7 +20,12 @@ class BottomSheetUtils {
showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted,
{required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async {
if (Platform.isAndroid) {
if (await PermissionService.isCalendarPermissionEnabled()) {
bool hasPermission = await PermissionService.isCalendarPermissionEnabled();
if (!hasPermission) {
PermissionStatus status = await Permission.calendarFullAccess.request();
hasPermission = status.isGranted;
}
if (hasPermission) {
_showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted,
onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed);
} else {

@ -301,9 +301,11 @@ class TextInputWidget extends StatelessWidget {
return Directionality(
textDirection: isArabic ? TextDirection.rtl : TextDirection.ltr,
child: Localizations.override(
context: context,
locale: const Locale('en', 'US'), // Force English locale for TextField
child: TextField(
hintLocales: const [Locale('en', 'US')],
enabled: isEnable,
scrollPadding: EdgeInsets.zero,
@ -344,6 +346,7 @@ class TextInputWidget extends StatelessWidget {
isDense: true,
hintText: hintText,
hintStyle: TextStyle(
fontFamily: isArabic ? 'CairoArabic' : 'Poppins',
fontSize: 14.f,
height: 21 / 16,
@ -353,7 +356,7 @@ class TextInputWidget extends StatelessWidget {
),
prefixIconConstraints: BoxConstraints(minWidth: 30.h),
prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, isBold: true),
contentPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.only(right:isArabic ? 10.w :0),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,

@ -14,6 +14,7 @@ class UserAvatarWidget extends StatelessWidget {
final bool isCircular;
final double? borderRadius;
final String? customProfileImageData; // For family members or other users
final bool isFamilyMember; // New flag to prevent AppState fallback
const UserAvatarWidget({
Key? key,
@ -25,6 +26,7 @@ class UserAvatarWidget extends StatelessWidget {
this.isCircular = false,
this.borderRadius,
this.customProfileImageData,
this.isFamilyMember = false, // Default to false for backward compatibility
}) : super(key: key);
@override
@ -33,9 +35,17 @@ class UserAvatarWidget extends StatelessWidget {
final userGender = gender ?? appState.getAuthenticatedUser()?.gender ?? 1;
final userAge = age ?? appState.getAuthenticatedUser()?.age ?? 0;
// Use custom profile image data if provided, otherwise use AppState data
final profileImageData = customProfileImageData ?? appState.getProfileImageData;
// Determine which profile image data to use:
// - For family members: ONLY use customProfileImageData (no AppState fallback)
// - For main user: Use customProfileImageData if provided, otherwise AppState
final String? profileImageData;
if (isFamilyMember) {
// Family member mode: only use their specific image data, no fallback
profileImageData = customProfileImageData;
} else {
// Main user mode: use custom data or fall back to AppState
profileImageData = customProfileImageData ?? appState.getProfileImageData;
}
// Determine the default image based on gender and age
final String defaultImage;

Loading…
Cancel
Save