Compare commits

..

9 Commits

@ -0,0 +1,5 @@
<svg width="15" height="18" viewBox="0 0 15 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3341 2.73593C13.3465 0.996493 11.1438 0.387116 9.41507 1.40204L1.9783 5.76797C0.259805 6.77686 -0.316545 8.99649 0.665687 10.7264C1.25916 11.7717 2.29147 12.4098 3.39807 12.5307C3.79951 12.5746 4.16073 12.2868 4.20487 11.888C4.24902 11.4891 3.95937 11.1302 3.55794 11.0864C2.90888 11.0155 2.2965 10.6413 1.93949 10.0125C1.34085 8.95816 1.70266 7.61757 2.7223 7.01897L5.81204 5.20506L6.50525 6.3668C6.7112 6.71196 7.15978 6.82588 7.50718 6.62126C7.85458 6.41663 7.96924 5.97095 7.76329 5.62579L7.07116 4.46586L10.1591 2.65303C11.1684 2.06046 12.4671 2.40504 13.0603 3.44987C13.5393 4.29342 13.4044 5.3279 12.8 6.01304C12.5338 6.31478 12.5642 6.7738 12.8679 7.03829C13.1716 7.30278 13.6336 7.27258 13.8998 6.97083C14.9042 5.83238 15.1252 4.1292 14.3341 2.73593Z" fill="#ED1C2B"/>
<path d="M13.7153 8.93673C13.6985 9.04274 13.5953 9.10233 13.3889 9.22151L5.54435 13.7505C5.33793 13.8697 5.23473 13.9293 5.13452 13.8908C5.03432 13.8523 4.99968 13.7456 4.9304 13.5322C4.20822 11.3077 5.10997 8.80563 7.21681 7.58925C9.32364 6.37287 11.9414 6.84295 13.5068 8.58065C13.657 8.74735 13.732 8.83071 13.7153 8.93673Z" fill="#ED1C2B"/>
<path d="M14.3613 10.0554C14.2611 10.017 14.1579 10.0765 13.9515 10.1957L6.10671 14.7249C5.90032 14.8441 5.79713 14.9036 5.78034 15.0096C5.76356 15.1156 5.83862 15.199 5.98875 15.3657C7.55409 17.1039 10.1722 17.5743 12.2793 16.3578C14.3864 15.1412 15.2881 12.6387 14.5654 10.4139C14.4961 10.2006 14.4615 10.0939 14.3613 10.0554Z" fill="#ED1C2B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M23 14.75C22.3096 14.75 21.75 15.3096 21.75 16C21.75 16.6904 22.3096 17.25 23 17.25C23.6904 17.25 24.25 16.6904 24.25 16C24.25 15.3096 23.6904 14.75 23 14.75Z" fill="#8F9AA3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25 20C9.25 14.0629 14.0629 9.25 20 9.25C25.9371 9.25 30.75 14.0629 30.75 20C30.75 25.9371 25.9371 30.75 20 30.75C14.0629 30.75 9.25 25.9371 9.25 20ZM20.25 16C20.25 14.4812 21.4812 13.25 23 13.25C24.5188 13.25 25.75 14.4812 25.75 16C25.75 17.5188 24.5188 18.75 23 18.75C21.4812 18.75 20.25 17.5188 20.25 16ZM13.6495 21.2591C14.432 22.1485 15.7722 21.7914 16.2203 20.7727L16.4401 21.021C16.7157 21.3343 17.1898 21.3619 17.499 21.0828C17.8082 20.8036 17.8355 20.3234 17.5599 20.0102L17.3505 19.7721C16.568 18.8827 15.2277 19.2399 14.7796 20.2586L14.5599 20.0102C14.2843 19.697 13.8103 19.6693 13.501 19.9485C13.1918 20.2276 13.1645 20.7078 13.4401 21.021L13.6495 21.2591ZM22.3872 22.2861C22.7816 22.4128 22.9986 22.8351 22.8719 23.2295L22.7611 23.5746C22.7375 23.638 22.7358 23.7627 22.9187 23.7545C24.1221 23.6247 25.0403 24.7168 24.6653 25.8844L24.5545 26.2295C24.4279 26.6239 24.0055 26.8409 23.6111 26.7143C23.2167 26.5876 22.9997 26.1653 23.1264 25.7709L23.2372 25.4258C23.2886 25.2656 23.1536 25.2391 23.0796 25.2459C21.8762 25.3757 20.958 24.2836 21.333 23.116L21.4438 22.7709C21.5704 22.3765 21.9928 22.1595 22.3872 22.2861ZM26.9955 20C26.4457 20 26 20.4477 26 21C26 21.5523 26.4457 22 26.9955 22H27.0045C27.5543 22 28 21.5523 28 21C28 20.4477 27.5543 20 27.0045 20H26.9955ZM16 25C16 24.4477 16.4457 24 16.9955 24H17.0045C17.5543 24 18 24.4477 18 25C18 25.5523 17.5543 26 17.0045 26H16.9955C16.4457 26 16 25.5523 16 25ZM15.9955 15C15.4457 15 15 15.4477 15 16C15 16.5523 15.4457 17 15.9955 17H16.0045C16.5543 17 17 16.5523 17 16C17 15.4477 16.5543 15 16.0045 15H15.9955Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M20.75 12.5C20.75 12.0858 20.4142 11.75 20 11.75C19.5858 11.75 19.25 12.0858 19.25 12.5V13.25H18.5C18.0858 13.25 17.75 13.5858 17.75 14C17.75 14.4142 18.0858 14.75 18.5 14.75H19.25V15.5C19.25 15.9142 19.5858 16.25 20 16.25C20.4142 16.25 20.75 15.9142 20.75 15.5V14.75H21.5C21.9142 14.75 22.25 14.4142 22.25 14C22.25 13.5858 21.9142 13.25 21.5 13.25H20.75V12.5Z" fill="#8F9AA3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.948 9.25H20.052C20.9505 9.24997 21.6997 9.24995 22.2945 9.32991C22.9223 9.41432 23.4891 9.59999 23.9445 10.0555C24.4 10.5109 24.5857 11.0777 24.6701 11.7055C24.695 11.8911 24.7122 12.0917 24.724 12.3072C25.8622 12.4348 26.7867 12.726 27.506 13.4474C28.1807 14.1241 28.476 14.9793 28.6152 16.018C28.75 17.0237 28.75 18.3062 28.75 19.9137V28.75H29C29.5523 28.75 30 29.1977 30 29.75C30 30.3023 29.5523 30.75 29 30.75H11C10.4477 30.75 10 30.3023 10 29.75C10 29.1977 10.4477 28.75 11 28.75H11.25V19.9137C11.25 18.3062 11.25 17.0237 11.3848 16.018C11.524 14.9793 11.8193 14.1241 12.494 13.4474C13.2133 12.726 14.1378 12.4348 15.276 12.3072C15.2878 12.0917 15.305 11.8911 15.3299 11.7055C15.4143 11.0777 15.6 10.5109 16.0555 10.0555C16.5109 9.59999 17.0777 9.41432 17.7055 9.32991C18.3003 9.24995 19.0495 9.24997 19.948 9.25ZM17.9054 10.8165C17.4439 10.8786 17.2464 10.9858 17.1161 11.1161C16.9858 11.2464 16.8786 11.4439 16.8165 11.9054C16.7516 12.3884 16.75 13.036 16.75 14C16.75 14.964 16.7516 15.6116 16.8165 16.0946C16.8786 16.5561 16.9858 16.7536 17.1161 16.8839C17.2464 17.0142 17.4439 17.1214 17.9054 17.1835C18.3884 17.2484 19.036 17.25 20 17.25C20.964 17.25 21.6116 17.2484 22.0946 17.1835C22.5561 17.1214 22.7536 17.0142 22.8839 16.8839C23.0142 16.7536 23.1214 16.5561 23.1835 16.0946C23.2484 15.6116 23.25 14.964 23.25 14C23.25 13.036 23.2484 12.3884 23.1835 11.9054C23.1214 11.4439 23.0142 11.2464 22.8839 11.1161C22.7536 10.9858 22.5561 10.8786 22.0946 10.8165C21.6116 10.7516 20.964 10.75 20 10.75C19.036 10.75 18.3884 10.7516 17.9054 10.8165ZM23.25 28.75V27.4678C23.25 27.028 23.25 26.6486 23.2218 26.3374C23.192 26.0082 23.1259 25.6822 22.9486 25.375C22.7511 25.033 22.467 24.7489 22.125 24.5514C21.8178 24.3741 21.4918 24.308 21.1627 24.2782C20.8514 24.25 20.472 24.25 20.0323 24.25C19.5925 24.25 19.1486 24.25 18.8374 24.2782C18.5082 24.308 18.1822 24.3741 17.875 24.5514C17.533 24.7489 17.2489 25.033 17.0514 25.375C16.8741 25.6822 16.808 26.0082 16.7782 26.3374C16.75 26.6486 16.75 27.028 16.75 27.4677V28.75H18.25V27.5C18.25 27.0189 18.2507 26.7082 18.2721 26.4727C18.3015 26.2392 18.4223 25.7721 18.9727 25.7721C19.2082 25.7507 19.5189 25.75 20 25.75C20.4811 25.75 20.7918 25.7507 21.0273 25.7721C21.2608 25.7843 21.7279 25.9415 21.7279 26.4727C21.7493 26.7082 21.75 27.0189 21.75 27.5V28.75H23.25ZM15 21C15 20.4477 15.4477 20 16 20H16.009C16.5612 20 17.009 20.4477 17.009 21C17.009 21.5523 16.5612 22 16.009 22H16C15.4477 22 15 21.5523 15 21ZM18.991 21C18.991 20.4477 19.4388 20 19.991 20H20C20.5523 20 21 20.4477 21 21C21 21.5523 20.5523 22 20 22H19.991C19.4388 22 18.991 21.5523 18.991 21ZM22.9922 21C22.9922 20.4477 23.4399 20 23.9922 20H24.0011C24.5534 20 25.0011 20.4477 25.0011 21C25.0011 21.5523 24.5534 22 24.0011 22H23.9922C23.4399 22 22.9922 21.5523 22.9922 21Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M18.5303 20.4696C18.3897 20.329 18.1989 20.25 18 20.25C17.7425 20.25 17.4921 20.25 17.25 20.2507V24.3535C18.4043 24.6799 19.25 25.7412 19.25 27V28C19.25 28.4142 18.9142 28.75 18.5 28.75C18.0858 28.75 17.75 28.4142 17.75 28V27C17.75 26.3096 17.1904 25.75 16.5 25.75C15.8096 25.75 15.25 26.3096 15.25 27V28C15.25 28.4142 14.9142 28.75 14.5 28.75C14.0858 28.75 13.75 28.4142 13.75 28V27C13.75 25.7412 14.5957 24.6799 15.75 24.3535V20.2764C15.3284 20.2933 14.9463 20.321 14.6083 20.3665C13.7081 20.4875 12.9503 20.7464 12.3484 21.3483C11.7464 21.9502 11.4875 22.7081 11.3665 23.6082C11.25 24.4752 11.25 25.5775 11.25 26.9451V30C11.25 30.4142 11.5858 30.75 12 30.75H28C28.4142 30.75 28.75 30.4142 28.75 30V26.9451C28.75 25.5775 28.75 24.4752 28.6335 23.6082C28.5125 22.7081 28.2536 21.9502 27.6517 21.3483C27.0497 20.7464 26.2919 20.4875 25.3918 20.3665C25.1928 20.3397 24.9786 20.3191 24.75 20.3033V25.9506C25.1984 26.21 25.5 26.6948 25.5 27.25C25.5 28.0784 24.8284 28.75 24 28.75C23.1716 28.75 22.5 28.0784 22.5 27.25C22.5 26.6948 22.8017 26.21 23.25 25.9506V20.2538C22.8569 20.25 22.4396 20.25 22.0005 20.25C21.8016 20.25 21.6103 20.329 21.4697 20.4696L20 21.9393L18.5303 20.4696Z" fill="#8F9AA3"/>
<path d="M20 9.25C17.6528 9.25 15.75 11.1528 15.75 13.5V14.5C15.75 16.8472 17.6528 18.75 20 18.75C22.3472 18.75 24.25 16.8472 24.25 14.5V13.5C24.25 11.1528 22.3472 9.25 20 9.25Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 17.5869C11.75 13.0186 15.5222 9.25 20 9.25C24.4778 9.25 28.25 13.0186 28.25 17.5869C28.25 22.2513 24.4018 25.4121 21.1696 27.4294L21.1583 27.4365L21.1467 27.4431C20.7978 27.644 20.4026 27.75 20 27.75C19.5974 27.75 19.2022 27.644 18.8533 27.4431L18.84 27.4355L18.8271 27.4273C15.6075 25.3945 11.75 22.2671 11.75 17.5869ZM16.5 17.5C16.5 15.567 18.067 14 20 14C21.933 14 23.5 15.567 23.5 17.5C23.5 19.433 21.933 21 20 21C18.067 21 16.5 19.433 16.5 17.5Z" fill="#8F9AA3"/>
<path d="M14.9958 27.6578C14.9493 27.1487 14.5212 26.75 14 26.75C13.4477 26.75 13 27.1977 13 27.75C13 28.4639 13.4349 28.9848 13.8525 29.3139C14.2814 29.6519 14.84 29.9125 15.4411 30.1129C16.6538 30.5171 18.2656 30.75 20 30.75C21.7344 30.75 23.3462 30.5171 24.5589 30.1129C25.16 29.9125 25.7186 29.6519 26.1475 29.3139C26.5651 28.9848 27 28.4639 27 27.75C27 27.1977 26.5523 26.75 26 26.75C25.4788 26.75 25.0507 27.1487 25.0042 27.6578C24.9869 27.6768 24.9574 27.7053 24.9095 27.7431C24.7311 27.8836 24.4111 28.054 23.9264 28.2155C22.9675 28.5352 21.5793 28.75 20 28.75C18.4207 28.75 17.0325 28.5352 16.0736 28.2155C15.5889 28.054 15.2689 27.8836 15.0905 27.7431C15.0426 27.7053 15.0131 27.6768 14.9958 27.6578Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.0357 9.25C25.5617 9.25 25.9881 9.67639 25.9881 10.2024C25.9881 10.7417 25.9966 10.8274 26.0161 10.8959C26.0315 10.9499 26.0531 11.0019 26.0803 11.051C26.115 11.1132 26.1696 11.1798 26.551 11.5612L28.4388 13.449C28.8202 13.8304 28.8868 13.885 28.949 13.9197C28.9981 13.9469 29.0501 13.9685 29.1041 13.9839C29.1726 14.0034 29.2583 14.0119 29.7976 14.0119C30.3236 14.0119 30.75 14.4383 30.75 14.9643C30.75 15.4902 30.3236 15.9166 29.7976 15.9166C29.3989 15.9172 28.94 15.9177 28.5819 15.8157C28.3873 15.7602 28.1998 15.6825 28.023 15.5841C27.897 15.5141 27.7805 15.4317 27.6686 15.3406L25.0402 18.626L26.7071 20.2929C27.0976 20.6834 27.0976 21.3166 26.7071 21.7071C26.3166 22.0976 25.6834 22.0976 25.2929 21.7071L24.9767 21.3909L20.5751 25.7925C20.066 26.3015 19.6441 26.7235 19.2685 27.0298C18.8764 27.3495 18.4719 27.5972 17.9834 27.6944C17.6106 27.7685 17.2268 27.7685 16.8541 27.6944C16.7033 27.6644 16.5606 27.6201 16.4238 27.5638L15.3831 28.3071C15.1732 28.4571 14.962 28.6081 14.7694 28.7193C14.5522 28.8447 14.2807 28.9677 13.9455 28.9941C13.9025 28.9975 13.8594 28.9981 13.8163 28.9959C13.522 28.981 13.1623 28.8608 12.7837 28.6305L10.9571 30.4571C10.5666 30.8476 9.93342 30.8476 9.54289 30.4571C9.15237 30.0666 9.15237 29.4334 9.54289 29.0429L11.3713 27.2145C11.2346 26.9877 11.1412 26.775 11.083 26.5884C11.0343 26.4305 10.9655 26.0271 11.0801 25.6775C11.1948 25.3122 11.4474 24.96 11.6601 24.6638L12.4366 23.577C12.3801 23.44 12.3357 23.297 12.3056 23.146C12.2315 22.7732 12.2315 22.3894 12.3056 22.0166C12.4028 21.5281 12.6505 21.1237 12.9702 20.7315C13.2765 20.3559 13.6984 19.934 14.2075 19.4249L18.6091 15.0233L18.2929 14.7071C17.9024 14.3166 17.9024 13.6834 18.2929 13.2929C18.6834 12.9024 19.3166 12.9024 19.7071 13.2929L21.374 14.9598L24.6594 12.3314C24.5683 12.2195 24.4859 12.103 24.4159 11.9771C24.3175 11.8002 24.2398 11.6127 24.1843 11.4181C24.0823 11.06 24.0828 10.6011 24.0834 10.2024C24.0834 9.67639 24.5098 9.25 25.0357 9.25ZM22.7291 16.3149L23.6851 17.2709L26.2992 14.0032L25.9968 13.7008L22.7291 16.3149ZM13.7094 25.2359C13.8843 25.4145 14.071 25.6012 14.2623 25.7925C14.4395 25.9697 14.6064 26.1365 14.7642 26.2914L14.2498 26.6589C14.0609 26.7938 13.9399 26.8794 13.8497 26.9378C13.7418 26.8755 13.5895 26.7678 13.4111 26.5894C13.2332 26.4115 13.1263 26.2573 13.0645 26.1483C13.1271 26.0517 13.2127 25.9314 13.3417 25.7508L13.7094 25.2359Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -811,5 +811,8 @@
"allSet": "جاهز! الآن يمكنك تسجيل الدخول باستخدام Face ID / Biometric أو البصمة",
"enableQuickLogin":"تمكين تسجيل الدخول السريع",
"enableMsg":"تمكين تسجيل الدخول السريع سيسمح بالتحقق من خلال Face ID / Biometric الخاص بجهازك الحالي",
"notNow": "ليس الآن"
"notNow": "ليس الآن",
"pendingActivation": "في انتظار التنشيط",
"awaitingApproval": "انتظر القبول",
"ready": "جاهز"
}

@ -807,5 +807,8 @@
"allSet": "All Set! Now you can login with Face ID or Biometric",
"enableQuickLogin": "Enable Quick Login",
"enableMsg": "Enabling the quick login will verify through your existing device Face ID / Biometric",
"notNow": "Not Now"
"notNow": "Not Now",
"pendingActivation": "Pending Activation",
"awaitingApproval": "Awaiting Approval",
"ready": "Ready"
}

@ -100,6 +100,12 @@ class AppAssets {
static const String forward_chevron_icon = '$svgBasePath/forward_chevron_icon.svg';
static const String logout = '$svgBasePath/logout.svg';
static const String alarm_clock_icon = '$svgBasePath/alarm_clock_icon.svg';
static const String all_medications_icon = '$svgBasePath/all_medications_icon.svg';
static const String allergy_info_icon = '$svgBasePath/allergy_info_icon.svg';
static const String vaccine_info_icon = '$svgBasePath/vaccine_info_icon.svg';
static const String search_by_clinic_icon = '$svgBasePath/search_by_clinic_icon.svg';
static const String search_by_doctor_icon = '$svgBasePath/search_by_doctor_icon.svg';
static const String search_by_region_icon = '$svgBasePath/search_by_region_icon.svg';
//bottom navigation//
static const String homeBottom = '$svgBasePath/home_bottom.svg';

@ -6,13 +6,16 @@ import 'package:hmg_patient_app_new/core/location_util.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/common/common_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/insurance/insurance_repo.dart';
import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
import 'package:hmg_patient_app_new/features/lab/lab_repo.dart';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart';
@ -88,6 +91,7 @@ class AppDependencies {
getIt.registerLazySingleton<PayfortRepo>(() => PayfortRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<LocalAuthService>(() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()));
getIt.registerLazySingleton<HabibWalletRepo>(() => HabibWalletRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<MedicalFileRepo>(() => MedicalFileRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
// ViewModels
// Global/shared VMs LazySingleton
@ -141,6 +145,21 @@ class AppDependencies {
),
);
getIt.registerLazySingleton<MedicalFileViewModel>(
() => MedicalFileViewModel(
medicalFileRepo: getIt(),
errorHandlerService: getIt(),
),
);
getIt.registerLazySingleton<BookAppointmentsViewModel>(
() => BookAppointmentsViewModel(
bookAppointmentsRepo: getIt(),
errorHandlerService: getIt(),
),
);
getIt.registerLazySingleton<AuthenticationViewModel>(
() => AuthenticationViewModel(
authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()),

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:crypto/crypto.dart' as crypto;
@ -21,6 +23,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/dialogs/confirm_dialog.dart';
import 'package:hmg_patient_app_new/widgets/loading_dialog.dart';
import 'package:lottie/lottie.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:math' as dartMath;
@ -248,45 +251,43 @@ class Utils {
);
}
static bool isSAUDIIDValid(String id, type) {
if (type == 1) {
try {
id = id.toString();
id = id.trim();
var returnValue = int.parse(id);
var sum = 0;
if (returnValue > 0) {
var type = int.parse(id[0]);
if (id.length != 10) {
return false;
}
if (type != 2 && type != 1) {
return false;
}
static bool isSAUDIIDValid(
String id,
) {
try {
id = id.toString();
id = id.trim();
var returnValue = int.parse(id);
var sum = 0;
if (returnValue > 0) {
var type = int.parse(id[0]);
if (id.length != 10) {
return false;
}
if (type != 2 && type != 1) {
return false;
}
for (var i = 0; i < 10; i++) {
if (i % 2 == 0) {
var a = id[i];
var x = int.parse(a) * 2;
var b = x.toString();
if (b.length == 1) {
b = "0$b";
}
sum += int.parse(b[0]) + int.parse(b[1]);
} else {
sum += int.parse(id[i]);
for (var i = 0; i < 10; i++) {
if (i % 2 == 0) {
var a = id[i];
var x = int.parse(a) * 2;
var b = x.toString();
if (b.length == 1) {
b = "0$b";
}
sum += int.parse(b[0]) + int.parse(b[1]);
} else {
sum += int.parse(id[i]);
}
return sum % 10 == 0;
}
} catch (err) {
log("errr: ${err.toString()}");
return sum % 10 == 0;
}
return false;
} else {
return true;
} catch (err) {
log("errr: ${err.toString()}");
}
return false;
}
static Widget getNoDataWidget(BuildContext context, {String? errorText}) {
@ -339,7 +340,8 @@ class Utils {
).center;
}
static bool isVidaPlusProject(AppState appState, int projectID) {
static bool isVidaPlusProject(int projectID) {
AppState appState = getIt.get<AppState>();
bool isVidaPlus = false;
for (var element in appState.vidaPlusProjectList) {
if (element.projectID == projectID) {
@ -392,14 +394,14 @@ class Utils {
return '';
}
// Replace HTML line breaks with newlines
// Replace HTML line breaks with newlines
var withLineBreaks =
htmlString.replaceAll(RegExp(r'<br\s*\/?>', multiLine: true), '\n').replaceAll(RegExp(r'<\/p>', multiLine: true), '\n').replaceAll(RegExp(r'<divider>', multiLine: true), '\n');
// Remove all other HTML tags
// Remove all other HTML tags
var withoutTags = withLineBreaks.replaceAll(RegExp(r'<[^>]*>'), '');
// Decode HTML entities
// Decode HTML entities
var decodedString = withoutTags
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&')
@ -412,7 +414,7 @@ class Utils {
.replaceAll('&rdquo;', '"')
.replaceAll('&ldquo;', '"');
// Remove extra whitespace and normalize line breaks
// Remove extra whitespace and normalize line breaks
var normalizedString = decodedString
.replaceAll(RegExp(r'\n\s*\n'), '\n\n') // Replace multiple blank lines with double line break
.replaceAll(RegExp(r' +'), ' ') // Replace multiple spaces with single space
@ -423,13 +425,13 @@ class Utils {
Widget mDivider(Color color) {
return Divider(
// width: double.infinity,
// width: double.infinity,
height: 1,
color: color,
);
}
// New Ui Items
// New Ui Items
static String formatDateToDisplay(String isoDateString) {
try {
@ -437,7 +439,7 @@ class Utils {
final day = dateTime.day.toString().padLeft(2, '0');
final year = dateTime.year.toString();
// Map month number to short month name
// Map month number to short month name
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
final month = monthNames[dateTime.month - 1];
@ -463,7 +465,7 @@ class Utils {
static String formatHijriDateToDisplay(String hijriDateString) {
try {
// Assuming hijriDateString is in the format yyyy-MM-dd
// Assuming hijriDateString is in the format yyyy-MM-dd
final datePart = hijriDateString.split("T").first;
final parts = datePart.split('-');
if (parts.length != 3) return "";
@ -471,7 +473,7 @@ class Utils {
final day = parts[2].padLeft(2, '0');
final year = parts[0];
// Map month number to short month name (Hijri months)
// Map month number to short month name (Hijri months)
const hijriMonthNames = ['Muharram', 'Safar', 'Rabi I', 'Rabi II', 'Jumada I', 'Jumada II', 'Rajab', 'Sha\'ban', 'Ramadan', 'Shawwal', 'Dhu al-Qi\'dah', 'Dhu al-Hijjah'];
final monthIndex = int.tryParse(parts[1]) ?? 1;
final month = hijriMonthNames[monthIndex - 1];
@ -629,4 +631,12 @@ class Utils {
static String getAdvancePaymentTransID(int projectID, int fileNumber) {
return '$projectID-$fileNumber-${DateTime.now().millisecondsSinceEpoch}';
}
static Future<String> createFileFromString(String encodedStr, String ext) async {
Uint8List bytes = base64.decode(encodedStr);
String dir = (await getApplicationDocumentsDirectory()).path;
File file = File("$dir/" + DateTime.now().millisecondsSinceEpoch.toString() + "." + ext);
await file.writeAsBytes(bytes);
return file.path;
}
}

@ -1,6 +1,8 @@
import 'dart:developer';
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/services/dialog_service.dart';
class ValidationUtils {
@ -20,4 +22,80 @@ class ValidationUtils {
}
return true;
}
static bool isValidatedId({String? nationalId, required Function() onOkPress, CountryEnum? selectedCountry, bool? isTermsAccepted, String? dob}) {
bool isCorrectID = true;
if (nationalId == null || nationalId.isEmpty) {
_dialogService.showExceptionBottomSheet(message: "Please enter a national ID", onOkPressed: onOkPress);
isCorrectID = false;
}
if (nationalId != null && nationalId.isNotEmpty && selectedCountry != null) {
if (selectedCountry == CountryEnum.saudiArabia) {
if (!validateIqama(nationalId)) {
_dialogService.showExceptionBottomSheet(message: "Please enter a valid national ID", onOkPressed: onOkPress);
return false;
}
}
if (selectedCountry == CountryEnum.unitedArabEmirates) {
if (!validateUaeNationalId(nationalId)) {
_dialogService.showExceptionBottomSheet(message: "Please enter a valid national ID", onOkPressed: onOkPress);
return false;
}
}
if (dob == null || dob.isEmpty) {
_dialogService.showExceptionBottomSheet(message: "Please enter a valid date of birth", onOkPressed: onOkPress);
return false;
}
if (isTermsAccepted != null && !isTermsAccepted) {
_dialogService.showExceptionBottomSheet(message: "Please accept the terms and conditions", onOkPressed: onOkPress);
return false;
}
}
return isCorrectID;
}
static bool isValidatePhone({String? phoneNumber, required Function() onOkPress}) {
if (phoneNumber == null || phoneNumber.isEmpty) {
_dialogService.showExceptionBottomSheet(message: "Please enter a valid phone number", onOkPressed: onOkPress);
return false;
}
return true;
}
static bool isValidate({String? phoneNumber, required Function() onOkPress}) {
if (phoneNumber == null || phoneNumber.isEmpty) {
_dialogService.showExceptionBottomSheet(message: "Please enter a valid phone number", onOkPressed: onOkPress);
return false;
}
return true;
}
static bool validateIqama(String iqamaNumber) {
String cleanedIqama = iqamaNumber.replaceAll(RegExp(r'[^0-9]'), '');
if (cleanedIqama.length != 10) {
return false;
}
int firstDigit = int.parse(cleanedIqama[0]);
if (firstDigit != 2 && firstDigit != 1) {
return false;
}
int sum = 0;
for (int i = 0; i < 10; i++) {
int digit = int.parse(cleanedIqama[i]);
int weight = (i % 2 == 0) ? 2 : 1; // Alternate weights: 2, 1, 2, 1...
int product = digit * weight;
sum += (product > 9) ? product - 9 : product; // Sum digits if product > 9
}
return sum % 10 == 0;
}
static bool validateUaeNationalId(String id) {
// Must be exactly 15 digits
final regex = RegExp(r'^784\d{4}\d{7}\d{1}$');
return regex.hasMatch(id);
}
}

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/material.dart';
import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart';
@ -23,6 +24,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/request_model
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_activation_code_resp_model.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
@ -103,7 +105,7 @@ class AuthenticationViewModel extends ChangeNotifier {
}
}
void clearDefaultInputValues() {
Future<void> clearDefaultInputValues() async {
nationalIdController.clear();
phoneNumberController.clear();
dobController.clear();
@ -265,10 +267,7 @@ class AuthenticationViewModel extends ChangeNotifier {
if (phoneNumberController.text.isEmpty) {
phoneNumberController.text = "504278212";
}
bool isValidated = ValidationUtils.isValidatePhoneAndId(
phoneNumber: phoneNumberController.text,
nationalId: nationalIdController.text,
);
bool isValidated = ValidationUtils.isValidatePhoneAndId(phoneNumber: phoneNumberController.text, nationalId: nationalIdController.text);
if (!isValidated) {
return;
@ -289,7 +288,20 @@ class AuthenticationViewModel extends ChangeNotifier {
final result = await _authenticationRepo.checkPatientAuthentication(checkPatientAuthenticationReq: checkPatientAuthenticationReq);
result.fold(
(failure) async => await _errorHandlerService.handleError(failure: failure),
(failure) async => await _errorHandlerService.handleError(
failure: failure,
onUnHandledFailure: (failure) async {
LoaderBottomSheet.hideLoader();
await _dialogService.showCommonBottomSheetWithoutH(
message: failure.message,
label: LocaleKeys.notice.tr(),
onOkPressed: () {
_navigationService.pushAndReplace(AppRoutes.register);
},
onCancelPressed: () {
_navigationService.pop();
});
}),
(apiResponse) async {
if (apiResponse.messageStatus == 2) {
LoaderBottomSheet.hideLoader();
@ -335,7 +347,6 @@ class AuthenticationViewModel extends ChangeNotifier {
if (checkIsUserComingForRegister(request: payload)) {
_appState.setUserRegistrationPayload = RegistrationDataModelPayload.fromJson(payload);
print("====== Demo ==========");
}
final resultEither = await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er');
@ -345,7 +356,11 @@ class AuthenticationViewModel extends ChangeNotifier {
(apiResponse) async {
if (apiResponse.messageStatus == 2) {
LoaderBottomSheet.hideLoader();
await _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage ?? "ErrorEmpty");
await _dialogService.showCommonBottomSheetWithoutH(
message: apiResponse.errorMessage ?? "Something Went Wrong",
onOkPressed: () {
_navigationService.pop();
});
} else {
if (apiResponse.data != null && apiResponse.data['isSMSSent'] == true) {
LoaderBottomSheet.hideLoader();
@ -403,18 +418,25 @@ class AuthenticationViewModel extends ChangeNotifier {
request["ForRegisteration"] = _appState.getUserRegistrationPayload.isRegister;
request["isRegister"] = false;
// if (request.containsKey("OTP_SendType")) {
// request.remove("OTP_SendType");
// print("====== Demo: Removed OTP_SendType for Register state");
// }
print("====== Req");
final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: request, activationCode: activationCode.toString(), isRegister: true);
LoaderBottomSheet.hideLoader();
resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) {
resultEither.fold(
(failure) async => await _errorHandlerService.handleError(
failure: failure,
onUnHandledFailure: (failure) async {
LoaderBottomSheet.hideLoader();
await _dialogService.showCommonBottomSheetWithoutH(
message: failure.message,
label: LocaleKeys.notice.tr(),
onOkPressed: () {
_navigationService.pushAndReplace(AppRoutes.register);
},
onCancelPressed: () {
_navigationService.pop();
});
}), (apiResponse) {
final activation = CheckActivationCode.fromJson(apiResponse.data as Map<String, dynamic>);
if (_appState.getUserRegistrationPayload.isRegister == true) {
//TODO: KSA Version Came Hre
@ -425,18 +447,20 @@ class AuthenticationViewModel extends ChangeNotifier {
}
});
} else {
final resultEither = await _authenticationRepo.checkActivationCodeRepo(
newRequest: CheckActivationCodeRegisterReq.fromJson(request),
activationCode: activationCode,
isRegister: false,
);
resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async {
final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: CheckActivationCodeRegisterReq.fromJson(request), activationCode: activationCode, isRegister: false);
resultEither.fold(
(failure) async => await _errorHandlerService.handleError(
failure: failure,
onUnHandledFailure: (failure) async {
LoaderBottomSheet.hideLoader();
await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {});
},
), (apiResponse) async {
final activation = CheckActivationCode.fromJson(apiResponse.data as Map<String, dynamic>);
if (activation.errorCode == '699') {
// Todo: Hide Loader
// GifLoaderDialogUtils.hideDialog(context);
LoaderBottomSheet.hideLoader();
onWrongActivationCode(activation.errorEndUserMessage);
@ -465,7 +489,7 @@ class AuthenticationViewModel extends ChangeNotifier {
}
LoaderBottomSheet.hideLoader();
insertPatientIMEIData(loginTypeEnum.toInt);
clearDefaultInputValues();
await clearDefaultInputValues();
if (isUserAgreedBefore) {
navigateToHomeScreen();
} else {
@ -595,7 +619,7 @@ class AuthenticationViewModel extends ChangeNotifier {
Future<void> onRegistrationStart({required OTPTypeEnum otpTypeEnum}) async {
bool isOutSidePatient = selectedCountrySignup.countryCode == CountryEnum.unitedArabEmirates.countryCode ? true : false;
LoaderBottomSheet.showLoader();
final request = await RequestUtils.getPatientAuthenticationRequest(
phoneNumber: phoneNumberController.text,
nationId: nationalIdController.text,
@ -639,7 +663,7 @@ class AuthenticationViewModel extends ChangeNotifier {
print(apiResponse.data as Map<String, dynamic>);
if (apiResponse.data["MessageStatus"] == 1) {
//TODO: Here We Need to Show a Dialog Of Something in the case of Success.
clearDefaultInputValues(); // This will Clear All Default Values Of User.
await clearDefaultInputValues(); // This will Clear All Default Values Of User.
_navigationService.pushAndReplace(AppRoutes.loginScreen);
}
}
@ -651,24 +675,22 @@ class AuthenticationViewModel extends ChangeNotifier {
Future<void> checkUserStatusForRegistration({required dynamic response, required dynamic request}) async {
if (response is Map) {
if (response["MessageStatus"] == 2) {
LoaderBottomSheet.hideLoader();
print(response["ErrorEndUserMessage"]);
return;
}
if (response['hasFile'] == true) {
//TODO: Show Here Ok And Cancel Dialog and On OKPress it will go for sendActivationCode
_navigationService.context?.showBottomSheet(
child: ExceptionBottomSheet(
message: response["ErrorMessage"],
showCancel: true,
showOKButton: true,
onOkPressed: () {
_navigationService.popUntilNamed(AppRoutes.loginScreen);
},
onCancelPressed: () {
_navigationService.pop();
},
));
LoaderBottomSheet.hideLoader();
_dialogService.showCommonBottomSheetWithoutH(
message: response["ErrorMessage"],
onOkPressed: () async {
await clearDefaultInputValues();
_navigationService.pushAndReplace(AppRoutes.loginScreen);
},
onCancelPressed: () {
_navigationService.pop();
});
} else {
request['forRegister'] = true;
request['isRegister'] = true;
@ -727,41 +749,6 @@ class AuthenticationViewModel extends ChangeNotifier {
);
}
});
// this.authService.checkUserStatus(request).then((result) {
// // Keep loader active, continue to next step
// if (result is Map) {
// RegisterInfoResponse? resultSet;
// CheckUserStatusResponse res = CheckUserStatusResponse.fromJson(result as Map<String, dynamic>);
// nHICData = res;
// sharedPref.setObject(NHIC_DATA, res.toJson());
// resultSet = RegisterInfoResponse.fromJson(res.toJson());
//
// sendActivationCode(type, loginToken, resultSet, isSkipRegistration);
// } else {
// GifLoaderDialogUtils.hideDialog(context);
// context.showBottomSheet(
// child: ExceptionBottomSheet(
// message: result != null ? result : TranslationBase.of(context).somethingWentWrong,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// }
// }).catchError((err) {
// GifLoaderDialogUtils.hideDialog(context);
// context.showBottomSheet(
// child: ExceptionBottomSheet(
// message: err.toString(),
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// });
}
void setNHICData(dynamic data, dynamic request) {

@ -1,12 +1,394 @@
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart';
typedef OnDone = void Function(String text);
class ProvidedPinBoxTextAnimation {
static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) {
return ScaleTransition(
child: child,
scale: animation,
);
};
static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation<double> animation) {
return child;
};
}
class OTPWidget extends StatefulWidget {
final int maxLength;
final TextEditingController? controller;
final Color defaultBorderColor;
final Color pinBoxColor;
final double pinBoxBorderWidth;
final double pinBoxRadius;
final bool hideDefaultKeyboard;
final TextStyle? pinTextStyle;
final double pinBoxHeight;
final double pinBoxWidth;
final OnDone? onDone;
final bool hasError;
final Color errorBorderColor;
final Color textBorderColor;
final Function(String)? onTextChanged;
final bool autoFocus;
final FocusNode? focusNode;
final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition;
final Duration pinTextAnimatedSwitcherDuration;
final TextDirection textDirection;
final TextInputType keyboardType;
final EdgeInsets pinBoxOuterPadding;
OTPWidget({
Key? key,
this.maxLength = 4,
this.controller,
this.pinBoxWidth = 70.0,
this.pinBoxHeight = 100.0,
this.pinTextStyle,
this.onDone,
this.defaultBorderColor = Colors.black,
this.textBorderColor = Colors.black,
this.pinTextAnimatedSwitcherTransition,
this.pinTextAnimatedSwitcherDuration = const Duration(),
this.hasError = false,
this.errorBorderColor = Colors.red,
this.onTextChanged,
this.autoFocus = false,
this.focusNode,
this.textDirection = TextDirection.ltr,
this.keyboardType = TextInputType.number,
this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0),
this.pinBoxColor = Colors.white,
this.pinBoxBorderWidth = 2.0,
this.pinBoxRadius = 0,
this.hideDefaultKeyboard = false,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return OTPWidgetState();
}
}
class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
late AnimationController _highlightAnimationController;
late FocusNode focusNode;
String text = "";
int currentIndex = 0;
List<String> strList = [];
bool hasFocus = false;
@override
void didUpdateWidget(OTPWidget oldWidget) {
super.didUpdateWidget(oldWidget);
focusNode = widget.focusNode ?? focusNode;
if (oldWidget.maxLength < widget.maxLength) {
setState(() {
currentIndex = text.length;
});
widget.controller?.text = text;
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
} else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) {
setState(() {
text = text.substring(0, widget.maxLength);
currentIndex = text.length;
});
widget.controller?.text = text;
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
}
}
_calculateStrList() {
if (strList.length > widget.maxLength) {
strList.length = widget.maxLength;
}
while (strList.length < widget.maxLength) {
strList.add("");
}
}
@override
void initState() {
super.initState();
focusNode = widget.focusNode ?? FocusNode();
_highlightAnimationController = AnimationController(vsync: this);
_initTextController();
_calculateStrList();
widget.controller!.addListener(_controllerListener);
focusNode.addListener(_focusListener);
}
void _controllerListener() {
if (mounted == true) {
setState(() {
_initTextController();
});
var onTextChanged = widget.onTextChanged;
if (onTextChanged != null) {
onTextChanged(widget.controller?.text ?? "");
}
}
}
void _focusListener() {
if (mounted == true) {
setState(() {
hasFocus = focusNode.hasFocus;
});
}
}
void _initTextController() {
if (widget.controller == null) {
return;
}
strList.clear();
var text = widget.controller?.text ?? "";
if (text.isNotEmpty) {
if (text.length > widget.maxLength) {
throw Exception("TextEditingController length exceeded maxLength!");
}
}
for (var i = 0; i < text.length; i++) {
strList.add(text[i]);
}
}
double get _width {
var width = 0.0;
for (var i = 0; i < widget.maxLength; i++) {
width += widget.pinBoxWidth;
if (i == 0) {
width += widget.pinBoxOuterPadding.left;
} else if (i + 1 == widget.maxLength) {
width += widget.pinBoxOuterPadding.right;
} else {
width += widget.pinBoxOuterPadding.left;
}
}
return width;
}
@override
void dispose() {
if (widget.focusNode == null) {
focusNode.dispose();
} else {
focusNode.removeListener(_focusListener);
}
_highlightAnimationController.dispose();
widget.controller?.removeListener(_controllerListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
_otpTextInput(),
_touchPinBoxRow(),
],
);
}
Widget _touchPinBoxRow() {
return widget.hideDefaultKeyboard
? _pinBoxRow(context)
: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (hasFocus) {
FocusScope.of(context).requestFocus(FocusNode());
Future.delayed(Duration(milliseconds: 100), () {
FocusScope.of(context).requestFocus(focusNode);
});
} else {
FocusScope.of(context).requestFocus(focusNode);
}
},
child: _pinBoxRow(context),
);
}
Widget _otpTextInput() {
var transparentBorder = OutlineInputBorder(
borderSide: BorderSide(
color: Colors.transparent,
width: 0.0,
),
);
return SizedBox(
width: _width,
height: widget.pinBoxHeight,
child: TextField(
autofocus: !kIsWeb ? widget.autoFocus : false,
enableInteractiveSelection: false,
focusNode: focusNode,
controller: widget.controller,
keyboardType: widget.keyboardType,
inputFormatters: widget.keyboardType == TextInputType.number ? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly] : null,
// Enable SMS autofill
autofillHints: const [AutofillHints.oneTimeCode],
style: TextStyle(
height: 0.1,
color: Colors.transparent,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0),
focusedErrorBorder: transparentBorder,
errorBorder: transparentBorder,
disabledBorder: transparentBorder,
enabledBorder: transparentBorder,
focusedBorder: transparentBorder,
counterText: null,
counterStyle: null,
helperStyle: TextStyle(
height: 0.0,
color: Colors.transparent,
),
labelStyle: TextStyle(height: 0.1),
fillColor: Colors.transparent,
border: InputBorder.none,
),
cursorColor: Colors.transparent,
showCursor: false,
maxLength: widget.maxLength,
onChanged: _onTextChanged,
),
);
}
void _onTextChanged(text) {
var onTextChanged = widget.onTextChanged;
if (onTextChanged != null) {
onTextChanged(text);
}
setState(() {
this.text = text;
if (text.length >= currentIndex) {
for (int i = currentIndex; i < text.length; i++) {
strList[i] = text[i];
}
}
currentIndex = text.length;
});
if (text.length == widget.maxLength) {
FocusScope.of(context).requestFocus(FocusNode());
var onDone = widget.onDone;
if (onDone != null) {
onDone(text);
}
}
}
Widget _pinBoxRow(BuildContext context) {
_calculateStrList();
List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
return _buildPinCode(i, context);
});
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: pinCodes,
);
}
Widget _buildPinCode(int i, BuildContext context) {
Color borderColor;
Color pinBoxColor;
// Determine if OTP is complete
bool isComplete = text.length == widget.maxLength;
if (widget.hasError) {
borderColor = widget.errorBorderColor;
pinBoxColor = widget.pinBoxColor;
} else if (isComplete) {
borderColor = Colors.transparent;
pinBoxColor = AppColors.successColor;
} else if (i < text.length) {
borderColor = Colors.transparent;
pinBoxColor = AppColors.blackBgColor;
} else {
borderColor = Colors.transparent;
pinBoxColor = widget.pinBoxColor;
}
EdgeInsets insets;
if (i == 0) {
insets = EdgeInsets.only(
left: 0,
top: widget.pinBoxOuterPadding.top,
right: widget.pinBoxOuterPadding.right,
bottom: widget.pinBoxOuterPadding.bottom,
);
} else if (i == strList.length - 1) {
insets = EdgeInsets.only(
left: widget.pinBoxOuterPadding.left,
top: widget.pinBoxOuterPadding.top,
right: 0,
bottom: widget.pinBoxOuterPadding.bottom,
);
} else {
insets = widget.pinBoxOuterPadding;
}
return Container(
key: ValueKey<String>("container$i"),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0),
margin: insets,
decoration: BoxDecoration(
border: Border.all(
color: borderColor,
width: widget.pinBoxBorderWidth,
),
color: pinBoxColor,
borderRadius: BorderRadius.circular(widget.pinBoxRadius),
),
width: widget.pinBoxWidth,
height: widget.pinBoxHeight,
child: _animatedTextBox(strList[i], i),
);
}
Widget _animatedTextBox(String text, int i) {
if (widget.pinTextAnimatedSwitcherTransition != null) {
return AnimatedSwitcher(
duration: widget.pinTextAnimatedSwitcherDuration,
transitionBuilder: widget.pinTextAnimatedSwitcherTransition ??
(Widget child, Animation<double> animation) {
return child;
},
child: Text(
text,
key: ValueKey<String>("$text$i"),
style: widget.pinTextStyle,
),
);
} else {
return Text(
text,
key: ValueKey<String>("${strList[i]}$i"),
style: widget.pinTextStyle,
);
}
}
}
class OTPVerificationScreen extends StatefulWidget {
final String phoneNumber;
final Function(int code) checkActivationCode;
@ -25,36 +407,21 @@ class OTPVerificationScreen extends StatefulWidget {
class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
final int _otpLength = 4;
late final List<TextEditingController> _controllers;
late final List<FocusNode> _focusNodes;
late TextEditingController _otpController;
Timer? _resendTimer;
int _resendTime = 60;
bool _isOtpComplete = false;
@override
void initState() {
super.initState();
_controllers = List.generate(_otpLength, (_) => TextEditingController());
_focusNodes = List.generate(_otpLength, (_) => FocusNode());
_otpController = TextEditingController();
_startResendTimer();
// Focus the first field once the screen is built
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_focusNodes.isNotEmpty) {
FocusScope.of(context).requestFocus(_focusNodes[0]);
}
});
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
for (final f in _focusNodes) {
f.dispose();
}
_otpController.dispose();
_resendTimer?.cancel();
super.dispose();
}
@ -69,25 +436,23 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
});
}
void _onOtpChanged(int index, String value) {
if (value.length == 1 && index < _otpLength - 1) {
_focusNodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
void _onOtpChanged(String value) {
// Handle clipboard paste or programmatic input
if (value.length > 1) {
String? otp = _extractOtpFromText(value);
if (otp != null) {
autoFillOtp(otp);
return;
}
}
_checkOtpCompletion();
// The OTPWidget will automatically call onDone when complete
// This method can be used for any additional logic on text change
}
void _checkOtpCompletion() {
final isComplete = _controllers.every((c) => c.text.isNotEmpty);
if (isComplete != _isOtpComplete) {
setState(() => _isOtpComplete = isComplete);
if (isComplete) {
_verifyOtp();
}
}
void _onOtpCompleted(String otp) {
debugPrint('OTP Completed: $otp');
widget.checkActivationCode(int.parse(otp));
}
void _resendOtp() {
@ -95,8 +460,7 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
setState(() => _resendTime = 60);
_startResendTimer();
autoFillOtp("1234");
// call resend API here
widget.onResendOTPPressed(widget.phoneNumber);
}
}
@ -105,6 +469,68 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone;
}
/// Extract OTP from text using multiple patterns
String? _extractOtpFromText(String text) {
// Pattern 1: Find 4-6 consecutive digits
RegExp digitPattern = RegExp(r'\b\d{4,6}\b');
Match? match = digitPattern.firstMatch(text);
if (match != null) {
String digits = match.group(0)!;
if (digits.length >= _otpLength) {
return digits.substring(0, _otpLength);
}
}
// Pattern 2: Find digits separated by spaces or special characters
String cleanedText = text.replaceAll(RegExp(r'[^\d]'), '');
if (cleanedText.length >= _otpLength) {
return cleanedText.substring(0, _otpLength);
}
return null;
}
/// Paste OTP from clipboard
Future<void> _pasteFromClipboard() async {
try {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
String clipboardText = data.text!;
String? otp = _extractOtpFromText(clipboardText);
if (otp != null) {
autoFillOtp(otp);
// Show feedback to user
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('OTP pasted: $otp'),
duration: const Duration(seconds: 2),
backgroundColor: AppColors.successColor,
),
);
} else {
// Show error if no valid OTP found
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No valid OTP found in clipboard'),
duration: Duration(seconds: 2),
),
);
}
}
} catch (e) {
debugPrint('Error pasting from clipboard: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to paste from clipboard'),
duration: Duration(seconds: 2),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -134,56 +560,35 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
),
SizedBox(height: 40.h),
// OTP Input Fields
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(_otpLength, (index) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: _controllers[index],
builder: (context, value, _) {
final hasText = value.text.isNotEmpty;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: 70.h,
margin: EdgeInsets.symmetric(horizontal: 4.h),
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: _isOtpComplete ? AppColors.successColor : (hasText ? AppColors.blackBgColor : AppColors.whiteColor), borderRadius: 16),
child: Center(
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: TextStyle(
fontSize: 40.fSize,
fontWeight: FontWeight.bold,
color: AppColors.whiteColor,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Colors.transparent,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide.none,
),
),
onChanged: (v) => _onOtpChanged(index, v),
),
),
);
},
);
}),
// OTP Input Fields using new OTPWidget
Center(
child: AutofillGroup(
child: OTPWidget(
maxLength: _otpLength,
controller: _otpController,
pinBoxWidth: 75.h,
pinBoxHeight: 100.h,
autoFocus: true,
pinBoxRadius: 16,
pinBoxBorderWidth: 0,
pinBoxOuterPadding: EdgeInsets.symmetric(horizontal: 4.h),
defaultBorderColor: Colors.transparent,
textBorderColor: Colors.transparent,
errorBorderColor: AppColors.primaryRedColor,
pinBoxColor: AppColors.whiteColor,
pinTextStyle: TextStyle(
fontSize: 50.fSize,
fontWeight: FontWeight.bold,
color: AppColors.whiteColor,
),
onTextChanged: _onOtpChanged,
onDone: _onOtpCompleted,
),
),
),
const SizedBox(height: 32),
const SizedBox(height: 16),
// Resend OTP
Row(
@ -215,30 +620,50 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
);
}
void _verifyOtp() {
final otp = _controllers.map((c) => c.text).join();
debugPrint('Verifying OTP: $otp');
widget.checkActivationCode(int.parse(otp));
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('Verifying OTP: $otp')),
// );
// Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => RegisterNewStep2(null, {"nationalID": "12345678654321"})));
}
/// Auto fill OTP into text fields
void autoFillOtp(String otp) {
if (otp.length != _otpLength) return;
// Clear any existing text first
_otpController.clear();
// Add a small delay to ensure the UI is updated
Future.delayed(const Duration(milliseconds: 50), () {
_otpController.text = otp;
// Move cursor to the end
_otpController.selection = TextSelection.fromPosition(
TextPosition(offset: otp.length),
);
});
}
for (int i = 0; i < _otpLength; i++) {
_controllers[i].text = otp[i];
}
/// Clear OTP fields
void clearOtp() {
_otpController.clear();
}
/// Get current OTP value
String getCurrentOtp() {
return _otpController.text;
}
// Move focus to the last field
_focusNodes[_otpLength - 1].requestFocus();
/// Check if OTP is complete
bool isOtpComplete() {
return _otpController.text.length == _otpLength;
}
// Trigger completion check and color update
_checkOtpCompletion();
/// Simulate SMS received with OTP (for testing purposes)
void simulateSMSReceived(String otp) {
if (otp.length == _otpLength && RegExp(r'^\d+$').hasMatch(otp)) {
autoFillOtp(otp);
// Show a brief indicator that SMS was detected
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('OTP detected from SMS: $otp'),
duration: const Duration(seconds: 2),
backgroundColor: AppColors.successColor,
),
);
}
}
}

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class BookAppointmentsViewModel extends ChangeNotifier {
int selectedTabIndex = 0;
BookAppointmentsRepo bookAppointmentsRepo;
ErrorHandlerService errorHandlerService;
BookAppointmentsViewModel({required this.bookAppointmentsRepo, required this.errorHandlerService});
void onTabChanged(int index) {
selectedTabIndex = index;
notifyListeners();
}
}

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class HabibWalletViewModel extends ChangeNotifier {

@ -0,0 +1,270 @@
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/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import '../authentication/models/resp_models/authenticated_user_resp_model.dart';
abstract class MedicalFileRepo {
Future<Either<Failure, GenericApiModel<List<PatientVaccineResponseModel>>>> getPatientVaccinesList();
Future<Either<Failure, GenericApiModel<List<PatientSickLeavesResponseModel>>>> getPatientSickLeavesList();
Future<Either<Failure, GenericApiModel<dynamic>>> getPatientSickLeavePDF(PatientSickLeavesResponseModel patientSickLeavesResponseModel, AuthenticatedUser authenticatedUser);
Future<Either<Failure, GenericApiModel<List<PatientMedicalReportResponseModel>>>> getPatientMedicalReportsList();
Future<Either<Failure, GenericApiModel<dynamic>>> getPatientMedicalReportPDF(PatientMedicalReportResponseModel patientMedicalReportResponseModel, AuthenticatedUser authenticatedUser);
}
class MedicalFileRepoImp implements MedicalFileRepo {
final ApiClient apiClient;
final LoggerService loggerService;
MedicalFileRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<List<PatientVaccineResponseModel>>>> getPatientVaccinesList() async {
Map<String, dynamic> mapDevice = {"To": "0", "From": "0"};
try {
GenericApiModel<List<PatientVaccineResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_VACCINES,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['List_DoneVaccines'];
// if (list == null || list.isEmpty) {
// throw Exception("lab list is empty");
// }
final vaccinesList = list.map((item) => PatientVaccineResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientVaccineResponseModel>();
apiResponse = GenericApiModel<List<PatientVaccineResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: vaccinesList,
);
} catch (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()));
}
}
@override
Future<Either<Failure, GenericApiModel<List<PatientSickLeavesResponseModel>>>> getPatientSickLeavesList() async {
Map<String, dynamic> mapDevice = {};
try {
GenericApiModel<List<PatientSickLeavesResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_PATIENT_SICK_LEAVE_STATUS,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['List_SickLeave'];
// if (list == null || list.isEmpty) {
// throw Exception("lab list is empty");
// }
final vaccinesList = list.map((item) => PatientSickLeavesResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientSickLeavesResponseModel>();
apiResponse = GenericApiModel<List<PatientSickLeavesResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: vaccinesList,
);
} catch (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()));
}
}
@override
Future<Either<Failure, GenericApiModel>> getPatientSickLeavePDF(PatientSickLeavesResponseModel patientSickLeavesResponseModel, AuthenticatedUser authenticatedUser) async {
Map<String, dynamic> mapDevice = {
"RequestNo": patientSickLeavesResponseModel.requestNo,
"To": authenticatedUser.emailAddress,
"DateofBirth": authenticatedUser.dateofBirth,
"PatientIditificationNum": authenticatedUser.patientIdentificationNo,
"PatientMobileNumber": authenticatedUser.mobileNumber,
"PatientName": "${authenticatedUser.firstName!} ${authenticatedUser.lastName!}",
"ProjectName": patientSickLeavesResponseModel.projectName,
"DoctorName": patientSickLeavesResponseModel.doctorName,
"ProjectID": patientSickLeavesResponseModel.projectID,
"SetupID": patientSickLeavesResponseModel.setupID,
"IsDownload": true,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
SendSickLeaveEmail,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// final list = response['List_SickLeave'];
// if (list == null || list.isEmpty) {
// throw Exception("lab list is empty");
// }
// final vaccinesList = list.map((item) => PatientSickLeavesResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientSickLeavesResponseModel>();
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: response["Base64Data"],
);
} catch (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()));
}
}
@override
Future<Either<Failure, GenericApiModel<List<PatientMedicalReportResponseModel>>>> getPatientMedicalReportsList() async {
Map<String, dynamic> mapDevice = {
"IsReport": true,
"EncounterType": 1,
"RequestType": 1,
};
try {
GenericApiModel<List<PatientMedicalReportResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
REPORTS,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['GetPatientMedicalStatus'];
// if (list == null || list.isEmpty) {
// throw Exception("lab list is empty");
// }
final vaccinesList = list.map((item) => PatientMedicalReportResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientMedicalReportResponseModel>();
apiResponse = GenericApiModel<List<PatientMedicalReportResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: vaccinesList,
);
} catch (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()));
}
}
@override
Future<Either<Failure, GenericApiModel>> getPatientMedicalReportPDF(PatientMedicalReportResponseModel patientMedicalReportResponseModel, AuthenticatedUser authenticatedUser) async {
Map<String, dynamic> mapDevice = {
"SetupID": patientMedicalReportResponseModel.setupId,
"PrintDate": patientMedicalReportResponseModel.requestDate!,
"ProcedureID": "05005009",
"Reporttype": "MEDICAL REPORT",
"stamp": patientMedicalReportResponseModel.requestDate!,
"To": authenticatedUser.emailAddress,
"DateofBirth": authenticatedUser.dateofBirth,
"PatientIditificationNum": authenticatedUser.patientIdentificationNo,
"PatientMobileNumber": authenticatedUser.mobileNumber,
"PatientName": "${authenticatedUser.firstName!} ${authenticatedUser.lastName!}",
"ProjectName": patientMedicalReportResponseModel.projectName,
"ClinicName": patientMedicalReportResponseModel.clinicDescription,
"ProjectID": patientMedicalReportResponseModel.projectID,
"InvoiceNo": Utils.isVidaPlusProject(patientMedicalReportResponseModel.projectID!) ? patientMedicalReportResponseModel.invoiceNoVP : patientMedicalReportResponseModel.invoiceNo,
"InvoiceNo_VP": Utils.isVidaPlusProject(patientMedicalReportResponseModel.projectID!) ? patientMedicalReportResponseModel.invoiceNoVP : patientMedicalReportResponseModel.invoiceNo,
"PrintedByName": "${authenticatedUser.firstName!} ${authenticatedUser.lastName!}",
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
GET_MEDICAL_REPORT_PDF,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// final list = response['List_SickLeave'];
// if (list == null || list.isEmpty) {
// throw Exception("lab list is empty");
// }
// final vaccinesList = list.map((item) => PatientSickLeavesResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientSickLeavesResponseModel>();
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: response["MedicalReportBase64"],
);
} catch (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()));
}
}
}

@ -1,12 +1,220 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine_response_model.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class MedicalFileViewModel extends ChangeNotifier {
int selectedTabIndex = 0;
bool isPatientVaccineListLoading = false;
bool isPatientSickLeaveListLoading = false;
bool isPatientSickLeavePDFLoading = false;
bool isPatientMedicalReportsListLoading = false;
MedicalFileRepo medicalFileRepo;
ErrorHandlerService errorHandlerService;
List<PatientVaccineResponseModel> patientVaccineList = [];
List<PatientSickLeavesResponseModel> patientSickLeaveList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportRequestedList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportReadyList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportCancelledList = [];
String patientSickLeavePDFBase64 = "";
String patientMedicalReportPDFBase64 = "";
int selectedMedicalReportsTabIndex = 0;
MedicalFileViewModel({required this.medicalFileRepo, required this.errorHandlerService});
initMedicalFileProvider() {
isPatientVaccineListLoading = true;
isPatientMedicalReportsListLoading = true;
notifyListeners();
}
void onMedicalReportTabChange(int index) {
selectedMedicalReportsTabIndex = index;
if (index == 0) {
patientMedicalReportList = patientMedicalReportRequestedList;
} else if (index == 1) {
patientMedicalReportList = patientMedicalReportReadyList;
} else if (index == 2) {
patientMedicalReportList = patientMedicalReportCancelledList;
}
notifyListeners();
}
setIsPatientVaccineListLoading(bool isLoading) {
isPatientVaccineListLoading = isLoading;
notifyListeners();
}
setIsPatientSickLeavePDFLoading(bool isLoading) {
isPatientSickLeavePDFLoading = isLoading;
notifyListeners();
}
setIsPatientSickLeaveListLoading(bool val) {
if (val) {
patientSickLeaveList.clear();
patientSickLeavePDFBase64 = "";
}
isPatientSickLeaveListLoading = val;
notifyListeners();
}
setIsPatientMedicalReportsLoading(bool val) {
if (val) {
patientMedicalReportList.clear();
patientMedicalReportPDFBase64 = "";
}
isPatientMedicalReportsListLoading = val;
notifyListeners();
}
void onTabChanged(int index) {
selectedTabIndex = index;
notifyListeners();
}
Future<void> getPatientVaccinesList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientVaccineList.clear();
final result = await medicalFileRepo.getPatientVaccinesList();
result.fold(
(failure) async => await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
onError!(failure.message);
},
),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
patientVaccineList = apiResponse.data!;
isPatientVaccineListLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientSickLeaveList.clear();
final result = await medicalFileRepo.getPatientSickLeavesList();
result.fold(
(failure) async => await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
onError!(failure.message);
},
),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
patientSickLeaveList = apiResponse.data!;
isPatientSickLeaveListLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> getPatientSickLeavePDF(PatientSickLeavesResponseModel patientSickLeavesResponseModel, AuthenticatedUser authenticatedUser,
{Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await medicalFileRepo.getPatientSickLeavePDF(patientSickLeavesResponseModel, authenticatedUser);
result.fold(
(failure) async => await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
onError!(failure.message);
},
),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
patientSickLeavePDFBase64 = apiResponse.data!;
isPatientSickLeaveListLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> getPatientMedicalReportList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientMedicalReportList.clear();
final result = await medicalFileRepo.getPatientMedicalReportsList();
result.fold(
(failure) async => await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
onError!(failure.message);
},
),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
patientMedicalReportList = apiResponse.data!;
if (patientMedicalReportList.isNotEmpty) {
patientMedicalReportRequestedList = patientMedicalReportList.where((element) => element.status == 1).toList();
patientMedicalReportReadyList = patientMedicalReportList.where((element) => element.status == 2).toList();
patientMedicalReportCancelledList = patientMedicalReportList.where((element) => element.status == 4).toList();
}
onMedicalReportTabChange(0);
isPatientMedicalReportsListLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> getPatientMedicalReportPDF(PatientMedicalReportResponseModel patientMedicalReportResponseModel, AuthenticatedUser authenticatedUser,
{Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await medicalFileRepo.getPatientMedicalReportPDF(patientMedicalReportResponseModel, authenticatedUser);
result.fold(
(failure) async => await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
onError!(failure.message);
},
),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
patientMedicalReportPDFBase64 = apiResponse.data!;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
}

@ -0,0 +1,192 @@
class PatientMedicalReportResponseModel {
int? status;
String? encounterDate;
int? projectID;
int? invoiceNo;
int? encounterNo;
String? procedureId;
int? requestType;
String? setupId;
int? patientID;
int? doctorID;
int? clinicID;
String? requestDate;
bool? isRead;
dynamic isReadOn;
num? actualDoctorRate;
String? admissionDate;
int? admissionNumber;
String? appointmentDate;
int? appointmentNO;
String? appointmentTime;
String? clinicDescription;
dynamic clinicDescriptionN;
num? decimalDoctorRate;
String? docName;
dynamic docNameN;
String? doctorImageURL;
String? doctorName;
dynamic doctorNameN;
num? doctorRate;
num? doctorStarsRate;
int? invoiceNoVP;
dynamic invoiceType;
bool? isDoctorAllowVedioCall;
bool? isExecludeDoctor;
bool? isInOutPatient;
String? isInOutPatientDescription;
String? isInOutPatientDescriptionN;
int? noOfPatientsRate;
String? projectName;
dynamic projectNameN;
int? sourceID;
dynamic sourceName;
dynamic sourceNameN;
String? statusDesc;
dynamic strAppointmentDate;
PatientMedicalReportResponseModel(
{this.status,
this.encounterDate,
this.projectID,
this.invoiceNo,
this.encounterNo,
this.procedureId,
this.requestType,
this.setupId,
this.patientID,
this.doctorID,
this.clinicID,
this.requestDate,
this.isRead,
this.isReadOn,
this.actualDoctorRate,
this.admissionDate,
this.admissionNumber,
this.appointmentDate,
this.appointmentNO,
this.appointmentTime,
this.clinicDescription,
this.clinicDescriptionN,
this.decimalDoctorRate,
this.docName,
this.docNameN,
this.doctorImageURL,
this.doctorName,
this.doctorNameN,
this.doctorRate,
this.doctorStarsRate,
this.invoiceNoVP,
this.invoiceType,
this.isDoctorAllowVedioCall,
this.isExecludeDoctor,
this.isInOutPatient,
this.isInOutPatientDescription,
this.isInOutPatientDescriptionN,
this.noOfPatientsRate,
this.projectName,
this.projectNameN,
this.sourceID,
this.sourceName,
this.sourceNameN,
this.statusDesc,
this.strAppointmentDate});
PatientMedicalReportResponseModel.fromJson(Map<String, dynamic> json) {
status = json['Status'];
encounterDate = json['EncounterDate'];
projectID = json['ProjectID'];
invoiceNo = json['InvoiceNo'];
encounterNo = json['EncounterNo'];
procedureId = json['ProcedureId'];
requestType = json['RequestType'];
setupId = json['SetupId'];
patientID = json['PatientID'];
doctorID = json['DoctorID'];
clinicID = json['ClinicID'];
requestDate = json['RequestDate'];
isRead = json['IsRead'];
isReadOn = json['IsReadOn'];
actualDoctorRate = json['ActualDoctorRate'];
admissionDate = json['AdmissionDate'];
admissionNumber = json['AdmissionNumber'];
appointmentDate = json['AppointmentDate'];
appointmentNO = json['AppointmentNO'];
appointmentTime = json['AppointmentTime'];
clinicDescription = json['ClinicDescription'];
clinicDescriptionN = json['ClinicDescriptionN'];
decimalDoctorRate = json['DecimalDoctorRate'];
docName = json['DocName'];
docNameN = json['DocNameN'];
doctorImageURL = json['DoctorImageURL'];
doctorName = json['DoctorName'];
doctorNameN = json['DoctorNameN'];
doctorRate = json['DoctorRate'];
doctorStarsRate = json['DoctorStarsRate'];
invoiceNoVP = json['InvoiceNo_VP'];
invoiceType = json['InvoiceType'];
isDoctorAllowVedioCall = json['IsDoctorAllowVedioCall'];
isExecludeDoctor = json['IsExecludeDoctor'];
isInOutPatient = json['IsInOutPatient'];
isInOutPatientDescription = json['IsInOutPatientDescription'];
isInOutPatientDescriptionN = json['IsInOutPatientDescriptionN'];
noOfPatientsRate = json['NoOfPatientsRate'];
projectName = json['ProjectName'];
projectNameN = json['ProjectNameN'];
sourceID = json['SourceID'];
sourceName = json['SourceName'];
sourceNameN = json['SourceNameN'];
statusDesc = json['StatusDesc'];
strAppointmentDate = json['StrAppointmentDate'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['Status'] = this.status;
data['EncounterDate'] = this.encounterDate;
data['ProjectID'] = this.projectID;
data['InvoiceNo'] = this.invoiceNo;
data['EncounterNo'] = this.encounterNo;
data['ProcedureId'] = this.procedureId;
data['RequestType'] = this.requestType;
data['SetupId'] = this.setupId;
data['PatientID'] = this.patientID;
data['DoctorID'] = this.doctorID;
data['ClinicID'] = this.clinicID;
data['RequestDate'] = this.requestDate;
data['IsRead'] = this.isRead;
data['IsReadOn'] = this.isReadOn;
data['ActualDoctorRate'] = this.actualDoctorRate;
data['AdmissionDate'] = this.admissionDate;
data['AdmissionNumber'] = this.admissionNumber;
data['AppointmentDate'] = this.appointmentDate;
data['AppointmentNO'] = this.appointmentNO;
data['AppointmentTime'] = this.appointmentTime;
data['ClinicDescription'] = this.clinicDescription;
data['ClinicDescriptionN'] = this.clinicDescriptionN;
data['DecimalDoctorRate'] = this.decimalDoctorRate;
data['DocName'] = this.docName;
data['DocNameN'] = this.docNameN;
data['DoctorImageURL'] = this.doctorImageURL;
data['DoctorName'] = this.doctorName;
data['DoctorNameN'] = this.doctorNameN;
data['DoctorRate'] = this.doctorRate;
data['DoctorStarsRate'] = this.doctorStarsRate;
data['InvoiceNo_VP'] = this.invoiceNoVP;
data['InvoiceType'] = this.invoiceType;
data['IsDoctorAllowVedioCall'] = this.isDoctorAllowVedioCall;
data['IsExecludeDoctor'] = this.isExecludeDoctor;
data['IsInOutPatient'] = this.isInOutPatient;
data['IsInOutPatientDescription'] = this.isInOutPatientDescription;
data['IsInOutPatientDescriptionN'] = this.isInOutPatientDescriptionN;
data['NoOfPatientsRate'] = this.noOfPatientsRate;
data['ProjectName'] = this.projectName;
data['ProjectNameN'] = this.projectNameN;
data['SourceID'] = this.sourceID;
data['SourceName'] = this.sourceName;
data['SourceNameN'] = this.sourceNameN;
data['StatusDesc'] = this.statusDesc;
data['StrAppointmentDate'] = this.strAppointmentDate;
return data;
}
}

@ -0,0 +1,176 @@
class PatientSickLeavesResponseModel {
String? setupID;
int? projectID;
int? patientID;
int? patientType;
int? clinicID;
int? doctorID;
int? requestNo;
String? requestDate;
int? sickLeaveDays;
int? appointmentNo;
int? admissionNo;
dynamic reportDate;
num? actualDoctorRate;
String? appointmentDate;
String? clinicName;
double? decimalDoctorRate;
String? doctorImageURL;
String? doctorName;
num? doctorRate;
num? doctorStarsRate;
String? doctorTitle;
int? employeeID;
String? endDate;
int? gender;
String? genderDescription;
bool? isActiveDoctorProfile;
bool? isDoctorAllowVedioCall;
bool? isExecludeDoctor;
bool? isInOutPatient;
String? isInOutPatientDescription;
String? isInOutPatientDescriptionN;
bool? isLiveCareAppointment;
dynamic medicalDirectorApprovedStatus;
int? noOfPatientsRate;
dynamic patientName;
String? projectName;
String? qR;
List<String>? speciality;
String? startDate;
int? status;
String? strRequestDate;
PatientSickLeavesResponseModel(
{this.setupID,
this.projectID,
this.patientID,
this.patientType,
this.clinicID,
this.doctorID,
this.requestNo,
this.requestDate,
this.sickLeaveDays,
this.appointmentNo,
this.admissionNo,
this.reportDate,
this.actualDoctorRate,
this.appointmentDate,
this.clinicName,
this.decimalDoctorRate,
this.doctorImageURL,
this.doctorName,
this.doctorRate,
this.doctorStarsRate,
this.doctorTitle,
this.employeeID,
this.endDate,
this.gender,
this.genderDescription,
this.isActiveDoctorProfile,
this.isDoctorAllowVedioCall,
this.isExecludeDoctor,
this.isInOutPatient,
this.isInOutPatientDescription,
this.isInOutPatientDescriptionN,
this.isLiveCareAppointment,
this.medicalDirectorApprovedStatus,
this.noOfPatientsRate,
this.patientName,
this.projectName,
this.qR,
this.speciality,
this.startDate,
this.status,
this.strRequestDate});
PatientSickLeavesResponseModel.fromJson(Map<String, dynamic> json) {
setupID = json['SetupID'];
projectID = json['ProjectID'];
patientID = json['PatientID'];
patientType = json['PatientType'];
clinicID = json['ClinicID'];
doctorID = json['DoctorID'];
requestNo = json['RequestNo'];
requestDate = json['RequestDate'];
sickLeaveDays = json['SickLeaveDays'];
appointmentNo = json['AppointmentNo'];
admissionNo = json['AdmissionNo'];
reportDate = json['ReportDate'];
actualDoctorRate = json['ActualDoctorRate'];
appointmentDate = json['AppointmentDate'];
clinicName = json['ClinicName'];
decimalDoctorRate = json['DecimalDoctorRate'];
doctorImageURL = json['DoctorImageURL'];
doctorName = json['DoctorName'];
doctorRate = json['DoctorRate'];
doctorStarsRate = json['DoctorStarsRate'];
doctorTitle = json['DoctorTitle'];
employeeID = json['EmployeeID'];
endDate = json['EndDate'];
gender = json['Gender'];
genderDescription = json['GenderDescription'];
isActiveDoctorProfile = json['IsActiveDoctorProfile'];
isDoctorAllowVedioCall = json['IsDoctorAllowVedioCall'];
isExecludeDoctor = json['IsExecludeDoctor'];
isInOutPatient = json['IsInOutPatient'];
isInOutPatientDescription = json['IsInOutPatientDescription'];
isInOutPatientDescriptionN = json['IsInOutPatientDescriptionN'];
isLiveCareAppointment = json['IsLiveCareAppointment'];
medicalDirectorApprovedStatus = json['MedicalDirectorApprovedStatus'];
noOfPatientsRate = json['NoOfPatientsRate'];
patientName = json['PatientName'];
projectName = json['ProjectName'];
qR = json['QR'];
speciality = json['Speciality'].cast<String>();
startDate = json['StartDate'];
status = json['Status'];
strRequestDate = json['StrRequestDate'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['SetupID'] = this.setupID;
data['ProjectID'] = this.projectID;
data['PatientID'] = this.patientID;
data['PatientType'] = this.patientType;
data['ClinicID'] = this.clinicID;
data['DoctorID'] = this.doctorID;
data['RequestNo'] = this.requestNo;
data['RequestDate'] = this.requestDate;
data['SickLeaveDays'] = this.sickLeaveDays;
data['AppointmentNo'] = this.appointmentNo;
data['AdmissionNo'] = this.admissionNo;
data['ReportDate'] = this.reportDate;
data['ActualDoctorRate'] = this.actualDoctorRate;
data['AppointmentDate'] = this.appointmentDate;
data['ClinicName'] = this.clinicName;
data['DecimalDoctorRate'] = this.decimalDoctorRate;
data['DoctorImageURL'] = this.doctorImageURL;
data['DoctorName'] = this.doctorName;
data['DoctorRate'] = this.doctorRate;
data['DoctorStarsRate'] = this.doctorStarsRate;
data['DoctorTitle'] = this.doctorTitle;
data['EmployeeID'] = this.employeeID;
data['EndDate'] = this.endDate;
data['Gender'] = this.gender;
data['GenderDescription'] = this.genderDescription;
data['IsActiveDoctorProfile'] = this.isActiveDoctorProfile;
data['IsDoctorAllowVedioCall'] = this.isDoctorAllowVedioCall;
data['IsExecludeDoctor'] = this.isExecludeDoctor;
data['IsInOutPatient'] = this.isInOutPatient;
data['IsInOutPatientDescription'] = this.isInOutPatientDescription;
data['IsInOutPatientDescriptionN'] = this.isInOutPatientDescriptionN;
data['IsLiveCareAppointment'] = this.isLiveCareAppointment;
data['MedicalDirectorApprovedStatus'] = this.medicalDirectorApprovedStatus;
data['NoOfPatientsRate'] = this.noOfPatientsRate;
data['PatientName'] = this.patientName;
data['ProjectName'] = this.projectName;
data['QR'] = this.qR;
data['Speciality'] = this.speciality;
data['StartDate'] = this.startDate;
data['Status'] = this.status;
data['StrRequestDate'] = this.strRequestDate;
return data;
}
}

@ -0,0 +1,160 @@
class PatientVaccineResponseModel {
String? setupID;
int? projectID;
int? patientID;
int? invoiceNo;
String? procedureID;
String? vaccineName;
Null? vaccineNameN;
String? invoiceDate;
int? doctorID;
int? clinicID;
String? firstName;
String? middleName;
String? lastName;
Null? firstNameN;
Null? middleNameN;
Null? lastNameN;
String? dateofBirth;
int? actualDoctorRate;
String? age;
String? clinicName;
Null? decimalDoctorRate;
Null? doctorImageURL;
String? doctorName;
int? doctorRate;
int? doctorStarsRate;
String? doctorTitle;
int? gender;
Null? genderDescription;
Null? invoiceNoVP;
bool? isActiveDoctorProfile;
bool? isDoctorAllowVedioCall;
bool? isExecludeDoctor;
int? noOfPatientsRate;
String? patientName;
String? projectName;
String? qR;
String? vaccinationDate;
PatientVaccineResponseModel(
{this.setupID,
this.projectID,
this.patientID,
this.invoiceNo,
this.procedureID,
this.vaccineName,
this.vaccineNameN,
this.invoiceDate,
this.doctorID,
this.clinicID,
this.firstName,
this.middleName,
this.lastName,
this.firstNameN,
this.middleNameN,
this.lastNameN,
this.dateofBirth,
this.actualDoctorRate,
this.age,
this.clinicName,
this.decimalDoctorRate,
this.doctorImageURL,
this.doctorName,
this.doctorRate,
this.doctorStarsRate,
this.doctorTitle,
this.gender,
this.genderDescription,
this.invoiceNoVP,
this.isActiveDoctorProfile,
this.isDoctorAllowVedioCall,
this.isExecludeDoctor,
this.noOfPatientsRate,
this.patientName,
this.projectName,
this.qR,
this.vaccinationDate});
PatientVaccineResponseModel.fromJson(Map<String, dynamic> json) {
setupID = json['SetupID'];
projectID = json['ProjectID'];
patientID = json['PatientID'];
invoiceNo = json['InvoiceNo'];
procedureID = json['ProcedureID'];
vaccineName = json['VaccineName'];
vaccineNameN = json['VaccineNameN'];
invoiceDate = json['InvoiceDate'];
doctorID = json['DoctorID'];
clinicID = json['ClinicID'];
firstName = json['FirstName'];
middleName = json['MiddleName'];
lastName = json['LastName'];
firstNameN = json['FirstNameN'];
middleNameN = json['MiddleNameN'];
lastNameN = json['LastNameN'];
dateofBirth = json['DateofBirth'];
actualDoctorRate = json['ActualDoctorRate'];
age = json['Age'];
clinicName = json['ClinicName'];
decimalDoctorRate = json['DecimalDoctorRate'];
doctorImageURL = json['DoctorImageURL'];
doctorName = json['DoctorName'];
doctorRate = json['DoctorRate'];
doctorStarsRate = json['DoctorStarsRate'];
doctorTitle = json['DoctorTitle'];
gender = json['Gender'];
genderDescription = json['GenderDescription'];
invoiceNoVP = json['InvoiceNo_VP'];
isActiveDoctorProfile = json['IsActiveDoctorProfile'];
isDoctorAllowVedioCall = json['IsDoctorAllowVedioCall'];
isExecludeDoctor = json['IsExecludeDoctor'];
noOfPatientsRate = json['NoOfPatientsRate'];
patientName = json['PatientName'];
projectName = json['ProjectName'];
qR = json['QR'];
vaccinationDate = json['VaccinationDate'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['SetupID'] = this.setupID;
data['ProjectID'] = this.projectID;
data['PatientID'] = this.patientID;
data['InvoiceNo'] = this.invoiceNo;
data['ProcedureID'] = this.procedureID;
data['VaccineName'] = this.vaccineName;
data['VaccineNameN'] = this.vaccineNameN;
data['InvoiceDate'] = this.invoiceDate;
data['DoctorID'] = this.doctorID;
data['ClinicID'] = this.clinicID;
data['FirstName'] = this.firstName;
data['MiddleName'] = this.middleName;
data['LastName'] = this.lastName;
data['FirstNameN'] = this.firstNameN;
data['MiddleNameN'] = this.middleNameN;
data['LastNameN'] = this.lastNameN;
data['DateofBirth'] = this.dateofBirth;
data['ActualDoctorRate'] = this.actualDoctorRate;
data['Age'] = this.age;
data['ClinicName'] = this.clinicName;
data['DecimalDoctorRate'] = this.decimalDoctorRate;
data['DoctorImageURL'] = this.doctorImageURL;
data['DoctorName'] = this.doctorName;
data['DoctorRate'] = this.doctorRate;
data['DoctorStarsRate'] = this.doctorStarsRate;
data['DoctorTitle'] = this.doctorTitle;
data['Gender'] = this.gender;
data['GenderDescription'] = this.genderDescription;
data['InvoiceNo_VP'] = this.invoiceNoVP;
data['IsActiveDoctorProfile'] = this.isActiveDoctorProfile;
data['IsDoctorAllowVedioCall'] = this.isDoctorAllowVedioCall;
data['IsExecludeDoctor'] = this.isExecludeDoctor;
data['NoOfPatientsRate'] = this.noOfPatientsRate;
data['PatientName'] = this.patientName;
data['ProjectName'] = this.projectName;
data['QR'] = this.qR;
data['VaccinationDate'] = this.vaccinationDate;
return data;
}
}

@ -34,6 +34,8 @@ abstract class MyAppointmentsRepo {
{required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, required String scannedCode, required int checkInType});
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointmentsForTimeLine();
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList();
}
class MyAppointmentsRepoImp implements MyAppointmentsRepo {
@ -444,4 +446,49 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList() async {
Map<String, dynamic> mapDevice = {
"Top": 0,
"beforeDays": 0,
"exludType": 4,
};
try {
GenericApiModel<List<PatientAppointmentHistoryResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_MY_DOCTOR,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['PatientDoctorAppointmentResultList'];
// if (list == null || list.isEmpty) {
// throw Exception("Appointments list is empty");
// }
final appointmentsList = list.map((item) => PatientAppointmentHistoryResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientAppointmentHistoryResponseModel>();
apiResponse = GenericApiModel<List<PatientAppointmentHistoryResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: appointmentsList,
);
} catch (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()));
}
}
}

@ -13,6 +13,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
bool isMyAppointmentsLoading = false;
bool isAppointmentPatientShareLoading = false;
bool isTimeLineAppointmentsLoading = false;
bool isPatientMyDoctorsLoading = false;
List<PatientAppointmentHistoryResponseModel> patientAppointmentsHistoryList = [];
@ -21,6 +22,8 @@ class MyAppointmentsViewModel extends ChangeNotifier {
List<PatientAppointmentHistoryResponseModel> patientTimelineAppointmentsList = [];
List<PatientAppointmentHistoryResponseModel> patientMyDoctorsList = [];
PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel;
MyAppointmentsViewModel({required this.myAppointmentsRepo, required this.errorHandlerService});
@ -35,9 +38,11 @@ class MyAppointmentsViewModel extends ChangeNotifier {
patientUpcomingAppointmentsHistoryList.clear();
patientArrivedAppointmentsHistoryList.clear();
patientTimelineAppointmentsList.clear();
patientMyDoctorsList.clear();
isMyAppointmentsLoading = true;
isAppointmentPatientShareLoading = true;
isTimeLineAppointmentsLoading = true;
isPatientMyDoctorsLoading = true;
notifyListeners();
}
@ -56,6 +61,11 @@ class MyAppointmentsViewModel extends ChangeNotifier {
notifyListeners();
}
setIsPatientMyDoctorsLoading(bool val) {
isPatientMyDoctorsLoading = val;
notifyListeners();
}
setAppointmentReminder(bool value, PatientAppointmentHistoryResponseModel item) {
int index = patientAppointmentsHistoryList.indexOf(item);
if (index != -1) {
@ -263,4 +273,24 @@ class MyAppointmentsViewModel extends ChangeNotifier {
},
);
}
Future<void> getPatientMyDoctors({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await myAppointmentsRepo.getPatientDoctorsList();
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
patientMyDoctorsList = apiResponse.data!;
isPatientMyDoctorsLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
}

@ -780,6 +780,9 @@ abstract class LocaleKeys {
static const resultsPending = 'resultsPending';
static const resultsAvailable = 'resultsAvailable';
static const viewReport = 'viewReport';
static const checkAvailability = 'checkAvailability';
static const readInstructions = 'readInstructions';
static const searchLabReport = 'searchLabReport';
static const prescriptionDeliveryError = 'prescriptionDeliveryError';
static const receiveOtpToast = 'receiveOtpToast';
static const enterPhoneNumber = 'enterPhoneNumber';
@ -802,12 +805,13 @@ abstract class LocaleKeys {
static const loginByOTP = 'loginByOTP';
static const guest = 'guest';
static const switchAccount = 'switchAccount';
static const checkAvailability = 'checkAvailability';
static const readInstructions = 'readInstructions';
static const searchLabReport = 'searchLabReport';
static const lastloginBy = 'lastloginBy';
static const allSet ='allSet';
static const lastLoginBy = 'lastLoginBy';
static const allSet = 'allSet';
static const enableQuickLogin = 'enableQuickLogin';
static const enableMsg = 'enableMsg';
static const notNow = 'notNow';
static const pendingActivation = 'pendingActivation';
static const awaitingApproval = 'awaitingApproval';
static const ready = 'ready';
}

@ -9,7 +9,8 @@ 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/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.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/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
@ -96,7 +97,10 @@ void main() async {
),
),
ChangeNotifierProvider<MedicalFileViewModel>(
create: (_) => MedicalFileViewModel(),
create: (_) => MedicalFileViewModel(
medicalFileRepo: getIt(),
errorHandlerService: getIt(),
),
),
ChangeNotifierProvider<MyAppointmentsViewModel>(
create: (_) => MyAppointmentsViewModel(
@ -116,6 +120,12 @@ void main() async {
errorHandlerService: getIt(),
),
),
ChangeNotifierProvider<BookAppointmentsViewModel>(
create: (_) => BookAppointmentsViewModel(
bookAppointmentsRepo: getIt(),
errorHandlerService: getIt(),
),
),
ChangeNotifierProvider<AuthenticationViewModel>(
create: (_) => AuthenticationViewModel(
authenticationRepo: getIt(),

@ -370,7 +370,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
onSuccess: (value) async {
print(value);
await myAppointmentsViewModel.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(appState, widget.patientAppointmentHistoryResponseModel.projectID)
advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,

@ -0,0 +1,160 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.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/extensions/widget_extensions.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/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
import '../../widgets/chip/app_custom_chip_widget.dart';
class MyDoctorsPage extends StatelessWidget {
MyDoctorsPage({super.key});
late MyAppointmentsViewModel myAppointmentsViewModel;
@override
Widget build(BuildContext context) {
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.myDoctor.tr(context: context),
child: SingleChildScrollView(
child: Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
ListView.separated(
scrollDirection: Axis.vertical,
itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : myAppointmentsVM.patientMyDoctorsList.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.h, right: 24.h),
itemBuilder: (context, index) {
return myAppointmentsVM.isPatientMyDoctorsLoading
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 63.h,
height: 63.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: true),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
],
),
],
),
),
],
),
],
),
),
)
: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
width: 63.h,
height: 63.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: false),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(myAppointmentsVM.patientMyDoctorsList[index].doctorName).toString().toText16(isBold: true).toShimmer2(isShow: false),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].clinicName).toShimmer2(isShow: false, width: 16.h),
AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].projectName).toShimmer2(isShow: false, width: 16.h),
],
),
],
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText16(),
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor),
],
),
],
),
),
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
SizedBox(height: 60.h),
],
);
}),
),
),
);
}
}

@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/enums.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/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/extensions/context_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -28,13 +29,17 @@ class LoginScreen extends StatefulWidget {
}
class LoginScreenState extends State<LoginScreen> {
late FocusNode _nationalIdFocusNode;
@override
void initState() {
super.initState();
_nationalIdFocusNode = FocusNode();
}
@override
void dispose() {
_nationalIdFocusNode.dispose();
super.dispose();
}
@ -48,12 +53,14 @@ class LoginScreenState extends State<LoginScreen> {
Navigator.of(context).pop();
},
onLanguageChanged: (String value) {
// context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US'));
context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA'));
},
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside
// Dismiss the keyboard and unfocus any focused widget when tapping outside
_nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
child: Padding(
@ -70,6 +77,7 @@ class LoginScreenState extends State<LoginScreen> {
labelText: "${LocaleKeys.nationalId.tr()} / ${LocaleKeys.fileNo.tr()}",
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
keyboardType: TextInputType.number,
isEnable: true,
prefix: null,
@ -80,7 +88,7 @@ class LoginScreenState extends State<LoginScreen> {
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h),
leadingIcon: AppAssets.student_card,
errorMessage: "Please enter a valid national ID or file number",
hasError: true,
hasError: false,
),
SizedBox(height: 16.h), // Adjusted to sizer unit (approx 16px)
CustomButton(
@ -88,20 +96,16 @@ class LoginScreenState extends State<LoginScreen> {
icon: AppAssets.login1,
iconColor: Colors.white,
onPressed: () {
showLoginModelSheet(context: context, phoneNumberController: authVm.phoneNumberController, authViewModel: authVm);
// if (nationIdController.text.isNotEmpty) {
_nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus();
// } else {
// showBottomSheet(
// child: ExceptionBottomSheet(
// message: TranslationBase.of(context).pleaseEnterNationalIdOrFileNo,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// }
if (ValidationUtils.isValidatedId(
nationalId: authVm.nationalIdController.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
showLoginModelSheet(context: context, phoneNumberController: authVm.phoneNumberController, authViewModel: authVm);
}
},
),
SizedBox(height: 10.h), // Adjusted to sizer unit (approx 14px)
@ -121,11 +125,10 @@ class LoginScreenState extends State<LoginScreen> {
TextSpan(
text: LocaleKeys.registernow.tr(),
style: context.dynamicTextStyle(
color: AppColors.primaryRedColor,
fontSize: 14.fSize, // Adjusted to sizer unit
height: 26 / 16, // Ratio
fontWeight: FontWeight.w500,
),
color: AppColors.primaryRedColor,
fontSize: 14.fSize, // Adjusted to sizer unit
height: 26 / 16, // Ratio
fontWeight: FontWeight.w500),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.of(context).push(
@ -172,7 +175,13 @@ class LoginScreenState extends State<LoginScreen> {
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(),
onPressed: () async {
await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms);
if (ValidationUtils.isValidatePhone(
phoneNumber: phoneNumberController!.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms);
}
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
@ -195,9 +204,13 @@ class LoginScreenState extends State<LoginScreen> {
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(),
onPressed: () async {
log("phoneNumberController: ${phoneNumberController == null}");
log("phoneNumberControllerVa: ${phoneNumberController?.text}");
await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp);
if (ValidationUtils.isValidatePhone(
phoneNumber: phoneNumberController!.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp);
}
},
backgroundColor: Colors.white,
borderColor: AppColors.borderOnlyColor,

@ -2,11 +2,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.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/enums.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/core/utils/validation_utils.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/authentication/authentication_view_model.dart';
@ -26,19 +25,25 @@ class RegisterNew extends StatefulWidget {
}
class _RegisterNew extends State<RegisterNew> {
late FocusNode _nationalIdFocusNode;
late FocusNode _dobFocusNode;
@override
void initState() {
super.initState();
_nationalIdFocusNode = FocusNode();
_dobFocusNode = FocusNode();
}
@override
void dispose() {
_nationalIdFocusNode.dispose();
_dobFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
AuthenticationViewModel authVm = context.read<AuthenticationViewModel>();
return Scaffold(
@ -48,11 +53,14 @@ class _RegisterNew extends State<RegisterNew> {
Navigator.of(context).pop();
},
onLanguageChanged: (String value) {
// context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US'));
context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA'));
},
),
body: GestureDetector(
onTap: () {
// Dismiss keyboard and unfocus all input fields
_nationalIdFocusNode.unfocus();
_dobFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: ScrollConfiguration(
@ -90,6 +98,7 @@ class _RegisterNew extends State<RegisterNew> {
labelText: LocaleKeys.nationalIdNumber.tr(),
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
isEnable: true,
prefix: null,
isAllowRadius: true,
@ -104,6 +113,7 @@ class _RegisterNew extends State<RegisterNew> {
labelText: LocaleKeys.dob.tr(),
hintText: "11 July, 1994",
controller: authVm.dobController,
focusNode: _dobFocusNode,
isEnable: true,
prefix: null,
isAllowRadius: true,
@ -156,7 +166,21 @@ class _RegisterNew extends State<RegisterNew> {
text: "Register",
icon: AppAssets.note_edit,
onPressed: () {
showRegisterModel(context: context, authVM: authVm);
// Dismiss keyboard before proceeding
_nationalIdFocusNode.unfocus();
_dobFocusNode.unfocus();
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatedId(
nationalId: authVm.nationalIdController.text,
selectedCountry: authVm.selectedCountrySignup,
isTermsAccepted: authVm.isTermsAccepted,
dob: authVm.dobController.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
showRegisterModel(context: context, authVM: authVm);
}
},
),
SizedBox(height: 14),
@ -216,13 +240,24 @@ class _RegisterNew extends State<RegisterNew> {
isEnableCountryDropdown: false,
onCountryChange: authVM.onCountryChange,
onChange: authVM.onPhoneNumberChange,
autoFocus: true,
buttons: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(),
onPressed: () async {
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms);
// Dismiss keyboard before validation
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatePhone(
phoneNumber: authVM.phoneNumberController.text,
onOkPress: () {
Navigator.of(context).pop();
},
)) {
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms);
}
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
@ -245,7 +280,17 @@ class _RegisterNew extends State<RegisterNew> {
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(),
onPressed: () async {
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp);
// Dismiss keyboard before validation
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatePhone(
phoneNumber: authVM.phoneNumberController.text,
onOkPress: () {
Navigator.of(context).pop();
},
)) {
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp);
}
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.borderOnlyColor,

@ -42,7 +42,7 @@ class _SavedLogin extends State<SavedLogin> {
authVm.nationalIdController.text = appState.getSelectDeviceByImeiRespModelElement!.identificationNo!;
if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) {
authVm.loginWithFingerPrintFace((){});
authVm.loginWithFingerPrintFace(() {});
}
super.initState();
@ -84,7 +84,7 @@ class _SavedLogin extends State<SavedLogin> {
children: [
// Last login info
("${LocaleKeys.lastloginBy.tr()} ${loginType.displayName}").toText14(isBold: true, color: AppColors.greyTextColor),
("${LocaleKeys.lastLoginBy.tr()} ${loginType.displayName}").toText14(isBold: true, color: AppColors.greyTextColor),
(appState.getSelectDeviceByImeiRespModelElement!.createdOn != null
? DateUtil.getFormattedDate(DateUtil.convertStringToDate(appState.getSelectDeviceByImeiRespModelElement!.createdOn!), "d MMMM, y 'at' HH:mm")
: '--')
@ -101,7 +101,7 @@ class _SavedLogin extends State<SavedLogin> {
text: "${LocaleKeys.loginBy.tr()} ${loginType.displayName}",
onPressed: () {
if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) {
authVm.loginWithFingerPrintFace((){});
authVm.loginWithFingerPrintFace(() {});
} else {
// int? val = loginType.toInt;
authVm.checkUserAuthentication(otpTypeEnum: loginType == LoginTypeEnum.sms ? OTPTypeEnum.sms : OTPTypeEnum.whatsapp);
@ -114,8 +114,9 @@ class _SavedLogin extends State<SavedLogin> {
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(0, 10, 0, 10),
icon: getTypeIcons(loginType.toInt), //loginType == LoginTypeEnum.sms ? AppAssets.sms :AppAssets.whatsapp,
iconColor: loginType != LoginTypeEnum.whatsapp ? Colors.white: null ,
icon: getTypeIcons(loginType.toInt),
//loginType == LoginTypeEnum.sms ? AppAssets.sms :AppAssets.whatsapp,
iconColor: loginType != LoginTypeEnum.whatsapp ? Colors.white : null,
),
),
],
@ -226,7 +227,7 @@ class _SavedLogin extends State<SavedLogin> {
iconColor: null,
onPressed: () {
if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) {
authVm.loginWithFingerPrintFace((){});
authVm.loginWithFingerPrintFace(() {});
} else {
loginType = LoginTypeEnum.whatsapp;
int? val = loginType.toInt;
@ -272,11 +273,12 @@ class _SavedLogin extends State<SavedLogin> {
width: MediaQuery.of(context).size.width * 0.05,
),
Expanded(
child: Container(
child: SizedBox(
height: 56,
child: CustomButton(
text: LocaleKeys.switchAccount.tr(),
onPressed: () {
onPressed: () async {
await authVm.clearDefaultInputValues();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => LoginScreen()),
);

@ -0,0 +1,159 @@
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';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
import 'package:provider/provider.dart';
class BookAppointmentPage extends StatefulWidget {
const BookAppointmentPage({super.key});
@override
State<BookAppointmentPage> createState() => _BookAppointmentPageState();
}
class _BookAppointmentPageState extends State<BookAppointmentPage> {
late AppState appState;
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.bookAppo.tr(context: context),
child: SingleChildScrollView(
child: Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
CustomTabBar(
activeTextColor: Color(0xffED1C2B),
activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
tabs: [
CustomTabBarModel(null, "General".needTranslation),
CustomTabBarModel(null, "LiveCare".needTranslation),
],
onTabChange: (index) {
bookAppointmentsVM.onTabChanged(index);
},
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
getSelectedTabData(bookAppointmentsVM.selectedTabIndex),
],
);
}),
),
),
);
}
Widget getSelectedTabData(int index) {
switch (index) {
case 0:
return Column(
children: [
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h),
SizedBox(width: 12.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Search By Clinic".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500),
"Tap to select clinic".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
],
),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h),
],
).onPress(() {
Navigator.of(context).push(
FadePage(
page: SelectClinicPage(),
),
);
}),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h),
SizedBox(width: 12.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Search By Doctor".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500),
"Tap to select".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
],
),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h),
],
).onPress(() {}),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.search_by_region_icon, width: 40.h, height: 40.h),
SizedBox(width: 12.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Search By Region".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500),
"Central Region".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
],
),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h),
],
).onPress(() {}),
],
),
),
),
],
).paddingSymmetrical(24.h, 0.h);
default:
SizedBox.shrink();
}
return Container();
}
}

@ -0,0 +1,29 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class SelectClinicPage extends StatefulWidget {
const SelectClinicPage({super.key});
@override
State<SelectClinicPage> createState() => _SelectClinicPageState();
}
class _SelectClinicPageState extends State<SelectClinicPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.selectClinic.tr(context: context),
child: SingleChildScrollView(
child: Column(
children: [],
),
),
),
);
}
}

@ -7,7 +7,7 @@ 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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';

@ -12,7 +12,7 @@ import 'package:hmg_patient_app_new/extensions/int_extensions.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/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
@ -64,6 +64,7 @@ class _LandingPageState extends State<LandingPage> {
habibWalletVM.getPatientBalanceAmount();
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
myAppointmentsViewModel.getPatientMyDoctors();
prescriptionsViewModel.initPrescriptionsViewModel();
}
});

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart';
import 'package:hmg_patient_app_new/presentation/home/landing_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart';
import 'package:hmg_patient_app_new/widgets/bottom_navigation/bottom_navigation.dart';
@ -23,7 +24,7 @@ class _LandingNavigationState extends State<LandingNavigation> {
children: [
const LandingPage(),
MedicalFilePage(),
const LandingPage(),
const BookAppointmentPage(),
const LandingPage(),
const LandingPage(),
],

@ -4,7 +4,7 @@ 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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';

@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.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/presentation/appointments/my_doctors_page.dart';
import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
@ -85,6 +87,22 @@ class SmallServiceCard extends StatelessWidget {
),
);
break;
case "my_doctors":
Navigator.of(context).push(
FadePage(
page: MyDoctorsPage(),
),
);
break;
case "sick_leaves":
Navigator.of(context).push(
FadePage(
page: PatientSickleavesListPage(),
),
);
break;
default:
// Handle unknown service
break;

@ -21,8 +21,9 @@ class CollapsingListView extends StatelessWidget {
VoidCallback? history;
Widget? bottomChild;
bool isClose;
bool isLeading;
CollapsingListView({required this.title, this.child, this.search, this.isClose = false, this.bottomChild, this.report, this.logout, this.history});
CollapsingListView({required this.title, this.child, this.search, this.isClose = false, this.bottomChild, this.report, this.logout, this.history, this.isLeading = true});
@override
Widget build(BuildContext context) {
@ -33,17 +34,18 @@ class CollapsingListView extends StatelessWidget {
CustomScrollView(
slivers: [
SliverAppBar(
automaticallyImplyLeading: false,
pinned: true,
expandedHeight: 100,
stretch: true,
systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light),
surfaceTintColor: Colors.transparent,
backgroundColor: AppColors.bgScaffoldColor,
leading: IconButton(
leading: isLeading ? IconButton(
icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h),
padding: EdgeInsets.only(left: 12),
onPressed: () => Navigator.pop(context),
),
) : SizedBox.shrink(),
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
final double maxHeight = 100;

@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart';
@ -57,7 +58,7 @@ class _LabOrdersPageState extends State<LabOrdersPage> {
}
},
child: SingleChildScrollView(
padding: EdgeInsets.all(24),
padding: EdgeInsets.all(24.h),
physics: NeverScrollableScrollPhysics(),
child: Consumer<LabViewModel>(
builder: (context, model, child) {

@ -13,23 +13,33 @@ 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/insurance/insurance_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.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/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart';
import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart';
import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart';
import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/medical_reports_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/vaccine_list_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_card.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_sick_leave_card.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_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/custom_tab_bar.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
import 'package:provider/provider.dart';
import '../prescriptions/prescription_detail_page.dart';
import 'widgets/medical_file_appointment_card.dart';
class MedicalFilePage extends StatefulWidget {
@ -43,6 +53,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
late InsuranceViewModel insuranceViewModel;
late AppState appState;
late MyAppointmentsViewModel myAppointmentsViewModel;
late MedicalFileViewModel medicalFileViewModel;
int currentIndex = 0;
@ -50,6 +61,8 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
void initState() {
scheduleMicrotask(() {
insuranceViewModel.initInsuranceProvider();
medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
medicalFileViewModel.getPatientSickLeaveList();
});
super.initState();
}
@ -58,16 +71,17 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
Widget build(BuildContext context) {
insuranceViewModel = Provider.of<InsuranceViewModel>(context, listen: false);
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(top: 80.0),
body: CollapsingListView(
title: LocaleKeys.medicalFile.tr(context: context),
isLeading: false,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.medicalFile.tr(context: context).toText22(isBold: true).paddingSymmetrical(24.h, 0.0),
SizedBox(height: 16.h),
TextInputWidget(
labelText: LocaleKeys.search.tr(context: context),
@ -318,116 +332,257 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
SizedBox(height: 16.h),
Consumer<PrescriptionsViewModel>(builder: (context, prescriptionVM, child) {
return prescriptionVM.isPrescriptionsOrdersLoading
? const MoviesShimmerWidget()
: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: Colors.white,
borderRadius: 20.h,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
children: [
ListView.separated(
itemCount: prescriptionVM.patientPrescriptionOrders.length,
shrinkWrap: true,
padding: const EdgeInsets.only(left: 0, right: 8),
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Row(
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.prescription_item_icon,
width: 40.h,
height: 40.h,
),
SizedBox(width: 8.h),
Row(
mainAxisSize: MainAxisSize.max,
? const MoviesShimmerWidget().paddingSymmetrical(24.h, 0.h)
: prescriptionVM.patientPrescriptionOrders.isNotEmpty
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: Colors.white,
borderRadius: 20.h,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
children: [
ListView.separated(
itemCount: prescriptionVM.patientPrescriptionOrders.length,
shrinkWrap: true,
padding: const EdgeInsets.only(left: 0, right: 8),
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Row(
children: [
Column(
children: [
// SizedBox(width: 150.h, child: prescriptionVM.prescriptionDetailsList[index].itemDescription!.toText12(isBold: true, maxLine: 1)),
// SizedBox(
// width: 150.h,
// child:
// "Prescribed By: ${widget.patientAppointmentHistoryResponseModel.doctorTitle} ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}"
// .needTranslation
// .toText10(weight: FontWeight.w500, color: AppColors.greyTextColor, letterSpacing: -0.4),
// ),
],
),
SizedBox(width: 68.h),
Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 18.h,
height: 13.h,
fit: BoxFit.contain,
Image.network(
prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!,
width: 63.h,
height: 63.h,
fit: BoxFit.fill,
).circle(100),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true),
SizedBox(height: 4.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!),
AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate), false),
),
],
),
],
),
),
SizedBox(width: 40.h),
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor),
],
),
],
).onPress(() {
prescriptionVM.setPrescriptionsDetailsLoading();
Navigator.of(context).push(
FadePage(
page: PrescriptionDetailPage(prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]),
),
);
}),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
SizedBox(height: 16.h),
const Divider(color: AppColors.dividerColor),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "All Prescriptions".needTranslation,
onPressed: () {
Navigator.of(context).push(
FadePage(
page: PrescriptionsListPage(),
),
);
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.requests,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
).onPress(() {
prescriptionVM.setPrescriptionsDetailsLoading();
// Navigator.of(context).push(
// FadePage(
// page: PrescriptionDetailPage(prescriptionsResponseModel: getPrescriptionRequestModel()),
// ),
// );
}),
SizedBox(height: 16.h),
const Divider(color: AppColors.dividerColor),
SizedBox(height: 16.h),
Row(
SizedBox(width: 10.h),
Expanded(
child: CustomButton(
text: "All Medications".needTranslation,
onPressed: () {},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.all_medications_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
),
),
],
),
],
),
),
).paddingSymmetrical(24.h, 0.h)
: SizedBox.shrink();
}),
SizedBox(height: 24.h),
//My Doctor Section
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.myDoctor.tr(context: context).toText18(isBold: true),
Row(
children: [
LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
).onPress(() {
myAppointmentsViewModel.setIsPatientMyDoctorsLoading(true);
myAppointmentsViewModel.getPatientMyDoctors();
Navigator.of(context).push(
FadePage(
page: MyDoctorsPage(),
),
);
}),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
return SizedBox(
height: 120.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : myAppointmentsVM.patientMyDoctorsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 24.h, right: 24.h),
itemBuilder: (context, index) {
return myAppointmentsVM.isPatientMyDoctorsLoading
? SizedBox(
width: 80.h,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 64.h,
height: 64.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: true, radius: 50.h),
SizedBox(height: 8.h),
Expanded(
child: CustomButton(
text: "All Prescriptions".needTranslation,
onPressed: () {
// Navigator.of(context)
// .push(
// FadePage(
// page: PrescriptionsListPage(),
// ),
// )
// .then((val) {
// prescriptionsViewModel.setPrescriptionsDetailsLoading();
// prescriptionsViewModel.getPrescriptionDetails(getPrescriptionRequestModel());
// });
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.requests,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
),
child: ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true),
),
],
),
],
),
),
).paddingSymmetrical(24.h, 0.h);
)
: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SizedBox(
width: 80.h,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
width: 64.h,
height: 64.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: false, radius: 50.h),
SizedBox(height: 8.h),
Expanded(
child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName)
.toString()
.toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2)
.toShimmer2(isShow: false),
),
],
),
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
),
);
}),
SizedBox(height: 24.h),
"Others".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 13, mainAxisSpacing: 13),
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
MedicalFileCard(
label: "Eye Test Results".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.eye_result_icon,
isLargeText: true,
iconSize: 40.h,
),
MedicalFileCard(
label: "Allergy Info".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.allergy_info_icon,
isLargeText: true,
iconSize: 40.h,
),
MedicalFileCard(
label: "Vaccine Info".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.vaccine_info_icon,
isLargeText: true,
iconSize: 40.h,
).onPress(() {
Navigator.of(context).push(
FadePage(
page: VaccineListPage(),
),
);
}),
],
).paddingSymmetrical(24.h, 0.0),
SizedBox(height: 24.h),
],
);
case 1:
@ -448,17 +603,113 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
padding: EdgeInsets.only(top: 12),
shrinkWrap: true,
children: [
MedicalFileCard(label: "Update Insurance".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon),
MedicalFileCard(label: "Insurance Approvals".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon),
MedicalFileCard(label: "My Invoices List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon),
MedicalFileCard(label: "Ancillary Orders List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon),
MedicalFileCard(
label: "Update Insurance".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.eye_result_icon,
isLargeText: false,
iconSize: 36.h)
.onPress(() {
Navigator.of(context).push(
FadePage(
page: InsuranceHomePage(),
),
);
}),
MedicalFileCard(
label: "Insurance Approvals".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.eye_result_icon,
isLargeText: false,
iconSize: 36.h),
MedicalFileCard(
label: "My Invoices List".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.eye_result_icon,
isLargeText: false,
iconSize: 36.h),
MedicalFileCard(
label: "Ancillary Orders List".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.eye_result_icon,
isLargeText: false,
iconSize: 36.h),
],
).paddingSymmetrical(24.h, 0.0),
SizedBox(height: 16.h),
],
);
case 2:
return Container();
// Requests Tab Data
return Column(
children: [
Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
return medicalFileVM.isPatientSickLeaveListLoading
? PatientSickLeaveCard(
patientSickLeavesResponseModel: PatientSickLeavesResponseModel(),
isLoading: true,
).paddingSymmetrical(24.h, 0.0)
: medicalFileVM.patientSickLeaveList.isNotEmpty
? PatientSickLeaveCard(
patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first,
isLoading: false,
).paddingSymmetrical(24.h, 0.0)
: SizedBox.shrink();
}),
SizedBox(height: 16.h),
GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 13, mainAxisSpacing: 13),
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
MedicalFileCard(
label: LocaleKeys.monthlyReports.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.eye_result_icon,
isLargeText: false,
iconSize: 40.h,
),
MedicalFileCard(
label: "Medical Reports".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.allergy_info_icon,
isLargeText: false,
iconSize: 40.h,
).onPress(() {
medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
medicalFileViewModel.getPatientMedicalReportList();
Navigator.of(context).push(
FadePage(
page: MedicalReportsPage(),
),
);
}),
MedicalFileCard(
label: "Sick Leave Report".needTranslation,
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.vaccine_info_icon,
isLargeText: false,
iconSize: 40.h,
).onPress(() {
Navigator.of(context).push(
FadePage(
page: PatientSickleavesListPage(),
),
);
}),
],
).paddingSymmetrical(24.h, 0.0),
SizedBox(height: 24.h),
],
);
case 3:
return Container();
default:

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.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/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_medical_report_card.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart';
import 'package:provider/provider.dart';
class MedicalReportsPage extends StatefulWidget {
const MedicalReportsPage({super.key});
@override
State<MedicalReportsPage> createState() => _MedicalReportsPageState();
}
class _MedicalReportsPageState extends State<MedicalReportsPage> {
late MedicalFileViewModel medicalFileViewModel;
@override
Widget build(BuildContext context) {
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "Medical Reports".needTranslation,
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 16.h),
CustomTabBar(
activeTextColor: Color(0xffED1C2B),
activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
tabs: [
CustomTabBarModel(null, "Requested".needTranslation),
CustomTabBarModel(null, "Ready".needTranslation),
CustomTabBarModel(null, "Cancelled".needTranslation),
],
onTabChange: (index) {
medicalFileViewModel.onMedicalReportTabChange(index);
},
).paddingSymmetrical(24.h, 0.h),
Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
return ListView.separated(
padding: EdgeInsets.only(top: 24.h),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading ? 3 : medicalFileViewModel.patientMedicalReportList.length,
// medicalFileViewModel.patientMedicalReportList.isNotEmpty
// ? medicalFileViewModel.patientMedicalReportList.length
// : 1,
itemBuilder: (context, index) {
return medicalFileViewModel.isPatientMedicalReportsListLoading
? PatientMedicalReportCard(
patientMedicalReportResponseModel: PatientMedicalReportResponseModel(),
medicalFileViewModel: medicalFileVM,
isLoading: true,
).paddingSymmetrical(24.h, 0.h)
: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: PatientMedicalReportCard(
patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index],
medicalFileViewModel: medicalFileVM,
isLoading: false,
),
).paddingSymmetrical(24.h, 0.h),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
);
}),
SizedBox(height: 24.h),
],
),
),
),
);
}
}

@ -0,0 +1,88 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
import 'widgets/patient_sick_leave_card.dart';
class PatientSickleavesListPage extends StatefulWidget {
const PatientSickleavesListPage({super.key});
@override
State<PatientSickleavesListPage> createState() => _PatientSickleavesListPageState();
}
class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
late MedicalFileViewModel medicalFileViewModel;
@override
void initState() {
scheduleMicrotask(() {
medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
medicalFileViewModel.getPatientSickLeaveList(onError: (error) {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}",
child: SingleChildScrollView(
child: Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListView.separated(
scrollDirection: Axis.vertical,
itemCount: medicalFileVM.isPatientSickLeaveListLoading ? 3 : medicalFileVM.patientSickLeaveList.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return medicalFileVM.isPatientSickLeaveListLoading
? PatientSickLeaveCard(
patientSickLeavesResponseModel: PatientSickLeavesResponseModel(),
isLoading: true,
).paddingSymmetrical(24.h, 0.0)
: medicalFileVM.patientSickLeaveList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: PatientSickLeaveCard(
patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first,
isLoading: false,
).paddingSymmetrical(24.h, 0.0),
),
),
)
: SizedBox.shrink();
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h),
),
SizedBox(height: 60.h),
],
);
}),
),
),
);
}
}

@ -0,0 +1,181 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.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/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
import '../../widgets/chip/app_custom_chip_widget.dart';
class VaccineListPage extends StatefulWidget {
const VaccineListPage({super.key});
@override
State<VaccineListPage> createState() => _VaccineListPageState();
}
class _VaccineListPageState extends State<VaccineListPage> {
late MedicalFileViewModel medicalFileViewModel;
@override
void initState() {
scheduleMicrotask(() {
medicalFileViewModel.setIsPatientVaccineListLoading(true);
medicalFileViewModel.getPatientVaccinesList(onError: (error) {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "Vaccine Info".needTranslation,
child: SingleChildScrollView(
child: Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
ListView.separated(
scrollDirection: Axis.vertical,
itemCount: medicalFileVM.isPatientVaccineListLoading ? 5 : medicalFileVM.patientVaccineList.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.h, right: 24.h),
itemBuilder: (context, index) {
return medicalFileVM.isPatientVaccineListLoading
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 63.h,
height: 63.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: true),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
],
),
],
),
),
],
),
],
),
),
)
: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image.network(
// medicalFileVM.patientVaccineList[index].doctorImageURL,
// width: 63.h,
// height: 63.h,
// fit: BoxFit.fill,
// ).circle(100).toShimmer2(isShow: false),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(medicalFileVM.patientVaccineList[index].doctorName).toString().toText16(isBold: true).toShimmer2(isShow: false),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(medicalFileVM.patientVaccineList[index].vaccinationDate), false)),
AppCustomChipWidget(labelText: medicalFileVM.patientVaccineList[index].vaccineName).toShimmer2(isShow: false, width: 16.h),
AppCustomChipWidget(labelText: medicalFileVM.patientVaccineList[index].clinicName).toShimmer2(isShow: false, width: 16.h),
AppCustomChipWidget(labelText: medicalFileVM.patientVaccineList[index].projectName).toShimmer2(isShow: false, width: 16.h),
],
),
],
),
),
],
),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// "".toText16(),
// Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor),
// ],
// ),
],
),
),
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
SizedBox(height: 60.h),
],
);
}),
),
),
);
}
}

@ -36,7 +36,7 @@ class MedicalFileCard extends StatelessWidget {
children: [
Utils.buildSvgWithAssets(icon: svgIcon, width: iconSize.h, height: iconSize.h, fit: BoxFit.contain),
SizedBox(height: 12.h),
isLargeText ? label.toText14(color: textColor, isBold: true) : label.toText11(color: textColor, isBold: true),
isLargeText ? label.toText14(color: textColor, isBold: true, maxlines: 1) : label.toText11(color: textColor, isBold: true, maxLine: 2),
],
),
),

@ -0,0 +1,159 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.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/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/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_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';
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:open_filex/open_filex.dart';
import 'package:share_plus/share_plus.dart';
class PatientMedicalReportCard extends StatelessWidget {
PatientMedicalReportCard({super.key, required this.patientMedicalReportResponseModel, required this.medicalFileViewModel, this.isLoading = false});
PatientMedicalReportResponseModel patientMedicalReportResponseModel;
MedicalFileViewModel medicalFileViewModel;
bool isLoading = true;
@override
Widget build(BuildContext context) {
AppState _appState = getIt.get<AppState>();
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL!,
width: 63.h,
height: 63.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: isLoading),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(isLoading ? "" : patientMedicalReportResponseModel.doctorName!).toText16(isBold: true).toShimmer2(isShow: isLoading),
SizedBox(height: 4.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: isLoading ? "" : patientMedicalReportResponseModel.clinicDescription!).toShimmer2(isShow: isLoading),
AppCustomChipWidget(labelText: isLoading ? "" : patientMedicalReportResponseModel.projectName!).toShimmer2(isShow: isLoading),
AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: isLoading
? ""
: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientMedicalReportResponseModel.requestDate), false)}, ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientMedicalReportResponseModel.requestDate), false)}")
.toShimmer2(isShow: isLoading),
AppCustomChipWidget(
icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, labelText: isLoading ? "" : "Rating: ${patientMedicalReportResponseModel.decimalDoctorRate}")
.toShimmer2(isShow: isLoading),
],
),
],
),
),
],
),
patientMedicalReportResponseModel.status == 2
? Padding(
padding: EdgeInsets.only(top: 16.h),
child: Row(
children: [
Expanded(
child: CustomButton(
text: "Share",
onPressed: () {
getMedicalReportPDF(true, context, _appState);
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.download_1,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
).toShimmer2(isShow: isLoading),
),
SizedBox(width: 16.h),
Expanded(
child: CustomButton(
text: "Download",
onPressed: () async {
getMedicalReportPDF(false, context, _appState);
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.download_1,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
).toShimmer2(isShow: isLoading),
),
],
),
)
: SizedBox.shrink()
],
),
),
);
}
void getMedicalReportPDF(bool isShare, BuildContext context, AppState _appState) async {
LoaderBottomSheet.showLoader();
await medicalFileViewModel.getPatientMedicalReportPDF(patientMedicalReportResponseModel, _appState.getAuthenticatedUser()!).then((val) async {
LoaderBottomSheet.hideLoader();
if (medicalFileViewModel.patientMedicalReportPDFBase64.isNotEmpty) {
String path = await Utils.createFileFromString(medicalFileViewModel.patientMedicalReportPDFBase64, "pdf");
if (isShare) {
Share.shareXFiles([XFile(path)], text: "Medical Report");
} else {
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file".needTranslation),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
}
}
});
}
}

@ -0,0 +1,191 @@
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/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/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_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/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
import 'package:open_filex/open_filex.dart';
import 'package:provider/provider.dart';
class PatientSickLeaveCard extends StatelessWidget {
PatientSickLeaveCard({super.key, required this.patientSickLeavesResponseModel, this.isLoading = false});
late MedicalFileViewModel medicalFileViewModel;
PatientSickLeavesResponseModel patientSickLeavesResponseModel;
bool isLoading;
@override
Widget build(BuildContext context) {
AppState _appState = getIt.get<AppState>();
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, hasShadow: true),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}".toText16(isBold: true),
AppCustomChipWidget(
labelText: isLoading ? "" : getStatusText(context),
backgroundColor: getStatusColor().withOpacity(0.15),
textColor: getStatusColor(),
).toShimmer2(isShow: isLoading, width: 100.h),
],
),
SizedBox(height: 16.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!,
width: 30.h,
height: 30.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: isLoading),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(isLoading ? "" : patientSickLeavesResponseModel.doctorName!).toText16(isBold: true).toShimmer2(isShow: isLoading),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientSickLeavesResponseModel.appointmentDate), false),
).toShimmer2(isShow: isLoading),
AppCustomChipWidget(labelText: isLoading ? "Pending Activation" : patientSickLeavesResponseModel.clinicName!).toShimmer2(isShow: isLoading),
],
),
],
),
),
],
),
SizedBox(height: 16.h),
Row(
children: [
isLoading
? Container().toShimmer2(isShow: true, height: 40.h, width: 100.h, radius: 12.h)
: Expanded(
flex: 6,
child: CustomButton(
text: "Download Report".needTranslation,
onPressed: () async {
LoaderBottomSheet.showLoader();
await medicalFileViewModel.getPatientSickLeavePDF(patientSickLeavesResponseModel, _appState.getAuthenticatedUser()!).then((val) async {
LoaderBottomSheet.hideLoader();
if (medicalFileViewModel.patientSickLeavePDFBase64.isNotEmpty) {
String path = await Utils.createFileFromString(medicalFileViewModel.patientSickLeavePDFBase64, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file".needTranslation),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
}
});
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
icon: AppAssets.download,
iconColor: AppColors.primaryRedColor,
iconSize: 14.h,
).toShimmer2(isShow: isLoading),
),
SizedBox(width: 8.h),
Expanded(
flex: 1,
child: Container(
height: 40.h,
width: 40.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 10.h,
),
child: Padding(
padding: EdgeInsets.all(10.h),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
width: 10.h,
height: 10.h,
fit: BoxFit.contain,
),
),
).toShimmer2(isShow: isLoading).onPress(() {
Navigator.of(context).push(
FadePage(
page: PatientSickleavesListPage(),
),
);
}),
),
],
),
],
),
),
);
}
String getStatusText(BuildContext context) {
String statusText = "";
if (patientSickLeavesResponseModel.status == 1) {
statusText = LocaleKeys.pendingActivation.tr(context: context);
} else if (patientSickLeavesResponseModel.status == 2) {
statusText = LocaleKeys.ready.tr(context: context);
} else if (patientSickLeavesResponseModel.status == 3) {
statusText = LocaleKeys.awaitingApproval.tr(context: context);
} else {
statusText = "";
}
return statusText;
}
Color getStatusColor() {
Color statusColor = Colors.white;
if (patientSickLeavesResponseModel.status == 1) {
statusColor = Color(0xffCC9B14);
} else if (patientSickLeavesResponseModel.status == 2) {
statusColor = Color(0xff359846);
} else if (patientSickLeavesResponseModel.status == 3) {
statusColor = Color(0xffD02127);
} else {
statusColor = Colors.white;
}
return statusColor;
}
}

@ -1,10 +1,28 @@
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/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.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/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.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/bottomsheet/exception_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:provider/provider.dart';
abstract class DialogService {
Future<void> showErrorBottomSheet({required String message, Function()? onOkPressed});
Future<void> showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress});
// TODO : Need to be Fixed showPhoneNumberPickerSheet ( From Login ADn Signup Bottom Sheet Move Here
}
class DialogServiceImp implements DialogService {
@ -23,9 +41,166 @@ class DialogServiceImp implements DialogService {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) => _ErrorBottomSheet(message: message, onOkPressed: onOkPressed),
builder: (_) => _ErrorBottomSheet(message: message, onOkPressed: onOkPressed ?? () {}),
);
}
@override
Future<void> showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}) async {
final context = navigationService.navigatorKey.currentContext;
if (context == null) return;
await showModalBottomSheet(
context: context,
isScrollControlled: false,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) => ExceptionBottomSheet(
message: message,
showCancel: onCancelPressed != null ? true : false,
onOkPressed: onOkPressed,
onCancelPressed: () {
if (onCancelPressed != null) {
Navigator.of(context).pop();
}
},
),
);
}
@override
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async {
final context = navigationService.navigatorKey.currentContext;
if (context == null) return;
showCommonBottomSheetWithoutHeight(context, title: label ?? "", child: exceptionBottomSheetWidget(context: context, message: message, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed),
callBackFunc: () {
});
}
@override
Future<void> showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async {
final context = navigationService.navigatorKey.currentContext;
if (context == null) return;
showCommonBottomSheetWithoutHeight(context,
title: label ?? "", child: showPhoneNumberPickerWidget(context: context, message: message, onSMSPress: onSMSPress, onWhatsappPress: onWhatsappPress), callBackFunc: () {});
}
}
Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) {
return Column(
children: [
(message ?? "").toText16(isBold: false, color: AppColors.textColor),
SizedBox(height: 10.h),
SizedBox(height: 24.h),
if (onOkPressed != null && onCancelPressed != null)
Row(
children: [
Expanded(
child: CustomButton(
text: LocaleKeys.cancel.tr(),
onPressed: () {
Navigator.of(context).pop();
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
icon: AppAssets.cancel,
iconColor: AppColors.primaryRedColor,
),
),
SizedBox(width: 5.h),
Expanded(
child: CustomButton(
text: onCancelPressed != null ? LocaleKeys.confirm.tr() : LocaleKeys.ok.tr(),
onPressed: onOkPressed,
backgroundColor: AppColors.bgGreenColor,
borderColor: AppColors.bgGreenColor,
textColor: Colors.white,
icon: AppAssets.confirm,
),
),
],
),
if (onOkPressed != null && onCancelPressed == null)
Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: CustomButton(
text: LocaleKeys.ok.tr(),
onPressed: (onOkPressed != null && onCancelPressed == null)
? () {
Navigator.of(context).pop();
}
: onOkPressed,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
textColor: Colors.white,
icon: AppAssets.confirm,
),
),
],
);
}
Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) {
return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) {
AuthenticationViewModel authViewModel = context.read<AuthenticationViewModel>();
return Column(
children: [
(message ?? "").toText16(isBold: false, color: AppColors.textColor),
SizedBox(height: 10.h),
Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(),
onPressed: onSMSPress,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
textColor: AppColors.whiteColor,
icon: AppAssets.message,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.h),
child: LocaleKeys.oR.tr().toText16(color: AppColors.textColor),
),
],
),
Padding(
padding: EdgeInsets.only(bottom: 10.h, top: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(),
onPressed: onWhatsappPress,
backgroundColor: Colors.white,
borderColor: AppColors.borderOnlyColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
iconColor: null,
),
),
],
);
//return Padding(
// padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
// child: SingleChildScrollView(
// child: GenericBottomSheet(
// countryCode: authViewModel.selectedCountrySignup.countryCode,
// initialPhoneNumber: "",
// textController: authViewModel.phoneNumberController,
// isEnableCountryDropdown: true,
// onCountryChange: authViewModel.onCountryChange,
// onChange: authViewModel.onPhoneNumberChange,
// buttons: [
//
// ],
// ),
// ),
// );
});
}
class _ErrorBottomSheet extends StatelessWidget {
@ -71,7 +246,7 @@ class _ErrorBottomSheet extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
),
),
child: const Text("OK", style: TextStyle(color: Colors.white)).onPress((){
child: const Text("OK", style: TextStyle(color: Colors.white)).onPress(() {
Navigator.of(context).pop();
}),
),

@ -3,12 +3,13 @@ import 'dart:io';
import 'package:hmg_patient_app_new/core/exceptions/api_exception.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/core/utils/loading_utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
abstract class ErrorHandlerService {
Future<void> handleError({required Failure failure, Function() onOkPressed});
Future<void> handleError({required Failure failure, Function() onOkPressed, Function(Failure)? onUnHandledFailure});
}
class ErrorHandlerServiceImp implements ErrorHandlerService {
@ -23,7 +24,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService {
});
@override
Future<void> handleError({required Failure failure, Function()? onOkPressed}) async {
Future<void> handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure}) async {
if (failure is APIException) {
loggerService.errorLogs("API Exception: ${failure.message}");
} else if (failure is ServerFailure) {
@ -44,10 +45,15 @@ class ErrorHandlerServiceImp implements ErrorHandlerService {
} else if (failure is InvalidCredentials) {
loggerService.errorLogs("Invalid Credentials : ${failure.message}");
await _showDialog(failure, title: "Unknown Error");
} else if (failure is UserIntimationFailure) {
if (onUnHandledFailure != null) {
onUnHandledFailure(failure);
} else {
await _showDialog(failure, title: "Error", onOkPressed: onOkPressed);
}
} else {
loggerService.errorLogs("Unhandled failure type: $failure");
await _showDialog(failure, title: "Error");
await _showDialog(failure, title: "Error", onOkPressed: onOkPressed);
}
}
@ -55,6 +61,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService {
if (LoadingUtils.isLoading) {
LoadingUtils.hideFullScreenLoader();
}
await dialogService.showErrorBottomSheet(message: failure.message, onOkPressed: onOkPressed);
}
}

@ -32,11 +32,11 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
title: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Align(
alignment: Alignment.centerLeft,
alignment: context.locale.languageCode == "ar" ? Alignment.centerRight : Alignment.centerLeft,
child: GestureDetector(
onTap: onBackPressed,
child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 32.h, height: 32.h),
@ -53,7 +53,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
if (!hideLogoAndLang)
Expanded(
child: Align(
alignment: Alignment.centerRight,
alignment: context.locale.languageCode == "ar" ? Alignment.centerLeft : Alignment.centerRight,
child: LanguageSelector(
currentLanguage: context.locale.languageCode,
showOnlyIcon: false,

@ -22,6 +22,7 @@ class GenericBottomSheet extends StatefulWidget {
final bool isEnableCountryDropdown;
final bool isFromSavedLogin;
Function(String?)? onChange;
final bool autoFocus;
// FocusNode myFocusNode;
@ -36,6 +37,7 @@ class GenericBottomSheet extends StatefulWidget {
this.isEnableCountryDropdown = false,
this.isFromSavedLogin = false,
this.onChange,
this.autoFocus = false,
// required this.myFocusNode
});
@ -44,17 +46,28 @@ class GenericBottomSheet extends StatefulWidget {
}
class _GenericBottomSheetState extends State<GenericBottomSheet> {
late FocusNode _textFieldFocusNode;
@override
void initState() {
super.initState();
_textFieldFocusNode = FocusNode();
if (!widget.isForEmail && widget.textController != null) {
widget.textController!.text = widget.initialPhoneNumber ?? "";
}
// Auto focus the text field if specified
if (widget.autoFocus && widget.textController != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_textFieldFocusNode.requestFocus();
});
}
}
@override
void dispose() {
_textFieldFocusNode.dispose();
super.dispose();
}
@ -65,6 +78,8 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
bottom: Platform.isIOS ? false : true,
child: GestureDetector(
onTap: () {
// Dismiss keyboard and unfocus text field
_textFieldFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: Directionality(
@ -90,6 +105,9 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
: LocaleKeys.enterPhoneNumber.tr().toText24()),
InkWell(
onTap: () {
// Dismiss keyboard before closing
_textFieldFocusNode.unfocus();
FocusScope.of(context).unfocus();
Navigator.of(context).pop();
},
child: Padding(
@ -115,6 +133,8 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
labelText: widget.isForEmail ? LocaleKeys.email : LocaleKeys.phoneNumber,
hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx",
controller: widget.textController!,
focusNode: _textFieldFocusNode,
autoFocus: widget.autoFocus,
padding: EdgeInsets.all(8.h),
keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number,
onChange: (value) {

@ -126,25 +126,27 @@ void showCommonBottomSheetWithoutHeight(
top: false,
left: false,
right: false,
child:isCloseButtonVisible ? Container(
padding: EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 12),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bottomSheetBgColor, borderRadius: 24.h),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16.h,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
child: isCloseButtonVisible
? Container(
padding: EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 12),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bottomSheetBgColor, borderRadius: 24.h),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16.h,
children: [
if (title.isNotEmpty) title.toText20(weight: FontWeight.w600).expanded,
Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() {
Navigator.of(context).pop();
}),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
title.toText20(weight: FontWeight.w600).expanded,
Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() {
Navigator.of(context).pop();
}),
],
),
child,
],
),
child,
],
)) : child,
))
: child,
);
}).then((value) {
callBackFunc();

@ -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/enums.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
@ -60,6 +62,7 @@ class _CustomCountryDropdownState extends State<CustomCountryDropdown> {
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
return Container(
height: 40.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 10.h),
@ -120,7 +123,11 @@ class _CustomCountryDropdownState extends State<CustomCountryDropdown> {
),
if (!widget.isFromBottomSheet)
Text(
selectedCountry != null ? selectedCountry!.displayName : "Select Country",
selectedCountry != null
? appState.getLanguageCode() == "ar"
? selectedCountry!.nameArabic
: selectedCountry!.displayName
: "Select Country",
style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2),
),
],
@ -132,7 +139,7 @@ class _CustomCountryDropdownState extends State<CustomCountryDropdown> {
if (textFocusNode.hasFocus) {
textFocusNode.unfocus();
}
AppState appState = getIt.get<AppState>();
RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset offset = renderBox.localToGlobal(Offset.zero);
@ -172,7 +179,9 @@ class _CustomCountryDropdownState extends State<CustomCountryDropdown> {
children: [
Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h),
if (!widget.isFromBottomSheet) SizedBox(width: 12.h),
if (!widget.isFromBottomSheet) Text(country.displayName, style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2)),
if (!widget.isFromBottomSheet)
Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName,
style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2)),
],
),
),

@ -75,31 +75,32 @@ class TextInputWidget extends StatelessWidget {
final FocusNode _focusNode = FocusNode();
KeyboardActionsConfig get _keyboardActionsConfig {
return KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.ALL,
keyboardBarColor: const Color(0xFFCAD1D9), //Apple keyboard color
actions: [
KeyboardActionsItem(
focusNode: focusNode ?? _focusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () => node.unfocus(),
child: Container(
padding: const EdgeInsets.all(12.0),
child: "Done".toText16(weight: FontWeight.w500, color: AppColors.infoColor),
),
);
}
],
),
],
);
}
// KeyboardActionsConfig get _keyboardActionsConfig {
// return KeyboardActionsConfig(
// keyboardActionsPlatform: KeyboardActionsPlatform.ALL,
// keyboardBarColor: const Color(0xFFCAD1D9), //Apple keyboard color
// actions: [
// KeyboardActionsItem(
// focusNode: focusNode ?? _focusNode,
// toolbarButtons: [
// (node) {
// return GestureDetector(
// onTap: () => node.unfocus(),
// child: Container(
// padding: const EdgeInsets.all(12.0),
// child: "Done".toText16(weight: FontWeight.w500, color: AppColors.infoColor),
// ),
// );
// }
// ],
// ),
// ],
// );
// }
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
final errorColor = AppColors.primaryRedColor;
return Column(
mainAxisSize: MainAxisSize.min,
@ -132,7 +133,7 @@ class TextInputWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabelText(),
_buildLabelText().paddingOnly(right: (appState.getLanguageCode() == "ar" ? 10 : 0)),
_buildTextField(context),
],
),
@ -181,6 +182,7 @@ class TextInputWidget extends StatelessWidget {
onTap: () async {
bool isGregorian = true;
final picked = await showHijriGregBottomSheet(context,
isShowTimeSlots: true,
switcherIcon: Utils.buildSvgWithAssets(icon: AppAssets.language, width: 24.h, height: 24.h),
language: appState.getLanguageCode()!,
initialDate: DateTime.now(),
@ -215,46 +217,42 @@ class TextInputWidget extends StatelessWidget {
}
Widget _buildTextField(BuildContext context) {
return KeyboardActions(
config: _keyboardActionsConfig,
disableScroll: true,
child: TextField(
enabled: isEnable,
scrollPadding: EdgeInsets.zero,
keyboardType: keyboardType,
controller: controller,
readOnly: isReadOnly,
textAlignVertical: TextAlignVertical.top,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
onChanged: onChange,
focusNode: focusNode ?? _focusNode,
autofocus: autoFocus,
textInputAction: TextInputAction.done,
cursorHeight: isWalletAmountInput! ? 40.h : 18.h,
style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 21 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -0.2),
decoration: InputDecoration(
isDense: true,
hintText: hintText,
hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2),
prefixIconConstraints: BoxConstraints(minWidth: 45.h),
prefixIcon: prefix == null
? null
: Text(
"+" + prefix!,
style: TextStyle(
fontSize: 14.fSize,
height: 21 / 14,
fontWeight: FontWeight.w500,
color: Color(0xff2E303A),
letterSpacing: -0.2,
),
return TextField(
enabled: isEnable,
scrollPadding: EdgeInsets.zero,
keyboardType: keyboardType,
controller: controller,
readOnly: isReadOnly,
textAlignVertical: TextAlignVertical.top,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
onChanged: onChange,
focusNode: focusNode ?? _focusNode,
autofocus: autoFocus,
textInputAction: TextInputAction.done,
cursorHeight: isWalletAmountInput! ? 40.h : 18.h,
style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 21 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -0.2),
decoration: InputDecoration(
isDense: true,
hintText: hintText,
hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2),
prefixIconConstraints: BoxConstraints(minWidth: 45.h),
prefixIcon: prefix == null
? null
: Text(
"+" + prefix!,
style: TextStyle(
fontSize: 14.fSize,
height: 21 / 14,
fontWeight: FontWeight.w500,
color: Color(0xff2E303A),
letterSpacing: -0.2,
),
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
),
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
);
}

@ -1,377 +0,0 @@
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
typedef OnDone = void Function(String text);
class ProvidedPinBoxTextAnimation {
static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) {
return ScaleTransition(
child: child,
scale: animation,
);
};
static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation<double> animation) {
return child;
};
}
class OTPWidget extends StatefulWidget {
final int maxLength;
late TextEditingController? controller;
final Color defaultBorderColor;
final Color pinBoxColor;
final double pinBoxBorderWidth;
final double pinBoxRadius;
final bool hideDefaultKeyboard;
final TextStyle? pinTextStyle;
final double pinBoxHeight;
final double pinBoxWidth;
final OnDone? onDone;
final bool hasError;
final Color errorBorderColor;
final Color textBorderColor;
final Function(String)? onTextChanged;
final bool autoFocus;
final FocusNode? focusNode;
final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition;
final Duration pinTextAnimatedSwitcherDuration;
final TextDirection textDirection;
final TextInputType keyboardType;
final EdgeInsets pinBoxOuterPadding;
OTPWidget({
Key? key,
this.maxLength = 4,
this.controller,
this.pinBoxWidth = 70.0,
this.pinBoxHeight = 70.0,
this.pinTextStyle,
this.onDone,
this.defaultBorderColor = Colors.black,
this.textBorderColor = Colors.black,
this.pinTextAnimatedSwitcherTransition,
this.pinTextAnimatedSwitcherDuration = const Duration(),
this.hasError = false,
this.errorBorderColor = Colors.red,
this.onTextChanged,
this.autoFocus = false,
this.focusNode,
this.textDirection = TextDirection.ltr,
this.keyboardType = TextInputType.number,
this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0),
this.pinBoxColor = Colors.white,
this.pinBoxBorderWidth = 2.0,
this.pinBoxRadius = 0,
this.hideDefaultKeyboard = false,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return OTPWidgetState();
}
}
class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
late AnimationController _highlightAnimationController;
late FocusNode focusNode;
String text = "";
int currentIndex = 0;
List<String> strList = [];
bool hasFocus = false;
@override
void didUpdateWidget(OTPWidget oldWidget) {
super.didUpdateWidget(oldWidget);
focusNode = widget.focusNode ?? focusNode;
if (oldWidget.maxLength < widget.maxLength) {
setState(() {
currentIndex = text.length;
});
widget.controller?.text = text;
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
} else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) {
setState(() {
text = text.substring(0, widget.maxLength);
currentIndex = text.length;
});
widget.controller?.text = text;
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
}
}
_calculateStrList() {
if (strList.length > widget.maxLength) {
strList.length = widget.maxLength;
}
while (strList.length < widget.maxLength) {
strList.add("");
}
}
@override
void initState() {
super.initState();
focusNode = widget.focusNode ?? FocusNode();
_highlightAnimationController = AnimationController(vsync: this);
_initTextController();
_calculateStrList();
widget.controller!.addListener(_controllerListener);
focusNode.addListener(_focusListener);
}
void _controllerListener() {
if (mounted == true) {
setState(() {
_initTextController();
});
var onTextChanged = widget.onTextChanged;
if (onTextChanged != null) {
onTextChanged(widget.controller?.text ?? "");
}
}
}
void _focusListener() {
if (mounted == true) {
setState(() {
hasFocus = focusNode?.hasFocus ?? false;
});
}
}
void _initTextController() {
if (widget.controller == null) {
return;
}
strList.clear();
var text = widget.controller?.text ?? "";
if (text.isNotEmpty) {
if (text.length > widget.maxLength) {
throw Exception("TextEditingController length exceeded maxLength!");
}
}
for (var i = 0; i < text.length; i++) {
strList.add(text[i]);
}
}
double get _width {
var width = 0.0;
for (var i = 0; i < widget.maxLength; i++) {
width += widget.pinBoxWidth;
if (i == 0) {
width += widget.pinBoxOuterPadding.left;
} else if (i + 1 == widget.maxLength) {
width += widget.pinBoxOuterPadding.right;
} else {
width += widget.pinBoxOuterPadding.left;
}
}
return width;
}
@override
void dispose() {
if (widget.focusNode == null) {
focusNode.dispose();
} else {
focusNode.removeListener(_focusListener);
}
_highlightAnimationController.dispose();
widget.controller?.removeListener(_controllerListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
_otpTextInput(),
_touchPinBoxRow(),
],
);
}
Widget _touchPinBoxRow() {
return widget.hideDefaultKeyboard
? _pinBoxRow(context)
: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (hasFocus) {
FocusScope.of(context).requestFocus(FocusNode());
Future.delayed(Duration(milliseconds: 100), () {
FocusScope.of(context).requestFocus(focusNode);
});
} else {
FocusScope.of(context).requestFocus(focusNode);
}
},
child: _pinBoxRow(context),
);
}
Widget _otpTextInput() {
var transparentBorder = OutlineInputBorder(
borderSide: BorderSide(
color: Colors.transparent,
width: 0.0,
),
);
return Container(
width: _width,
height: widget.pinBoxHeight,
child: TextField(
autofocus: !kIsWeb ? widget.autoFocus : false,
enableInteractiveSelection: false,
focusNode: focusNode,
controller: widget.controller,
keyboardType: widget.keyboardType,
inputFormatters: widget.keyboardType == TextInputType.number ? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly] : null,
style: TextStyle(
height: 0.1,
color: Colors.transparent,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0),
focusedErrorBorder: transparentBorder,
errorBorder: transparentBorder,
disabledBorder: transparentBorder,
enabledBorder: transparentBorder,
focusedBorder: transparentBorder,
counterText: null,
counterStyle: null,
helperStyle: TextStyle(
height: 0.0,
color: Colors.transparent,
),
labelStyle: TextStyle(height: 0.1),
fillColor: Colors.transparent,
border: InputBorder.none,
),
cursorColor: Colors.transparent,
showCursor: false,
maxLength: widget.maxLength,
onChanged: _onTextChanged,
),
);
}
void _onTextChanged(text) {
var onTextChanged = widget.onTextChanged;
if (onTextChanged != null) {
onTextChanged(text);
}
setState(() {
this.text = text;
if (text.length >= currentIndex) {
for (int i = currentIndex; i < text.length; i++) {
strList[i] = text[i];
}
}
currentIndex = text.length;
});
if (text.length == widget.maxLength) {
FocusScope.of(context).requestFocus(FocusNode());
var onDone = widget.onDone;
if (onDone != null) {
onDone(text);
}
}
}
Widget _pinBoxRow(BuildContext context) {
_calculateStrList();
List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
return _buildPinCode(i, context);
});
return Row(
children: pinCodes,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
);
}
Widget _buildPinCode(int i, BuildContext context) {
Color borderColor;
Color pinBoxColor = widget.pinBoxColor;
if (widget.hasError) {
borderColor = widget.errorBorderColor;
} else if (i < text.length) {
borderColor = widget.textBorderColor;
} else {
borderColor = widget.defaultBorderColor;
pinBoxColor = widget.pinBoxColor;
}
EdgeInsets insets;
if (i == 0) {
insets = EdgeInsets.only(
left: 0,
top: widget.pinBoxOuterPadding.top,
right: widget.pinBoxOuterPadding.right,
bottom: widget.pinBoxOuterPadding.bottom,
);
} else if (i == strList.length - 1) {
insets = EdgeInsets.only(
left: widget.pinBoxOuterPadding.left,
top: widget.pinBoxOuterPadding.top,
right: 0,
bottom: widget.pinBoxOuterPadding.bottom,
);
} else {
insets = widget.pinBoxOuterPadding;
}
return Container(
key: ValueKey<String>("container$i"),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0),
margin: insets,
child: _animatedTextBox(strList[i], i),
decoration: BoxDecoration(
border: Border.all(
color: borderColor,
width: widget.pinBoxBorderWidth,
),
color: pinBoxColor,
borderRadius: BorderRadius.circular(widget.pinBoxRadius),
),
width: widget.pinBoxWidth,
height: widget.pinBoxHeight,
);
}
Widget _animatedTextBox(String text, int i) {
if (widget.pinTextAnimatedSwitcherTransition != null) {
return AnimatedSwitcher(
duration: widget.pinTextAnimatedSwitcherDuration,
transitionBuilder: widget.pinTextAnimatedSwitcherTransition ??
(Widget child, Animation<double> animation) {
return child;
},
child: Text(
text,
key: ValueKey<String>("$text$i"),
style: widget.pinTextStyle,
),
);
} else {
return Text(
text,
key: ValueKey<String>("${strList[i]}$i"),
style: widget.pinTextStyle,
);
}
}
}

@ -76,6 +76,8 @@ dependencies:
flutter_nfc_kit: ^3.6.0
barcode_scan2: ^4.5.1
keyboard_actions: ^4.2.0
path_provider: ^2.0.8
open_filex: ^4.7.0
dev_dependencies:
flutter_test:

Loading…
Cancel
Save