Merge pull request 'Completed User Info Selection and Symptoms Checker till getSymptomsByOrgans (faiz_dev)' (#114) from faiz_dev into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/114
pull/123/head
Haroon6138 1 month ago
commit 79beaf6fb7

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -0,0 +1,10 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_27024)">
<path d="M16.9795 19.2095C16.93 19.7925 17.3257 22.8783 17.5299 24.0883C17.6272 24.6615 17.8937 25.5955 17.7978 26.0914C17.6609 26.7825 17.6213 27.6769 17.6967 28.1976C17.7442 28.5139 17.8939 29.9725 17.6798 30.506C17.5677 30.7858 17.3624 32.2195 17.3624 32.2195C16.8274 33.5702 17.1293 33.5038 17.1293 33.5038C17.2949 33.707 17.5786 33.5197 17.5786 33.5197C17.7947 33.6575 17.9444 33.487 17.9444 33.487C18.1299 33.6405 18.3461 33.4681 18.3461 33.4681C18.5792 33.5891 18.7953 33.366 18.7953 33.366C18.9291 33.4334 18.9619 33.3482 18.9619 33.3482C19.3635 33.3225 18.7378 32.0373 18.7378 32.0373C18.588 30.883 18.8865 30.2406 18.8865 30.2406C19.8642 27.3411 19.9139 26.5716 19.5231 25.4789C19.413 25.1634 19.3851 25.0385 19.4358 24.9016C19.5529 24.5855 19.4675 23.3141 19.6103 22.8093C19.8859 21.8356 20.1577 19.3663 20.2994 18.2142C20.4897 16.6622 19.625 14.5813 19.625 14.5813C19.4356 13.7345 19.7133 10.7174 19.7133 10.7174C20.101 11.3208 20.0862 12.3858 20.0862 12.3858C20.0246 13.5028 20.9885 15.2099 20.9885 15.2099C21.4517 15.9154 21.6271 16.5847 21.6271 16.6344C21.6271 16.8376 21.5826 17.3296 21.5826 17.3296L21.6004 17.758C21.6083 17.8671 21.6697 18.2428 21.6598 18.4244C21.5875 19.5419 21.765 19.3317 21.765 19.3317C21.9148 19.3317 22.0795 18.4324 22.0795 18.4324C22.0795 18.6643 22.0228 19.3585 22.1479 19.6204C22.2975 19.9327 22.4076 19.5668 22.4095 19.4934C22.4491 18.0705 22.5346 18.4433 22.5346 18.4433C22.6178 19.5976 22.7201 19.8583 22.9035 19.7681C23.0424 19.7018 22.9154 18.3829 22.9154 18.3829C23.1533 19.1664 23.3337 19.2911 23.3337 19.2911C23.7263 19.5668 23.4835 18.8053 23.429 18.6545C23.1385 17.8533 23.1295 17.5757 23.1295 17.5757C23.4925 18.2956 23.7661 18.2689 23.7661 18.2689C24.1201 18.1559 23.4567 17.1364 23.0679 16.6481C22.8696 16.3992 22.6138 16.066 22.5395 15.8682C22.4185 15.533 22.3271 14.4556 22.3271 14.4556C22.2905 13.1843 21.9762 12.632 21.9762 12.632C21.4388 11.7719 21.3377 10.1673 21.3377 10.1673L21.3139 7.45832C21.1255 5.61048 19.7639 5.59712 19.7639 5.59712C18.3877 5.39226 18.1962 4.94768 18.1962 4.94768C17.9047 4.52818 18.0712 3.72403 18.0712 3.72403C18.3131 3.52731 18.4064 3.00505 18.4064 3.00505C18.808 2.69711 18.7883 2.2465 18.6028 2.25139C18.4539 2.2553 18.4877 2.13202 18.4877 2.13202C18.7388 0.103572 16.938 0 16.938 0H16.6631C16.6631 0 14.8615 0.103572 15.1123 2.13153C15.1123 2.13153 15.146 2.25497 14.9958 2.2509C14.8108 2.24601 14.7936 2.69662 15.1937 3.00457C15.1937 3.00457 15.2869 3.52666 15.5289 3.72354C15.5289 3.72354 15.6954 4.52769 15.4039 4.94719C15.4039 4.94719 15.2131 5.39177 13.8362 5.59663C13.8362 5.59663 12.4723 5.60999 12.2869 7.45784L12.2611 10.1668C12.2611 10.1668 12.1619 11.7714 11.6224 12.6316C11.6224 12.6316 11.3096 13.1839 11.2734 14.4551C11.2734 14.4551 11.1818 15.5325 11.0612 15.8677C10.9878 16.0646 10.7321 16.3978 10.5322 16.6476C10.14 17.135 9.48096 18.1523 9.83353 18.2684C9.83353 18.2684 10.1086 18.2951 10.4701 17.5752C10.4701 17.5752 10.4626 17.8509 10.1726 18.654C10.1156 18.8029 9.8731 19.5644 10.2659 19.2906C10.2659 19.2906 10.4478 19.1657 10.6843 18.3824C10.6843 18.3824 10.5574 19.7013 10.6986 19.7676C10.8831 19.858 10.9837 19.5971 11.0669 18.4428C11.0669 18.4428 11.1523 18.0701 11.1919 19.4929C11.1938 19.5663 11.3015 19.9322 11.4518 19.6199C11.5786 19.358 11.5216 18.6649 11.5216 18.4319C11.5216 18.4319 11.6843 19.3312 11.8364 19.3312C11.8364 19.3312 12.0154 19.5414 11.9419 18.4239C11.93 18.2414 11.994 17.8667 12.002 17.7576L12.0193 17.3291C12.0193 17.3291 11.9747 16.8383 11.9747 16.6339C11.9747 16.5832 12.1502 15.9149 12.6132 15.2095C12.6132 15.2095 13.5761 13.5015 13.5141 12.3853C13.5141 12.3853 13.5007 11.3203 13.8885 10.7169C13.8885 10.7169 14.1637 13.7339 13.9772 14.5809C13.9772 14.5809 13.111 16.6617 13.3021 18.2137C13.4428 19.3689 13.7141 21.835 13.9903 22.8088C14.1345 23.3127 14.0492 24.5837 14.1648 24.9011C14.2169 25.039 14.1896 25.166 14.0775 25.4784C13.6888 26.5711 13.7383 27.3407 14.7161 30.2401C14.7161 30.2401 15.017 30.8825 14.8649 32.0368C14.8649 32.0368 14.2402 33.322 14.6403 33.3477C14.6403 33.3477 14.6716 33.4329 14.8069 33.3655C14.8069 33.3655 15.023 33.5886 15.2566 33.4676C15.2566 33.4676 15.4728 33.6402 15.6577 33.4865C15.6577 33.4865 15.8059 33.657 16.022 33.5192C16.022 33.5192 16.3056 33.7106 16.4742 33.5033C16.4742 33.5033 16.7737 33.5697 16.2405 32.219C16.2405 32.219 16.0363 30.7871 15.9238 30.5055C15.7091 29.9722 15.8607 28.5105 15.9065 28.1972C15.9803 27.6734 15.9407 26.7812 15.8059 26.0909C15.7072 25.5961 15.9744 24.662 16.0731 24.0878C16.2758 22.879 16.6732 19.793 16.6235 19.209L16.7877 19.2667C16.9053 19.267 16.9795 19.2095 16.9795 19.2095Z" fill="#2E3039"/>
</g>
<defs>
<clipPath id="clip0_1_27024">
<rect width="33.6" height="33.6" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

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

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9998 1.25C9.92869 1.25 8.24976 2.92893 8.24976 5C8.24976 6.07361 8.80491 7.11878 9.47398 7.87011C9.81596 8.25412 10.2117 8.58958 10.6283 8.8334C11.0379 9.07316 11.5124 9.25 11.9998 9.25C12.4871 9.25 12.9616 9.07316 13.3712 8.8334C13.7878 8.58958 14.1836 8.25412 14.5255 7.87011C15.1946 7.11878 15.7498 6.07361 15.7498 5C15.7498 2.92893 14.0708 1.25 11.9998 1.25Z" fill="#8F9AA3"/>
<path d="M7.91515 8.34597C7.94794 8.37041 7.98252 8.41503 8.05168 8.50427C8.15094 8.63235 8.25213 8.75374 8.35378 8.86789C8.7849 9.35201 9.29974 9.79407 9.87058 10.1282C10.4203 10.4499 11.1584 10.7502 11.9998 10.7502C12.8411 10.7502 13.5792 10.4499 14.1289 10.1282C14.6998 9.79407 15.2146 9.35201 15.6457 8.86789C15.7474 8.75376 15.8485 8.63238 15.9478 8.50432C16.0171 8.41483 16.0518 8.37008 16.0847 8.34559C16.1822 8.27306 16.3055 8.26531 16.4114 8.32507C16.4471 8.34525 16.483 8.38124 16.5549 8.45323C18.2219 9.92647 19.404 12.5039 18.3564 14.9962C18.0511 15.7227 17.3445 16.2043 16.5513 16.2043L16.2745 16.2021C16.0468 16.2002 15.933 16.1993 15.8506 16.2633C15.7683 16.3274 15.7412 16.438 15.6869 16.6592L14.7228 20.5913C14.4139 21.851 13.2977 22.7504 11.9999 22.7504C10.7021 22.7504 9.58598 21.851 9.2771 20.5913L8.31293 16.6592C8.25869 16.438 8.23157 16.3274 8.14925 16.2633C8.06692 16.1993 7.95305 16.2002 7.72532 16.2021L7.44856 16.2043C6.6554 16.2043 5.94881 15.7227 5.64342 14.9962C4.59582 12.5039 5.77798 9.92648 7.44498 8.45325C7.51667 8.38147 7.55251 8.34558 7.58811 8.32543C7.69403 8.26548 7.81756 8.27324 7.91515 8.34597Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.75 15C12.75 14.5858 12.4142 14.25 12 14.25C11.5858 14.25 11.25 14.5858 11.25 15V18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V15Z" fill="#2E3039"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.25C11.1339 1.25 10.3597 1.52688 9.52381 1.99594C8.7139 2.45043 7.78586 3.12145 6.61887 3.96524L5.10984 5.05632C4.1733 5.73346 3.42528 6.27429 2.86109 6.77487C2.27855 7.29173 1.84274 7.807 1.5663 8.45513C1.28925 9.10469 1.22225 9.77045 1.25941 10.5381C1.29528 11.2789 1.43264 12.1727 1.60393 13.2872L1.91918 15.3387C2.16256 16.9225 2.35635 18.1836 2.64105 19.1662C2.93544 20.1821 3.35016 20.9887 4.0914 21.6052C4.82957 22.2192 5.7089 22.4926 6.78306 22.6231C7.828 22.75 9.14615 22.75 10.8111 22.75H13.1889C14.8539 22.75 16.172 22.75 17.2169 22.6231C18.2911 22.4926 19.1704 22.2192 19.9086 21.6052C20.6499 20.9887 21.0646 20.1821 21.359 19.1662C21.6437 18.1837 21.8374 16.9225 22.0808 15.3387L22.3961 13.2871C22.5674 12.1726 22.7047 11.2789 22.7406 10.5381C22.7778 9.77045 22.7108 9.10469 22.4337 8.45513C22.1573 7.807 21.7215 7.29173 21.1389 6.77487C20.5747 6.2743 19.8267 5.73347 18.8902 5.05633L17.3811 3.96525C16.2142 3.12146 15.2861 2.45043 14.4762 1.99594C13.6403 1.52688 12.8661 1.25 12 1.25ZM7.46043 5.20777C8.67318 4.33092 9.53198 3.7114 10.2579 3.30406C10.9664 2.90645 11.483 2.75 12 2.75C12.517 2.75 13.0336 2.90645 13.7421 3.30406C14.468 3.7114 15.3268 4.33092 16.5396 5.20777L17.9794 6.24884C18.9551 6.95427 19.6418 7.45189 20.1434 7.8969C20.633 8.33129 20.8961 8.6735 21.054 9.04362C21.2112 9.41232 21.2731 9.83 21.2423 10.4655C21.2107 11.119 21.0862 11.9356 20.9073 13.0995L20.6063 15.0585C20.353 16.7066 20.1726 17.8709 19.9182 18.7487C19.6704 19.6038 19.3761 20.0971 18.9494 20.452C18.5197 20.8094 17.9613 21.0217 17.0361 21.134C16.0921 21.2487 14.865 21.25 13.1388 21.25H10.8612C9.13497 21.25 7.90793 21.2487 6.96392 21.134C6.03868 21.0217 5.48034 20.8094 5.0506 20.452C4.62394 20.0971 4.32959 19.6038 4.08178 18.7487C3.82742 17.8709 3.647 16.7066 3.39373 15.0585L3.09269 13.0995C2.91382 11.9356 2.7893 11.119 2.75766 10.4655C2.72689 9.83 2.78878 9.41232 2.94604 9.04362C3.1039 8.6735 3.36702 8.33129 3.85661 7.8969C4.35817 7.45189 5.04492 6.95427 6.02058 6.24884L7.46043 5.20777Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1661 3.95442C10.7553 3.82074 11.369 3.75 12.0002 3.75C16.5565 3.75 20.2502 7.44365 20.2502 12C20.2502 14.3316 19.2833 16.4376 17.7269 17.9387V16C17.7269 15.5858 17.3911 15.25 16.9769 15.25C16.5626 15.25 16.2269 15.5858 16.2269 16V19.5C16.2269 19.9142 16.5626 20.25 16.9769 20.25H20.5002C20.9144 20.25 21.2502 19.9142 21.2502 19.5C21.2502 19.0858 20.9144 18.75 20.5002 18.75H19.0357C20.7162 16.9987 21.7502 14.6202 21.7502 12C21.7502 6.61522 17.3849 2.25 12.0002 2.25C11.2566 2.25 10.5315 2.33338 9.8342 2.49161C9.43025 2.58327 9.1771 2.98504 9.26876 3.38898C9.36042 3.79293 9.76218 4.04608 10.1661 3.95442Z" fill="#2B353E"/>
<path d="M3.5 3.76562C3.08579 3.76562 2.75 4.10141 2.75 4.51562C2.75 4.92984 3.08579 5.26562 3.5 5.26562H4.94958C3.27784 7.01543 2.25 9.38763 2.25 12.0001C2.25 17.3849 6.61522 21.7501 12 21.7501C12.7436 21.7501 13.4687 21.6667 14.166 21.5085C14.5699 21.4168 14.8231 21.015 14.7314 20.6111C14.6397 20.2071 14.238 19.954 13.834 20.0457C13.2449 20.1793 12.6312 20.2501 12 20.2501C7.44365 20.2501 3.75 16.5564 3.75 12.0001C3.75 9.68013 4.70717 7.58354 6.25 6.0839L6.25 8.00008C6.25 8.41429 6.58579 8.75008 7 8.75008C7.41421 8.75008 7.75 8.41429 7.75 8.00008L7.75 4.51562C7.75 4.10141 7.41421 3.76562 7 3.76562L3.5 3.76562Z" fill="#2B353E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,3 @@
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3286 2.69938C20.35 2.69897 20.3714 2.69933 20.3928 2.70045C23.661 2.87123 26.0252 4.18642 26.9215 6.63726C27.7812 8.98782 27.1501 12.0533 25.3157 15.4357C25.0787 15.8726 24.5324 16.0347 24.0955 15.7977C23.6585 15.5608 23.4964 15.0145 23.7334 14.5775C25.4808 11.3556 25.8242 8.87752 25.231 7.25551C24.6766 5.73949 23.1698 4.65557 20.3309 4.49971C16.1883 4.58993 11.818 6.83789 8.27071 10.4289C6.7718 11.9463 5.31466 13.7628 4.36882 15.6822C3.42279 17.602 3.02302 19.55 3.50569 21.3927C3.69277 22.1069 4.04066 22.589 4.67608 23.0939C5.57342 23.807 6.41967 24.1687 7.46019 24.2696C8.53543 24.3738 9.86963 24.2049 11.75 23.7446C15.4014 22.631 17.6813 20.9756 19.6272 19.2448C19.7876 19.0443 20.034 18.9097 20.331 18.9097C20.8232 18.9097 21.1638 19.272 21.2279 19.6827C21.7111 21.2628 21.3119 22.6974 20.5801 24.8969C20.4232 25.3685 19.9137 25.6237 19.442 25.4668C18.9704 25.3098 18.7153 24.8003 18.8722 24.3287C19.28 23.1028 19.5184 22.2748 19.6042 21.6215C17.7553 23.0989 15.4915 24.489 12.25 25.4739C12.2344 25.4786 12.2187 25.483 12.2029 25.4868C10.2524 25.9657 8.67373 26.1956 7.28654 26.0612C5.86012 25.9229 4.69201 25.4057 3.55624 24.5032C2.67855 23.8057 2.06862 23.0101 1.76444 21.8488C1.13472 19.4447 1.69385 17.0384 2.75421 14.8865C3.81477 12.7343 5.4115 10.7621 6.99014 9.16393C10.7681 5.33933 15.5731 2.79018 20.3286 2.69938ZM19.4866 20.1101C19.4865 20.11 19.4865 20.11 19.4865 20.1099L20.3284 19.8253L19.4866 20.1101ZM20.3284 19.8253L20.3223 19.8184L20.2584 19.7472M20.3284 19.8253L20.3223 19.8184Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.8011 1.62239C15.6024 1.12587 14.2555 1.12587 13.0568 1.62239C12.5696 1.8242 12.1382 2.13201 11.673 2.53776C11.2173 2.93519 10.6906 3.46195 10.0247 4.12781L10.0247 4.12784L10.0246 4.12791L4.12791 10.0246L4.1279 10.0246L4.1278 10.0247C3.46194 10.6906 2.93519 11.2174 2.53776 11.673C2.13201 12.1382 1.8242 12.5696 1.62239 13.0568C1.12587 14.2555 1.12587 15.6024 1.62239 16.8011C1.8242 17.2883 2.13201 17.7197 2.53776 18.1849C2.9352 18.6405 3.46199 19.1673 4.12788 19.8332L4.16671 19.872C4.83264 20.538 5.35945 21.0648 5.81512 21.4622C6.28029 21.868 6.71171 22.1758 7.19893 22.3776C8.39764 22.8741 9.7445 22.8741 10.9432 22.3776C11.4304 22.1758 11.8618 21.868 12.327 21.4622C12.4628 21.3438 12.5307 21.2846 12.5566 21.2077C12.575 21.1533 12.5771 21.091 12.5625 21.0355C12.5419 20.9571 12.4759 20.8911 12.3441 20.7593L10.913 19.3282C10.5225 18.9377 10.5225 18.3045 10.913 17.914C11.3035 17.5235 11.9367 17.5235 12.3272 17.914L13.7061 19.2928C13.9061 19.4928 14.0061 19.5928 14.1304 19.5928C14.2546 19.5928 14.3546 19.4928 14.5546 19.2928L15.7919 18.0556C15.9919 17.8556 16.0919 17.7556 16.0919 17.6313C16.0919 17.5071 15.9919 17.4071 15.7919 17.2071L14.413 15.8282C14.0225 15.4377 14.0225 14.8045 14.413 14.414C14.8035 14.0235 15.4367 14.0235 15.8272 14.414L17.2061 15.7928C17.4061 15.9928 17.5061 16.0928 17.6304 16.0928C17.7546 16.0928 17.8546 15.9928 18.0546 15.7928L19.2919 14.5556C19.4919 14.3556 19.5919 14.2556 19.5919 14.1313C19.5919 14.0071 19.4919 13.9071 19.2919 13.7071L17.913 12.3282C17.5225 11.9377 17.5225 11.3045 17.913 10.914C18.3035 10.5235 18.9367 10.5235 19.3272 10.914L20.7574 12.3441C20.8902 12.4769 20.9566 12.5433 21.0357 12.5638C21.0905 12.578 21.152 12.5759 21.2057 12.558C21.2833 12.5322 21.3429 12.4638 21.4622 12.327C21.868 11.8618 22.1758 11.4304 22.3776 10.9432C22.8741 9.7445 22.8741 8.39764 22.3776 7.19893C22.1758 6.71171 21.868 6.28029 21.4622 5.81512C21.0648 5.35945 20.538 4.83264 19.872 4.16671L19.8332 4.12788L19.8331 4.12777C19.1673 3.46193 18.6405 2.93518 18.1849 2.53776C17.7197 2.13201 17.2883 1.8242 16.8011 1.62239Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.6905 5.77665C20.09 6.15799 20.1047 6.79098 19.7234 7.19048L9.22336 18.1905C9.03745 18.3852 8.78086 18.4968 8.51163 18.4999C8.2424 18.5031 7.98328 18.3975 7.79289 18.2071L4.29289 14.7071C3.90237 14.3166 3.90237 13.6834 4.29289 13.2929C4.68342 12.9024 5.31658 12.9024 5.70711 13.2929L8.48336 16.0692L18.2766 5.80953C18.658 5.41003 19.291 5.39531 19.6905 5.77665Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

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

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because it is too large Load Diff

@ -0,0 +1,73 @@
import 'dart:convert';
import 'dart:io';
void main() async {
final file = File('assets/json/body_symptoms_data.json');
final content = await file.readAsString();
print('File size: ${content.length} characters');
// Split into two parts
final parts = content.split('export const SymptomsData = [');
if (parts.length != 2) {
print('ERROR: Could not split file properly');
exit(1);
}
print('Split into ${parts.length} parts');
// Process BodySymptomsData
var bodyPart = parts[0]
.replaceAll('export const BodySymptomsData = ', '')
.trim()
.replaceAll(RegExp(r';$'), '');
print('Parsing body symptoms...');
Map<String, dynamic> bodySymptomsData;
try {
bodySymptomsData = json.decode(bodyPart);
print('✅ Body symptoms parsed: ${bodySymptomsData.length} body parts');
} catch (e) {
print('❌ Error parsing body symptoms: $e');
exit(1);
}
// Process SymptomsData
print('Processing symptoms data...');
var symptomsPart = parts[1]
.trim()
.replaceAll(RegExp(r'\];$'), '')
.replaceAllMapped(RegExp(r'\n(\s*)id:'), (m) => '\n${m.group(1)}"id":')
.replaceAllMapped(RegExp(r'\n(\s*)type:'), (m) => '\n${m.group(1)}"type":')
.replaceAllMapped(RegExp(r'\n(\s*)name:'), (m) => '\n${m.group(1)}"name":')
.replaceAllMapped(RegExp(r'\n(\s*)common_name:'), (m) => '\n${m.group(1)}"common_name":');
print('Parsing symptoms...');
List<dynamic> symptomsData;
try {
symptomsData = json.decode('[$symptomsPart]');
print('✅ Symptoms parsed: ${symptomsData.length} symptoms');
} catch (e) {
print('❌ Error parsing symptoms: $e');
print('First 1000 chars:');
print('[$symptomsPart]'.substring(0, 1000));
exit(1);
}
// Create final JSON
print('Creating final JSON structure...');
final jsonStructure = {
'bodySymptoms': bodySymptomsData,
'symptoms': symptomsData,
};
// Write to file
print('Writing to file...');
final encoder = JsonEncoder.withIndent(' ');
await file.writeAsString(encoder.convert(jsonStructure));
print('\n✅ SUCCESS! File converted to proper JSON');
print('✅ Body parts: ${bodySymptomsData.length}');
print('✅ Symptoms: ${symptomsData.length}');
}

@ -14,7 +14,7 @@ var PACKAGES_ORDERS = '/api/orders';
var PACKAGES_ORDER_HISTORY = '/api/orders/items';
var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara';
// var BASE_URL = 'http://10.50.100.198:2018/';
var BASE_URL = 'https://uat.hmgwebservices.com/';
var BASE_URL = 'https://uat.hmgwebservices.com/';
// var BASE_URL = 'https://hmgwebservices.com/';
// var BASE_URL = 'http://10.201.204.103/';
// var BASE_URL = 'https://orash.cloudsolutions.com.sa/';
@ -181,6 +181,7 @@ var GET_TABLE_REQUEST = 'Services/Community.svc/REST/CreateVaccinationTable';
///
/// use get all cities from the e-referral, already calling this api there don't use multiple same api calls
var GET_CITIES_REQUEST = 'Services/Lists.svc/REST/GetAllCities';
///
///
///
@ -444,8 +445,6 @@ var H2O_UPDATE_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetai
var H2O_UNDO_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity";
//E_Referral Services
// Encillary Orders
var GET_ANCILLARY_ORDERS = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList';
@ -512,12 +511,6 @@ var PATIENT_ER_INSERT_PRES_ORDER = 'Services/Patients.svc/REST/PatientER_InsertP
var BLOOD_DONATION_REGISTER_BLOOD_TYPE = 'Services/PatientVarification.svc/REST/BloodDonation_RegisterBloodType';
var ADD_USER_AGREEMENT_FOR_BLOOD_DONATION = 'Services/PatientVarification.svc/REST/AddUserAgreementForBloodDonation';
// HHC RC SERVICES
var HHC_GET_ALL_SERVICES_RC = "api/HHC/getallhhc";
var ADD_HHC_ORDER_RC = "api/HHC/add";
var GET_ALL_HHC_ORDERS_RC = 'api/hhc/list';
var UPDATE_HHC_ORDER_RC = 'api/hhc/update';
// RRT RC SERVICES
var ADD_RRT_ORDER_RC = "api/rrt/add";
var GET_ALL_RRT_ORDERS_RC = "api/rrt/list";
@ -716,7 +709,9 @@ class ApiConsts {
static String baseUrl = 'https://hmgwebservices.com/'; // HIS API URL PROD
static String rcBaseUrl = 'https://rc.hmg.com/'; // RC API URL PROD
static String rcBaseUrl = 'https://rc.hmg.com/'; // dRC API URL PROD
static String hmgPharmacyApiBaseUrl = 'https://hmgpharmacyapi.hmg.com/'; // dRC API URL PROD
static String symptomsCheckerApi = '${hmgPharmacyApiBaseUrl}symptomsapi/api/SymptomChecker'; // dRC API URL PROD
static var payFortEnvironment = FortEnvironment.production;
static var applePayMerchantId = "merchant.com.hmgwebservices";
@ -850,6 +845,12 @@ class ApiConsts {
static final String updateHHCOrder = 'api/hhc/update';
static final String addHHCOrder = 'api/HHC/add';
// SYMPTOMS CHECKER
static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName';
static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors';
static final String getGeneralSuggestion = '$symptomsCheckerApi/GetGeneralSggestion';
static final String diagnosis = '$symptomsCheckerApi/diagnosis';
static final String explain = '$symptomsCheckerApi/explain';
//E-REFERRAL SERVICES
@ -860,7 +861,6 @@ class ApiConsts {
static final createEReferral = "Services/Patients.svc/REST/CreateEReferral";
static final getEReferrals = "Services/Patients.svc/REST/GetEReferrals";
// ************ static values for Api ****************
static final double appVersionID = 50.3;
static final int appChannelId = 3;
@ -868,3 +868,7 @@ class ApiConsts {
static final String appGeneralId = "Cs2020@2016\$2958";
static final String sessionID = 'TMRhVmkGhOsvamErw';
}
class ApiKeyConstants {
static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng';
}

@ -189,7 +189,6 @@ class AppAssets {
static const String hmg_contact_icon = '$svgBasePath/hmg_contact_icon.svg';
static const String services_medical_file_icon = '$svgBasePath/services_medical_file_icon.svg';
//bottom navigation//
static const String homeBottom = '$svgBasePath/home_bottom.svg';
static const String bookAppoBottom = '$svgBasePath/book_appo_bottom.svg';
@ -202,23 +201,39 @@ class AppAssets {
static const String heart = '$svgBasePath/heart.svg';
static const String alertSquare = '$svgBasePath/alert-square.svg';
static const String arrowRight = '$svgBasePath/arrow-right.svg';
static const String tickIcon = '$svgBasePath/tick.svg';
// Symptoms Checker
static const String calendarGrey = '$svgBasePath/calendar-grey.svg';
static const String weightScale = '$svgBasePath/weight-scale.svg';
static const String rulerIcon = '$svgBasePath/ruler.svg';
static const String genderIcon = '$svgBasePath/gender.svg';
static const String bodyIcon = '$svgBasePath/body_icon.svg';
static const String rotateIcon = '$svgBasePath/rotate_icon.svg';
static const String refreshIcon = '$svgBasePath/refresh.svg';
static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg';
// PNGS //
static const String hmg_logo = '$pngBasePath/hmg_logo.png';
static const String livecare_service = '$pngBasePath/livecare_service.png';
static const String male_img = '$pngBasePath/male_img.png';
static const String hmgLogo = '$pngBasePath/hmg_logo.png';
static const String liveCareService = '$pngBasePath/livecare_service.png';
static const String maleImg = '$pngBasePath/male_img.png';
static const String femaleImg = '$pngBasePath/female_img.png';
static const String babyGirlImg = '$pngBasePath/baby_girl_img.png';
static const String babyBoyImg = '$pngBasePath/baby_img.png';
static const String apple_pay = '$pngBasePath/Apple_Pay.png';
static const String applePay = '$pngBasePath/Apple_Pay.png';
static const String mada = '$pngBasePath/Mada.png';
static const String Mastercard = '$pngBasePath/Mastercard.png';
static const String tamara_en = '$pngBasePath/tamara_en.png';
static const String mastercard = '$pngBasePath/Mastercard.png';
static const String tamaraEng = '$pngBasePath/tamara_en.png';
static const String visa = '$pngBasePath/visa.png';
static const String lockIcon = '$pngBasePath/lock-icon.png';
static const String dummy_user = '$pngBasePath/dummy_user.png';
static const String dummyUser = '$pngBasePath/dummy_user.png';
static const String comprehensiveCheckupEn = '$pngBasePath/cc_en.png';
static const String comprehensiveCheckupAr = '$pngBasePath/cc_er.png';
static const String maleIcon = '$pngBasePath/male_icon.png';
static const String femaleIcon = '$pngBasePath/female_icon.png';
static const String fullBodyFront = '$pngBasePath/full_body_front.png';
static const String fullBodyBack = '$pngBasePath/full_body_back.png';
}
class AppAnimations {
@ -234,9 +249,9 @@ class AppAnimations {
static const String splashLaunching = '$lottieBasePath/splash_launching.json';
static const String noData = '$lottieBasePath/Nodata.json';
static const String ripple = '$lottieBasePath/Ripple.json';
static const String pending_loading_animation = '$lottieBasePath/pending_loading_animation.json';
static const String pendingLoadingAnimation = '$lottieBasePath/pending_loading_animation.json';
static const String ambulance = '$lottieBasePath/ambulance.json';
static const String ambulance_alert = '$lottieBasePath/ambulance_alert.json';
static const String rrt_ambulance = '$lottieBasePath/rrt_ambulance.json';
static const String ambulanceAlert = '$lottieBasePath/ambulance_alert.json';
static const String rrtAmbulance = '$lottieBasePath/rrt_ambulance.json';
}

@ -39,6 +39,8 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
@ -96,6 +98,9 @@ class AppDependencies {
final sharedPreferences = await SharedPreferences.getInstance();
getIt.registerLazySingleton<CacheService>(() => CacheServiceImp(sharedPreferences: sharedPreferences, loggerService: getIt()));
getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt()));
getIt.registerLazySingleton<LocalAuthService>(
() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()),
);
// Repositories
getIt.registerLazySingleton<CommonRepo>(() => CommonRepoImp(loggerService: getIt()));
@ -107,9 +112,6 @@ class AppDependencies {
getIt.registerLazySingleton<PrescriptionsRepo>(() => PrescriptionsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<InsuranceRepo>(() => InsuranceRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
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()));
getIt.registerLazySingleton<ImmediateLiveCareRepo>(() => ImmediateLiveCareRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
@ -118,6 +120,7 @@ class AppDependencies {
getIt.registerLazySingleton<LocationRepo>(() => LocationRepoImpl(apiClient: getIt()));
getIt.registerLazySingleton<ContactUsRepo>(() => ContactUsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<HmgServicesRepo>(() => HmgServicesRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<SymptomsCheckerRepo>(() => SymptomsCheckerRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<BloodDonationRepo>(() => BloodDonationRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
// ViewModels
@ -126,14 +129,11 @@ class AppDependencies {
getIt.registerLazySingleton<LabViewModel>(() => LabViewModel(labRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()));
getIt.registerLazySingleton<RadiologyViewModel>(
() => RadiologyViewModel(
radiologyRepo: getIt(),
errorHandlerService: getIt(),
navigationService: getIt()
),
() => RadiologyViewModel(radiologyRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()),
);
getIt.registerLazySingleton<PrescriptionsViewModel>(() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<PrescriptionsViewModel>(
() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<InsuranceViewModel>(() => InsuranceViewModel(insuranceRepo: getIt(), errorHandlerService: getIt()));
@ -223,6 +223,7 @@ class AppDependencies {
() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()),
);
getIt.registerLazySingleton<SymptomsCheckerViewModel>(() => SymptomsCheckerViewModel(errorHandlerService: getIt(), symptomsCheckerRepo: getIt()));
getIt.registerLazySingleton<HmgServicesViewModel>(
() => HmgServicesViewModel(bookAppointmentsRepo: getIt(), hmgServicesRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()),
);

@ -32,6 +32,8 @@ enum AppEnvironmentTypeEnum { dev, uat, preProd, qa, staging, prod }
enum FamilyFileEnum { active, inactive, blocked, deleted, pending, rejected }
enum BodyView { front, back }
extension CalenderExtension on CalenderEnum {
int get toInt {
switch (this) {
@ -172,7 +174,7 @@ extension FamilyFileEnumExtenshion on FamilyFileEnum {
return isArabic ? 'محذوف' : 'Deleted';
case FamilyFileEnum.pending:
return isArabic ? 'قيد الانتظار' : 'Pending';
case FamilyFileEnum.rejected:
case FamilyFileEnum.rejected:
return isArabic ? 'مرفوض' : 'Rejected';
}
}
@ -239,3 +241,7 @@ extension ServiceTypeEnumExt on ServiceTypeEnum {
}
}
}
// SymptomsChecker
enum PossibleConditionsSeverityEnum { seekMedicalAdvice, monitorOnly, emergency }

@ -25,7 +25,13 @@ class CalenderUtilsNew {
});
}
FutureOr<bool> createOrUpdateEvent({required String title, required String description, required String location, DateTime? scheduleDateTime, String? eventId, int? reminderMinutes}) async {
FutureOr<bool> createOrUpdateEvent(
{required String title,
required String description,
required String location,
DateTime? scheduleDateTime,
String? eventId,
int? reminderMinutes}) async {
if (writableCalender.isEmpty) {
await getCalenders();
}
@ -38,12 +44,14 @@ class CalenderUtilsNew {
// String eventId = await getEventIdIfEventExist(title!.split("#").last);
// if (eventId.isEmpty) {
eventResult = await DeviceCalendar.instance.createEvent(
calendarId: writableCalendars!.id,
title: title,
description: description,
startDate: scheduleDateTime!,
endDate: scheduleDateTime!.add(Duration(minutes: 30)),
reminderMinutes: reminderMinutes);
calendarId: writableCalendars!.id,
title: title,
description: description,
startDate: scheduleDateTime!,
endDate: scheduleDateTime!.add(Duration(minutes: 30)),
// reminderMinutes: reminderMinutes, // TODO : NEED TO CONFIRM THIS FROM TAHA
);
return eventResult.isNotEmpty;
// }

@ -8,7 +8,6 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:google_api_availability/google_api_availability.dart';
@ -723,10 +722,10 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset(AppAssets.mada, width: 25.h, height: 25.h),
Image.asset(AppAssets.tamara_en, width: 25.h, height: 25.h),
Image.asset(AppAssets.tamaraEng, width: 25.h, height: 25.h),
Image.asset(AppAssets.visa, width: 25.h, height: 25.h),
Image.asset(AppAssets.Mastercard, width: 25.h, height: 25.h),
Image.asset(AppAssets.apple_pay, width: 25.h, height: 25.h),
Image.asset(AppAssets.mastercard, width: 25.h, height: 25.h),
Image.asset(AppAssets.applePay, width: 25.h, height: 25.h),
],
);
}
@ -871,7 +870,8 @@ class Utils {
}
return isHavePrivilege;
}
static void openWebView({ required String url}) {
static void openWebView({required String url}) {
Uri uri = Uri.parse(url);
launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}

@ -17,7 +17,7 @@ extension NavigationExtensions on BuildContext {
Navigator.of(this).pop();
}
void navigateTo(Widget page) {
void pushNavigateTo(Widget page) {
Navigator.push(this, MaterialPageRoute(builder: (context) => page));
}

@ -3,12 +3,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices;
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/location_util.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/doctor_response_mapper.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';
@ -16,29 +14,24 @@ 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/authentication/models/resp_models/authenticated_user_resp_model.dart';
import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_repo.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/RRTRequestModel.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart';
import 'package:hmg_patient_app_new/features/location/location_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/PatientER_RC.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/RRTRequestModel.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/PatientERTransportationMethod.dart'
show PatientERTransportationMethod;
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/ProjectAvgERWaitingTime.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/rrt_procedures_response_model.dart';
import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart' show PlaceDetails;
import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart';
import 'package:hmg_patient_app_new/features/location/location_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_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_repo.dart';
import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_map_screen.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_request_type_select.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/terms_and_condition.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/call_ambulance_page.dart';
@ -85,7 +78,6 @@ class EmergencyServicesViewModel extends ChangeNotifier {
List<ProjectAvgERWaitingTime> nearestERList = [];
List<ProjectAvgERWaitingTime> nearestERFilteredList = [];
List<HospitalsModel>? hospitalList;
List<HospitalsModel>? hmgHospitalList;
List<HospitalsModel>? hmcHospitalList;
@ -104,7 +96,6 @@ class EmergencyServicesViewModel extends ChangeNotifier {
AmbulanceCallingPlace callingPlace = AmbulanceCallingPlace.FROM_HOSPITAL;
AmbulanceDirection ambulanceDirection = AmbulanceDirection.ONE_WAY;
bool patientHasAdvanceERBalance = false;
bool isERBookAppointment = false;
late EROnlineCheckInPaymentDetailsResponse erOnlineCheckInPaymentDetailsResponse;
@ -118,14 +109,11 @@ class EmergencyServicesViewModel extends ChangeNotifier {
RRTServiceData? ordersRRT;
TextEditingController rrtNotes = TextEditingController();
List allOrders = [];
List orderDisplayList = [];
bool historyLoading= false;
bool historyLoading = false;
OrderDislpay currentlyDisplayedOrder = OrderDislpay.ALL;
setSelectedRRTProcedure(RRTProceduresResponseModel procedure) {
selectedRRTProcedure = procedure;
notifyListeners();
@ -161,7 +149,6 @@ class EmergencyServicesViewModel extends ChangeNotifier {
String? termsAndConditions;
Future<void> getRRTProcedures({Function(dynamic)? onSuccess, Function(String)? onError}) async {
print("the app state is ${appState.isAuthenticated}");
if (!appState.isAuthenticated) {
dialogService.showErrorBottomSheet(
@ -797,8 +784,8 @@ class EmergencyServicesViewModel extends ChangeNotifier {
historyLoading = false;
ambulanceOrders = apiResponse.data;
allOrders.clear();
allOrders.addAll(ambulanceOrders??[]);
allOrders.addAll(ordersRRT?.completedOrders??[]);
allOrders.addAll(ambulanceOrders ?? []);
allOrders.addAll(ordersRRT?.completedOrders ?? []);
changeOrderDisplayItems(OrderDislpay.ALL);
notifyListeners();
},
@ -889,8 +876,7 @@ class EmergencyServicesViewModel extends ChangeNotifier {
});
}
RRTRequestModel createRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction){
RRTRequestModel createRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) {
AuthenticatedUser? user = appState.getAuthenticatedUser();
if (user == null) throw Exception("Authentication Required to Continue");
@ -898,7 +884,6 @@ class EmergencyServicesViewModel extends ChangeNotifier {
Procedures procedures = new Procedures();
rrtRequestModel.procedures = [];
procedures.serviceID = selectedRRTProcedure?.procedureID;
rrtRequestModel.latitude = ((result?.geometry.location.lat) ?? place?.lat);
@ -920,7 +905,7 @@ class EmergencyServicesViewModel extends ChangeNotifier {
notifyListeners();
}
FutureOr<void> submitRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) async {
FutureOr<void> submitRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) async {
RRTRequestModel request = createRRTRequest(result, place, placePrediction);
navServices.push(CustomPageRoute(page: RequestingServicesPage()));
@ -929,9 +914,9 @@ class EmergencyServicesViewModel extends ChangeNotifier {
navServices.pushAndRemoveUntil(
CustomPageRoute(
page: TrackingScreen(
isRRTOrder: true,
state: OrderTrackingState.failed,
)),
isRRTOrder: true,
state: OrderTrackingState.failed,
)),
ModalRoute.withName("/EmergencyServicesPage"));
}, (success) {
getRRTOrders(shouldNavigateToTrackingScreen: true);
@ -939,7 +924,7 @@ class EmergencyServicesViewModel extends ChangeNotifier {
}
Future<void> getRRTOrders({bool shouldNavigateToTrackingScreen = false, bool showLoader = false}) async {
if(shouldNavigateToTrackingScreen == false && showLoader ) {
if (shouldNavigateToTrackingScreen == false && showLoader) {
LoaderBottomSheet.showLoader(loadingText: "Fetching Orders");
}
historyLoading = true;
@ -947,39 +932,40 @@ class EmergencyServicesViewModel extends ChangeNotifier {
int? id = appState.getAuthenticatedUser()?.patientId;
var response = await emergencyServicesRepo.getRRTOrders(id: id);
if(shouldNavigateToTrackingScreen == false && showLoader ) {
LoaderBottomSheet.hideLoader();}
if (shouldNavigateToTrackingScreen == false && showLoader) {
LoaderBottomSheet.hideLoader();
}
response.fold(
(failure) async {
historyLoading = false;
notifyListeners();
(failure) async {
historyLoading = false;
notifyListeners();
if (shouldNavigateToTrackingScreen) {
navServices.pushAndRemoveUntil(CustomPageRoute(page: TrackingScreen(isRRTOrder: true,state: OrderTrackingState.waitingForCall)), ModalRoute.withName("/EmergencyServicesPage"));
navServices.pushAndRemoveUntil(CustomPageRoute(page: TrackingScreen(isRRTOrder: true, state: OrderTrackingState.waitingForCall)),
ModalRoute.withName("/EmergencyServicesPage"));
}
},
(apiResponse) {
(apiResponse) {
if (shouldNavigateToTrackingScreen) {
navServices.pushAndRemoveUntil(
CustomPageRoute(
page: TrackingScreen(
state: OrderTrackingState.waitingForCall,
isRRTOrder: true,
rrtOrder: apiResponse.data?.pendingOrders.first,
)),
state: OrderTrackingState.waitingForCall,
isRRTOrder: true,
rrtOrder: apiResponse.data?.pendingOrders.first,
)),
ModalRoute.withName("/EmergencyServicesPage"));
}
historyLoading = false;
ordersRRT = apiResponse.data;
allOrders.clear();
allOrders.addAll(ambulanceOrders??[]);
allOrders.addAll(ordersRRT?.completedOrders??[]);
allOrders.addAll(ambulanceOrders ?? []);
allOrders.addAll(ordersRRT?.completedOrders ?? []);
changeOrderDisplayItems(OrderDislpay.ALL);
notifyListeners();
},
);
}
FutureOr<void> cancelRRTOrder(int? orderID, {bool shouldPop = false}) async {
dialogService.showCommonBottomSheetWithoutH(
message: "Do you want to cancel the request".needTranslation,
@ -998,52 +984,54 @@ class EmergencyServicesViewModel extends ChangeNotifier {
});
}
void changeOrderDisplayItems(OrderDislpay currentlyDisplayedOrder){
void changeOrderDisplayItems(OrderDislpay currentlyDisplayedOrder) {
this.currentlyDisplayedOrder = currentlyDisplayedOrder;
switch(currentlyDisplayedOrder){
switch (currentlyDisplayedOrder) {
case OrderDislpay.ALL:
orderDisplayList = allOrders;
break;
orderDisplayList = allOrders;
break;
case OrderDislpay.RRT:
orderDisplayList = ordersRRT?.completedOrders ?? [];
break;
case OrderDislpay.AMBULANCE:
orderDisplayList = ambulanceOrders??[];
orderDisplayList = ambulanceOrders ?? [];
break;
}
notifyListeners();
}
void openRRT(){
void openRRT() {
print("the app state is ${appState.isAuthenticated}");
if (appState.isAuthenticated) {
if(agreedToTermsAndCondition == false){
dialogService.showErrorBottomSheet(message: "You Need To Agree To Terms And Conditions".needTranslation, onOkPressed: (){
if(navServices.context == null ) return;
showCommonBottomSheetWithoutHeight(
navServices.context!,
padding: EdgeInsets.only(top: 24.h),
titleWidget: Transform.flip(
flipX: isArabic,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_back,
iconColor: Color(0xff2B353E),
fit: BoxFit.contain,
),
).onPress(() {
navServices.pop();
}),
// title: "Rapid Response Team (RRT)".needTranslation,
child: RrtRequestTypeSelect(),
isFullScreen: false,
isCloseButtonVisible: true,
hasBottomPadding: false,
backgroundColor: AppColors.bottomSheetBgColor,
callBackFunc: () {
navServices.pop();
},
);
});
if (agreedToTermsAndCondition == false) {
dialogService.showErrorBottomSheet(
message: "You Need To Agree To Terms And Conditions".needTranslation,
onOkPressed: () {
if (navServices.context == null) return;
showCommonBottomSheetWithoutHeight(
navServices.context!,
padding: EdgeInsets.only(top: 24.h),
titleWidget: Transform.flip(
flipX: isArabic,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_back,
iconColor: Color(0xff2B353E),
fit: BoxFit.contain,
),
).onPress(() {
navServices.pop();
}),
// title: "Rapid Response Team (RRT)".needTranslation,
child: RrtRequestTypeSelect(),
isFullScreen: false,
isCloseButtonVisible: true,
hasBottomPadding: false,
backgroundColor: AppColors.bottomSheetBgColor,
callBackFunc: () {
navServices.pop();
},
);
});
return;
}
placeValueInController();
@ -1051,7 +1039,7 @@ class EmergencyServicesViewModel extends ChangeNotifier {
isShowConfirmDialog: true,
onSuccess: (position) async {
updateBottomSheetState(BottomSheetType.FIXED);
bool result = await navServices.push(
bool result = await navServices.push(
CustomPageRoute(
page: MapUtilityScreen(
confirmButtonString: "Submit Request".needTranslation,
@ -1061,16 +1049,15 @@ class EmergencyServicesViewModel extends ChangeNotifier {
),
direction: AxisDirection.down),
);
if(result){
LocationViewModel locationViewModel = getIt.get<LocationViewModel>();
GeocodeResponse? response = locationViewModel.geocodeResponse;
PlaceDetails? placeDetails = locationViewModel.placeDetails;
PlacePrediction? placePrediction = locationViewModel.selectedPrediction;
submitRRTRequest(response?.results.first, placeDetails, placePrediction);
}
if (result) {
LocationViewModel locationViewModel = getIt.get<LocationViewModel>();
GeocodeResponse? response = locationViewModel.geocodeResponse;
PlaceDetails? placeDetails = locationViewModel.placeDetails;
PlacePrediction? placePrediction = locationViewModel.selectedPrediction;
submitRRTRequest(response?.results.first, placeDetails, placePrediction);
}
});
} else{
} else {
dialogService.showErrorBottomSheet(
message: "You Need To Login First To Continue".needTranslation,
onOkPressed: () {
@ -1079,24 +1066,22 @@ class EmergencyServicesViewModel extends ChangeNotifier {
});
}
}
clearRRTData(){
clearRRTData() {
selectedRRTProcedure = null;
}
FutureOr<void> getTermsAndConditions() async {
LoaderBottomSheet.showLoader(loadingText: "Fetching Terms And Conditions".needTranslation);
var response = await emergencyServicesRepo.getTermsAndCondition();
LoaderBottomSheet.hideLoader();
response.fold((failure)=>errorHandlerService.handleError(failure: failure),(success){
response.fold((failure) => errorHandlerService.handleError(failure: failure), (success) {
termsAndConditions = success.data;
print("the response terms are $termsAndConditions");
notifyListeners();
navServices.push(
CustomPageRoute(
page: TermsAndCondition(termsAndCondition:success.data??""), direction: AxisDirection.down),
CustomPageRoute(page: TermsAndCondition(termsAndCondition: success.data ?? ""), direction: AxisDirection.down),
);
});
}
}

@ -0,0 +1,422 @@
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
class OrganData {
// ==================== FRONT VIEW ====================
static const List<OrganModel> frontViewOrgans = [
// ========== HEAD & FACE ==========
OrganModel(
id: 'head',
name: 'whole_head',
description: 'Head',
bodyView: BodyView.front,
position: OrganPosition(x: 0.50, y: 0.03),
),
OrganModel(
id: 'left_eye',
name: 'eyes',
description: 'Left Eye',
bodyView: BodyView.front,
position: OrganPosition(x: 0.43, y: 0.07),
),
OrganModel(
id: 'right_eye',
name: 'eyes',
description: 'Right Eye',
bodyView: BodyView.front,
position: OrganPosition(x: 0.57, y: 0.07),
),
OrganModel(
id: 'nose_mouth',
name: 'oral_cavity',
description: 'Nose/Mouth',
bodyView: BodyView.front,
position: OrganPosition(x: 0.50, y: 0.09),
),
// ========== NECK ==========
OrganModel(
id: 'throat',
name: 'neck_or_throat',
description: 'Throat',
bodyView: BodyView.front,
position: OrganPosition(x: 0.50, y: 0.15),
),
// ========== SHOULDERS ==========
OrganModel(
id: 'left_shoulder',
name: 'upper_arm',
description: 'Left Shoulder',
bodyView: BodyView.front,
position: OrganPosition(x: 0.22, y: 0.22),
),
OrganModel(
id: 'right_shoulder',
name: 'upper_arm',
description: 'Right Shoulder',
bodyView: BodyView.front,
position: OrganPosition(x: 0.8, y: 0.22),
),
// ========== CHEST ==========
OrganModel(
id: 'left_chest',
name: 'chest',
description: 'Left Chest',
bodyView: BodyView.front,
position: OrganPosition(x: 0.38, y: 0.22),
),
OrganModel(
id: 'center_chest',
name: 'chest',
description: 'Center Chest',
bodyView: BodyView.front,
position: OrganPosition(x: 0.52, y: 0.25),
),
OrganModel(
id: 'right_chest',
name: 'chest',
description: 'Right Chest',
bodyView: BodyView.front,
position: OrganPosition(x: 0.65, y: 0.22),
),
// ========== RIBS ==========
OrganModel(
id: 'left_ribs',
name: 'chest',
description: 'Left Ribs',
bodyView: BodyView.front,
position: OrganPosition(x: 0.38, y: 0.35),
),
OrganModel(
id: 'right_ribs',
name: 'chest',
description: 'Right Ribs',
bodyView: BodyView.front,
position: OrganPosition(x: 0.64, y: 0.35),
),
// ========== ABDOMEN ==========
OrganModel(
id: 'upper_abdomen',
name: 'upper_abdomen',
description: 'Upper Abdomen',
bodyView: BodyView.front,
position: OrganPosition(x: 0.51, y: 0.31),
),
OrganModel(
id: 'navel',
name: 'mid_abdomen',
description: 'Navel',
bodyView: BodyView.front,
position: OrganPosition(x: 0.51, y: 0.38),
),
OrganModel(
id: 'lower_abdomen',
name: 'lower_abdomen',
description: 'Lower Abdomen',
bodyView: BodyView.front,
position: OrganPosition(x: 0.51, y: 0.44),
),
// ========== PELVIS ==========
OrganModel(
id: 'left_groin',
name: 'sexual_organs',
description: 'Left Groin',
bodyView: BodyView.front,
position: OrganPosition(x: 0.42, y: 0.49),
),
OrganModel(
id: 'right_groin',
name: 'sexual_organs',
description: 'Right Groin',
bodyView: BodyView.front,
position: OrganPosition(x: 0.59, y: 0.49),
),
// ========== LEFT ARM ==========
OrganModel(
id: 'left_elbow',
name: 'elbow',
description: 'Left Elbow',
bodyView: BodyView.front,
position: OrganPosition(x: 0.21, y: 0.35),
),
OrganModel(
id: 'left_forearm',
name: 'forearm',
description: 'Left Forearm',
bodyView: BodyView.front,
position: OrganPosition(x: 0.19, y: 0.43),
),
OrganModel(
id: 'left_wrist',
name: 'hand',
description: 'Left Wrist',
bodyView: BodyView.front,
position: OrganPosition(x: 0.14, y: 0.5),
),
// ========== RIGHT ARM ==========
OrganModel(
id: 'right_elbow',
name: 'elbow',
description: 'Right Elbow',
bodyView: BodyView.front,
position: OrganPosition(x: 0.79, y: 0.35),
),
OrganModel(
id: 'right_forearm',
name: 'forearm',
description: 'Right Forearm',
bodyView: BodyView.front,
position: OrganPosition(x: 0.81, y: 0.43),
),
OrganModel(
id: 'right_wrist',
name: 'hand',
description: 'Right Wrist',
bodyView: BodyView.front,
position: OrganPosition(x: 0.85, y: 0.5),
),
// ========== LEFT LEG ==========
OrganModel(
id: 'left_thigh',
name: 'thigh',
description: 'Left Thigh',
bodyView: BodyView.front,
position: OrganPosition(x: 0.4, y: 0.60),
),
OrganModel(
id: 'left_knee',
name: 'knee',
description: 'Left Knee',
bodyView: BodyView.front,
position: OrganPosition(x: 0.4, y: 0.72),
),
OrganModel(
id: 'left_shin',
name: 'lower_leg',
description: 'Left Shin',
bodyView: BodyView.front,
position: OrganPosition(x: 0.41, y: 0.82),
),
OrganModel(
id: 'left_ankle',
name: 'foot',
description: 'Left Ankle',
bodyView: BodyView.front,
position: OrganPosition(x: 0.43, y: 0.91),
),
// ========== RIGHT LEG ==========
OrganModel(
id: 'right_thigh',
name: 'thigh',
description: 'Right Thigh',
bodyView: BodyView.front,
position: OrganPosition(x: 0.65, y: 0.60),
),
OrganModel(
id: 'right_knee',
name: 'knee',
description: 'Right Knee',
bodyView: BodyView.front,
position: OrganPosition(x: 0.6, y: 0.72),
),
OrganModel(
id: 'right_shin',
name: 'lower_leg',
description: 'Right Shin',
bodyView: BodyView.front,
position: OrganPosition(x: 0.57, y: 0.82),
),
OrganModel(
id: 'right_ankle',
name: 'foot',
description: 'Right Ankle',
bodyView: BodyView.front,
position: OrganPosition(x: 0.57, y: 0.91),
),
];
// ==================== BACK VIEW ====================
static const List<OrganModel> backViewOrgans = [
// ========== HEAD & NECK ==========
OrganModel(
id: 'back_of_head',
name: 'whole_head',
description: 'Back of Head',
bodyView: BodyView.back,
position: OrganPosition(x: 0.50, y: 0.04),
),
OrganModel(
id: 'neck_back',
name: 'nape_of_neck',
description: 'Neck',
bodyView: BodyView.back,
position: OrganPosition(x: 0.50, y: 0.14),
),
// ========== SHOULDERS ==========
OrganModel(
id: 'left_shoulder_back',
name: 'upper_arm',
description: 'Left Shoulder',
bodyView: BodyView.back,
position: OrganPosition(x: 0.33, y: 0.19),
),
OrganModel(
id: 'right_shoulder_back',
name: 'upper_arm',
description: 'Right Shoulder',
bodyView: BodyView.back,
position: OrganPosition(x: 0.67, y: 0.19),
),
// ========== UPPER BACK ==========
OrganModel(
id: 'upper_spine',
name: 'back',
description: 'Upper Spine',
bodyView: BodyView.back,
position: OrganPosition(x: 0.50, y: 0.23),
),
OrganModel(
id: 'left_upper_back',
name: 'back',
description: 'Left Upper Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.38, y: 0.28),
),
OrganModel(
id: 'right_upper_back',
name: 'back',
description: 'Right Upper Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.62, y: 0.28),
),
// ========== MID BACK ==========
OrganModel(
id: 'mid_spine',
name: 'back',
description: 'Mid Spine',
bodyView: BodyView.back,
position: OrganPosition(x: 0.50, y: 0.35),
),
OrganModel(
id: 'left_mid_back',
name: 'back',
description: 'Left Mid Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.38, y: 0.35),
),
OrganModel(
id: 'right_mid_back',
name: 'back',
description: 'Right Mid Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.62, y: 0.35),
),
// ========== LOWER BACK ==========
OrganModel(
id: 'lower_spine',
name: 'lower_back',
description: 'Lower Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.50, y: 0.43),
),
OrganModel(
id: 'left_lower_back',
name: 'lower_back',
description: 'Left Lower Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.40, y: 0.43),
),
OrganModel(
id: 'right_lower_back',
name: 'lower_back',
description: 'Right Lower Back',
bodyView: BodyView.back,
position: OrganPosition(x: 0.60, y: 0.43),
),
// ========== GLUTES ==========
OrganModel(
id: 'sacrum',
name: 'lower_back',
description: 'Sacrum',
bodyView: BodyView.back,
position: OrganPosition(x: 0.50, y: 0.52),
),
OrganModel(
id: 'left_glute',
name: 'buttocks',
description: 'Left Glute',
bodyView: BodyView.back,
position: OrganPosition(x: 0.45, y: 0.56),
),
OrganModel(
id: 'right_glute',
name: 'buttocks',
description: 'Right Glute',
bodyView: BodyView.back,
position: OrganPosition(x: 0.55, y: 0.56),
),
// ========== LEGS ==========
OrganModel(
id: 'left_hamstring',
name: 'thigh',
description: 'Left Hamstring',
bodyView: BodyView.back,
position: OrganPosition(x: 0.43, y: 0.65),
),
OrganModel(
id: 'right_hamstring',
name: 'thigh',
description: 'Right Hamstring',
bodyView: BodyView.back,
position: OrganPosition(x: 0.57, y: 0.65),
),
OrganModel(
id: 'left_calf',
name: 'lower_leg',
description: 'Left Calf',
bodyView: BodyView.back,
position: OrganPosition(x: 0.43, y: 0.79),
),
OrganModel(
id: 'right_calf',
name: 'lower_leg',
description: 'Right Calf',
bodyView: BodyView.back,
position: OrganPosition(x: 0.57, y: 0.79),
),
];
// Helper methods
static List<OrganModel> getOrgansForView(BodyView view) {
return view == BodyView.front ? frontViewOrgans : backViewOrgans;
}
static List<OrganModel> getAllOrgans() {
return [...frontViewOrgans, ...backViewOrgans];
}
static OrganModel? getOrganById(String id) {
try {
return getAllOrgans().firstWhere((organ) => organ.id == id);
} catch (e) {
return null;
}
}
}

@ -0,0 +1,323 @@
import 'package:hmg_patient_app_new/features/symptoms_checker/models/symptom_model.dart';
class SymptomsMappingData {
// Map of organ IDs to their possible symptoms
static final Map<String, List<SymptomModel>> organSymptomsMap = {
// HEAD & FACE
'head': [
SymptomModel(id: 'head_s1', name: 'Headache', organId: 'head'),
SymptomModel(id: 'head_s2', name: 'Dizziness', organId: 'head'),
SymptomModel(id: 'head_s3', name: 'Migraine', organId: 'head'),
SymptomModel(id: 'head_s4', name: 'Head pressure', organId: 'head'),
SymptomModel(id: 'head_s5', name: 'Vertigo', organId: 'head'),
SymptomModel(id: 'head_s6', name: 'Confusion', organId: 'head'),
SymptomModel(id: 'head_s7', name: 'Memory problems', organId: 'head'),
],
'left_eye': [
SymptomModel(id: 'left_eye_s1', name: 'Blurred vision', organId: 'left_eye'),
SymptomModel(id: 'left_eye_s2', name: 'Eye pain', organId: 'left_eye'),
SymptomModel(id: 'left_eye_s3', name: 'Redness', organId: 'left_eye'),
SymptomModel(id: 'left_eye_s4', name: 'Itching', organId: 'left_eye'),
SymptomModel(id: 'left_eye_s5', name: 'Watering', organId: 'left_eye'),
SymptomModel(id: 'left_eye_s6', name: 'Sensitivity to light', organId: 'left_eye'),
SymptomModel(id: 'left_eye_s7', name: 'Double vision', organId: 'left_eye'),
],
'right_eye': [
SymptomModel(id: 'right_eye_s1', name: 'Blurred vision', organId: 'right_eye'),
SymptomModel(id: 'right_eye_s2', name: 'Eye pain', organId: 'right_eye'),
SymptomModel(id: 'right_eye_s3', name: 'Redness', organId: 'right_eye'),
SymptomModel(id: 'right_eye_s4', name: 'Itching', organId: 'right_eye'),
SymptomModel(id: 'right_eye_s5', name: 'Watering', organId: 'right_eye'),
SymptomModel(id: 'right_eye_s6', name: 'Sensitivity to light', organId: 'right_eye'),
SymptomModel(id: 'right_eye_s7', name: 'Double vision', organId: 'right_eye'),
],
'nose_mouth': [
SymptomModel(id: 'nose_mouth_s1', name: 'Nasal congestion', organId: 'nose_mouth'),
SymptomModel(id: 'nose_mouth_s2', name: 'Runny nose', organId: 'nose_mouth'),
SymptomModel(id: 'nose_mouth_s3', name: 'Nosebleed', organId: 'nose_mouth'),
SymptomModel(id: 'nose_mouth_s4', name: 'Loss of smell', organId: 'nose_mouth'),
SymptomModel(id: 'nose_mouth_s5', name: 'Mouth pain', organId: 'nose_mouth'),
SymptomModel(id: 'nose_mouth_s6', name: 'Dry mouth', organId: 'nose_mouth'),
SymptomModel(id: 'nose_mouth_s7', name: 'Difficulty swallowing', organId: 'nose_mouth'),
],
// NECK & THROAT
'throat': [
SymptomModel(id: 'throat_s1', name: 'Sore throat', organId: 'throat'),
SymptomModel(id: 'throat_s2', name: 'Difficulty swallowing', organId: 'throat'),
SymptomModel(id: 'throat_s3', name: 'Hoarseness', organId: 'throat'),
SymptomModel(id: 'throat_s4', name: 'Throat pain', organId: 'throat'),
SymptomModel(id: 'throat_s5', name: 'Swollen glands', organId: 'throat'),
SymptomModel(id: 'throat_s6', name: 'Cough', organId: 'throat'),
SymptomModel(id: 'throat_s7', name: 'Difficulty breathing', organId: 'throat'),
],
// SHOULDERS
'left_shoulder': [
SymptomModel(id: 'left_shoulder_s1', name: 'Shoulder pain', organId: 'left_shoulder'),
SymptomModel(id: 'left_shoulder_s2', name: 'Stiffness', organId: 'left_shoulder'),
SymptomModel(id: 'left_shoulder_s3', name: 'Limited mobility', organId: 'left_shoulder'),
SymptomModel(id: 'left_shoulder_s4', name: 'Swelling', organId: 'left_shoulder'),
SymptomModel(id: 'left_shoulder_s5', name: 'Weakness', organId: 'left_shoulder'),
SymptomModel(id: 'left_shoulder_s6', name: 'Clicking sound', organId: 'left_shoulder'),
],
'right_shoulder': [
SymptomModel(id: 'right_shoulder_s1', name: 'Shoulder pain', organId: 'right_shoulder'),
SymptomModel(id: 'right_shoulder_s2', name: 'Stiffness', organId: 'right_shoulder'),
SymptomModel(id: 'right_shoulder_s3', name: 'Limited mobility', organId: 'right_shoulder'),
SymptomModel(id: 'right_shoulder_s4', name: 'Swelling', organId: 'right_shoulder'),
SymptomModel(id: 'right_shoulder_s5', name: 'Weakness', organId: 'right_shoulder'),
SymptomModel(id: 'right_shoulder_s6', name: 'Clicking sound', organId: 'right_shoulder'),
],
// CHEST
'left_chest': [
SymptomModel(id: 'left_chest_s1', name: 'Chest pain', organId: 'left_chest'),
SymptomModel(id: 'left_chest_s2', name: 'Tightness', organId: 'left_chest'),
SymptomModel(id: 'left_chest_s3', name: 'Pressure', organId: 'left_chest'),
SymptomModel(id: 'left_chest_s4', name: 'Sharp pain', organId: 'left_chest'),
SymptomModel(id: 'left_chest_s5', name: 'Burning sensation', organId: 'left_chest'),
SymptomModel(id: 'left_chest_s6', name: 'Palpitations', organId: 'left_chest'),
SymptomModel(id: 'left_chest_s7', name: 'Shortness of breath', organId: 'left_chest'),
],
'center_chest': [
SymptomModel(id: 'center_chest_s1', name: 'Chest pain', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s2', name: 'Tightness', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s3', name: 'Pressure', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s4', name: 'Sharp pain', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s5', name: 'Burning sensation', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s6', name: 'Heartburn', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s7', name: 'Difficulty breathing', organId: 'center_chest'),
SymptomModel(id: 'center_chest_s8', name: 'Palpitations', organId: 'center_chest'),
],
'right_chest': [
SymptomModel(id: 'right_chest_s1', name: 'Chest pain', organId: 'right_chest'),
SymptomModel(id: 'right_chest_s2', name: 'Tightness', organId: 'right_chest'),
SymptomModel(id: 'right_chest_s3', name: 'Pressure', organId: 'right_chest'),
SymptomModel(id: 'right_chest_s4', name: 'Sharp pain', organId: 'right_chest'),
SymptomModel(id: 'right_chest_s5', name: 'Burning sensation', organId: 'right_chest'),
SymptomModel(id: 'right_chest_s6', name: 'Shortness of breath', organId: 'right_chest'),
],
// RIBS
'left_ribs': [
SymptomModel(id: 'left_ribs_s1', name: 'Rib pain', organId: 'left_ribs'),
SymptomModel(id: 'left_ribs_s2', name: 'Pain when breathing', organId: 'left_ribs'),
SymptomModel(id: 'left_ribs_s3', name: 'Tenderness', organId: 'left_ribs'),
SymptomModel(id: 'left_ribs_s4', name: 'Bruising', organId: 'left_ribs'),
SymptomModel(id: 'left_ribs_s5', name: 'Difficulty moving', organId: 'left_ribs'),
],
'right_ribs': [
SymptomModel(id: 'right_ribs_s1', name: 'Rib pain', organId: 'right_ribs'),
SymptomModel(id: 'right_ribs_s2', name: 'Pain when breathing', organId: 'right_ribs'),
SymptomModel(id: 'right_ribs_s3', name: 'Tenderness', organId: 'right_ribs'),
SymptomModel(id: 'right_ribs_s4', name: 'Bruising', organId: 'right_ribs'),
SymptomModel(id: 'right_ribs_s5', name: 'Difficulty moving', organId: 'right_ribs'),
],
// ABDOMEN
'upper_abdomen': [
SymptomModel(id: 'upper_abdomen_s1', name: 'Abdominal pain', organId: 'upper_abdomen'),
SymptomModel(id: 'upper_abdomen_s2', name: 'Bloating', organId: 'upper_abdomen'),
SymptomModel(id: 'upper_abdomen_s3', name: 'Nausea', organId: 'upper_abdomen'),
SymptomModel(id: 'upper_abdomen_s4', name: 'Vomiting', organId: 'upper_abdomen'),
SymptomModel(id: 'upper_abdomen_s5', name: 'Heartburn', organId: 'upper_abdomen'),
SymptomModel(id: 'upper_abdomen_s6', name: 'Loss of appetite', organId: 'upper_abdomen'),
SymptomModel(id: 'upper_abdomen_s7', name: 'Indigestion', organId: 'upper_abdomen'),
],
'navel': [
SymptomModel(id: 'navel_s1', name: 'Abdominal pain', organId: 'navel'),
SymptomModel(id: 'navel_s2', name: 'Cramping', organId: 'navel'),
SymptomModel(id: 'navel_s3', name: 'Bloating', organId: 'navel'),
SymptomModel(id: 'navel_s4', name: 'Gas', organId: 'navel'),
SymptomModel(id: 'navel_s5', name: 'Tenderness', organId: 'navel'),
],
'lower_abdomen': [
SymptomModel(id: 'lower_abdomen_s1', name: 'Lower abdominal pain', organId: 'lower_abdomen'),
SymptomModel(id: 'lower_abdomen_s2', name: 'Cramping', organId: 'lower_abdomen'),
SymptomModel(id: 'lower_abdomen_s3', name: 'Bloating', organId: 'lower_abdomen'),
SymptomModel(id: 'lower_abdomen_s4', name: 'Constipation', organId: 'lower_abdomen'),
SymptomModel(id: 'lower_abdomen_s5', name: 'Diarrhea', organId: 'lower_abdomen'),
SymptomModel(id: 'lower_abdomen_s6', name: 'Urinary discomfort', organId: 'lower_abdomen'),
],
// PELVIS & GROIN
'left_groin': [
SymptomModel(id: 'left_groin_s1', name: 'Groin pain', organId: 'left_groin'),
SymptomModel(id: 'left_groin_s2', name: 'Swelling', organId: 'left_groin'),
SymptomModel(id: 'left_groin_s3', name: 'Tenderness', organId: 'left_groin'),
SymptomModel(id: 'left_groin_s4', name: 'Limited mobility', organId: 'left_groin'),
],
'right_groin': [
SymptomModel(id: 'right_groin_s1', name: 'Groin pain', organId: 'right_groin'),
SymptomModel(id: 'right_groin_s2', name: 'Swelling', organId: 'right_groin'),
SymptomModel(id: 'right_groin_s3', name: 'Tenderness', organId: 'right_groin'),
SymptomModel(id: 'right_groin_s4', name: 'Limited mobility', organId: 'right_groin'),
],
// ARMS - LEFT
'left_elbow': [
SymptomModel(id: 'left_elbow_s1', name: 'Elbow pain', organId: 'left_elbow'),
SymptomModel(id: 'left_elbow_s2', name: 'Stiffness', organId: 'left_elbow'),
SymptomModel(id: 'left_elbow_s3', name: 'Swelling', organId: 'left_elbow'),
SymptomModel(id: 'left_elbow_s4', name: 'Limited mobility', organId: 'left_elbow'),
SymptomModel(id: 'left_elbow_s5', name: 'Tingling', organId: 'left_elbow'),
],
'left_forearm': [
SymptomModel(id: 'left_forearm_s1', name: 'Forearm pain', organId: 'left_forearm'),
SymptomModel(id: 'left_forearm_s2', name: 'Weakness', organId: 'left_forearm'),
SymptomModel(id: 'left_forearm_s3', name: 'Numbness', organId: 'left_forearm'),
SymptomModel(id: 'left_forearm_s4', name: 'Tingling', organId: 'left_forearm'),
SymptomModel(id: 'left_forearm_s5', name: 'Swelling', organId: 'left_forearm'),
],
'left_wrist': [
SymptomModel(id: 'left_wrist_s1', name: 'Wrist pain', organId: 'left_wrist'),
SymptomModel(id: 'left_wrist_s2', name: 'Stiffness', organId: 'left_wrist'),
SymptomModel(id: 'left_wrist_s3', name: 'Swelling', organId: 'left_wrist'),
SymptomModel(id: 'left_wrist_s4', name: 'Limited mobility', organId: 'left_wrist'),
SymptomModel(id: 'left_wrist_s5', name: 'Tingling', organId: 'left_wrist'),
SymptomModel(id: 'left_wrist_s6', name: 'Numbness', organId: 'left_wrist'),
],
// ARMS - RIGHT
'right_elbow': [
SymptomModel(id: 'right_elbow_s1', name: 'Elbow pain', organId: 'right_elbow'),
SymptomModel(id: 'right_elbow_s2', name: 'Stiffness', organId: 'right_elbow'),
SymptomModel(id: 'right_elbow_s3', name: 'Swelling', organId: 'right_elbow'),
SymptomModel(id: 'right_elbow_s4', name: 'Limited mobility', organId: 'right_elbow'),
SymptomModel(id: 'right_elbow_s5', name: 'Tingling', organId: 'right_elbow'),
],
'right_forearm': [
SymptomModel(id: 'right_forearm_s1', name: 'Forearm pain', organId: 'right_forearm'),
SymptomModel(id: 'right_forearm_s2', name: 'Weakness', organId: 'right_forearm'),
SymptomModel(id: 'right_forearm_s3', name: 'Numbness', organId: 'right_forearm'),
SymptomModel(id: 'right_forearm_s4', name: 'Tingling', organId: 'right_forearm'),
SymptomModel(id: 'right_forearm_s5', name: 'Swelling', organId: 'right_forearm'),
],
'right_wrist': [
SymptomModel(id: 'right_wrist_s1', name: 'Wrist pain', organId: 'right_wrist'),
SymptomModel(id: 'right_wrist_s2', name: 'Stiffness', organId: 'right_wrist'),
SymptomModel(id: 'right_wrist_s3', name: 'Swelling', organId: 'right_wrist'),
SymptomModel(id: 'right_wrist_s4', name: 'Limited mobility', organId: 'right_wrist'),
SymptomModel(id: 'right_wrist_s5', name: 'Tingling', organId: 'right_wrist'),
SymptomModel(id: 'right_wrist_s6', name: 'Numbness', organId: 'right_wrist'),
],
// LEGS - LEFT
'left_thigh': [
SymptomModel(id: 'left_thigh_s1', name: 'Thigh pain', organId: 'left_thigh'),
SymptomModel(id: 'left_thigh_s2', name: 'Muscle cramps', organId: 'left_thigh'),
SymptomModel(id: 'left_thigh_s3', name: 'Weakness', organId: 'left_thigh'),
SymptomModel(id: 'left_thigh_s4', name: 'Numbness', organId: 'left_thigh'),
SymptomModel(id: 'left_thigh_s5', name: 'Swelling', organId: 'left_thigh'),
],
'left_knee': [
SymptomModel(id: 'left_knee_s1', name: 'Knee pain', organId: 'left_knee'),
SymptomModel(id: 'left_knee_s2', name: 'Stiffness', organId: 'left_knee'),
SymptomModel(id: 'left_knee_s3', name: 'Swelling', organId: 'left_knee'),
SymptomModel(id: 'left_knee_s4', name: 'Limited mobility', organId: 'left_knee'),
SymptomModel(id: 'left_knee_s5', name: 'Clicking sound', organId: 'left_knee'),
SymptomModel(id: 'left_knee_s6', name: 'Instability', organId: 'left_knee'),
],
'left_shin': [
SymptomModel(id: 'left_shin_s1', name: 'Shin pain', organId: 'left_shin'),
SymptomModel(id: 'left_shin_s2', name: 'Tenderness', organId: 'left_shin'),
SymptomModel(id: 'left_shin_s3', name: 'Swelling', organId: 'left_shin'),
SymptomModel(id: 'left_shin_s4', name: 'Numbness', organId: 'left_shin'),
],
'left_ankle': [
SymptomModel(id: 'left_ankle_s1', name: 'Ankle pain', organId: 'left_ankle'),
SymptomModel(id: 'left_ankle_s2', name: 'Stiffness', organId: 'left_ankle'),
SymptomModel(id: 'left_ankle_s3', name: 'Swelling', organId: 'left_ankle'),
SymptomModel(id: 'left_ankle_s4', name: 'Limited mobility', organId: 'left_ankle'),
SymptomModel(id: 'left_ankle_s5', name: 'Instability', organId: 'left_ankle'),
],
// LEGS - RIGHT
'right_thigh': [
SymptomModel(id: 'right_thigh_s1', name: 'Thigh pain', organId: 'right_thigh'),
SymptomModel(id: 'right_thigh_s2', name: 'Muscle cramps', organId: 'right_thigh'),
SymptomModel(id: 'right_thigh_s3', name: 'Weakness', organId: 'right_thigh'),
SymptomModel(id: 'right_thigh_s4', name: 'Numbness', organId: 'right_thigh'),
SymptomModel(id: 'right_thigh_s5', name: 'Swelling', organId: 'right_thigh'),
],
'right_knee': [
SymptomModel(id: 'right_knee_s1', name: 'Knee pain', organId: 'right_knee'),
SymptomModel(id: 'right_knee_s2', name: 'Stiffness', organId: 'right_knee'),
SymptomModel(id: 'right_knee_s3', name: 'Swelling', organId: 'right_knee'),
SymptomModel(id: 'right_knee_s4', name: 'Limited mobility', organId: 'right_knee'),
SymptomModel(id: 'right_knee_s5', name: 'Clicking sound', organId: 'right_knee'),
SymptomModel(id: 'right_knee_s6', name: 'Instability', organId: 'right_knee'),
],
'right_shin': [
SymptomModel(id: 'right_shin_s1', name: 'Shin pain', organId: 'right_shin'),
SymptomModel(id: 'right_shin_s2', name: 'Tenderness', organId: 'right_shin'),
SymptomModel(id: 'right_shin_s3', name: 'Swelling', organId: 'right_shin'),
SymptomModel(id: 'right_shin_s4', name: 'Numbness', organId: 'right_shin'),
],
'right_ankle': [
SymptomModel(id: 'right_ankle_s1', name: 'Ankle pain', organId: 'right_ankle'),
SymptomModel(id: 'right_ankle_s2', name: 'Stiffness', organId: 'right_ankle'),
SymptomModel(id: 'right_ankle_s3', name: 'Swelling', organId: 'right_ankle'),
SymptomModel(id: 'right_ankle_s4', name: 'Limited mobility', organId: 'right_ankle'),
SymptomModel(id: 'right_ankle_s5', name: 'Instability', organId: 'right_ankle'),
],
// BACK VIEW ORGANS
'back_head': [
SymptomModel(id: 'back_head_s1', name: 'Back of head pain', organId: 'back_head'),
SymptomModel(id: 'back_head_s2', name: 'Neck stiffness', organId: 'back_head'),
SymptomModel(id: 'back_head_s3', name: 'Tension headache', organId: 'back_head'),
],
'neck': [
SymptomModel(id: 'neck_s1', name: 'Neck pain', organId: 'neck'),
SymptomModel(id: 'neck_s2', name: 'Stiffness', organId: 'neck'),
SymptomModel(id: 'neck_s3', name: 'Limited mobility', organId: 'neck'),
SymptomModel(id: 'neck_s4', name: 'Muscle spasm', organId: 'neck'),
SymptomModel(id: 'neck_s5', name: 'Tingling', organId: 'neck'),
],
'upper_back': [
SymptomModel(id: 'upper_back_s1', name: 'Upper back pain', organId: 'upper_back'),
SymptomModel(id: 'upper_back_s2', name: 'Stiffness', organId: 'upper_back'),
SymptomModel(id: 'upper_back_s3', name: 'Muscle tension', organId: 'upper_back'),
SymptomModel(id: 'upper_back_s4', name: 'Sharp pain', organId: 'upper_back'),
SymptomModel(id: 'upper_back_s5', name: 'Difficulty breathing', organId: 'upper_back'),
],
'mid_back': [
SymptomModel(id: 'mid_back_s1', name: 'Mid back pain', organId: 'mid_back'),
SymptomModel(id: 'mid_back_s2', name: 'Stiffness', organId: 'mid_back'),
SymptomModel(id: 'mid_back_s3', name: 'Muscle spasm', organId: 'mid_back'),
SymptomModel(id: 'mid_back_s4', name: 'Tenderness', organId: 'mid_back'),
],
'lower_back': [
SymptomModel(id: 'lower_back_s1', name: 'Lower back pain', organId: 'lower_back'),
SymptomModel(id: 'lower_back_s2', name: 'Stiffness', organId: 'lower_back'),
SymptomModel(id: 'lower_back_s3', name: 'Sharp pain', organId: 'lower_back'),
SymptomModel(id: 'lower_back_s4', name: 'Sciatica', organId: 'lower_back'),
SymptomModel(id: 'lower_back_s5', name: 'Limited mobility', organId: 'lower_back'),
SymptomModel(id: 'lower_back_s6', name: 'Muscle spasm', organId: 'lower_back'),
],
};
/// Get symptoms for a specific organ ID
static List<SymptomModel> getSymptomsForOrgan(String organId) {
return organSymptomsMap[organId] ?? [];
}
/// Get symptoms for multiple organ IDs
static Map<String, List<SymptomModel>> getSymptomsForOrgans(List<String> organIds) {
Map<String, List<SymptomModel>> result = {};
for (String organId in organIds) {
List<SymptomModel> symptoms = getSymptomsForOrgan(organId);
if (symptoms.isNotEmpty) {
result[organId] = symptoms;
}
}
return result;
}
/// Check if an organ has symptoms mapped
static bool hasSymptoms(String organId) {
return organSymptomsMap.containsKey(organId) && organSymptomsMap[organId]!.isNotEmpty;
}
}

@ -0,0 +1,209 @@
import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart';
class TriageQuestionsData {
static List<TriageQuestionModel> getSampleTriageQuestions() {
return [
// Question 1: Pain Location
TriageQuestionModel(
id: 'q1',
question: 'Where do you feel the chest pain, and where does the pain spread or move to?',
options: [
TriageOptionModel(
id: 'q1_opt1',
text: 'It is all over',
severityScore: 3,
),
TriageOptionModel(
id: 'q1_opt2',
text: 'It is behind the breast bone',
severityScore: 5,
),
TriageOptionModel(
id: 'q1_opt3',
text: 'Moves or spread to the upper limb; for example the shoulder, arm, or fingers',
severityScore: 7,
),
TriageOptionModel(
id: 'q1_opt4',
text: 'Moves or spread to the upper back; between the shoulder blades',
severityScore: 6,
),
TriageOptionModel(
id: 'q1_opt5',
text: 'Moves or spread to the neck or jaw',
severityScore: 8,
),
],
),
// Question 2: Pain Intensity
TriageQuestionModel(
id: 'q2',
question: 'How would you describe the intensity of your chest pain?',
options: [
TriageOptionModel(
id: 'q2_opt1',
text: 'Mild - Barely noticeable, does not interfere with daily activities',
severityScore: 2,
),
TriageOptionModel(
id: 'q2_opt2',
text: 'Moderate - Noticeable but manageable, some interference with activities',
severityScore: 4,
),
TriageOptionModel(
id: 'q2_opt3',
text: 'Severe - Significantly interferes with activities, difficult to ignore',
severityScore: 7,
),
TriageOptionModel(
id: 'q2_opt4',
text: 'Very severe - Unbearable, cannot perform any activities',
severityScore: 9,
),
],
),
// Question 3: Pain Duration
TriageQuestionModel(
id: 'q3',
question: 'How long have you been experiencing this chest pain?',
options: [
TriageOptionModel(
id: 'q3_opt1',
text: 'Less than 5 minutes',
severityScore: 3,
),
TriageOptionModel(
id: 'q3_opt2',
text: 'Between 5 to 15 minutes',
severityScore: 5,
),
TriageOptionModel(
id: 'q3_opt3',
text: 'Between 15 to 30 minutes',
severityScore: 7,
),
TriageOptionModel(
id: 'q3_opt4',
text: 'More than 30 minutes',
severityScore: 8,
),
TriageOptionModel(
id: 'q3_opt5',
text: 'Comes and goes (intermittent)',
severityScore: 4,
),
],
),
// Question 4: Associated Symptoms
TriageQuestionModel(
id: 'q4',
question: 'Are you experiencing any of these symptoms along with chest pain?',
options: [
TriageOptionModel(
id: 'q4_opt1',
text: 'Shortness of breath or difficulty breathing',
severityScore: 8,
),
TriageOptionModel(
id: 'q4_opt2',
text: 'Sweating, nausea, or vomiting',
severityScore: 7,
),
TriageOptionModel(
id: 'q4_opt3',
text: 'Dizziness or lightheadedness',
severityScore: 7,
),
TriageOptionModel(
id: 'q4_opt4',
text: 'Rapid or irregular heartbeat',
severityScore: 6,
),
TriageOptionModel(
id: 'q4_opt5',
text: 'None of the above',
severityScore: 2,
),
],
),
// Question 5: Triggering Factors
TriageQuestionModel(
id: 'q5',
question: 'What triggers or worsens your chest pain?',
options: [
TriageOptionModel(
id: 'q5_opt1',
text: 'Physical activity or exertion',
severityScore: 6,
),
TriageOptionModel(
id: 'q5_opt2',
text: 'Emotional stress or anxiety',
severityScore: 4,
),
TriageOptionModel(
id: 'q5_opt3',
text: 'Deep breathing or coughing',
severityScore: 3,
),
TriageOptionModel(
id: 'q5_opt4',
text: 'Eating or lying down',
severityScore: 3,
),
TriageOptionModel(
id: 'q5_opt5',
text: 'Nothing specific, pain is constant',
severityScore: 7,
),
],
),
];
}
/// Calculate total severity score from answered questions
static int calculateTotalScore(List<TriageQuestionModel> questions) {
int totalScore = 0;
int answeredCount = 0;
for (var question in questions) {
if (question.isConfirmed && question.confirmedOption != null) {
totalScore += question.confirmedOption!.severityScore ?? 0;
answeredCount++;
}
}
// Return average score or 0 if no questions answered
return answeredCount > 0 ? (totalScore / answeredCount * 10).round() : 0;
}
/// Get risk level based on score
static String getRiskLevel(int score) {
if (score >= 70) {
return 'High Risk - Seek immediate medical attention';
} else if (score >= 50) {
return 'Moderate Risk - Consult a doctor soon';
} else if (score >= 30) {
return 'Low to Moderate Risk - Monitor symptoms';
} else {
return 'Low Risk - Self-care may be sufficient';
}
}
/// Get suggested condition based on score
static String getSuggestedCondition(int score) {
if (score >= 70) {
return 'Acute Coronary Syndrome';
} else if (score >= 50) {
return 'Angina or Cardiac concern';
} else if (score >= 30) {
return 'Non-cardiac chest pain';
} else {
return 'Musculoskeletal chest pain';
}
}
}

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
class ConditionsModel {
final IconData icon;
final String title;
final int percentage;
final String tagText;
final String clinic;
final List<String> symptoms;
final String description;
final String? monitorNote;
final String? appointmentLabel;
final PossibleConditionsSeverityEnum possibleConditionsSeverityEnum;
ConditionsModel({
required this.icon,
required this.title,
required this.percentage,
required this.tagText,
required this.clinic,
required this.symptoms,
required this.description,
required this.possibleConditionsSeverityEnum,
this.monitorNote,
this.appointmentLabel,
});
}
List<ConditionsModel> dummyConditions = [
ConditionsModel(
icon: Icons.psychology_alt,
possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.seekMedicalAdvice,
title: "Migraine",
percentage: 87,
tagText: "Seek Medical Advice",
clinic: "Internal Medicine Clinic",
symptoms: ["Headache", "Nausea", "Sensitivity to light"],
description: "A migraine is a type of headache that can cause severe throbbing pain, usually on one side of the head.",
appointmentLabel: "Book Appointment",
),
ConditionsModel(
icon: Icons.deblur,
title: "Tension Headache",
percentage: 37,
tagText: "Monitor",
monitorNote: "No need to visit doctor",
clinic: "GP Clinic",
symptoms: ["Mild head pressure", "Scalp tenderness"],
description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.",
possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.monitorOnly,
),
ConditionsModel(
icon: Icons.medication_liquid,
title: "Meningitis",
percentage: 28,
tagText: "Emergency",
clinic: "Neurology Clinic",
symptoms: ["Mild head pressure", "Scalp tenderness"],
description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.",
appointmentLabel: "Book Appointment",
possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.emergency,
),
ConditionsModel(
icon: Icons.psychology_alt,
title: "Migraine",
percentage: 87,
tagText: "Seek Medical Advice",
clinic: "Internal Medicine Clinic",
symptoms: ["Headache", "Nausea", "Sensitivity to light"],
description: "A migraine is a type of headache that can cause severe throbbing pain, usually on one side of the head.",
appointmentLabel: "Book Appointment",
possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.seekMedicalAdvice,
),
ConditionsModel(
icon: Icons.deblur,
title: "Tension Headache",
percentage: 37,
tagText: "Monitor",
monitorNote: "No need to visit doctor",
clinic: "GP Clinic",
symptoms: ["Mild head pressure", "Scalp tenderness"],
description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.",
possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.monitorOnly,
),
ConditionsModel(
icon: Icons.medication_liquid,
title: "Meningitis",
percentage: 28,
tagText: "Emergency",
clinic: "Neurology Clinic",
symptoms: ["Mild head pressure", "Scalp tenderness"],
description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.",
appointmentLabel: "Book Appointment",
possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.emergency,
),
];

@ -0,0 +1,43 @@
import 'package:hmg_patient_app_new/core/enums.dart';
class OrganModel {
final String id;
final String name;
final String description;
final BodyView bodyView;
final OrganPosition position;
const OrganModel({
required this.id,
required this.name,
required this.description,
required this.bodyView,
required this.position,
});
OrganModel copyWith({
String? id,
String? name,
String? description,
BodyView? bodyView,
OrganPosition? position,
}) {
return OrganModel(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
bodyView: bodyView ?? this.bodyView,
position: position ?? this.position,
);
}
}
class OrganPosition {
final double x; // Percentage from left (0.0 to 1.0)
final double y; // Percentage from top (0.0 to 1.0)
const OrganPosition({
required this.x,
required this.y,
});
}

@ -0,0 +1,151 @@
class BodySymptomResponseModel {
final DataDetails? dataDetails;
BodySymptomResponseModel({
this.dataDetails,
});
factory BodySymptomResponseModel.fromJson(Map<String, dynamic> json) {
return BodySymptomResponseModel(
dataDetails: json['dataDetails'] != null ? DataDetails.fromJson(json['dataDetails']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class DataDetails {
final List<OrganSymptomResult>? result;
final int? id;
final dynamic exception;
final int? status;
final bool? isCanceled;
final bool? isCompleted;
final bool? isCompletedSuccessfully;
final int? creationOptions;
final dynamic asyncState;
final bool? isFaulted;
DataDetails({
this.result,
this.id,
this.exception,
this.status,
this.isCanceled,
this.isCompleted,
this.isCompletedSuccessfully,
this.creationOptions,
this.asyncState,
this.isFaulted,
});
factory DataDetails.fromJson(Map<String, dynamic> json) {
return DataDetails(
result: json['Result'] != null ? (json['Result'] as List).map((item) => OrganSymptomResult.fromJson(item)).toList() : null,
id: json['Id'],
exception: json['Exception'],
status: json['Status'],
isCanceled: json['IsCanceled'],
isCompleted: json['IsCompleted'],
isCompletedSuccessfully: json['IsCompletedSuccessfully'],
creationOptions: json['CreationOptions'],
asyncState: json['AsyncState'],
isFaulted: json['IsFaulted'],
);
}
Map<String, dynamic> toJson() {
return {
'Result': result?.map((item) => item.toJson()).toList(),
'Id': id,
'Exception': exception,
'Status': status,
'IsCanceled': isCanceled,
'IsCompleted': isCompleted,
'IsCompletedSuccessfully': isCompletedSuccessfully,
'CreationOptions': creationOptions,
'AsyncState': asyncState,
'IsFaulted': isFaulted,
};
}
}
class OrganSymptomResult {
final String? name;
final List<BodySymptom>? bodySymptoms;
OrganSymptomResult({
this.name,
this.bodySymptoms,
});
factory OrganSymptomResult.fromJson(Map<String, dynamic> json) {
return OrganSymptomResult(
name: json['name'],
bodySymptoms: json['bodySymptoms'] != null ? (json['bodySymptoms'] as List).map((item) => BodySymptom.fromJson(item)).toList() : null,
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'bodySymptoms': bodySymptoms?.map((item) => item.toJson()).toList(),
};
}
}
class BodySymptom {
final String? id;
final String? type;
final String? symptomsName;
final String? name;
final String? commonName;
final String? nameAr;
final String? commonNameAr;
BodySymptom({
this.id,
this.type,
this.symptomsName,
this.name,
this.commonName,
this.nameAr,
this.commonNameAr,
});
factory BodySymptom.fromJson(Map<String, dynamic> json) {
return BodySymptom(
id: json['Id'],
type: json['type'],
symptomsName: json['symptoms_name'],
name: json['name'],
commonName: json['common_name'],
nameAr: json['nameAr'],
commonNameAr: json['common_nameAr'],
);
}
Map<String, dynamic> toJson() {
return {
'Id': id,
'type': type,
'symptoms_name': symptomsName,
'name': name,
'common_name': commonName,
'nameAr': nameAr,
'common_nameAr': commonNameAr,
};
}
// Helper method to get display name based on locale
String getDisplayName(bool isArabic) {
if (isArabic) {
return commonNameAr ?? nameAr ?? commonName ?? name ?? '';
}
return commonName ?? name ?? '';
}
}

@ -0,0 +1,105 @@
class SymptomModel {
final String id;
final String name;
final String nameAr;
final String? organId;
const SymptomModel({
required this.id,
required this.name,
this.nameAr = '',
this.organId,
});
SymptomModel copyWith({
String? id,
String? name,
String? nameAr,
String? organId,
}) {
return SymptomModel(
id: id ?? this.id,
name: name ?? this.name,
nameAr: nameAr ?? this.nameAr,
organId: organId ?? this.organId,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'nameAr': nameAr,
'organId': organId,
};
}
factory SymptomModel.fromJson(Map<String, dynamic> json) {
return SymptomModel(
id: json['id'] ?? '',
name: json['name'] ?? '',
nameAr: json['nameAr'] ?? '',
organId: json['organId'],
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SymptomModel && other.id == id;
}
@override
int get hashCode => id.hashCode;
@override
String toString() {
return 'SymptomModel(id: $id, name: $name, nameAr: $nameAr, organId: $organId)';
}
}
/// Class to group symptoms by organ
class OrganSymptomsGroup {
final String organId;
final String organName;
final List<SymptomModel> symptoms;
final Set<String> selectedSymptomIds;
OrganSymptomsGroup({
required this.organId,
required this.organName,
required this.symptoms,
Set<String>? selectedSymptomIds,
}) : selectedSymptomIds = selectedSymptomIds ?? {};
/// Get count of selected symptoms
int get selectedCount => selectedSymptomIds.length;
/// Check if any symptoms are selected
bool get hasSelectedSymptoms => selectedSymptomIds.isNotEmpty;
/// Get list of selected symptoms
List<SymptomModel> get selectedSymptoms {
return symptoms.where((symptom) => selectedSymptomIds.contains(symptom.id)).toList();
}
OrganSymptomsGroup copyWith({
String? organId,
String? organName,
List<SymptomModel>? symptoms,
Set<String>? selectedSymptomIds,
}) {
return OrganSymptomsGroup(
organId: organId ?? this.organId,
organName: organName ?? this.organName,
symptoms: symptoms ?? this.symptoms,
selectedSymptomIds: selectedSymptomIds ?? this.selectedSymptomIds,
);
}
@override
String toString() {
return 'OrganSymptomsGroup(organId: $organId, organName: $organName, symptomsCount: ${symptoms.length}, selectedCount: $selectedCount)';
}
}

@ -0,0 +1,85 @@
class TriageQuestionModel {
final String id;
final String question;
final List<TriageOptionModel> options;
int? selectedOptionIndex;
int? confirmedOptionIndex; // Confirmed answer when user presses Next
TriageQuestionModel({
required this.id,
required this.question,
required this.options,
this.selectedOptionIndex,
this.confirmedOptionIndex,
});
bool get isAnswered => selectedOptionIndex != null;
bool get isConfirmed => confirmedOptionIndex != null;
void selectOption(int index) {
selectedOptionIndex = index;
}
void confirmSelection() {
confirmedOptionIndex = selectedOptionIndex;
}
void clearSelection() {
selectedOptionIndex = null;
}
TriageOptionModel? get selectedOption {
if (selectedOptionIndex != null && selectedOptionIndex! < options.length) {
return options[selectedOptionIndex!];
}
return null;
}
TriageOptionModel? get confirmedOption {
if (confirmedOptionIndex != null && confirmedOptionIndex! < options.length) {
return options[confirmedOptionIndex!];
}
return null;
}
TriageQuestionModel copyWith({
String? id,
String? question,
List<TriageOptionModel>? options,
int? selectedOptionIndex,
int? confirmedOptionIndex,
}) {
return TriageQuestionModel(
id: id ?? this.id,
question: question ?? this.question,
options: options ?? this.options,
selectedOptionIndex: selectedOptionIndex ?? this.selectedOptionIndex,
confirmedOptionIndex: confirmedOptionIndex ?? this.confirmedOptionIndex,
);
}
}
class TriageOptionModel {
final String id;
final String text;
final int? severityScore; // Optional: for calculating risk scores
TriageOptionModel({
required this.id,
required this.text,
this.severityScore,
});
TriageOptionModel copyWith({
String? id,
String? text,
int? severityScore,
}) {
return TriageOptionModel(
id: id ?? this.id,
text: text ?? this.text,
severityScore: severityScore ?? this.severityScore,
);
}
}

@ -0,0 +1,86 @@
import 'dart:convert';
import 'dart:developer';
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/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:http/http.dart' as http;
abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
required List<String> organNames,
});
}
class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
final ApiClient apiClient;
final LoggerService loggerService;
SymptomsCheckerRepoImp({
required this.apiClient,
required this.loggerService,
});
@override
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
required List<String> organNames,
}) async {
try {
// API expects a direct JSON array: ["mid_abdomen", "chest"]
// Not an object like: {"organNames": [...]}
// Since ApiClient.post expects Map<String, dynamic> and encodes it as object,
// we make direct HTTP call here to send array body
final String requestBody = jsonEncode(organNames);
loggerService.logInfo("GetBodySymptomsByName Request: $requestBody");
log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}");
log("GetBodySymptomsByName Request Body: $requestBody");
// Make direct HTTP POST request with JSON array body
final response = await http.post(
Uri.parse(ApiConsts.getBodySymptomsByName),
headers: {'Content-Type': 'application/json', 'Accept': 'text/plain'},
body: requestBody,
);
final int statusCode = response.statusCode;
log("GetBodySymptomsByName Response Status: $statusCode");
loggerService.logInfo("GetBodySymptomsByName Response Status: $statusCode");
try {
// Parse the response
final responseBody = jsonDecode(response.body);
loggerService.logInfo("GetBodySymptomsByName API Success: $responseBody");
log("GetBodySymptomsByName Response: $responseBody");
BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(responseBody);
GenericApiModel<BodySymptomResponseModel> apiResponse = GenericApiModel<BodySymptomResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: bodySymptomResponse,
);
return Right(apiResponse);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetBodySymptomsByName response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
return Left(DataParsingFailure(e.toString()));
}
} catch (e, stackTrace) {
loggerService.logError("Exception in getBodySymptomsByName: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
}

@ -0,0 +1,424 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping_data.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class SymptomsCheckerViewModel extends ChangeNotifier {
final SymptomsCheckerRepo symptomsCheckerRepo;
final ErrorHandlerService errorHandlerService;
SymptomsCheckerViewModel({
required this.symptomsCheckerRepo,
required this.errorHandlerService,
});
// State variables
bool isBodyHidden = false;
BodyView _currentView = BodyView.front;
final Set<String> _selectedOrganIds = {};
bool _isBottomSheetExpanded = false;
// Tooltip state
String? _tooltipOrganId;
Timer? _tooltipTimer;
// API loading states
bool isBodySymptomsLoading = false;
// API data storage - using API models directly
BodySymptomResponseModel? bodySymptomResponse;
// Selected symptoms tracking (organId -> Set of symptom IDs)
final Map<String, Set<String>> _selectedSymptomsByOrgan = {};
// User Info Flow State
int _userInfoCurrentPage = 0;
bool _isSinglePageEditMode = false; // Track if editing single page or full flow
String? _selectedGender;
DateTime? _dateOfBirth;
int? _selectedAge;
double _selectedHeight = 170;
bool _isHeightCm = true;
double _selectedWeight = 60;
bool _isWeightKg = true;
// Getters
bool isPossibleConditionsLoading = false;
// User Info Getters
int get userInfoCurrentPage => _userInfoCurrentPage;
bool get isSinglePageEditMode => _isSinglePageEditMode;
String? get selectedGender => _selectedGender;
DateTime? get dateOfBirth => _dateOfBirth;
int? get selectedAge => _selectedAge;
double? get selectedHeight => _selectedHeight;
bool get isHeightCm => _isHeightCm;
double? get selectedWeight => _selectedWeight;
bool get isWeightKg => _isWeightKg;
BodyView get currentView => _currentView;
Set<String> get selectedOrganIds => _selectedOrganIds;
bool get isBottomSheetExpanded => _isBottomSheetExpanded;
String? get tooltipOrganId => _tooltipOrganId;
/// Get organs for current view
List<OrganModel> get currentOrgans => OrganData.getOrgansForView(_currentView);
/// Get all selected organs from both views
List<OrganModel> get selectedOrgans {
final allOrgans = [
...OrganData.frontViewOrgans,
...OrganData.backViewOrgans,
];
return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList();
}
/// Check if any organs are selected
bool get hasSelectedOrgans => _selectedOrganIds.isNotEmpty;
/// Get count of selected organs
int get selectedOrgansCount => _selectedOrganIds.length;
List<OrganSymptomResult> get organSymptomsResults {
if (bodySymptomResponse?.dataDetails?.result == null) {
return [];
}
return bodySymptomResponse!.dataDetails!.result ?? [];
}
int get totalSelectedSymptomsCount {
return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length);
}
bool get hasSelectedSymptoms {
return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty);
}
void toggleView() {
_currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front;
notifyListeners();
}
toggleIsBodyHidden() {
isBodyHidden = !isBodyHidden;
notifyListeners();
}
void toggleOrganSelection(String organId) {
if (_selectedOrganIds.contains(organId)) {
_selectedOrganIds.remove(organId);
} else {
_selectedOrganIds.add(organId);
}
// Show tooltip
_showTooltip(organId);
notifyListeners();
}
void _showTooltip(String organId) {
_tooltipTimer?.cancel();
_tooltipOrganId = organId;
notifyListeners();
// Hide tooltip after 2 seconds
_tooltipTimer = Timer(const Duration(seconds: 1), () {
_tooltipOrganId = null;
notifyListeners();
});
}
/// Hide tooltip immediately
void hideTooltip() {
_tooltipTimer?.cancel();
_tooltipOrganId = null;
notifyListeners();
}
void removeOrgan(String organId) {
_selectedOrganIds.remove(organId);
notifyListeners();
}
void clearAllSelections() {
_selectedOrganIds.clear();
notifyListeners();
}
void toggleBottomSheet() {
_isBottomSheetExpanded = !_isBottomSheetExpanded;
notifyListeners();
}
void setBottomSheetExpanded(bool isExpanded) {
_isBottomSheetExpanded = isExpanded;
notifyListeners();
}
bool validateSelection() {
return _selectedOrganIds.isNotEmpty;
}
List<String> getSelectedOrganIds() {
return _selectedOrganIds.toList();
}
List<String> getSelectedOrganNames() {
return selectedOrgans.map((organ) => organ.description).toList();
}
Future<void> initializeSymptomGroups({
Function()? onSuccess,
Function(String)? onError,
}) async {
if (_selectedOrganIds.isEmpty) {
if (onError != null) {
onError('No organs selected');
}
return;
}
List<String> organNames = selectedOrgans.map((organ) => organ.name).toList();
await getBodySymptomsByName(
organNames: organNames,
onSuccess: (response) {
if (onSuccess != null) {
onSuccess();
}
},
onError: (error) {
if (onError != null) {
onError(error);
}
},
);
}
void toggleSymptomSelection(String organId, String symptomId) {
if (!_selectedSymptomsByOrgan.containsKey(organId)) {
_selectedSymptomsByOrgan[organId] = {};
}
if (_selectedSymptomsByOrgan[organId]!.contains(symptomId)) {
_selectedSymptomsByOrgan[organId]!.remove(symptomId);
} else {
_selectedSymptomsByOrgan[organId]!.add(symptomId);
}
notifyListeners();
}
bool isSymptomSelected(String organId, String symptomId) {
return _selectedSymptomsByOrgan[organId]?.contains(symptomId) ?? false;
}
List<BodySymptom> getAllSelectedSymptoms() {
List<BodySymptom> allSymptoms = [];
if (bodySymptomResponse?.dataDetails?.result == null) {
return allSymptoms;
}
for (var organResult in bodySymptomResponse!.dataDetails!.result!) {
String? matchingOrganId;
for (var organ in selectedOrgans) {
if (organ.name == organResult.name) {
matchingOrganId = organ.id;
break;
}
}
if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) {
final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!;
if (organResult.bodySymptoms != null) {
for (var symptom in organResult.bodySymptoms!) {
if (symptom.id != null && selectedIds.contains(symptom.id)) {
allSymptoms.add(symptom);
}
}
}
}
}
return allSymptoms;
}
void clearAllSymptomSelections() {
_selectedSymptomsByOrgan.clear();
notifyListeners();
}
void reset() {
_currentView = BodyView.front;
_selectedOrganIds.clear();
_selectedSymptomsByOrgan.clear();
bodySymptomResponse = null;
_isBottomSheetExpanded = false;
_tooltipTimer?.cancel();
_tooltipOrganId = null;
// Reset user info flow
_userInfoCurrentPage = 0;
_isSinglePageEditMode = false;
_selectedGender = null;
_dateOfBirth = null;
_selectedAge = null;
_selectedHeight = 170;
_isHeightCm = true;
_selectedWeight = 60;
_isWeightKg = true;
notifyListeners();
}
// User Info Flow Methods
/// Set current page in user info flow
void setUserInfoPage(int page, {bool isSinglePageEdit = false}) {
_userInfoCurrentPage = page;
_isSinglePageEditMode = isSinglePageEdit;
notifyListeners();
}
/// Set single page edit mode
void setSinglePageEditMode(bool isSingleEdit) {
_isSinglePageEditMode = isSingleEdit;
notifyListeners();
}
/// Navigate to next page in user info flow
void nextUserInfoPage() {
if (_userInfoCurrentPage < 3) {
_userInfoCurrentPage++;
notifyListeners();
}
}
/// Navigate to previous page in user info flow
void previousUserInfoPage() {
if (_userInfoCurrentPage > 0) {
_userInfoCurrentPage--;
notifyListeners();
}
}
/// Set selected gender
void setGender(String gender) {
_selectedGender = gender;
notifyListeners();
}
/// Set selected age
void setAge(int age) {
_selectedAge = age;
notifyListeners();
}
/// Set date of birth
void setDateOfBirth(DateTime dateOfBirth) {
_dateOfBirth = dateOfBirth;
// Calculate age from date of birth
final now = DateTime.now();
int age = now.year - dateOfBirth.year;
if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) {
age--;
}
_selectedAge = age;
notifyListeners();
}
/// Set selected height
void setHeight(double height, bool isCm) {
_selectedHeight = height;
_isHeightCm = isCm;
notifyListeners();
}
/// Set selected weight
void setWeight(double weight, bool isKg) {
_selectedWeight = weight;
_isWeightKg = isKg;
notifyListeners();
}
/// Check if user info page is last
bool get isUserInfoLastPage => _userInfoCurrentPage == 3;
bool get isUserInfoFirstPage => _userInfoCurrentPage == 0;
/// Validate and submit user info
Map<String, dynamic> getUserInfoData() {
return {
'gender': _selectedGender,
'dateOfBirth': _dateOfBirth?.toIso8601String(),
'age': _selectedAge,
'height': _selectedHeight,
'heightUnit': _isHeightCm ? 'cm' : 'ft',
'weight': _selectedWeight,
'weightUnit': _isWeightKg ? 'kg' : 'lbs',
};
}
Future<void> getBodySymptomsByName({
required List<String> organNames,
Function(BodySymptomResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isBodySymptomsLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getBodySymptomsByName(
organNames: organNames,
);
result.fold(
(failure) async {
isBodySymptomsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isBodySymptomsLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
bodySymptomResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms');
}
}
},
);
}
@override
void dispose() {
_tooltipTimer?.cancel();
super.dispose();
}
}

@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
@ -147,6 +148,9 @@ void main() async {
ChangeNotifierProvider<HmgServicesViewModel>(
create: (_) => getIt.get<HmgServicesViewModel>(),
),
ChangeNotifierProvider<SymptomsCheckerViewModel>(
create: (_) => getIt.get<SymptomsCheckerViewModel>(),
),
ChangeNotifierProvider<BloodDonationViewModel>(
create: (_) => getIt.get<BloodDonationViewModel>(),
)

@ -5,7 +5,7 @@ import 'dart:io';
import 'package:device_calendar/device_calendar.dart';
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/api_consts.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/utils/calender_utils_new.dart';
@ -31,11 +31,7 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointmen
import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_card.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart';
import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -221,7 +217,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
borderRadius: BorderRadius.circular(24.r),
// Todo: what is this???? Api Key??? 😲
child: Image.network(
"https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng",
"https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
fit: BoxFit.contain,
),
),
@ -343,17 +339,17 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
)
// : SizedBox.shrink()
: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 16.h,
mainAxisSpacing: 16.w,
mainAxisExtent: 115.h,
),
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
MedicalFileCard(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 16.h,
mainAxisSpacing: 16.w,
mainAxisExtent: 115.h,
),
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
MedicalFileCard(
label: LocaleKeys.labResults.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
@ -442,13 +438,15 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
);
Navigator.of(context).push(
CustomPageRoute(
page: PrescriptionDetailPage(isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel),
page: PrescriptionDetailPage(
isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel),
),
);
} else {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "You don't have any prescriptions for this appointment.".needTranslation),
child: Utils.getErrorWidget(
loadingText: "You don't have any prescriptions for this appointment.".needTranslation),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
@ -469,11 +467,11 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
// Navigator.of(context).push(
// CustomPageRoute(
// page: VaccineListPage(),
// ),
// );
}),
],
),
// ),
// );
}),
],
),
// Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [

@ -149,7 +149,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
],
).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
@ -191,7 +191,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamara_en, width: 72.h, height: 25.h)
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
"Tamara"

@ -187,7 +187,7 @@ class AppointmentCard extends StatelessWidget {
).toShimmer2(isShow: isLoading),
AppCustomChipWidget(
isIconPNG: true,
icon: getIt.get<AppState>().getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
icon: getIt.get<AppState>().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
iconSize: 18.h,
labelText: isLoading ? 'Cardiology' : "Patient: ${getIt.get<AppState>().getAuthenticatedUser()!.firstName!}",
).toShimmer2(isShow: isLoading),

@ -1,7 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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';

@ -4,11 +4,11 @@ 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/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_config.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/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';
@ -25,7 +25,6 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immed
import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -71,145 +70,137 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
regionalViewModel = Provider.of<AppointmentViaRegionViewmodel>(context, listen: true);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.bookAppo.tr(context: context),
isLeading: true,
leadingCallback: () {
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
},
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),
SizedBox(height: 24.h),
"Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h),
SizedBox(height: 16.h),
Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
return myAppointmentsVM.isPatientMyDoctorsLoading
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.bookAppo.tr(context: context),
isLeading: true,
child: SingleChildScrollView(
child: Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 64.w,
height: 64.h,
fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: true, radius: 50.r),
SizedBox(height: 8.h),
("Dr. John Smith Smith Smith")
.toString()
.toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2)
.toShimmer2(isShow: true),
],
)
: myAppointmentsVM.patientMyDoctorsList.isEmpty
? Container(
width: SizeConfig.screenWidth,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.r,
hasShadow: false,
),
child: Utils.getNoDataWidget(
context,
noDataText: "You don't have any completed visits yet".needTranslation,
isSmallWidget: true,
width: 62.w,
height: 62.h,
),
).paddingSymmetrical(24.w, 0.h)
: SizedBox(
height: 110.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientMyDoctorsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 24.w, right: 24.w),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SizedBox(
// width: 80.w,
child: Column(
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),
SizedBox(height: 24.h),
if (appState.isAuthenticated) ...[
Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
return myAppointmentsVM.isPatientMyDoctorsLoading
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 64.w,
height: 64.h,
fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: false, radius: 50.r),
).circle(100).toShimmer2(isShow: true, radius: 50.r),
SizedBox(height: 8.h),
SizedBox(
width: 80.w,
child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName)
.toString()
.toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2)
.toShimmer2(isShow: false),
),
("Dr. John Smith Smith Smith")
.toString()
.toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2)
.toShimmer2(isShow: true),
],
),
).onPress(() async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID,
projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID,
doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID,
));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
),
)
: myAppointmentsVM.patientMyDoctorsList.isEmpty
? SizedBox()
: Column(
children: [
if (appState.isAuthenticated) ...[],
"Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h),
SizedBox(height: 16.h),
SizedBox(
height: 110.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientMyDoctorsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 24.w, right: 24.w),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SizedBox(
// width: 80.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
width: 64.w,
height: 64.h,
fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: false, radius: 50.r),
SizedBox(height: 8.h),
SizedBox(
width: 80.w,
child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName)
.toString()
.toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2)
.toShimmer2(isShow: false),
),
],
),
).onPress(() async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID,
projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID,
doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID,
));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
),
),
],
);
}),
],
],
);
}),
],
);
}),
),
),
),
),
_buildSymptomsBottomCard(),
],
),
);
}
@ -460,6 +451,37 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
return Container();
}
Widget _buildSymptomsBottomCard() {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Row(
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Not sure? help me choose a clinic!".needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor),
SizedBox(height: 4.h),
"Mention your symptoms and find the list of doctors accordingly".needTranslation.toText12(
fontWeight: FontWeight.w500,
color: AppColors.greyTextColor,
),
],
),
),
SizedBox(width: 16.w),
CustomButton(
height: 40.h,
text: "",
onPressed: () => context.navigateWithName(AppRoutes.userInfoSelection),
icon: AppAssets.arrow_forward,
)
],
).paddingAll(24.w),
);
}
void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) {
regionalViewModel.flush();
regionalViewModel.setBottomSheetType(type);

@ -64,7 +64,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
child: Row(
children: [
Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h,
height: 52.h,
),

@ -147,7 +147,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -186,7 +186,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamara_en, width: 72.h, height: 25.h),
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
"Tamara".needTranslation.toText16(isBold: true),
],

@ -147,7 +147,7 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
child: Row(
children: [
Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h,
height: 52.h,
),

@ -152,7 +152,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -191,7 +191,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamara_en, width: 72.h, height: 25.h),
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
"Tamara".needTranslation.toText16(isBold: true),
],
@ -244,7 +244,10 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Insurance expired or inactive".needTranslation.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500).paddingSymmetrical(24.h, 0.h),
"Insurance expired or inactive"
.needTranslation
.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500)
.paddingSymmetrical(24.h, 0.h),
CustomButton(
text: LocaleKeys.updateInsurance.tr(context: context),
onPressed: () {
@ -274,7 +277,10 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Amount before tax".needTranslation.toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13,
Utils.getPaymentAmountWithSymbol(
bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientShare!.toString().toText16(isBold: true),
AppColors.blackColor,
13,
isSaudiCurrency: true),
],
).paddingSymmetrical(24.h, 0.h),
@ -283,7 +289,9 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
children: [
"VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor),
Utils.getPaymentAmountWithSymbol(
bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientTaxAmount!.toString().toText14(isBold: true, color: AppColors.greyTextColor),
bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientTaxAmount!
.toString()
.toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor,
13,
isSaudiCurrency: true),
@ -295,7 +303,9 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
children: [
"".needTranslation.toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(
bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString().toText24(isBold: true), AppColors.blackColor, 17,
bookAppointmentsVM.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString().toText24(isBold: true),
AppColors.blackColor,
17,
isSaudiCurrency: true),
],
).paddingSymmetrical(24.h, 0.h),
@ -374,8 +384,10 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
onSuccess: (apiResponse) async {
if (apiResponse.data["status"].toString().toLowerCase() == "success") {
tamaraOrderID = apiResponse.data["tamara_order_id"].toString();
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
await payfortViewModel.markAppointmentAsTamaraPaid(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, appointmentNo: DateTime.now().millisecondsSinceEpoch);
await payfortViewModel.updateTamaraRequestStatus(
responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
await payfortViewModel.markAppointmentAsTamaraPaid(
projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, appointmentNo: DateTime.now().millisecondsSinceEpoch);
// await myAppointmentsViewModel.addAdvanceNumberRequest(
// advanceNumber: "Tamara-Advance-0000",
// paymentReference: tamaraOrderID,
@ -418,7 +430,8 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
// }
// });
} else {
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
await payfortViewModel.updateTamaraRequestStatus(
responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
@ -519,7 +532,13 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
startApplePay() async {
showCommonBottomSheet(context,
child: Utils.getLoadingWidget(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.3, isCloseButtonVisible: false, isDismissible: false, isFullScreen: false);
child: Utils.getLoadingWidget(),
callBackFunc: (str) {},
title: "",
height: ResponsiveExtension.screenHeight * 0.3,
isCloseButtonVisible: false,
isDismissible: false,
isFullScreen: false);
transID = Utils.getAppointmentTransID(
bookAppointmentsViewModel.selectedDoctor.projectID!,
bookAppointmentsViewModel.selectedDoctor.clinicID!,
@ -529,7 +548,9 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest();
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 2);
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(),
projectId: bookAppointmentsViewModel.selectedDoctor.projectID!,
integrationId: 2);
applePayInsertRequest.clientRequestID = transID;
applePayInsertRequest.clinicID = bookAppointmentsViewModel.selectedDoctor.clinicID!;
@ -621,7 +642,8 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
checkInType: 2,
onSuccess: (apiResponse) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: apiResponse.data["SuccessMsg"]), callBackFunc: () {
showCommonBottomSheetWithoutHeight(context,
title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: apiResponse.data["SuccessMsg"]), callBackFunc: () {
Navigator.of(context).pop();
Navigator.pushAndRemoveUntil(
context,
@ -633,7 +655,8 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
},
onError: (error) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, child: Utils.getErrorWidget(loadingText: error), callBackFunc: () {
showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, child: Utils.getErrorWidget(loadingText: error),
callBackFunc: () {
Navigator.of(context).pop();
}, isFullScreen: false);
},

@ -2,7 +2,6 @@ import 'dart:developer';
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';
@ -22,6 +21,7 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/map/location_map_widget.dart';
import 'package:maps_launcher/maps_launcher.dart';
import 'package:provider/provider.dart';
@ -155,100 +155,26 @@ class _CmcSelectionReviewPageState extends State<CmcSelectionReviewPage> {
}
Widget _buildHospitalMap(HospitalsModel selectedHospital) {
final String lat = selectedHospital.latitude ?? "0.0";
final String lng = selectedHospital.longitude ?? "0.0";
final String lat = ((selectedHospital.latitude == null || selectedHospital.latitude!.isEmpty) ? "0.0" : selectedHospital.latitude) ?? "0.0";
final String lng = ((selectedHospital.longitude == null || selectedHospital.longitude!.isEmpty) ? "0.0" : selectedHospital.longitude) ?? "0.0";
log("selectedHospital: $lng and $lat");
if (lat == "0.0" || lng == "0.0") return SizedBox.shrink();
final String staticMapUrl =
"https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7C$lat,$lng&key=AIzaSyCyDbWUM9d_sBUGIE8PcuShzPaqO08NSC8";
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Image.network(
staticMapUrl,
height: 200.h,
width: double.infinity,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 200.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 200.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: Icon(
Icons.error_outline,
size: 48.h,
color: AppColors.greyTextColor,
),
),
);
},
),
),
Positioned(
bottom: 12.h,
right: 12.w,
child: InkWell(
onTap: () => _launchDirections(selectedHospital),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(1000.r),
boxShadow: [
BoxShadow(
color: Color.fromARGB(26, 0, 0, 0),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.directions_icon,
width: 16.w,
height: 16.h,
),
SizedBox(width: 6.w),
Text(
"Get Directions".needTranslation,
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
color: AppColors.blackColor,
letterSpacing: -0.4,
),
),
],
),
),
),
),
],
final appState = getIt.get<AppState>();
final isArabic = appState.isArabic();
final hospitalName = isArabic ? (selectedHospital.nameN ?? selectedHospital.name ?? '') : (selectedHospital.name ?? '');
return LocationMapWidget(
latitude: double.parse(lat),
longitude: double.parse(lng),
address: hospitalName,
title: "Hospital Location".needTranslation,
showTitle: false,
showAddress: false,
padding: EdgeInsets.zero,
onDirectionsTap: () => _launchDirections(selectedHospital),
);
}

@ -131,8 +131,8 @@ class RrtRequestTypeSelect extends StatelessWidget {
children: [
Image.asset(AppAssets.mada, width: 24.h, height: 24.h),
Image.asset(AppAssets.visa, width: 24.h, height: 24.h),
Image.asset(AppAssets.Mastercard, width: 24.h, height: 24.h),
Image.asset(AppAssets.apple_pay, width: 24.h, height: 24.h),
Image.asset(AppAssets.mastercard, width: 24.h, height: 24.h),
Image.asset(AppAssets.applePay, width: 24.h, height: 24.h),
],
),
Column(

@ -97,7 +97,7 @@ class EmergencyServicesPage extends StatelessWidget {
}),
],
),
Lottie.asset(AppAnimations.ambulance_alert,
Lottie.asset(AppAnimations.ambulanceAlert,
repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain),
SizedBox(height: 8.h),
"Confirmation".needTranslation.toText28(color: AppColors.whiteColor, isBold: true),
@ -200,7 +200,7 @@ class EmergencyServicesPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Lottie.asset(AppAnimations.ambulance_alert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain),
Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain),
SizedBox(height: 8.h),
LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true),
SizedBox(height: 8.h),
@ -313,7 +313,7 @@ class EmergencyServicesPage extends StatelessWidget {
}),
],
),
Lottie.asset(AppAnimations.ambulance_alert,
Lottie.asset(AppAnimations.ambulanceAlert,
repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain),
SizedBox(height: 8.h),
LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true),

@ -137,7 +137,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -176,7 +176,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamara_en, width: 72.h, height: 25.h),
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
"Tamara".needTranslation.toText16(isBold: true),
],

@ -120,7 +120,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
children: [
Image.asset(AppAssets.visa, width: 40.h, height: 40.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
],
).toShimmer2(isShow: false),
SizedBox(height: 16.h),

@ -73,7 +73,14 @@ class ServicesPage extends StatelessWidget {
true,
route: AppRoutes.eReferralPage,
),
HmgServicesComponentModel(3, "Blood Donation".needTranslation, "".needTranslation, AppAssets.blood_donation_icon, bgColor: AppColors.bloodDonationCardColor, true, route: null, onTap: () async {
HmgServicesComponentModel(
3,
"Blood Donation".needTranslation,
"".needTranslation,
AppAssets.blood_donation_icon,
bgColor: AppColors.bloodDonationCardColor,
true,
route: null, onTap: () async {
LoaderBottomSheet.showLoader(loadingText: "Fetching Data...");
await bloodDonationViewModel.getRegionSelectedClinics(onSuccess: (val) async {
await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) {
@ -244,7 +251,7 @@ class ServicesPage extends StatelessWidget {
fit: BoxFit.contain,
),
Utils.buildImgWithAssets(
icon: AppAssets.male_img,
icon: AppAssets.maleImg,
height: 28.h,
width: 28.w,
border: 1,

@ -142,7 +142,7 @@ class LandingPageData {
static List<ServiceCardData> getServiceCardsList = [
ServiceCardData(
icon: AppAssets.livecare_service,
icon: AppAssets.liveCareService,
title: "LiveCare",
subtitle: "Explore our app, View our services and offers",
largeCardIcon: AppAssets.livecare_icon,

@ -9,7 +9,6 @@ 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/cache_consts.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/int_extensions.dart';
@ -40,14 +39,11 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card
import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/cache_service.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/countdown_timer.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dart';
import 'package:provider/provider.dart';
@ -95,19 +91,359 @@ class _LandingPageState extends State<LandingPage> {
if (appState.isAuthenticated) {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory();
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
myAppointmentsViewModel.getPatientMyDoctors();
prescriptionsViewModel.initPrescriptionsViewModel();
insuranceViewModel.initInsuranceProvider();
immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory();
emergencyServicesViewModel.checkPatientERAdvanceBalance();
}
});
super.initState();
}
Widget buildOptionsForAuthenticatedUser() {
return Column(
children: [
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Appointments & Visits".toText16(isBold: true),
Row(
children: [
LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
),
],
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage()));
}),
SizedBox(height: 16.h),
Consumer<MyAppointmentsViewModel>(
builder: (context, myAppointmentsVM, child) {
return myAppointmentsVM.isMyAppointmentsLoading
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(),
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: true,
isFromHomePage: true,
),
).paddingSymmetrical(24.h, 0.h)
: myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty
? myAppointmentsVM.patientAppointmentsHistoryList.length == 1
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first,
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: false,
isFromHomePage: true,
),
).paddingSymmetrical(24.h, 0.h)
: Swiper(
itemCount: myAppointmentsVM.isMyAppointmentsLoading
? 3
: myAppointmentsVM.patientAppointmentsHistoryList.length < 3
? myAppointmentsVM.patientAppointmentsHistoryList.length
: 3,
layout: SwiperLayout.STACK,
loop: true,
itemWidth: MediaQuery.of(context).size.width - 48.h,
indicatorLayout: PageIndicatorLayout.COLOR,
axisDirection: AxisDirection.right,
controller: _controller,
itemHeight: 200.h,
pagination: const SwiperPagination(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(top: 210 + 8 + 24),
builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor),
),
itemBuilder: (BuildContext context, int index) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index],
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: false,
isFromHomePage: true,
),
);
},
)
: Container(
width: double.infinity,
decoration:
RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Column(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h),
SizedBox(height: 12.h),
"You do not have any upcoming appointment. Please book an appointment".needTranslation.toText12(isCenter: true),
SizedBox(height: 12.h),
CustomButton(
text: LocaleKeys.bookAppo.tr(context: context),
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage()));
},
backgroundColor: Color(0xffFEE9EA),
borderColor: Color(0xffFEE9EA),
textColor: Color(0xffED1C2B),
fontSize: 14.f,
fontWeight: FontWeight.w500,
padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
icon: AppAssets.add_icon,
iconColor: AppColors.primaryRedColor,
height: 46.h,
),
],
),
),
).paddingSymmetrical(24.h, 0.h);
},
),
// Consumer for LiveCare pending request
Consumer<ImmediateLiveCareViewModel>(
builder: (context, immediateLiveCareVM, child) {
return immediateLiveCareVM.patientHasPendingLiveCareRequest
? Column(
children: [
SizedBox(height: 12.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: true,
side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h),
),
width: double.infinity,
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppCustomChipWidget(
labelText: immediateLiveCareViewModel.patientLiveCareHistoryList[0].stringCallStatus,
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20),
textColor: AppColors.alertColor,
),
Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h),
// Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.contain),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"You have a pending LiveCare request".needTranslation.toText12(isBold: true),
Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.blackColor,
width: 20.h,
height: 15.h,
fit: BoxFit.contain,
),
],
),
],
),
),
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage()));
}),
SizedBox(height: 12.h),
],
)
: SizedBox(height: 12.h);
},
),
// Consumer for ER Online Check-In pending request
Consumer<EmergencyServicesViewModel>(
builder: (context, emergencyServicesVM, child) {
return emergencyServicesVM.patientHasAdvanceERBalance
? Column(
children: [
SizedBox(height: 4.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: true,
side: BorderSide(color: AppColors.primaryRedColor, width: 3.h),
),
width: double.infinity,
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppCustomChipWidget(
labelText: "ER Online Check-In Request",
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10),
textColor: AppColors.primaryRedColor,
),
Utils.buildSvgWithAssets(
icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"You have ER Online Check-In Request".needTranslation.toText12(isBold: true),
Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.blackColor,
width: 20.h,
height: 15.h,
fit: BoxFit.contain,
),
],
),
],
),
),
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome()));
// context.read<EmergencyServicesViewModel>().navigateToEROnlineCheckIn();
}),
SizedBox(height: 12.h),
],
)
: SizedBox(height: 12.h);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Quick Links".needTranslation.toText16(isBold: true),
Row(
children: [
"View medical file".needTranslation.toText12(color: AppColors.primaryRedColor),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
),
],
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: MedicalFilePage()));
}),
SizedBox(height: 16.h),
Container(
height: 121.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
children: [
Expanded(
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: LandingPageData.getLoggedInServiceCardsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SmallServiceCard(
icon: LandingPageData.getLoggedInServiceCardsList[index].icon,
title: LandingPageData.getLoggedInServiceCardsList[index].title,
subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle,
iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor,
textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor,
backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor,
isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold,
serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName,
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => 10.width,
),
),
],
),
).paddingSymmetrical(24.h, 0.h),
],
);
}
Widget buildOptionsForNotAuthenticatedUser() {
return Container(
height: 127.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
children: [
Expanded(
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: LandingPageData.getNotLoggedInServiceCardsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SmallServiceCard(
serviceName: LandingPageData.getNotLoggedInServiceCardsList[index].serviceName,
icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon,
title: LandingPageData.getNotLoggedInServiceCardsList[index].title,
subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle,
iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor,
textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor,
backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor,
isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold,
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => 0.width,
),
),
],
),
).paddingSymmetrical(24.h, 0.h);
}
@override
Widget build(BuildContext context) {
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
@ -136,25 +472,16 @@ class _LandingPageState extends State<LandingPage> {
Navigator.of(context).push(springPageRoute(ProfileSettings()));
},
name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'),
imageUrl: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
imageUrl: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
).expanded
: CustomButton(
text: LocaleKeys.loginOrRegister.tr(context: context),
onPressed: () async {
await authVM.onLoginPressed();
// Navigator.pushReplacementNamed(
// // context,
// context,
// AppRoutes.zoomCallPage,
// // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1),
// arguments: CallArguments("test123", "123", "Patient", "40", "0", true, 1),
// // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)),
// );
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
backgroundColor: Color(0xffFEE9EA),
borderColor: Color(0xffFEE9EA),
textColor: Color(0xffED1C2B),
fontSize: 14.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
@ -165,22 +492,8 @@ class _LandingPageState extends State<LandingPage> {
mainAxisSize: MainAxisSize.min,
spacing: 12.h,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 18.h, width: 18.h).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: MedicalFilePage(),
// page: LoginScreen(),
),
);
}),
Utils.buildSvgWithAssets(icon: AppAssets.search_icon, height: 18.h, width: 18.h).onPress(() {
// Navigator.of(context).push(
// CustomPageRoute(
// page: MedicalFilePage(),
// // page: LoginScreen(),
// ),
// );
}),
Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 18.h, width: 18.h),
Utils.buildSvgWithAssets(icon: AppAssets.search_icon, height: 18.h, width: 18.h).onPress(() {}),
Utils.buildSvgWithAssets(icon: AppAssets.contact_icon, height: 18.h, width: 18.h).onPress(() {
showCommonBottomSheetWithoutHeight(
context,
@ -194,445 +507,7 @@ class _LandingPageState extends State<LandingPage> {
),
],
).paddingSymmetrical(24.h, 0.h),
appState.isAuthenticated
? Column(
children: [
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Appointments & Visits".toText16(isBold: true),
Row(
children: [
LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
),
],
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage()));
}),
SizedBox(height: 16.h),
Consumer2<MyAppointmentsViewModel, ImmediateLiveCareViewModel>(
builder: (context, myAppointmentsVM, immediateLiveCareVM, child) {
return myAppointmentsVM.isMyAppointmentsLoading
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(),
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: true,
isFromHomePage: true,
),
).paddingSymmetrical(24.h, 0.h)
: myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty
? myAppointmentsVM.patientAppointmentsHistoryList.length == 1
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first,
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: false,
isFromHomePage: true,
),
).paddingSymmetrical(24.h, 0.h)
: Swiper(
itemCount: myAppointmentsVM.isMyAppointmentsLoading
? 3
: myAppointmentsVM.patientAppointmentsHistoryList.length < 3
? myAppointmentsVM.patientAppointmentsHistoryList.length
: 3,
layout: SwiperLayout.STACK,
loop: true,
itemWidth: MediaQuery.of(context).size.width - 48.h,
indicatorLayout: PageIndicatorLayout.COLOR,
axisDirection: AxisDirection.right,
controller: _controller,
itemHeight: 255.h,
pagination: SwiperPagination(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(top: 240.h + 8 + 24),
builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor),
),
itemBuilder: (BuildContext context, int index) {
return (myAppointmentsVM.isPatientHasQueueAppointment && index == 0)
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
side: BorderSide(color: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), width: 2.w),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppCustomChipWidget(
labelText: myAppointmentsVM.currentQueueStatus == 0 ? "In Queue".needTranslation : "Your Turn".needTranslation,
backgroundColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.20),
textColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus),
),
Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h),
],
),
SizedBox(height: 10.h),
"Hala ${appState!.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true),
SizedBox(height: 8.h),
"Thank you for your patience, here is your queue number."
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 8.h),
"IMD W-A-5".needTranslation.toText28(isBold: true),
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
"Serving Now: ".needTranslation.toText14(isBold: true),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
"IMD W-A-2".needTranslation.toText12(isBold: true),
SizedBox(width: 8.w),
AppCustomChipWidget(
deleteIcon: AppAssets.call_for_vitals,
labelText: "Call for vital signs".needTranslation,
iconColor: AppColors.primaryRedColor,
textColor: AppColors.primaryRedColor,
iconSize: 14.w,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h),
),
],
),
],
),
SizedBox(height: 8.h),
CustomButton(
text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus),
onPressed: () {},
backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus),
borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01),
textColor: Utils.getCardButtonTextColor(myAppointmentsVM.currentQueueStatus),
fontSize: 12.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconColor: AppColors.whiteColor,
iconSize: 18.h,
),
],
),
),
)
: (immediateLiveCareVM.patientHasPendingLiveCareRequest && index == 0)
? Column(
children: [
SizedBox(height: 12.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: true,
side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Immediate LiveCare Request".needTranslation.toText16(isBold: true),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
AppCustomChipWidget(
labelText: immediateLiveCareVM.patientLiveCareHistoryList[0].stringCallStatus,
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20),
textColor: AppColors.alertColor,
),
SizedBox(width: 8.w),
AppCustomChipWidget(
icon: AppAssets.appointment_calendar_icon,
labelText: DateUtil.formatDateToDate(
DateUtil.convertStringToDate(immediateLiveCareVM.patientLiveCareHistoryList[0].arrivalTime), false)),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h),
// Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.cover),
],
),
SizedBox(height: 10.h),
"Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true),
SizedBox(height: 8.h),
"Your turn is after ${immediateLiveCareVM.patientLiveCareHistoryList[0].patCount} patients.".toText14(isBold: true),
SizedBox(height: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Expected waiting time: ".toText12(isBold: true),
SizedBox(height: 8.h),
ValueListenableBuilder<Duration>(
valueListenable: immediateLiveCareVM.durationNotifier,
builder: (context, duration, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
buildTime(duration),
],
);
},
),
SizedBox(height: 12.h),
],
),
// CustomButton(
// text: "View Details".needTranslation,
// onPressed: () async {
// Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage()));
// },
// backgroundColor: Color(0xffFEE9EA),
// borderColor: Color(0xffFEE9EA),
// textColor: Color(0xffED1C2B),
// fontSize: 14.f,
// fontWeight: FontWeight.w500,
// borderRadius: 12.r,
// padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
// height: 40.h,
// ),
],
),
),
).paddingSymmetrical(0.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage()));
}),
SizedBox(height: 12.h),
],
)
: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: AppointmentCard(
patientAppointmentHistoryResponseModel:
myAppointmentsVM.patientAppointmentsHistoryList[immediateLiveCareViewModel.patientHasPendingLiveCareRequest ? --index : index],
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: false,
isFromHomePage: true,
),
);
},
)
: Container(
width: double.infinity,
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Column(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h),
SizedBox(height: 12.h),
"You do not have any upcoming appointment. Please book an appointment"
.needTranslation
.toText12(isCenter: true),
SizedBox(height: 12.h),
CustomButton(
text: LocaleKeys.bookAppo.tr(context: context),
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage()));
},
backgroundColor: Color(0xffFEE9EA),
borderColor: Color(0xffFEE9EA),
textColor: Color(0xffED1C2B),
fontSize: 14.f,
fontWeight: FontWeight.w500,
padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
icon: AppAssets.add_icon,
iconColor: AppColors.primaryRedColor,
height: 46.h,
),
],
),
),
).paddingSymmetrical(24.h, 0.h);
},
),
// Consumer for ER Online Check-In pending request
Consumer<EmergencyServicesViewModel>(
builder: (context, emergencyServicesVM, child) {
return emergencyServicesVM.patientHasAdvanceERBalance
? Column(
children: [
SizedBox(height: 4.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: true,
side: BorderSide(color: AppColors.primaryRedColor, width: 3.h),
),
width: double.infinity,
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppCustomChipWidget(
labelText: "ER Online Check-In Request",
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10),
textColor: AppColors.primaryRedColor,
),
Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"You have ER Online Check-In Request".needTranslation.toText12(isBold: true),
Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.blackColor,
width: 20.h,
height: 15.h,
fit: BoxFit.contain,
),
],
),
],
),
),
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome()));
// context.read<EmergencyServicesViewModel>().navigateToEROnlineCheckIn();
}),
SizedBox(height: 12.h),
],
)
: SizedBox(height: 12.h);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Quick Links".needTranslation.toText16(isBold: true),
Row(
children: [
"View medical file".needTranslation.toText12(color: AppColors.primaryRedColor),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
),
],
).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: MedicalFilePage()));
}),
SizedBox(height: 16.h),
Container(
height: 121.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
children: [
Expanded(
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: LandingPageData.getLoggedInServiceCardsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SmallServiceCard(
icon: LandingPageData.getLoggedInServiceCardsList[index].icon,
title: LandingPageData.getLoggedInServiceCardsList[index].title,
subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle,
iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor,
textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor,
backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor,
isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold,
serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName,
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => 10.width,
),
),
],
),
).paddingSymmetrical(24.h, 0.h),
],
)
: Container(
height: 127.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
children: [
Expanded(
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: LandingPageData.getNotLoggedInServiceCardsList.length,
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
child: SmallServiceCard(
serviceName: LandingPageData.getNotLoggedInServiceCardsList[index].serviceName,
icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon,
title: LandingPageData.getNotLoggedInServiceCardsList[index].title,
subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle,
iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor,
textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor,
backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor,
isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold,
),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => 0.width,
),
),
],
),
).paddingSymmetrical(24.h, 0.h),
if (appState.isAuthenticated) buildOptionsForAuthenticatedUser() else buildOptionsForNotAuthenticatedUser(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -647,7 +522,7 @@ class _LandingPageState extends State<LandingPage> {
Navigator.of(context).push(CustomPageRoute(page: ServicesPage()));
}),
],
).paddingSymmetrical(24.w, 0.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(
height: 340.h,
child: ListView.separated(
@ -694,19 +569,15 @@ class _LandingPageState extends State<LandingPage> {
isDone: isDone,
onPressed: () {
// sharedPref.setBool(HAS_ENABLED_QUICK_LOGIN, true);
authVM.loginWithFingerPrintFace(() async {
authVM.loginWithFingerPrintFace(() {
isDone = true;
cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: true);
setState(() {});
await Future.delayed(Duration(milliseconds: 2000)).then((value) {
if (mounted) Navigator.pop(context);
});
});
},
);
},
),
// height: isDone == false ? ResponsiveExtension.screenHeight * 0.5 : ResponsiveExtension.screenHeight * 0.3,
isFullScreen: false,
callBackFunc: () {
isDone = true;

@ -1,12 +1,13 @@
import 'package:flutter/material.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/presentation/book_appointment/book_appointment_page.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/hmg_services/services_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/presentation/todo_section/todo_page.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/widgets/bottom_navigation/bottom_navigation.dart';
class LandingNavigation extends StatefulWidget {
@ -18,12 +19,11 @@ class LandingNavigation extends StatefulWidget {
class _LandingNavigationState extends State<LandingNavigation> {
int _currentIndex = 0;
late AppState appState;
final PageController _pageController = PageController();
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
AppState appState = getIt.get<AppState>();
return Scaffold(
body: PageView(
controller: _pageController,
@ -31,7 +31,7 @@ class _LandingNavigationState extends State<LandingNavigation> {
children: [
const LandingPage(),
appState.isAuthenticated ? MedicalFilePage() : /* need add feedback page */ FeedbackPage(),
BookAppointmentPage(),
SizedBox(),
const ToDoPage(),
ServicesPage(),
],
@ -40,6 +40,10 @@ class _LandingNavigationState extends State<LandingNavigation> {
currentIndex: _currentIndex,
onTap: (index) {
setState(() => _currentIndex = index);
if (_currentIndex == 2) {
context.navigateWithName(AppRoutes.bookAppointmentPage);
return;
}
_pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
},
),

@ -34,7 +34,7 @@ class LargeServiceCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.livecare_service, width: 220.w, fit: BoxFit.contain),
Image.asset(AppAssets.liveCareService, width: 220.w, fit: BoxFit.contain),
SizedBox(height: 10.h),
Row(
children: [

@ -5,7 +5,6 @@ import 'package:flutter/material.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/hmg_services/hmg_services_view_model.dart';
@ -14,9 +13,11 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get
import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_order_detail_page.dart';
import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_selection_review_page.dart';
import 'package:hmg_patient_app_new/presentation/home_health_care/widgets/hhc_ui_selection_helper.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/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/map/map_utility_screen.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
@ -79,6 +80,15 @@ class _HhcProceduresPageState extends State<HhcProceduresPage> {
}
final canCancel = order.statusId == 1 || order.statusId == 2;
final isArabic = getIt.get<AppState>().isArabic();
// Extract services from orderselectedservice
List<dynamic> selectedServices = [];
if (order.orderselectedservice != null) {
if (order.orderselectedservice is List) {
selectedServices = order.orderselectedservice as List;
}
}
return Container(
width: double.infinity,
@ -129,6 +139,85 @@ class _HhcProceduresPageState extends State<HhcProceduresPage> {
],
),
// Services List
if (selectedServices.isNotEmpty) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: AppColors.greyColor.withAlpha(51),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.medical_services_outlined,
size: 18.h,
color: AppColors.primaryRedColor,
),
SizedBox(width: 6.w),
"Requested Services".needTranslation.toText14(
weight: FontWeight.w600,
color: AppColors.blackColor,
),
],
),
SizedBox(height: 8.h),
...selectedServices.asMap().entries.map((entry) {
int index = entry.key;
var service = entry.value;
String serviceName = '';
if (service is Map) {
if (isArabic) {
serviceName = service['SelectedServiceNameAR']?.toString() ??
service['selectedServiceNameAR']?.toString() ??
service['SelectedServiceName']?.toString() ??
service['selectedServiceName']?.toString() ??
'';
} else {
serviceName = service['SelectedServiceName']?.toString() ?? service['selectedServiceName']?.toString() ?? '';
}
}
return Padding(
padding: EdgeInsets.only(top: index > 0 ? 6.h : 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 6.h),
width: 6.w,
height: 6.w,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
shape: BoxShape.circle,
),
),
SizedBox(width: 8.w),
Expanded(
child: serviceName.toText14(
color: AppColors.blackColor,
weight: FontWeight.w500,
maxlines: 3,
),
),
],
),
);
}).toList(),
],
),
),
],
SizedBox(height: 12.h),
// Info message
@ -207,55 +296,6 @@ class _HhcProceduresPageState extends State<HhcProceduresPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 8.h),
if (viewModel.selectedHhcServices.isNotEmpty) ...[
SizedBox(height: 16.h),
Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.primaryRedColor.withValues(alpha: 0.1),
borderRadius: 16.r,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Selected Services".needTranslation.toText12(
color: AppColors.textColorLight,
fontWeight: FontWeight.w600,
),
"${viewModel.selectedHhcServices.length} service(s) selected".toText14(
isBold: true,
weight: FontWeight.bold,
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
"Total Amount".needTranslation.toText12(
color: AppColors.textColorLight,
fontWeight: FontWeight.w600,
),
Utils.getPaymentAmountWithSymbol(
viewModel.getHhcSelectedServicesTotal().toStringAsFixed(2).toText16(
isBold: true,
weight: FontWeight.bold,
color: AppColors.primaryRedColor,
),
AppColors.primaryRedColor,
14,
isSaudiCurrency: true,
),
],
),
],
),
),
],
SizedBox(height: 16.h),
Text(
'Select Services'.needTranslation,
@ -324,18 +364,35 @@ class _HhcProceduresPageState extends State<HhcProceduresPage> {
);
},
),
// Summary Section
],
);
},
);
}
void _proceedWithSelectedService() {
void _proceedWithSelectedService() async {
final navigationServices = getIt.get<NavigationService>();
final appState = getIt.get<AppState>();
final hmgServicesViewModel = context.read<HmgServicesViewModel>();
if (hmgServicesViewModel.selectedHhcServices.isNotEmpty) {
hmgServicesViewModel.setSelectedServiceForHhcOrder(hmgServicesViewModel.selectedHhcServices.first);
Navigator.of(context).pushReplacement(
bool result = await navigationServices.push(
CustomPageRoute(
page: MapUtilityScreen(
confirmButtonString: "Submit Request ".needTranslation,
titleString: "Select Location",
subTitleString: "Please select the location".needTranslation,
isGmsAvailable: appState.isGMSAvailable,
),
direction: AxisDirection.down),
);
if (!result) {
return;
}
Navigator.of(context).push(
CustomPageRoute(
page: HhcSelectionReviewPage(selectedServices: hmgServicesViewModel.selectedHhcServices),
direction: AxisDirection.left,
@ -379,16 +436,48 @@ class _HhcProceduresPageState extends State<HhcProceduresPage> {
if (pendingOrder == null && hmgServicesViewModel.selectedHhcServices.isNotEmpty) {
return SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h),
child: CustomButton(
borderWidth: 0,
text: "Next".needTranslation,
onPressed: _proceedWithSelectedService,
textColor: AppColors.whiteColor,
borderRadius: 12.r,
borderColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
child: Container(
color: AppColors.whiteColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Selected Services Summary Widget
Container(
margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Selected Services".needTranslation.toText12(
color: AppColors.textColorLight,
fontWeight: FontWeight.w600,
),
"${hmgServicesViewModel.selectedHhcServices.length} service(s) selected".toText14(
isBold: true,
weight: FontWeight.bold,
),
],
),
],
),
),
// Next Button
Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w),
child: CustomButton(
borderWidth: 0,
text: "Next".needTranslation,
onPressed: _proceedWithSelectedService,
textColor: AppColors.whiteColor,
borderRadius: 12.r,
borderColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
),
],
),
),
);

@ -10,13 +10,14 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/location/location_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/map/location_map_widget.dart';
import 'package:maps_launcher/maps_launcher.dart';
import 'package:provider/provider.dart';
@ -57,6 +58,8 @@ class _HhcSelectionReviewPageState extends State<HhcSelectionReviewPage> {
children: [
_buildSelectedServicesCard(isArabic),
SizedBox(height: 16.h),
_buildLocationMap(),
SizedBox(height: 16.h),
],
),
),
@ -104,6 +107,36 @@ class _HhcSelectionReviewPageState extends State<HhcSelectionReviewPage> {
);
}
Widget _buildLocationMap() {
final locationViewModel = getIt.get<LocationViewModel>();
final geocodeResponse = locationViewModel.geocodeResponse;
final mapCapturedLocation = locationViewModel.mapCapturedLocation;
// Check if we have location data
if (mapCapturedLocation == null || (mapCapturedLocation.lat == 0.0 && mapCapturedLocation.lng == 0.0)) {
return SizedBox.shrink();
}
final double lat = mapCapturedLocation.lat ?? 0.0;
final double lng = mapCapturedLocation.lng ?? 0.0;
if (lat == 0.0 || lng == 0.0) return SizedBox.shrink();
// Get address from geocode response
String address = "Selected Location".needTranslation;
if (geocodeResponse != null && geocodeResponse.results.isNotEmpty) {
address = geocodeResponse.results.first.formattedAddress;
}
return LocationMapWidget(
latitude: lat,
longitude: lng,
address: address,
title: "Service Location".needTranslation,
onDirectionsTap: () => _launchDirectionsToLocation(lat, lng, address),
);
}
Widget _buildBottomButton() {
return SafeArea(
top: false,
@ -133,16 +166,9 @@ class _HhcSelectionReviewPageState extends State<HhcSelectionReviewPage> {
);
}
void _launchDirections(HospitalsModel selectedHospital) {
final double lat = double.parse(selectedHospital.latitude ?? "0.0");
final double lng = double.parse(selectedHospital.longitude ?? "0.0");
void _launchDirectionsToLocation(double lat, double lng, String locationName) {
if (lat != 0.0 && lng != 0.0) {
MapsLauncher.launchCoordinates(
lat,
lng,
selectedHospital.name ?? "Hospital",
);
MapsLauncher.launchCoordinates(lat, lng, locationName);
}
}

File diff suppressed because one or more lines are too long

@ -130,10 +130,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
fit: BoxFit.contain,
),
Utils.buildImgWithAssets(
icon: AppAssets.male_img,
height: 28.h,
width: 28.w,
border: 1,
icon: AppAssets.maleImg,
height: 32.h,
border: 1.5,
borderRadius: 50.r,
fit: BoxFit.contain,
),
@ -176,7 +175,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.w, height: 56.h),
Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

@ -173,9 +173,9 @@ class _FamilyCardsState extends State<FamilyCards> {
children: [
Utils.buildImgWithAssets(
icon: profile.gender == null
? AppAssets.dummy_user
? AppAssets.dummyUser
: profile.gender == 1
? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.male_img)
? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg)
: (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg),
width: 72.h,
height: 70.h,

@ -29,18 +29,14 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import '../../core/dependencies.dart' show getIt;
class ProfileSettings extends StatefulWidget {
ProfileSettings({Key? key}) : super(key: key);
const ProfileSettings({super.key});
@override
_ProfileSettingsState createState() {
return _ProfileSettingsState();
}
ProfileSettingsState createState() => ProfileSettingsState();
}
class _ProfileSettingsState extends State<ProfileSettings> {
class ProfileSettingsState extends State<ProfileSettings> {
@override
void initState() {
super.initState();
@ -184,7 +180,10 @@ class _ProfileSettingsState extends State<ProfileSettings> {
),
],
),
"Quick Actions".needTranslation.toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w),
"Quick Actions"
.needTranslation
.toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1)
.paddingOnly(left: 24.w, right: 24.w),
Container(
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h),
padding: EdgeInsets.only(top: 4.h, bottom: 4.h),
@ -192,9 +191,12 @@ class _ProfileSettingsState extends State<ProfileSettings> {
child: Column(
children: [
actionItem(AppAssets.language_change, "Language".needTranslation, () {
showCommonBottomSheetWithoutHeight(context, title: "Application Language".needTranslation, child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false);
showCommonBottomSheetWithoutHeight(context,
title: "Application Language".needTranslation, child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false);
}, trailingLabel: Utils.appState.isArabic() ? "العربية".needTranslation : "English".needTranslation),
1.divider,
actionItem(AppAssets.accessibility, "Symptoms Checker".needTranslation, () {}),
1.divider,
actionItem(AppAssets.accessibility, "Accessibility".needTranslation, () {}),
1.divider,
actionItem(AppAssets.bell, "Notifications Settings".needTranslation, () {}),
@ -203,7 +205,10 @@ class _ProfileSettingsState extends State<ProfileSettings> {
],
),
),
"Personal Information".needTranslation.toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w),
"Personal Information"
.needTranslation
.toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1)
.paddingOnly(left: 24.w, right: 24.w),
Container(
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h),
padding: EdgeInsets.only(top: 4.h, bottom: 4.h),
@ -220,7 +225,10 @@ class _ProfileSettingsState extends State<ProfileSettings> {
],
),
),
"Help & Support".needTranslation.toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w),
"Help & Support"
.needTranslation
.toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1)
.paddingOnly(left: 24.w, right: 24.w),
Container(
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h),
padding: EdgeInsets.only(top: 4.h, bottom: 4.h),
@ -261,12 +269,13 @@ class _ProfileSettingsState extends State<ProfileSettings> {
children: [
Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.greyTextColor),
label.toText14(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded,
if (trailingLabel.isNotEmpty) trailingLabel.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1),
if (trailingLabel.isNotEmpty)
trailingLabel.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1),
switchValue != null
? Switch(
value: switchValue,
onChanged: (value) {},
activeColor: AppColors.successColor,
activeThumbColor: AppColors.successColor,
activeTrackColor: AppColors.successColor.withValues(alpha: .15),
)
: Transform.scale(
@ -295,8 +304,8 @@ class FamilyCardWidget extends StatelessWidget {
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId);
final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID);
// final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
// final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID);
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
@ -312,7 +321,7 @@ class FamilyCardWidget extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(profile.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.w, height: 56.h),
Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,

@ -0,0 +1,207 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
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/insurance/insurance_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart';
import 'package:hmg_patient_app_new/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:provider/provider.dart';
class FamilyCardWidget extends StatelessWidget {
final Function() onAddFamilyMemberPress;
final Function(FamilyFileResponseModelLists member) onFamilySwitchPress;
final FamilyFileResponseModelLists profile;
const FamilyCardWidget({
super.key,
required this.onAddFamilyMemberPress,
required this.profile,
required this.onFamilySwitchPress(FamilyFileResponseModelLists member),
});
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId);
final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID);
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.h,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
"${profile.patientName}".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1),
AppCustomChipWidget(
icon: AppAssets.file_icon,
labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${profile.responseId}",
iconSize: 12.w,
),
],
).expanded,
Icon(Icons.qr_code, size: 56.h)
],
),
SizedBox(height: 4.h),
SizedBox(
child: Wrap(
alignment: WrapAlignment.start,
spacing: 4.w,
runSpacing: 4.h,
children: [
AppCustomChipWidget(
labelText: "${profile.age} Years Old".needTranslation,
),
isActive && appState.getAuthenticatedUser()!.bloodGroup != null
? AppCustomChipWidget(
icon: AppAssets.blood_icon,
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w),
labelText: "Blood: ${appState.getAuthenticatedUser()!.bloodGroup ?? ""}",
iconColor: AppColors.primaryRedColor)
: SizedBox(),
Selector<InsuranceViewModel, ({bool isEmpty, int? patientID, bool isLoading, String? cardValidTo})>(
selector: (context, insuranceVM) => (
isEmpty: insuranceVM.patientInsuranceList.isEmpty,
patientID: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.patientID : null,
isLoading: insuranceVM.isInsuranceLoading,
cardValidTo: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.cardValidTo : null
),
builder: (context, data, child) {
if (data.isEmpty) {
return const SizedBox();
} else if (profile.responseId != data.patientID) {
return SizedBox();
}
final isLoading = data.isLoading;
final isExpired = !isLoading && DateTime.now().isAfter(DateUtil.convertStringToDate(data.cardValidTo));
final String icon;
final String labelText;
final Color iconColor;
final Color backgroundColor;
if (isLoading) {
icon = AppAssets.cancel_circle_icon;
labelText = "Insurance".needTranslation;
iconColor = AppColors.primaryRedColor;
backgroundColor = AppColors.primaryRedColor;
} else if (isExpired) {
icon = AppAssets.cancel_circle_icon;
labelText = "Insurance Expired".needTranslation;
iconColor = AppColors.primaryRedColor;
backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15);
} else {
icon = AppAssets.insurance_active_icon;
labelText = "Insurance Active".needTranslation;
iconColor = AppColors.successColor;
backgroundColor = AppColors.successColor.withValues(alpha: 0.15);
}
return AppCustomChipWidget(
icon: icon,
labelText: labelText,
iconColor: iconColor,
iconSize: 12.w,
backgroundColor: backgroundColor,
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w),
// padding: EdgeInsets.zero,
).toShimmer2(isShow: isLoading);
},
)
],
),
),
],
).paddingOnly(top: 16.h, right: 16.w, left: 16.w, bottom: 12.h),
1.divider,
_buildActionButton(appState),
],
),
);
}
Widget _buildActionButton(AppState appState) {
final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
final int? currentUserId = appState.getAuthenticatedUser()?.patientId;
final int? superUserId = appState.getSuperUserID;
if (isParentUser) {
return _buildParentUserButton(currentUserId);
} else {
return _buildNonParentUserButton(superUserId);
}
}
Widget _buildParentUserButton(int? currentUserId) {
final canSwitch = profile.responseId != currentUserId;
return CustomButton(
icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family,
text: canSwitch ? "Switch Family File".needTranslation : "Add a new family member".needTranslation,
onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress,
backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor,
borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor,
textColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor,
iconColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor,
height: isFoldable ? 50.h : 40.h,
fontSize: 14.f,
).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h);
}
Widget _buildNonParentUserButton(int? superUserId) {
final canSwitchBack = superUserId != null && superUserId == profile.responseId;
return CustomButton(
icon: AppAssets.switch_user,
text: canSwitchBack ? "Switch Back To Family File".needTranslation : "Switch".needTranslation,
backgroundColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200,
borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200,
textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor,
iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor,
onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {},
height: isFoldable ? 50.h : 40.h,
fontSize: 14.f,
).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h);
}
// //TODO: Add family file switch logic here
// isRootUser
// ? CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {})
// .paddingOnly(top: 12, right: 16, left: 16, bottom: 16)
// : CustomButton(
// icon: AppAssets.add_family,
// backgroundColor: AppColors.secondaryLightRedColor,
// borderColor: AppColors.secondaryLightRedColor,
// textColor: AppColors.primaryRedColor,
// iconColor: AppColors.primaryRedColor,
// text: "Switch to this medical file".needTranslation,
// height: 40.h,
// fontSize: 14,
// onPressed: () {})
// .paddingOnly(top: 12, right: 16, left: 16, bottom: 16),
//
}

@ -0,0 +1,331 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.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:provider/provider.dart';
class OrganSelectorPage extends StatefulWidget {
const OrganSelectorPage({super.key});
@override
State<OrganSelectorPage> createState() => _OrganSelectorPageState();
}
class _OrganSelectorPageState extends State<OrganSelectorPage> {
late final AppState _appState;
@override
void initState() {
super.initState();
_appState = getIt.get<AppState>();
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (!viewModel.validateSelection()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one organ'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
return;
}
context.navigateWithName(AppRoutes.symptomsSelectorScreen);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
appBar: _buildAppBar(),
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
SizedBox(height: 8.h),
Expanded(
child: Stack(
children: [
_buildBodyViewer(viewModel),
_buildViewToggleButtons(viewModel),
_buildBottomSheet(viewModel),
],
),
),
],
);
},
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: AppColors.bgScaffoldColor,
elevation: 0,
leading: Transform.flip(
flipX: _appState.isArabic(),
child: IconButton(
icon: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_back,
width: 32.h,
height: 32.h,
),
padding: const EdgeInsets.only(left: 12),
onPressed: () => Navigator.pop(context),
highlightColor: Colors.transparent,
),
),
);
}
Widget _buildTitle() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
"Organ Selector".needTranslation,
style: TextStyle(
color: AppColors.textColor,
fontSize: 22.f,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) {
return Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 600),
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: InteractiveBodyWidget(
key: ValueKey<BodyView>(viewModel.currentView),
bodyImageAsset: viewModel.currentView == BodyView.front ? AppAssets.fullBodyFront : AppAssets.fullBodyBack,
organs: viewModel.currentOrgans,
selectedOrganIds: viewModel.selectedOrganIds,
onOrganTap: viewModel.toggleOrganSelection,
isBodyHidden: viewModel.isBodyHidden,
tooltipOrganId: viewModel.tooltipOrganId,
),
),
);
}
Widget _build3DFlipTransition(Widget child, Animation<double> animation) {
final rotateAnimation = Tween<double>(begin: 0.5, end: 0.0).animate(
CurvedAnimation(parent: animation, curve: Curves.easeInOut),
);
return AnimatedBuilder(
animation: rotateAnimation,
builder: (context, child) {
final angle = rotateAnimation.value * 3.14159;
final transform = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(angle);
return Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
},
child: child,
);
}
Widget _buildViewToggleButtons(SymptomsCheckerViewModel viewModel) {
return Positioned(
left: 16.w,
top: 36.h,
child: Column(
children: [
_buildToggleButton(
icon: AppAssets.bodyIcon,
onTap: viewModel.toggleIsBodyHidden,
),
SizedBox(height: 16.h),
_buildToggleButton(
icon: AppAssets.rotateIcon,
onTap: viewModel.toggleView,
),
],
),
);
}
Widget _buildToggleButton({required String icon, required VoidCallback onTap}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 48.w,
height: 48.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 11.r,
),
child: Center(
child: Utils.buildSvgWithAssets(
icon: icon,
width: 25.w,
height: 25.h,
),
),
),
);
}
Widget _buildBottomSheet(SymptomsCheckerViewModel viewModel) {
return Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: SafeArea(
top: false,
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (viewModel.isBottomSheetExpanded) ...[
SizedBox(height: 20.h),
_buildBottomSheetTitle(),
SizedBox(height: 12.h),
_buildSelectedOrgansContent(viewModel),
SizedBox(height: 16.h),
_buildNextButton(viewModel),
SizedBox(height: 16.h),
] else
SizedBox(height: 20.h),
],
),
_buildExpandCollapseButton(viewModel),
],
),
),
),
);
}
Widget _buildBottomSheetTitle() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
'Selected Organs'.needTranslation,
style: TextStyle(
fontSize: 16.f,
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
);
}
Widget _buildSelectedOrgansContent(SymptomsCheckerViewModel viewModel) {
if (viewModel.selectedOrgans.isEmpty) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
'No organs selected yet'.needTranslation,
style: TextStyle(
color: AppColors.greyTextColor,
fontSize: 14.f,
),
),
);
}
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: 150.h),
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: viewModel.selectedOrgans.map((organ) {
return AppCustomChipWidget(
labelText: organ.description,
backgroundColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
deleteIcon: AppAssets.cancel,
deleteIconColor: AppColors.primaryRedColor,
deleteIconHasColor: false,
onDeleteTap: () => viewModel.removeOrgan(organ.id),
);
}).toList(),
),
),
);
}
Widget _buildNextButton(SymptomsCheckerViewModel viewModel) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: CustomButton(
text: 'Next'.needTranslation,
onPressed: () => _onNextPressed(viewModel),
isDisabled: viewModel.selectedOrgans.isEmpty,
backgroundColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
),
);
}
Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) {
return Positioned(
right: 24.w,
top: -24.h,
child: GestureDetector(
onTap: viewModel.toggleBottomSheet,
behavior: HitTestBehavior.opaque,
child: Container(
width: 70.w,
height: 70.h,
alignment: Alignment.center,
child: Container(
width: 48.w,
height: 48.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 11.r,
),
child: Center(
child: AnimatedRotation(
duration: const Duration(milliseconds: 300),
turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrowRight,
width: 25.w,
height: 25.h,
),
),
),
),
),
),
);
}
}

@ -0,0 +1,177 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/conditions_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.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/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class PossibleConditionsScreen extends StatelessWidget {
const PossibleConditionsScreen({super.key});
Widget _buildLoadingShimmer() {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(16.w),
itemCount: 10,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 80.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.r),
),
),
);
},
);
}
Widget _buildPredictionsList(List<ConditionsModel> conditions) {
if (conditions.isEmpty) {
return Center(
child: Padding(
padding: EdgeInsets.all(24.h),
child: Text(
'No Predictions available'.needTranslation,
style: TextStyle(
fontSize: 16.h,
color: AppColors.greyTextColor,
),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
ListView.separated(
padding: EdgeInsets.zero,
itemCount: conditions.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => SizedBox(height: 16.h),
itemBuilder: (context, index) {
final conditionModel = conditions[index];
return ConditionCard(
icon: conditionModel.icon,
title: conditionModel.title,
percentage: conditionModel.percentage,
tagText: conditionModel.tagText,
clinic: conditionModel.clinic,
symptoms: conditionModel.symptoms,
description: conditionModel.description,
possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum,
onActionPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('We are not available for a week. May you Rest In Peace :('),
backgroundColor: AppColors.primaryRedColor,
),
);
},
);
},
).paddingSymmetrical(24.w, 0),
SizedBox(height: 24.h),
],
);
}
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
_restartOrganSelection(BuildContext context) async {
final symptomsCheckerVm = context.read<SymptomsCheckerViewModel>();
symptomsCheckerVm.reset();
context.pop();
await Future.delayed(Duration(seconds: 1)).whenComplete(() => context.pop());
}
_navigateToLandingPage() {
NavigationService navigationService = getIt.get<NavigationService>();
navigationService.replaceAllRoutesAndNavigateToLanding();
}
_buildTrailingSection(BuildContext context) {
return Row(
children: [
Container(
height: 40.h,
padding: EdgeInsets.all(8.w),
child: Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.refreshIcon,
height: 20.h,
width: 20.w,
iconColor: AppColors.textColor,
),
),
).onPress(() => _buildConfirmationBottomSheet(context: context, onConfirm: () => _restartOrganSelection(context))),
Container(
height: 40.h,
padding: EdgeInsets.all(8.w),
child: Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.homeBorderedIcon,
height: 20.h,
width: 20.w,
iconColor: AppColors.textColor,
),
),
).onPress(() => _buildConfirmationBottomSheet(context: context, onConfirm: () => _navigateToLandingPage())),
],
).paddingSymmetrical(20.w, 0);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "Possible Conditions".needTranslation,
trailing: _buildTrailingSection(context),
child: Consumer<SymptomsCheckerViewModel>(
builder: (context, symptomsCheckerViewModel, child) {
if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) {
return _buildLoadingShimmer();
}
return _buildPredictionsList(dummyConditions);
},
),
),
);
}
}

@ -0,0 +1,291 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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';
class RiskFactorsScreen extends StatefulWidget {
const RiskFactorsScreen({super.key});
@override
State<RiskFactorsScreen> createState() => _RiskFactorsScreenState();
}
class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
@override
void initState() {
super.initState();
// Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.initializeSymptomGroups();
});
}
void _onOptionSelected(int optionIndex) {}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (viewModel.hasSelectedSymptoms) {
// Navigate to triage screen
context.navigateWithName(AppRoutes.suggestionsScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one option before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
}
}
void _onPreviousPressed() {
context.pop();
}
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
Widget _buildOptionItem(int index, bool selected, String optionText) {
return GestureDetector(
onTap: () => _onOptionSelected(index),
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: selected ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(5.r),
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w),
),
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
optionText,
style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500),
),
),
],
),
),
);
}
Widget buildFactorsList() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
final offsetAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
));
return SlideTransition(
position: offsetAnimation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...List.generate(4, (index) {
return _buildOptionItem(index, false, "currentQuestion.options[index].text");
}),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
children: [
Expanded(
child: CollapsingListView(
title: "Risks".needTranslation,
leadingCallback: () => _buildConfirmationBottomSheet(
context: context,
onConfirm: () => {
context.pop(),
context.pop(),
}),
child: _buildEmptyState(),
// child: viewModel.organSymptomsGroups.isEmpty
// ? _buildEmptyState()
// : Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// SizedBox(height: 16.h),
// ...viewModel.organSymptomsGroups.map((group) {
// return Padding(
// padding: EdgeInsets.only(bottom: 16.h),
// child: Container(
// width: double.infinity,
// margin: EdgeInsets.symmetric(horizontal: 24.w),
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
// padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: Text(
// 'Possible symptoms related to "${group.organName}"',
// style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
// ),
// ),
// ],
// ),
// SizedBox(height: 24.h),
// Wrap(
// runSpacing: 12.h,
// spacing: 8.w,
// children: group.symptoms.map((symptom) {
// bool isSelected = viewModel.isSymptomSelected(group.organId, symptom.id);
// return GestureDetector(
// onTap: () => viewModel.toggleSymptomSelection(group.organId, symptom.id),
// child: CustomSelectableChip(
// label: symptom.name,
// selected: isSelected,
// activeColor: AppColors.primaryRedBorderColor,
// activeTextColor: AppColors.primaryRedBorderColor,
// inactiveBorderColor: AppColors.bottomNAVBorder,
// inactiveTextColor: AppColors.textColor,
// ),
// );
// }).toList(),
// ),
// ],
// ),
// ),
// );
// }),
// ],
// ),
),
),
_buildStickyBottomCard(context, viewModel),
],
);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: EdgeInsets.all(24.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor),
SizedBox(height: 16.h),
Text(
'No organs selected'.needTranslation,
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
SizedBox(height: 8.h),
Text(
'Please go back and select organs first'.needTranslation,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
],
),
),
);
}
Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "Previous".needTranslation,
onPressed: _onPreviousPressed,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: "Next".needTranslation,
onPressed: () => _onNextPressed(viewModel),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0),
);
}
}

@ -0,0 +1,292 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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';
class SuggestionsScreen extends StatefulWidget {
const SuggestionsScreen({super.key});
@override
State<SuggestionsScreen> createState() => _SuggestionsScreenState();
}
class _SuggestionsScreenState extends State<SuggestionsScreen> {
@override
void initState() {
super.initState();
// Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.initializeSymptomGroups();
});
}
void _onOptionSelected(int optionIndex) {}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (viewModel.hasSelectedSymptoms) {
// Navigate to triage screen
context.navigateWithName(AppRoutes.triageScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one option before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
}
}
void _onPreviousPressed() {
context.pop();
}
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
Widget _buildOptionItem(int index, bool selected, String optionText) {
return GestureDetector(
onTap: () => _onOptionSelected(index),
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: selected ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(5.r),
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w),
),
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
optionText,
style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500),
),
),
],
),
),
);
}
Widget buildFactorsList() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
final offsetAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
));
return SlideTransition(
position: offsetAnimation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...List.generate(4, (index) {
return _buildOptionItem(index, false, "currentQuestion.options[index].text");
}),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
children: [
Expanded(
child: CollapsingListView(
title: "Suggestions".needTranslation,
leadingCallback: () => _buildConfirmationBottomSheet(
context: context,
onConfirm: () => {
context.pop(),
context.pop(),
}),
child: _buildEmptyState(),
// child: viewModel.organSymptomsGroups.isEmpty
// ? _buildEmptyState()
// : Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// SizedBox(height: 16.h),
// ...viewModel.organSymptomsGroups.map((group) {
// return Padding(
// padding: EdgeInsets.only(bottom: 16.h),
// child: Container(
// width: double.infinity,
// margin: EdgeInsets.symmetric(horizontal: 24.w),
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
// padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: Text(
// 'Possible symptoms related to "${group.organName}"',
// style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
// ),
// ),
// ],
// ),
// SizedBox(height: 24.h),
// Wrap(
// runSpacing: 12.h,
// spacing: 8.w,
// children: group.symptoms.map((symptom) {
// bool isSelected = viewModel.isSymptomSelected(group.organId, symptom.id);
// return GestureDetector(
// onTap: () => viewModel.toggleSymptomSelection(group.organId, symptom.id),
// child: CustomSelectableChip(
// label: symptom.name,
// selected: isSelected,
// activeColor: AppColors.primaryRedBorderColor,
// activeTextColor: AppColors.primaryRedBorderColor,
// inactiveBorderColor: AppColors.bottomNAVBorder,
// inactiveTextColor: AppColors.textColor,
// ),
// );
// }).toList(),
// ),
// ],
// ),
// ),
// );
// }),
// ],
// ),
),
),
_buildStickyBottomCard(context, viewModel),
],
);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: EdgeInsets.all(24.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor),
SizedBox(height: 16.h),
Text(
'No organs selected'.needTranslation,
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
SizedBox(height: 8.h),
Text(
'Please go back and select organs first'.needTranslation,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
],
),
),
);
}
Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "Previous".needTranslation,
onPressed: _onPreviousPressed,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: "Next".needTranslation,
onPressed: () => _onNextPressed(viewModel),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0),
);
}
}

@ -0,0 +1,312 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/custom_selectable_chip.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:provider/provider.dart';
class SymptomsSelectorScreen extends StatefulWidget {
const SymptomsSelectorScreen({super.key});
@override
State<SymptomsSelectorScreen> createState() => _SymptomsSelectorScreenState();
}
class _SymptomsSelectorScreenState extends State<SymptomsSelectorScreen> {
@override
void initState() {
super.initState();
// Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.initializeSymptomGroups();
});
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (viewModel.hasSelectedSymptoms) {
// Navigate to triage screen
context.navigateWithName(AppRoutes.riskFactorsScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one symptom before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
}
}
void _onPreviousPressed() {
context.pop();
}
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
children: [
Expanded(
child: CollapsingListView(
title: "Symptoms Selector".needTranslation,
leadingCallback: () => _buildConfirmationBottomSheet(
context: context,
onConfirm: () => {
context.pop(),
context.pop(),
}),
child: viewModel.isBodySymptomsLoading
? _buildLoadingShimmer()
: viewModel.organSymptomsResults.isEmpty
? _buildEmptyState()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
...viewModel.organSymptomsResults.map((organResult) {
// Find matching organ ID from selected organs
String? organId;
String? organName;
for (var organ in viewModel.selectedOrgans) {
if (organ.name == organResult.name) {
organId = organ.id;
organName = organ.description;
break;
}
}
if (organId != null && organResult.bodySymptoms != null) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: _buildSymptomsSelectionCard(
viewModel,
organId,
organName ?? organResult.name ?? '',
organResult.bodySymptoms!,
),
);
}
return SizedBox.shrink();
}),
],
),
),
),
if (!viewModel.isBodySymptomsLoading) ...[
_buildStickyBottomCard(context, viewModel),
],
],
);
},
),
);
}
Widget _buildSymptomsSelectionCard(
SymptomsCheckerViewModel viewModel,
String organId,
String organName,
List<BodySymptom> symptoms,
) {
return Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Possible symptoms related to "$organName"',
style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
),
),
],
),
SizedBox(height: 24.h),
Wrap(
runSpacing: 12.h,
spacing: 8.w,
children: symptoms.map((symptom) {
final symptomId = symptom.id ?? '';
final symptomLabel = symptom.getDisplayName(false); // Pass isArabic from AppState if needed
bool isSelected = viewModel.isSymptomSelected(organId, symptomId);
return GestureDetector(
onTap: () => viewModel.toggleSymptomSelection(organId, symptomId),
child: CustomSelectableChip(
label: symptomLabel,
selected: isSelected,
activeColor: AppColors.primaryRedBorderColor,
activeTextColor: AppColors.primaryRedBorderColor,
inactiveBorderColor: AppColors.bottomNAVBorder,
inactiveTextColor: AppColors.textColor,
),
);
}).toList(),
),
],
),
);
}
Widget _buildLoadingShimmer() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Create 2-3 shimmer cards
...List.generate(3, (index) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: _buildShimmerCard(),
);
}),
],
);
}
Widget _buildShimmerCard() {
return Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Shimmer title
Container(
height: 40.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
),
).toShimmer2(isShow: true, radius: 24.r),
SizedBox(height: 16.h),
// Shimmer chips
Wrap(
runSpacing: 12.h,
spacing: 8.w,
children: List.generate(4, (index) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(color: AppColors.bottomNAVBorder, width: 1),
),
child: Text(
'Loading..',
style: TextStyle(fontSize: 14.f, color: AppColors.textColor),
),
).toShimmer2(isShow: true, radius: 24.r);
}),
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: EdgeInsets.all(24.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor),
SizedBox(height: 16.h),
Text(
'No organs selected'.needTranslation,
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
SizedBox(height: 8.h),
Text(
'Please go back and select organs first'.needTranslation,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
],
),
),
);
}
Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "Previous".needTranslation,
onPressed: _onPreviousPressed,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: "Next".needTranslation,
onPressed: () => _onNextPressed(viewModel),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0),
);
}
}

@ -0,0 +1,294 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
class TriageScreen extends StatefulWidget {
const TriageScreen({super.key});
@override
State<TriageScreen> createState() => _TriageScreenState();
}
class _TriageScreenState extends State<TriageScreen> {
late List<TriageQuestionModel> triageQuestions;
int currentQuestionIndex = 0;
@override
void initState() {
super.initState();
triageQuestions = TriageQuestionsData.getSampleTriageQuestions();
}
TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex];
bool get isFirstQuestion => currentQuestionIndex == 0;
bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1;
void _onOptionSelected(int optionIndex) {
setState(() {
currentQuestion.selectOption(optionIndex);
});
}
void _onPreviousPressed() {
if (!isFirstQuestion) {
setState(() {
currentQuestionIndex--;
});
}
}
void _onNextPressed() {
if (currentQuestion.isAnswered) {
currentQuestion.confirmSelection();
if (isLastQuestion) {
context.navigateWithName(AppRoutes.possibleConditionsScreen);
} else {
setState(() {
currentQuestionIndex++;
});
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select an option before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
}
}
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: "Triage".needTranslation,
// onLeadingTapped: () => _buildConfirmationBottomSheet(
// context: context,
// onConfirm: () => {
// context.pop(),
// context.pop(),
// }),
leadingCallback: () => context.pop(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
_buildQuestionCard(),
],
),
),
),
_buildStickyBottomCard(context),
],
),
);
}
Widget _buildQuestionCard() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
final offsetAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
));
return SlideTransition(
position: offsetAnimation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: Container(
key: ValueKey<int>(currentQuestionIndex),
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentQuestion.question,
style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.w500, color: AppColors.textColor),
),
SizedBox(height: 24.h),
...List.generate(currentQuestion.options.length, (index) {
bool selected = currentQuestion.selectedOptionIndex == index;
return _buildOptionItem(index, selected, currentQuestion.options[index].text);
}),
],
),
),
);
}
Widget _buildOptionItem(int index, bool selected, String optionText) {
return GestureDetector(
onTap: () => _onOptionSelected(index),
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: selected ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(5.r),
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w),
),
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
optionText,
style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500),
),
),
],
),
),
);
}
Widget _buildStickyBottomCard(BuildContext context) {
final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions);
final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore);
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: "Possible symptom: ".needTranslation,
style: TextStyle(
color: AppColors.greyTextColor,
fontWeight: FontWeight.w600,
fontSize: 14.f,
),
children: [
TextSpan(
text: suggestedCondition,
style: TextStyle(
color: AppColors.textColor,
fontWeight: FontWeight.w600,
fontSize: 14.f,
),
),
],
),
),
SizedBox(height: 16.h),
CustomRoundedProgressBar(
percentage: currentScore,
paddingBetween: 5.h,
color: AppColors.primaryRedColor,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17),
height: 8.h,
titleWidget: RichText(
text: TextSpan(
text: "$currentScore% ",
style: TextStyle(
color: AppColors.primaryRedColor,
fontWeight: FontWeight.w600,
fontSize: 14.f,
),
children: [
TextSpan(
text: "- Symptoms checker finding score".needTranslation,
style: TextStyle(
color: AppColors.textColor,
fontWeight: FontWeight.w500,
fontSize: 13.f,
),
),
],
),
),
),
SizedBox(height: 12.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "Previous".needTranslation,
onPressed: isFirstQuestion ? () {} : _onPreviousPressed,
isDisabled: isFirstQuestion,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation,
onPressed: _onNextPressed,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
],
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0),
);
}
}

@ -0,0 +1,274 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/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/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:provider/provider.dart';
class UserInfoSelectionScreen extends StatefulWidget {
const UserInfoSelectionScreen({super.key});
@override
State<UserInfoSelectionScreen> createState() => _UserInfoSelectionScreenState();
}
class _UserInfoSelectionScreenState extends State<UserInfoSelectionScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeUserInfo();
});
}
/// Initialize user info from appState if user is logged in
void _initializeUserInfo() {
final appState = getIt.get<AppState>();
final viewModel = context.read<SymptomsCheckerViewModel>();
if (appState.isAuthenticated) {
final user = appState.getAuthenticatedUser();
if (user == null) return;
// Populate gender (gender is int: 1=Male, 2=Female)
if (user.gender != null) {
String genderStr = user.gender == 1
? "Male"
: user.gender == 2
? "Female"
: "Other";
viewModel.setGender(genderStr);
}
if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) {
try {
DateTime dob = DateTime.parse(user.dateofBirth!);
viewModel.setDateOfBirth(dob);
} catch (e) {
// If date parsing fails, ignore and let user fill manually
}
}
// Note: AuthenticatedUser doesn't have height/weight fields
// User will need to fill these manually
}
// If not authenticated or fields are empty, user will fill them manually
}
_buildEditInfoTile({
required String leadingIcon,
required String title,
required String subTitle,
required VoidCallback onTap,
required String trailingIcon,
required BuildContext context,
Color? iconColor,
}) {
return InkWell(
onTap: onTap,
child: Row(
children: [
Expanded(
child: Row(
children: [
Container(
height: 40.h,
width: 40.h,
margin: EdgeInsets.only(right: 10.h),
padding: EdgeInsets.all(8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.greyColor),
child: Utils.buildSvgWithAssets(icon: leadingIcon, iconColor: iconColor)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title.toText16(weight: FontWeight.w500),
subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500),
],
),
],
),
),
Utils.buildSvgWithAssets(icon: trailingIcon, height: 24.h, width: 24.h),
],
),
);
}
Widget _getDivider() {
return Divider(
color: AppColors.dividerColor,
).paddingSymmetrical(0, 16.h);
}
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
String name = "";
if (appState.isAuthenticated) {
name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} ";
} else {
name = "Guest";
}
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, child) {
// Check if any field is empty
bool hasEmptyFields = viewModel.selectedGender == null ||
viewModel.selectedAge == null ||
viewModel.selectedHeight == null ||
viewModel.selectedWeight == null;
// Get display values
String genderText = viewModel.selectedGender ?? "Not set";
// Show age calculated from DOB, not the DOB itself
String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set";
String heightText =
viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set";
String weightText =
viewModel.selectedWeight != null ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" : "Not set";
return Column(
children: [
Expanded(
child: CollapsingListView(
title: "Symptoms Checker".needTranslation,
isLeading: true,
child: SingleChildScrollView(
child: Column(
children: [
Container(
width: double.infinity,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
child: Column(
children: [
"Hello $name, Is your information up to date?".needTranslation.toText18(
weight: FontWeight.w600,
color: AppColors.textColor,
),
SizedBox(height: 24.h),
_buildEditInfoTile(
context: context,
leadingIcon: AppAssets.genderIcon,
title: "Gender".needTranslation,
subTitle: genderText,
onTap: () {
viewModel.setUserInfoPage(0, isSinglePageEdit: true);
context.navigateWithName(AppRoutes.userInfoFlowManager);
},
trailingIcon: AppAssets.edit_icon,
),
_getDivider(),
_buildEditInfoTile(
context: context,
leadingIcon: AppAssets.calendarGrey,
title: "Age".needTranslation,
subTitle: ageText,
iconColor: AppColors.greyTextColor,
onTap: () {
viewModel.setUserInfoPage(1, isSinglePageEdit: true);
context.navigateWithName(AppRoutes.userInfoFlowManager);
},
trailingIcon: AppAssets.edit_icon,
),
_getDivider(),
_buildEditInfoTile(
context: context,
leadingIcon: AppAssets.rulerIcon,
title: "Height".needTranslation,
subTitle: heightText,
onTap: () {
viewModel.setUserInfoPage(2, isSinglePageEdit: true);
context.navigateWithName(AppRoutes.userInfoFlowManager);
},
trailingIcon: AppAssets.edit_icon,
),
_getDivider(),
_buildEditInfoTile(
context: context,
leadingIcon: AppAssets.weightScale,
title: "Weight".needTranslation,
subTitle: weightText,
onTap: () {
viewModel.setUserInfoPage(3, isSinglePageEdit: true);
context.navigateWithName(AppRoutes.userInfoFlowManager);
},
trailingIcon: AppAssets.edit_icon,
),
],
),
),
],
).paddingAll(24.w),
),
),
),
_buildBottomCard(context, hasEmptyFields),
],
);
},
),
);
}
Widget _buildBottomCard(BuildContext context, bool hasEmptyFields) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "No, Edit all".needTranslation,
icon: AppAssets.edit_icon,
iconColor: AppColors.primaryRedColor,
onPressed: () {
context.read<SymptomsCheckerViewModel>().setUserInfoPage(0, isSinglePageEdit: false);
context.navigateWithName(AppRoutes.userInfoFlowManager);
},
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: "Yes, It is".needTranslation,
icon: AppAssets.tickIcon,
iconColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor,
onPressed: hasEmptyFields
? () {} // Empty function for disabled state
: () => context.navigateWithName(AppRoutes.organSelectorPage),
backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor,
borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor,
textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
],
).paddingSymmetrical(24.w, 0),
),
);
}
}

@ -0,0 +1,47 @@
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:hmg_patient_app_new/core/app_export.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/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
/// Age selection page content
class AgeSelectionPage extends StatelessWidget {
final int? selectedAge;
final Function(int) onAgeSelected;
const AgeSelectionPage({
super.key,
required this.selectedAge,
required this.onAgeSelected,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Consumer<SymptomsCheckerViewModel>(
builder: (BuildContext context, symptomsViewModel, Widget? child) {
return Column(
children: [
"What is your Date of Birth?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor).paddingAll(24.w),
SizedBox(height: 30.h),
ThreeColumnDatePicker(
enableHaptic: true,
enableSound: true,
initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1),
onDateChanged: (date) {
symptomsViewModel.setDateOfBirth(date);
log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}');
},
)
],
);
},
),
);
}
}

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/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/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
/// Gender selection page content
class GenderSelectionPage extends StatelessWidget {
final String? selectedGender;
final Function(String) onGenderSelected;
GenderSelectionPage({
super.key,
required this.selectedGender,
required this.onGenderSelected,
});
_buildGenderOption(String iconPng, String label, bool isSelected) {
return Container(
height: 160.h,
width: 160.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, borderRadius: 24.r, side: isSelected ? BorderSide(color: AppColors.primaryRedColor, width: 2.5) : null),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Utils.buildImgWithAssets(icon: iconPng, height: 80.h, width: 80.h, fit: BoxFit.contain),
SizedBox(height: 8.h),
label.toText16(
weight: FontWeight.w500,
)
],
),
);
}
final genders = ["Male", "Female"];
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: EdgeInsets.all(24.w),
child: Consumer<SymptomsCheckerViewModel>(
builder: (BuildContext context, symptomsViewModel, Widget? child) {
return Column(
children: [
"What is your gender?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor),
SizedBox(height: 70.h),
Row(
children: [
Expanded(
child: InkWell(
onTap: () => onGenderSelected(genders[0]),
child: _buildGenderOption(AppAssets.maleIcon, "Male".needTranslation, symptomsViewModel.selectedGender == genders[0]),
),
),
SizedBox(width: 16.w),
Expanded(
child: InkWell(
onTap: () => onGenderSelected(genders[1]),
child: _buildGenderOption(AppAssets.femaleIcon, "Female".needTranslation, symptomsViewModel.selectedGender == genders[1]),
))
],
),
],
);
},
),
);
}
}

@ -0,0 +1,174 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.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/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
/// Height selection page content
class HeightSelectionPage extends StatelessWidget {
const HeightSelectionPage({super.key});
Widget _unitSelector(SymptomsCheckerViewModel viewModel) {
return Container(
height: 54.h,
padding: EdgeInsets.all(4.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(10.r),
),
child: LayoutBuilder(
builder: (context, constraints) {
final tabWidth = (constraints.maxWidth - 8.w) / 2;
return Stack(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
width: tabWidth,
height: constraints.maxHeight,
margin: EdgeInsets.only(left: viewModel.isHeightCm ? 0 : tabWidth + 8.w),
decoration: BoxDecoration(
color: AppColors.bottomNAVBorder,
borderRadius: BorderRadius.circular(7.r),
),
),
// Tab buttons
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (!viewModel.isHeightCm) {
final convertedHeight = viewModel.selectedHeight! * 30.48;
viewModel.setHeight(convertedHeight, true);
}
},
child: Container(
alignment: Alignment.center,
color: Colors.transparent,
child: Text(
'CM',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14.f,
color: viewModel.isHeightCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
if (viewModel.isHeightCm) {
final convertedHeight = viewModel.selectedHeight! / 30.48;
viewModel.setHeight(convertedHeight, false);
}
},
child: Container(
alignment: Alignment.center,
color: Colors.transparent,
child: Text(
'FT',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14.f,
color: !viewModel.isHeightCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6),
),
),
),
),
),
],
),
],
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, child) {
// Define min/max values based on unit
final minValue = viewModel.isHeightCm ? 50.0 : 1.6; // 50cm or 1.6ft (approx. 1'7")
final maxValue = viewModel.isHeightCm ? 280.0 : 9.2; // 280cm or 9.2ft (approx. 9'2")
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
Center(
child: Text(
'How tall are you?'.needTranslation,
style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
),
),
SizedBox(height: 24.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _unitSelector(viewModel),
),
SizedBox(height: 20.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Spacer(flex: 1),
Expanded(
flex: 3,
child: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, child) {
return Text.rich(
TextSpan(
children: [
TextSpan(
text:
viewModel.isHeightCm ? viewModel.selectedHeight?.round().toString() : viewModel.selectedHeight?.toStringAsFixed(1),
style: TextStyle(
fontSize: 90.f,
color: AppColors.textColor,
height: 1,
),
),
TextSpan(
text: viewModel.isHeightCm ? 'cm' : 'ft',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 24.f,
),
),
],
),
).paddingOnly(bottom: 100.h, left: 20.w);
},
),
),
Expanded(
child: HeightScale(
enableHaptic: true,
enableSound: true,
minValue: minValue,
maxValue: maxValue,
initialHeight: viewModel.selectedHeight ?? 100,
isCm: viewModel.isHeightCm,
onHeightChanged: (newHeight) {
log("height: $newHeight");
viewModel.setHeight(newHeight, viewModel.isHeightCm);
},
),
),
],
),
],
);
},
);
}
}

@ -0,0 +1,162 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.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/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
/// Weight selection page content
class WeightSelectionPage extends StatelessWidget {
final Function(double, bool) onWeightChanged;
const WeightSelectionPage({super.key, required this.onWeightChanged});
Widget _unitSelector(SymptomsCheckerViewModel viewModel) {
bool isKg = viewModel.isWeightKg;
return Container(
height: 54.h,
padding: EdgeInsets.all(4.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(10.r),
),
child: LayoutBuilder(
builder: (context, constraints) {
final tabWidth = (constraints.maxWidth - 8.w) / 2;
return Stack(
children: [
// Animated sliding indicator
AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
width: tabWidth,
height: constraints.maxHeight,
margin: EdgeInsets.only(left: isKg ? 0 : tabWidth + 8.w),
decoration: BoxDecoration(
color: AppColors.bottomNAVBorder,
borderRadius: BorderRadius.circular(7.r),
),
),
// Tab buttons
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (!isKg) {
// Convert from LBS to KG (1 lb = 0.453592 kg)
final convertedWeight = viewModel.selectedWeight! / 2.20462;
Future.microtask(() => onWeightChanged(convertedWeight, true));
}
},
child: Container(
alignment: Alignment.center,
color: Colors.transparent,
child: Text(
'KG',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14.f,
color: isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
if (isKg) {
// Convert from KG to LBS (1 kg = 2.20462 lbs)
final convertedWeight = viewModel.selectedWeight! * 2.20462;
Future.microtask(() => onWeightChanged(convertedWeight, false));
}
},
child: Container(
alignment: Alignment.center,
color: Colors.transparent,
child: Text(
'LBS',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14.f,
color: !isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6),
),
),
),
),
),
],
),
],
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Consumer<SymptomsCheckerViewModel>(builder: (context, viewModel, child) {
bool isKg = viewModel.isWeightKg;
// Define min/max values based on unit
final minValue = isKg ? 10.0 : 22.0; // 10kg or 22lbs
final maxValue = isKg ? 200.0 : 440.0; // 200kg or 440lbs
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
Center(
child: Text(
'What is your weight?'.needTranslation,
style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
),
),
SizedBox(height: 24.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _unitSelector(viewModel),
),
SizedBox(height: 60.h),
// Weight display centered
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
viewModel.selectedWeight!.round().toString(),
style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1),
),
SizedBox(width: 8.w),
Text(
isKg ? 'kg' : 'lbs',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f),
).paddingOnly(bottom: 10.h),
],
),
),
SizedBox(height: 40.h),
// Horizontal weight picker
WeightScale(
enableHaptic: true,
enableSound: true,
minValue: minValue,
maxValue: maxValue,
initialWeight: viewModel.selectedWeight!,
isKg: isKg,
onWeightChanged: (newWeight) {
log("weight: $newWeight");
Future.microtask(() => onWeightChanged(newWeight, isKg));
},
),
],
);
});
}
}

@ -0,0 +1,276 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.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/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:provider/provider.dart';
/// Manages the user info selection flow with PageView
/// Only the page content changes, header and footer remain fixed
class UserInfoFlowManager extends StatefulWidget {
const UserInfoFlowManager({super.key});
@override
State<UserInfoFlowManager> createState() => _UserInfoFlowManagerState();
}
class _UserInfoFlowManagerState extends State<UserInfoFlowManager> {
late PageController _pageController;
late SymptomsCheckerViewModel _viewModel;
// Page titles
final List<String> _pageTitles = [
"Your Gender",
"Your Birth Date",
"Your Height",
"Your Weight",
];
@override
void initState() {
super.initState();
_viewModel = context.read<SymptomsCheckerViewModel>();
// Initialize PageController with the current page from ViewModel
_pageController = PageController(initialPage: _viewModel.userInfoCurrentPage);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _onNext() {
// If in single page edit mode, just save and go back
if (_viewModel.isSinglePageEditMode) {
context.pop();
return;
}
// Otherwise, continue with normal flow
if (_viewModel.userInfoCurrentPage < 3) {
_viewModel.nextUserInfoPage();
_pageController.animateToPage(
_viewModel.userInfoCurrentPage,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
_submitUserInfo();
}
}
void _onPrevious() {
if (_viewModel.userInfoCurrentPage > 0) {
_viewModel.previousUserInfoPage();
_pageController.animateToPage(
_viewModel.userInfoCurrentPage,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
context.pop();
}
}
void _submitUserInfo() {
final userInfo = _viewModel.getUserInfoData();
// Log user info
log('User Info Submitted:');
log('Gender: ${userInfo['gender']}');
log('Age: ${userInfo['age']}');
log('Height: ${userInfo['height']} ${userInfo['heightUnit']}');
log('Weight: ${userInfo['weight']} ${userInfo['weightUnit']}');
// TODO: Save user info to backend/storage
// Navigate to symptoms checker or next screen
context.pop();
}
Widget _buildProgressBar() {
return Consumer<SymptomsCheckerViewModel>(
builder: (BuildContext context, viewModel, child) {
return Row(
children: List.generate(4, (index) {
final isCompleted = index < viewModel.userInfoCurrentPage;
final isCurrentStep = index == viewModel.userInfoCurrentPage;
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6.w),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: Stack(
children: [
// Background (grey)
Container(
height: 4.h,
decoration: BoxDecoration(
color: AppColors.greyLightColor,
borderRadius: BorderRadius.circular(8.r),
),
),
// Animated red fill from left to right
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 0.0,
end: isCompleted
? 1.0
: isCurrentStep
? 1.0
: 0.0,
),
builder: (context, value, child) {
return FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: value,
child: Container(
height: 4.h,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
borderRadius: BorderRadius.circular(8.r),
boxShadow: isCurrentStep && value > 0
? [
BoxShadow(
color: AppColors.primaryRedColor.withValues(alpha: 0.4),
blurRadius: 8.r,
spreadRadius: 1.r,
),
]
: null,
),
),
);
},
),
],
),
),
),
);
}),
);
},
);
}
Widget _buildStickyBottomCard() {
return Consumer<SymptomsCheckerViewModel>(builder: (BuildContext context, viewModel, child) {
bool isLastPage = viewModel.isUserInfoLastPage;
bool isFirstPage = viewModel.isUserInfoFirstPage;
bool isSingleEdit = viewModel.isSinglePageEditMode;
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)),
),
padding: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h),
child: SafeArea(
top: false,
child: isSingleEdit
? // Single page edit mode - show only Save button
CustomButton(
text: "Save".needTranslation,
onPressed: _onNext,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
)
: // Complete flow mode - show Previous/Next buttons
Row(
children: [
if (!isFirstPage) ...[
Expanded(
child: CustomButton(
text: "Previous".needTranslation,
onPressed: _onPrevious,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
),
),
SizedBox(width: 12.w),
],
Expanded(
child: CustomButton(
text: isLastPage ? "Submit".needTranslation : "Next".needTranslation,
onPressed: _onNext,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
physics: NeverScrollableScrollPhysics(),
title: _pageTitles[_viewModel.userInfoCurrentPage].needTranslation,
isLeading: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Only show progress bar in complete flow mode
if (!_viewModel.isSinglePageEditMode) _buildProgressBar(),
if (!_viewModel.isSinglePageEditMode) SizedBox(height: 24.h),
SizedBox(
height: 600.h,
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(), // Disable swipe
onPageChanged: (index) {
_viewModel.setUserInfoPage(index);
},
children: [
GenderSelectionPage(
selectedGender: _viewModel.selectedGender,
onGenderSelected: _viewModel.setGender,
),
AgeSelectionPage(
selectedAge: _viewModel.selectedAge,
onAgeSelected: _viewModel.setAge,
),
HeightSelectionPage(),
WeightSelectionPage(onWeightChanged: _viewModel.setWeight),
],
),
),
],
),
),
),
_buildStickyBottomCard(),
],
),
);
}
}

@ -0,0 +1,235 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class ThreeColumnDatePicker extends StatefulWidget {
final DateTime initialDate;
final ValueChanged<DateTime>? onDateChanged;
// Feedback config
final bool enableHaptic;
final bool enableSound;
final Duration feedbackDebounce;
const ThreeColumnDatePicker({
super.key,
required this.initialDate,
this.onDateChanged,
this.enableHaptic = true,
this.enableSound = true,
this.feedbackDebounce = const Duration(milliseconds: 80),
});
@override
State<ThreeColumnDatePicker> createState() => _ThreeColumnDatePickerState();
}
class _ThreeColumnDatePickerState extends State<ThreeColumnDatePicker> {
static const int yearRange = 100;
static const double _defaultItemExtent = 48.0; // will be scaled with .h
late final List<String> _days;
late final List<String> _months;
late final List<int> _years;
late FixedExtentScrollController _dayController;
late FixedExtentScrollController _monthController;
late FixedExtentScrollController _yearController;
int _selectedDay = 0;
int _selectedMonth = 0;
int _selectedYearIndex = 0;
// Debounce timer used for playing feedback only after small pause
Timer? _feedbackTimer;
double get _itemExtent => _defaultItemExtent.h;
@override
void initState() {
super.initState();
_days = List.generate(31, (i) => (i + 1).toString().padLeft(2, '0'));
_months = const ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
final currentYear = DateTime.now().year;
_years = List.generate(yearRange, (i) => currentYear - i);
_selectedDay = (widget.initialDate.day - 1).clamp(0, _days.length - 1);
_selectedMonth = (widget.initialDate.month - 1).clamp(0, _months.length - 1);
_selectedYearIndex = _years.indexOf(widget.initialDate.year);
if (_selectedYearIndex == -1) _selectedYearIndex = 0;
_dayController = FixedExtentScrollController(initialItem: _selectedDay);
_monthController = FixedExtentScrollController(initialItem: _selectedMonth);
_yearController = FixedExtentScrollController(initialItem: _selectedYearIndex);
}
@override
void dispose() {
_feedbackTimer?.cancel();
_dayController.dispose();
_monthController.dispose();
_yearController.dispose();
super.dispose();
}
void _emitDate() {
final day = int.parse(_days[_selectedDay]);
final month = _selectedMonth + 1;
final year = _years[_selectedYearIndex];
final date = DateTime(year, month, day);
widget.onDateChanged?.call(date);
}
// Schedule haptic + sound feedback with debounce (prevents spamming during fling)
void _scheduleFeedback() {
if (!(widget.enableHaptic || widget.enableSound)) return;
_feedbackTimer?.cancel();
_feedbackTimer = Timer(widget.feedbackDebounce, () {
// Haptic
if (widget.enableHaptic) {
// selection click is lightweight and appropriate for wheel ticks
HapticFeedback.selectionClick();
}
// Sound
if (widget.enableSound) {
// simple system click - note: may be muted by device settings
SystemSound.play(SystemSoundType.click);
}
});
}
Widget _wheel({
required FixedExtentScrollController controller,
required int itemCount,
required Widget Function(int index, bool selected) itemBuilder,
required ValueChanged<int> onSelectedItemChanged,
required int currentlySelectedIndex,
}) {
return Expanded(
child: SizedBox(
height: _itemExtent * 5, // show ~5 rows
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: _itemExtent,
physics: const BouncingScrollPhysics(),
diameterRatio: 2.2,
squeeze: 1.2,
perspective: 0.004,
// overAndUnderCenterOpacity: 0.6,
onSelectedItemChanged: (i) {
// update selected index, emit date and schedule feedback
onSelectedItemChanged(i);
_scheduleFeedback();
},
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
if (index < 0 || index >= itemCount) return null;
final bool selected = index == currentlySelectedIndex;
return Center(child: itemBuilder(index, selected));
},
childCount: itemCount,
),
),
),
);
}
Widget _styledText(String text, bool selected) {
return Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: selected ? 22.f : 20.f,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
color: selected ? AppColors.textColor : AppColors.greyTextColor.withValues(alpha: 0.9),
height: 1.0,
letterSpacing: selected ? -0.02 * 30 : -0.02 * 18,
),
);
}
@override
Widget build(BuildContext context) {
final pickerHeight = _itemExtent * 5;
final pointerSize = 20.w;
final pointerTop = (pickerHeight / 2) - (pointerSize / 2);
return LayoutBuilder(builder: (context, constraints) {
return SizedBox(
height: pickerHeight,
child: Stack(
children: [
Row(
children: [
// Day wheel
_wheel(
controller: _dayController,
itemCount: _days.length,
currentlySelectedIndex: _selectedDay,
onSelectedItemChanged: (i) {
setState(() => _selectedDay = i);
_emitDate();
},
itemBuilder: (index, selected) => _styledText(_days[index], selected),
),
// Month wheel
_wheel(
controller: _monthController,
itemCount: _months.length,
currentlySelectedIndex: _selectedMonth,
onSelectedItemChanged: (i) {
setState(() => _selectedMonth = i);
_emitDate();
},
itemBuilder: (index, selected) => _styledText(_months[index], selected),
),
// Year wheel
_wheel(
controller: _yearController,
itemCount: _years.length,
currentlySelectedIndex: _selectedYearIndex,
onSelectedItemChanged: (i) {
setState(() => _selectedYearIndex = i);
_emitDate();
},
itemBuilder: (index, selected) => _styledText(_years[index].toString(), selected),
),
],
),
// subtle center overlay (optional keeps layout consistent & highlights center row)
Positioned.fill(
child: IgnorePointer(
child: Center(
child: SizedBox(
height: _itemExtent,
),
),
),
),
// left red triangular pointer aligned to center row
Positioned(
left: 0.w,
top: pointerTop,
child: TriangleIndicator(
pointerSize: pointerSize,
// your TriangleIndicator supports direction param; use left as in the original
// if your TriangleIndicator doesn't accept direction, remove the param
direction: TriangleDirection.left,
),
),
],
),
);
});
}
}

@ -0,0 +1,169 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class HeightScale extends StatefulWidget {
final double minValue;
final double maxValue;
final double initialHeight;
final bool isCm;
final ValueChanged<double>? onHeightChanged;
// Feedback config
final bool enableHaptic;
final bool enableSound;
final Duration feedbackDebounce;
const HeightScale({
super.key,
required this.minValue,
required this.maxValue,
required this.initialHeight,
required this.isCm,
this.onHeightChanged,
this.enableHaptic = true,
this.enableSound = true,
this.feedbackDebounce = const Duration(milliseconds: 80),
});
@override
State<HeightScale> createState() => _HeightScaleState();
}
class _HeightScaleState extends State<HeightScale> {
late FixedExtentScrollController _scrollController;
// Debounce timer used for playing feedback only after small pause
Timer? _feedbackTimer;
// Get increment based on unit (CM = 1.0, FT = 0.1)
double get _increment => widget.isCm ? 1.0 : 0.1;
@override
void initState() {
super.initState();
int initialIndex = ((widget.initialHeight - widget.minValue) / _increment).round();
_scrollController = FixedExtentScrollController(initialItem: initialIndex);
}
// Schedule haptic + sound feedback with debounce (prevents spamming during fling)
void _scheduleFeedback() {
if (!(widget.enableHaptic || widget.enableSound)) return;
_feedbackTimer?.cancel();
_feedbackTimer = Timer(widget.feedbackDebounce, () {
// Haptic
if (widget.enableHaptic) {
// selection click is lightweight and appropriate for wheel ticks
HapticFeedback.selectionClick();
}
// Sound
if (widget.enableSound) {
// simple system click - note: may be muted by device settings
SystemSound.play(SystemSoundType.click);
}
});
}
@override
void dispose() {
_feedbackTimer?.cancel();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final pointerSize = 20.w;
final pickerHeight = 300.h;
final pointerTop = (pickerHeight / 2) - (pointerSize / 2);
return SizedBox(
height: pickerHeight,
child: Stack(
children: [
// Scrollable wheel picker
ListWheelScrollView.useDelegate(
controller: _scrollController,
itemExtent: 10.h,
diameterRatio: 2.0,
squeeze: 1.2,
perspective: 0.001,
physics: const BouncingScrollPhysics(),
onSelectedItemChanged: (index) {
final selectedValue = widget.minValue + (index * _increment);
widget.onHeightChanged?.call(selectedValue);
_scheduleFeedback();
},
childDelegate: ListWheelChildBuilderDelegate(
childCount: ((widget.maxValue - widget.minValue) / _increment).round() + 1,
builder: (context, index) {
final height = widget.minValue + (index * _increment);
// For CM: main mark every 10, mid mark every 5
// For FT: main mark every 1.0 (10 ticks), mid mark every 0.5 (5 ticks)
final isMainMark = widget.isCm ? height % 10 == 0 : (height * 10).round() % 10 == 0;
final isMidMark = widget.isCm ? height % 5 == 0 : (height * 10).round() % 5 == 0;
return SizedBox(
width: 100.w,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Number label for main marks
if (isMainMark)
SizedBox(
width: 30.w,
child: Text(
widget.isCm ? height.round().toString() : height.toStringAsFixed(1),
style: TextStyle(
fontSize: 11.f,
color: AppColors.greyTextColor,
fontWeight: FontWeight.w500,
height: 1,
),
textAlign: TextAlign.right,
),
)
else
SizedBox(width: 30.w),
SizedBox(width: 4.w),
// Ruler mark
Container(
width: isMainMark
? 40.w
: isMidMark
? 30.w
: 25.w,
height: isMainMark || isMidMark ? 2.5.h : 1.5.h,
decoration: BoxDecoration(
color: isMainMark
? AppColors.textColor
: isMidMark
? AppColors.textColorLight
: AppColors.textColorLight.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(2.r),
),
),
],
),
);
},
),
),
// Triangle indicator pointing to selected value
Positioned(
right: 0,
top: pointerTop,
child: TriangleIndicator(pointerSize: pointerSize),
),
],
),
);
}
}

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
enum TriangleDirection { left, right, up, down }
class TriangleIndicator extends StatelessWidget {
final double pointerSize;
final Color? color;
final TriangleDirection direction;
const TriangleIndicator({
super.key,
required this.pointerSize,
this.color,
this.direction = TriangleDirection.right,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(pointerSize, pointerSize),
painter: _TrianglePainter(
color: color ?? AppColors.primaryRedColor,
direction: direction,
),
);
}
}
class _TrianglePainter extends CustomPainter {
final Color color;
final TriangleDirection direction;
_TrianglePainter({required this.color, required this.direction});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final path = Path();
final w = size.width;
final h = size.height;
switch (direction) {
case TriangleDirection.right:
// apex on the right, base on the left
path.moveTo(0, h / 2);
path.lineTo(w, 0);
path.lineTo(w, h);
path.close();
break;
case TriangleDirection.left:
// apex on the left, base on the right
path.moveTo(w, h / 2);
path.lineTo(0, 0);
path.lineTo(0, h);
path.close();
break;
case TriangleDirection.up:
// apex on top, base on bottom
path.moveTo(w / 2, 0);
path.lineTo(0, h);
path.lineTo(w, h);
path.close();
break;
case TriangleDirection.down:
// apex on bottom, base on top
path.moveTo(w / 2, h);
path.lineTo(0, 0);
path.lineTo(w, 0);
path.close();
break;
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _TrianglePainter oldDelegate) {
return oldDelegate.color != color || oldDelegate.direction != direction;
}
}

@ -0,0 +1,180 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class WeightScale extends StatefulWidget {
final double minValue;
final double maxValue;
final double initialWeight;
final bool isKg;
final ValueChanged<double>? onWeightChanged;
// Feedback config
final bool enableHaptic;
final bool enableSound;
final Duration feedbackDebounce;
const WeightScale({
super.key,
required this.minValue,
required this.maxValue,
required this.initialWeight,
required this.isKg,
this.onWeightChanged,
this.enableHaptic = true,
this.enableSound = true,
this.feedbackDebounce = const Duration(milliseconds: 80),
});
@override
State<WeightScale> createState() => _WeightScaleState();
}
class _WeightScaleState extends State<WeightScale> {
late ScrollController _scrollController;
// Debounce timer used for playing feedback only after small pause
Timer? _feedbackTimer;
int? _lastReportedIndex;
final double _itemWidth = 8.0; // Width per weight unit
@override
void initState() {
super.initState();
int initialIndex = (widget.initialWeight - widget.minValue).round();
final initialOffset = initialIndex * _itemWidth;
_scrollController = ScrollController(initialScrollOffset: initialOffset);
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (!_scrollController.hasClients) return;
final offset = _scrollController.offset;
final index = (offset / _itemWidth).round();
final maxIndex = (widget.maxValue - widget.minValue).round();
if (index != _lastReportedIndex && index >= 0 && index <= maxIndex) {
_lastReportedIndex = index;
final selectedValue = widget.minValue + index;
widget.onWeightChanged?.call(selectedValue);
_scheduleFeedback();
}
}
// Schedule haptic + sound feedback with debounce (prevents spamming during fling)
void _scheduleFeedback() {
if (!(widget.enableHaptic || widget.enableSound)) return;
_feedbackTimer?.cancel();
_feedbackTimer = Timer(widget.feedbackDebounce, () {
// Haptic
if (widget.enableHaptic) {
// selection click is lightweight and appropriate for wheel ticks
HapticFeedback.selectionClick();
}
// Sound
if (widget.enableSound) {
// simple system click - note: may be muted by device settings
SystemSound.play(SystemSoundType.click);
}
});
}
@override
void dispose() {
_feedbackTimer?.cancel();
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final pointerSize = 20.h;
final pickerHeight = 100.h;
final itemCount = (widget.maxValue - widget.minValue).round() + 1;
return SizedBox(
height: pickerHeight,
child: Stack(
alignment: Alignment.center,
children: [
// Horizontal scrollable ruler with gradient fade
ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width / 2),
itemCount: itemCount,
itemBuilder: (context, index) {
final weight = (widget.minValue + index).round();
final isMainMark = weight % 10 == 0;
final isMidMark = weight % 5 == 0;
return SizedBox(
width: _itemWidth,
child: Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
// Ruler mark (vertical line)
Positioned(
bottom: 0,
child: Container(
width: isMainMark || isMidMark ? 2.5.w : 1.5.w,
height: isMainMark
? 40.h
: isMidMark
? 30.h
: 25.h,
decoration: BoxDecoration(
color: isMainMark
? AppColors.textColor
: isMidMark
? AppColors.textColorLight
: AppColors.textColorLight.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(2.r),
),
),
),
// Number label for main marks
if (isMainMark)
Positioned(
bottom: 45.h,
child: Text(
weight.toString(),
style: TextStyle(
fontSize: 11.f,
color: AppColors.greyTextColor,
fontWeight: FontWeight.w500,
height: 1,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.visible,
),
),
],
),
);
},
),
// Triangle indicator pointing to selected value
Positioned(
bottom: 0,
child: TriangleIndicator(
pointerSize: pointerSize,
direction: TriangleDirection.up,
),
),
],
),
);
}
}

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/enums.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/symptoms_checker/widgets/custom_progress_bar.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';
class ConditionCard extends StatelessWidget {
final IconData icon;
final String title;
final int percentage;
final String tagText;
final String clinic;
final List<String> symptoms;
final String description;
final String? monitorNote;
final String? appointmentLabel;
final PossibleConditionsSeverityEnum possibleConditionsSeverityEnum;
final VoidCallback? onActionPressed;
const ConditionCard({
super.key,
required this.icon,
required this.title,
required this.percentage,
required this.tagText,
required this.clinic,
required this.symptoms,
required this.description,
this.monitorNote,
this.appointmentLabel,
required this.possibleConditionsSeverityEnum,
this.onActionPressed,
});
Color getChipColorBySeverityEnum(PossibleConditionsSeverityEnum possibleConditionsSeverityEnum) {
switch (possibleConditionsSeverityEnum) {
case PossibleConditionsSeverityEnum.seekMedicalAdvice:
return AppColors.chipColorSeekMedicalAdvice;
case PossibleConditionsSeverityEnum.monitorOnly:
return AppColors.chipColorMonitor;
case PossibleConditionsSeverityEnum.emergency:
return AppColors.chipColorEmergency;
}
}
Color getChipTextColorBySeverityEnum(PossibleConditionsSeverityEnum possibleConditionsSeverityEnum) {
switch (possibleConditionsSeverityEnum) {
case PossibleConditionsSeverityEnum.seekMedicalAdvice:
return AppColors.chipTextColorSeekMedicalAdvice;
case PossibleConditionsSeverityEnum.monitorOnly:
return AppColors.chipColorMonitor;
case PossibleConditionsSeverityEnum.emergency:
return AppColors.chipColorEmergency;
}
}
_buildSymptomsRow() {
return Wrap(
spacing: 6.w,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (int i = 0; i < symptoms.length; i++) ...[
Text(symptoms[i], style: TextStyle(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, fontSize: 12.f)),
if (i != symptoms.length - 1)
Padding(
padding: EdgeInsets.symmetric(horizontal: 2.w),
child: Icon(Icons.circle, size: 7, color: AppColors.textColor),
),
],
],
);
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 48.w,
height: 48.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum).withValues(alpha: 0.2),
borderRadius: 12.r,
hasShadow: false,
),
child: Icon(icon, color: getChipTextColorBySeverityEnum(possibleConditionsSeverityEnum), size: 24.f),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
title,
style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
overflow: TextOverflow.ellipsis,
),
),
AppCustomChipWidget(
labelText: tagText,
backgroundColor: getChipColorBySeverityEnum(possibleConditionsSeverityEnum).withValues(alpha: 0.2),
textColor: getChipTextColorBySeverityEnum(possibleConditionsSeverityEnum),
),
],
),
CustomRoundedProgressBar(
percentage: percentage,
height: 6.h,
color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum),
backgroundColor: AppColors.scaffoldBgColor,
titleWidget: Row(
children: [
Text(
"$percentage%",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12.f,
color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum),
),
),
],
).paddingSymmetrical(0, 4.h),
),
],
),
),
],
).paddingAll(16.w),
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
clinic,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14.f,
color: AppColors.textColor,
),
),
_buildSymptomsRow(),
SizedBox(height: 16.h),
Text("Description".needTranslation, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14.f, color: AppColors.textColor)),
SizedBox(height: 2.h),
Text(description, style: TextStyle(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, fontSize: 12.f)),
if (possibleConditionsSeverityEnum == PossibleConditionsSeverityEnum.emergency)
CustomButton(
text: appointmentLabel ?? "Book Appointment".needTranslation,
onPressed: () {
if (onActionPressed != null) {
onActionPressed!();
}
},
backgroundColor: AppColors.lightRedButtonColor,
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
fontSize: 16.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 48.h,
icon: AppAssets.add_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 18.h,
).paddingOnly(top: 16.w),
if (monitorNote != null)
Container(
margin: EdgeInsets.only(top: 12.h),
child: AppCustomChipWidget(
labelText: monitorNote!,
backgroundColor: AppColors.whiteColor,
textColor: AppColors.textColor,
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)),
),
),
],
).paddingAll(16.w),
],
),
);
}
}

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
class CustomRoundedProgressBar extends StatelessWidget {
final int percentage;
final Color color;
final Color backgroundColor;
final double? height;
final double? paddingBetween;
final Widget? titleWidget;
const CustomRoundedProgressBar({
super.key,
this.titleWidget,
required this.percentage,
this.paddingBetween,
required this.color,
required this.backgroundColor,
this.height,
});
@override
Widget build(BuildContext context) {
final h = height ?? 8.h;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (titleWidget != null) ...[
titleWidget!,
if (paddingBetween != null) ...[
SizedBox(height: paddingBetween),
]
],
LayoutBuilder(
builder: (context, constraints) {
double radius = h / 2;
return Container(
height: h,
width: constraints.maxWidth,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(radius),
),
child: Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: (percentage.clamp(0, 100)) / 100,
child: Container(
height: height,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.horizontal(
left: Radius.circular(radius),
right: Radius.circular(radius),
),
),
),
),
),
);
},
),
],
);
}
}

@ -0,0 +1,134 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart';
class InteractiveBodyWidget extends StatefulWidget {
final String bodyImageAsset;
final List<OrganModel> organs;
final Set<String> selectedOrganIds;
final bool isBodyHidden;
final Function(String organId) onOrganTap;
final String? tooltipOrganId;
const InteractiveBodyWidget({
super.key,
required this.bodyImageAsset,
required this.organs,
required this.selectedOrganIds,
required this.onOrganTap,
this.isBodyHidden = false,
this.tooltipOrganId,
});
@override
State<InteractiveBodyWidget> createState() => _InteractiveBodyWidgetState();
}
class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
double? _imageAspectRatio;
final TransformationController _transformationController = TransformationController();
@override
void initState() {
super.initState();
_loadImageAspectRatio();
}
@override
void didUpdateWidget(InteractiveBodyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.bodyImageAsset != widget.bodyImageAsset) {
_loadImageAspectRatio();
// Reset zoom when switching views
_transformationController.value = Matrix4.identity();
}
}
@override
void dispose() {
_transformationController.dispose();
super.dispose();
}
Future<void> _loadImageAspectRatio() async {
final ByteData data = await rootBundle.load(widget.bodyImageAsset);
final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
final ui.FrameInfo frameInfo = await codec.getNextFrame();
final image = frameInfo.image;
if (mounted) {
setState(() {
_imageAspectRatio = image.width / image.height;
});
}
}
@override
Widget build(BuildContext context) {
if (_imageAspectRatio == null) {
return const Center(child: CircularProgressIndicator());
}
return LayoutBuilder(
builder: (context, constraints) {
return Center(
child: InteractiveViewer(
transformationController: _transformationController,
minScale: 0.5,
maxScale: 4.0,
clipBehavior: Clip.none,
child: AspectRatio(
aspectRatio: _imageAspectRatio!,
child: LayoutBuilder(
builder: (context, imageConstraints) {
return Stack(
clipBehavior: Clip.none,
children: [
// Body image
Positioned.fill(
child: Image.asset(
widget.bodyImageAsset,
fit: BoxFit.fill,
color: widget.isBodyHidden ? Colors.black.withValues(alpha: 0.5) : null,
),
),
// Organ dots
...widget.organs.map((organ) {
final isSelected = widget.selectedOrganIds.contains(organ.id);
final dotSize = 16.0;
final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2);
final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2);
return Positioned(
left: leftPos,
top: topPos,
child: OrganDot(isSelected: isSelected, onTap: () => widget.onOrganTap(organ.id), size: dotSize),
);
}),
// Tooltip
if (widget.tooltipOrganId != null)
...widget.organs.where((organ) => organ.id == widget.tooltipOrganId).map(
(organ) {
final centerX = organ.position.x * imageConstraints.maxWidth;
final centerY = organ.position.y * imageConstraints.maxHeight;
return OrganTooltipWidget(organName: organ.description, position: Offset(centerX, centerY));
},
),
],
);
},
),
),
),
);
},
);
}
}

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class OrganDot extends StatelessWidget {
final bool isSelected;
final VoidCallback onTap;
final double size;
const OrganDot({super.key, required this.isSelected, required this.onTap, this.size = 50.0});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
width: size.w,
height: size.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? AppColors.whiteColor : AppColors.blackColor.withValues(alpha: 0.6),
),
child: Padding(
padding: EdgeInsets.all(isSelected ? 2.w : 3.w),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor,
),
child: isSelected
? Center(
child: Container(
width: size.w * 0.25,
height: size.h * 0.25,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.whiteColor,
),
),
)
: null,
),
),
),
);
}
}

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class OrganTooltipWidget extends StatelessWidget {
final String organName;
final Offset position;
const OrganTooltipWidget({super.key, required this.organName, required this.position});
@override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: FractionalTranslation(
translation: const Offset(-0.5, -1.2), // Center horizontally, position above the dot
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.scaffoldBgColor,
borderRadius: 8.h,
hasShadow: true,
),
child: Text(
organName,
style: TextStyle(
color: AppColors.textColor,
fontSize: 12.f,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
}

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
class SelectedOrgansSection extends StatefulWidget {
final List<OrganModel> selectedOrgans;
final Function(String organId) onRemoveOrgan;
const SelectedOrgansSection({
super.key,
required this.selectedOrgans,
required this.onRemoveOrgan,
});
@override
State<SelectedOrgansSection> createState() => _SelectedOrgansSectionState();
}
class _SelectedOrgansSectionState extends State<SelectedOrgansSection> {
bool _isExpanded = true;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: AppColors.blackColor.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Selected Organs'.needTranslation,
style: TextStyle(
fontSize: 16.f,
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
Row(
children: [
if (widget.selectedOrgans.isNotEmpty)
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'${widget.selectedOrgans.length}',
style: TextStyle(
color: AppColors.whiteColor,
fontSize: 12.f,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 8.w),
AnimatedRotation(
turns: _isExpanded ? 0.5 : 0.0,
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.keyboard_arrow_down,
color: AppColors.textColorLight,
size: 24.h,
),
),
],
),
],
),
),
if (_isExpanded) ...[
SizedBox(height: 16.h),
if (widget.selectedOrgans.isEmpty)
Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Text(
'No organs selected yet'.needTranslation,
style: TextStyle(
color: AppColors.greyTextColor,
fontSize: 14.f,
),
),
)
else
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: widget.selectedOrgans.map((organ) {
return AppCustomChipWidget(
labelText: organ.description,
backgroundColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
deleteIcon: AppAssets.cross_circle,
deleteIconColor: AppColors.primaryRedColor,
deleteIconHasColor: true,
onDeleteTap: () => widget.onRemoveOrgan(organ.id),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
);
}).toList(),
),
],
],
),
);
}
}

@ -148,7 +148,7 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
],
).toShimmer2(isShow: todoVM.isProcessingPayment),
SizedBox(height: 16.h),

@ -221,7 +221,7 @@ class _AncillaryOrderDetailsListState extends State<AncillaryOrderDetailsList> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 56.w,
height: 56.h,
),

@ -3,11 +3,20 @@ import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart';
import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart';
import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/risk_factors_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/suggestions_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/symptoms_selector_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/splashPage.dart';
@ -24,6 +33,21 @@ class AppRoutes {
static const String zoomCallPage = '/zoomCallPage';
static const String bloodDonationPage = '/bloodDonationPage';
//appointments
static const String bookAppointmentPage = '/bookAppointmentPage';
// Symptoms Checker
static const String organSelectorPage = '/organSelectorPage';
static const String symptomsSelectorScreen = '/symptomsCheckerScreen';
static const String suggestionsScreen = '/suggestionsScreen';
static const String riskFactorsScreen = '/riskFactorsScreen';
static const String possibleConditionsScreen = '/possibleConditionsScreen';
static const String triageScreen = '/triageProgressScreen';
//UserInfoSelection
static const String userInfoSelection = '/userInfoSelection';
static const String userInfoFlowManager = '/userInfoFlowManager';
static Map<String, WidgetBuilder> get routes => {
initialRoute: (context) => SplashPage(),
loginScreen: (context) => LoginScreen(),
@ -31,10 +55,21 @@ class AppRoutes {
register: (context) => RegisterNew(),
registerStepTwo: (context) => RegisterNewStep2(),
medicalFilePage: (context) => MedicalFilePage(),
zoomCallPage: (context) => CallScreen(),
eReferralPage: (context) => NewReferralPage(),
comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(),
homeHealthCarePage: (context) => HhcProceduresPage(),
zoomCallPage: (context) => CallScreen(),
bloodDonationPage: (context) => BloodDonationPage()
organSelectorPage: (context) => OrganSelectorPage(),
symptomsSelectorScreen: (context) => SymptomsSelectorScreen(),
riskFactorsScreen: (context) => RiskFactorsScreen(),
suggestionsScreen: (context) => SuggestionsScreen(),
possibleConditionsScreen: (context) => PossibleConditionsScreen(),
triageScreen: (context) => TriageScreen(),
bloodDonationPage: (context) => BloodDonationPage(),
bookAppointmentPage: (context) => BookAppointmentPage(),
userInfoSelection: (context) => UserInfoSelectionScreen(),
userInfoFlowManager: (context) => UserInfoFlowManager(),
//
};
}

@ -10,7 +10,7 @@ class AppTheme {
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
),
hintColor: Colors.grey[400],

@ -9,6 +9,7 @@ class AppColors {
static const lightGreyEFColor = Color(0xffeaeaff);
static const greyF7Color = Color(0xffF7F7F7);
static const lightGrayColor = Color(0xff808080);
static const greyTextColorLight = Color(0xFFA2A2A2);
// New UI Colors
static const whiteColor = Color(0xFFffffff);
@ -82,6 +83,13 @@ class AppColors {
static const Color infoBannerIconColor = Color(0xFFCC9B14);
static const Color infoBannerTextColor = Color(0xFF856404);
// SymptomsChecker
static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15);
static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103);
static const Color chipColorMonitor = Color(0xFF18C273);
static const Color chipColorEmergency = Color(0xFFED1C2B);
// Services Page Colors
static const Color eReferralCardColor = Color(0xFFFF8012);
static const Color bloodDonationCardColor = Color(0xFFFF5662);

@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/utils/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/theme/colors.dart';
@ -26,6 +27,7 @@ class CollapsingListView extends StatelessWidget {
bool isClose;
bool isLeading;
VoidCallback? leadingCallback;
ScrollPhysics? physics;
CollapsingListView({
super.key,
@ -41,7 +43,9 @@ class CollapsingListView extends StatelessWidget {
this.requests,
this.isLeading = true,
this.trailing,
this.leadingCallback});
this.leadingCallback,
this.physics,
});
@override
Widget build(BuildContext context) {
@ -51,6 +55,7 @@ class CollapsingListView extends StatelessWidget {
body: Column(
children: [
CustomScrollView(
physics: physics,
slivers: [
SliverAppBar(
automaticallyImplyLeading: false,
@ -67,10 +72,10 @@ class CollapsingListView extends StatelessWidget {
icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h),
padding: EdgeInsets.only(left: 12),
onPressed: () {
if(leadingCallback != null) {
if (leadingCallback != null) {
leadingCallback!();
} else {
Navigator.pop(context);
context.pop();
}
},
highlightColor: Colors.transparent,
@ -99,7 +104,8 @@ class CollapsingListView extends StatelessWidget {
t,
)!,
child: Padding(
padding: EdgeInsets.only(left: appState.isArabic() ? 0 : leftPadding, right: appState.isArabic() ? leftPadding : 0, bottom: bottomPadding),
padding: EdgeInsets.only(
left: appState.isArabic() ? 0 : leftPadding, right: appState.isArabic() ? leftPadding : 0, bottom: bottomPadding),
child: Row(
spacing: 4.h,
children: [
@ -116,11 +122,18 @@ class CollapsingListView extends StatelessWidget {
color: AppColors.blackColor,
letterSpacing: -0.5),
).expanded,
if (logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!),
if (report != null) actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(report!),
if (history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(history!),
if (instructions != null) actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(instructions!),
if (requests != null) actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon).onPress(requests!),
if (logout != null)
actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!),
if (report != null)
actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(report!),
if (history != null)
actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon)
.onPress(history!),
if (instructions != null)
actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(instructions!),
if (requests != null)
actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon)
.onPress(requests!),
if (search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(search!).paddingOnly(right: 24),
if (trailing != null) trailing!,
],

@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/app_export.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/theme/colors.dart';
class CustomButton extends StatelessWidget {
final String text;
@ -64,10 +65,10 @@ class CustomButton extends StatelessWidget {
width: width,
padding: padding,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: isDisabled ? backgroundColor.withValues(alpha: .5) : backgroundColor,
color: isDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor,
borderRadius: radius,
customBorder: BorderRadius.circular(radius),
side: borderSide ?? BorderSide(width: borderWidth.h, color: borderColor)),
side: borderSide ?? BorderSide(width: borderWidth.h, color: isDisabled ? Colors.transparent : borderColor)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@ -86,7 +87,7 @@ class CustomButton extends StatelessWidget {
overflow: textOverflow,
style: context.dynamicTextStyle(
fontSize: fontS,
color: isDisabled ? textColor.withValues(alpha: 0.5) : textColor,
color: isDisabled ? AppColors.greyTextColor : textColor,
letterSpacing: 0,
fontWeight: fontWeight,
),

@ -67,7 +67,12 @@ class CustomChipWidget extends StatelessWidget {
Utils.buildSvgWithAssets(icon: iconAsset!),
SizedBox(width: 4.h),
],
chipText.toText10(isBold: true, color: isSelected ? Colors.white : textColor ?? chipType.color, maxlines: 1, weight: FontWeight.w500, letterSpacing: -0.5),
chipText.toText10(
isBold: true,
color: isSelected ? Colors.white : textColor ?? chipType.color,
maxlines: 1,
weight: FontWeight.w500,
letterSpacing: -0.5),
],
),
),

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class CustomSelectableChip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback? onTap;
final Color activeColor;
final Color activeTextColor;
final Color inactiveBorderColor;
final Color inactiveTextColor;
const CustomSelectableChip({
super.key,
required this.label,
required this.selected,
this.onTap,
this.activeColor = const Color(0xFFD03C32), // red accent
this.activeTextColor = Colors.white,
this.inactiveBorderColor = const Color(0xFFE8E8E8),
this.inactiveTextColor = const Color(0xFF222222),
});
@override
Widget build(BuildContext context) {
final double radius = 8.0;
return AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h),
decoration: BoxDecoration(
color: selected ? activeColor.withValues(alpha: 0.12) : AppColors.whiteColor,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: selected ? activeColor : inactiveBorderColor,
width: 1,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(radius),
onTap: onTap,
child: Text(
label,
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w500,
color: selected ? activeTextColor : inactiveTextColor,
letterSpacing: -0.02 * 18,
height: 1.0,
),
),
),
);
}
}

@ -215,8 +215,11 @@ class TextInputWidget extends StatelessWidget {
language: appState.getLanguageCode()!,
initialDate: DateTime.now(),
fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins",
okWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)),
cancelWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)),
okWidget:
Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)),
cancelWidget: Padding(
padding: EdgeInsets.only(right: 8.h),
child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)),
onCalendarTypeChanged: (bool value) {
isGregorian = value;
});
@ -276,7 +279,12 @@ class TextInputWidget extends StatelessWidget {
decoration: InputDecoration(
isDense: true,
hintText: hintText,
hintStyle: TextStyle(fontSize: 14.f, height: 21 / 16, fontWeight: FontWeight.w500, color: hintColor != null ? AppColors.textColor : Color(0xff898A8D), letterSpacing: -0.75),
hintStyle: TextStyle(
fontSize: 14.f,
height: 21 / 16,
fontWeight: FontWeight.w500,
color: hintColor != null ? AppColors.textColor : Color(0xff898A8D),
letterSpacing: -0.75),
prefixIconConstraints: BoxConstraints(minWidth: 30.h),
prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, weight: FontWeight.w500),
contentPadding: EdgeInsets.zero,

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:maps_launcher/maps_launcher.dart';
/// A reusable location map widget that displays a static map with address and directions button
/// Can be used in any review screen (HHC, CMC, etc.)
class LocationMapWidget extends StatelessWidget {
/// The latitude coordinate of the location
final double latitude;
/// The longitude coordinate of the location
final double longitude;
/// The formatted address or location name to display
final String address;
/// The title to show above the map (e.g., "Service Location", "Hospital Location")
final String title;
/// The zoom level for the map (default: 14)
final int zoomLevel;
/// The size of the map image (default: 350x165)
final String mapSize;
/// Optional callback when directions button is pressed
/// If not provided, will use default MapsLauncher
final VoidCallback? onDirectionsTap;
/// Whether to show the address container (default: true)
final bool showAddress;
/// Whether to show the title (default: true)
final bool showTitle;
/// Custom map type (default: roadmap)
final String mapType;
final EdgeInsets? padding;
const LocationMapWidget({
super.key,
required this.latitude,
required this.longitude,
required this.address,
required this.title,
this.zoomLevel = 14,
this.mapSize = '350x165',
this.onDirectionsTap,
this.showAddress = true,
this.showTitle = true,
this.mapType = 'roadmap',
this.padding,
});
void _defaultLaunchDirections() {
if (latitude != 0.0 && longitude != 0.0) {
MapsLauncher.launchCoordinates(latitude, longitude, address);
}
}
@override
Widget build(BuildContext context) {
final String staticMapUrl =
"https://maps.googleapis.com/maps/api/staticmap?center=$latitude,$longitude&zoom=$zoomLevel&size=$mapSize&maptype=$mapType&markers=color:red%7C$latitude,$longitude&key=${ApiKeyConstants.googleMapsApiKey}";
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 16.r,
),
padding: padding ?? EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
if (showTitle) ...[
Text(
title,
style: TextStyle(
fontSize: 16.f,
fontWeight: FontWeight.w700,
color: AppColors.blackColor,
letterSpacing: -0.5,
),
),
SizedBox(height: 12.h),
],
// Address display
if (showAddress) ...[
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.h),
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: AppColors.greyColor.withAlpha(51),
width: 1,
),
),
child: Row(
children: [
Icon(Icons.location_on, color: AppColors.primaryRedColor, size: 20.h),
SizedBox(width: 8.w),
Expanded(
child: Text(
address,
style: TextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w500,
color: AppColors.blackColor,
letterSpacing: -0.4,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
SizedBox(height: 16.h),
],
// Map display with bottom button overlay
Stack(
children: [
ClipRRect(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(24.r),
child: Image.network(
staticMapUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 165.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(24.r),
),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 165.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(24.r),
),
child: Center(
child: Icon(
Icons.error_outline,
size: 48.h,
color: AppColors.greyTextColor,
),
),
);
},
),
),
Positioned(
bottom: 0,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.785,
child: CustomButton(
text: "Get Directions".needTranslation,
onPressed: onDirectionsTap ?? _defaultLaunchDirections,
backgroundColor: AppColors.textColor.withValues(alpha: 0.8),
borderColor: AppColors.textColor.withValues(alpha: 0.01),
textColor: AppColors.whiteColor,
fontSize: 14.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppAssets.directions_icon,
iconColor: AppColors.whiteColor,
iconSize: 14.h,
).paddingAll(12.h),
),
),
],
),
],
),
);
}
}

@ -1,29 +1,13 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/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/emergency_services/emergency_services_view_model.dart';
import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart';
import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart';
import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart';
import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart';
import 'package:hmg_patient_app_new/features/location/location_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/AddressItem.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart' show AppointmentBottomSheet;
import 'package:hmg_patient_app_new/presentation/emergency_services/widgets/location_input_bottom_sheet.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/CustomSwitch.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/ExpandableBottomSheet.dart';
import 'package:hmg_patient_app_new/widgets/expandable_bottom_sheet/model/BottomSheetType.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/map/HMSMap.dart';
import 'package:hmg_patient_app_new/widgets/map/gms_map.dart';
@ -42,14 +26,22 @@ import '../../../widgets/common_bottom_sheet.dart';
/// it results [true] if the user clicks on the submit button
/// and [false] if the user closes the screen without giving the consent to proceed for the request
class MapUtilityScreen extends StatelessWidget {
final String confirmButtonString;
final String titleString;
final String subTitleString;
final bool isGmsAvailable;
final VoidCallback? onCrossClicked;
final VoidCallback? onSubmitted;
const MapUtilityScreen({super.key, required this.confirmButtonString, required this.titleString, required this.subTitleString, required this.isGmsAvailable, this.onCrossClicked});
const MapUtilityScreen({
super.key,
required this.confirmButtonString,
required this.titleString,
required this.subTitleString,
required this.isGmsAvailable,
this.onCrossClicked,
this.onSubmitted,
});
@override
Widget build(BuildContext context) {
@ -57,15 +49,9 @@ class MapUtilityScreen extends StatelessWidget {
floatingActionButton: Padding(
padding: EdgeInsetsDirectional.only(end: 8.h, bottom: 68.h),
child: DecoratedBox(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, borderRadius: 12.h),
child: Utils.buildSvgWithAssets(
icon: AppAssets.locate_me, width: 24.h, height: 24.h)
.paddingAll(12.h)
.onPress(() {
context
.read<LocationViewModel>()
.moveToCurrentLocation();
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h),
child: Utils.buildSvgWithAssets(icon: AppAssets.locate_me, width: 24.h, height: 24.h).paddingAll(12.h).onPress(() {
context.read<LocationViewModel>().moveToCurrentLocation();
}),
),
),
@ -74,38 +60,26 @@ class MapUtilityScreen extends StatelessWidget {
children: [
if (isGmsAvailable)
GMSMap(
currentLocation:
context.read<LocationViewModel>().getGMSLocation(),
onCameraMoved: (value) => context
.read<LocationViewModel>()
.handleGMSMapCameraMoved(value),
onCameraIdle:
context.read<LocationViewModel>().handleOnCameraIdle,
currentLocation: context.read<LocationViewModel>().getGMSLocation(),
onCameraMoved: (value) => context.read<LocationViewModel>().handleGMSMapCameraMoved(value),
onCameraIdle: context.read<LocationViewModel>().handleOnCameraIdle,
myLocationEnabled: true,
inputController:
context.read<LocationViewModel>().gmsController,
inputController: context.read<LocationViewModel>().gmsController,
showCenterMarker: true,
)
else
HMSMap(
currentLocation:
context.read<LocationViewModel>().getHMSLocation(),
onCameraMoved: (value) => context
.read<LocationViewModel>()
.handleHMSMapCameraMoved(value),
onCameraIdle:
context.read<LocationViewModel>().handleOnCameraIdle,
currentLocation: context.read<LocationViewModel>().getHMSLocation(),
onCameraMoved: (value) => context.read<LocationViewModel>().handleHMSMapCameraMoved(value),
onCameraIdle: context.read<LocationViewModel>().handleOnCameraIdle,
myLocationEnabled: false,
inputController:
context.read<LocationViewModel>().hmsController,
inputController: context.read<LocationViewModel>().hmsController,
showCenterMarker: true,
),
Align(
alignment: AlignmentDirectional.topStart,
child: Utils.buildSvgWithAssets(
icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h)
.onPress(() {
onCrossClicked?.call();
child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h).onPress(() {
onCrossClicked?.call();
// context
// .read<EmergencyServicesViewModel>()
// .flushPickupInformation();
@ -120,8 +94,7 @@ class MapUtilityScreen extends StatelessWidget {
Widget FixedBottomSheet(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (details){
},
onVerticalDragUpdate: (details) {},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
@ -150,20 +123,23 @@ class MapUtilityScreen extends StatelessWidget {
spacing: 4.h,
children: [
titleString.toText21(
weight: FontWeight.w600,
color: AppColors.textColor,
),
weight: FontWeight.w600,
color: AppColors.textColor,
),
subTitleString.needTranslation.toText12(
fontWeight: FontWeight.w500,
color: AppColors.greyTextColor,
)
fontWeight: FontWeight.w500,
color: AppColors.greyTextColor,
)
],
),
CustomButton(
text: confirmButtonString.needTranslation,
onPressed: () {
///indicates that the screen has resulted success and should be closed
Navigator.pop(context,true);
text: confirmButtonString.needTranslation,
onPressed: () {
if (onSubmitted != null) {
onSubmitted!();
} else {
Navigator.pop(context, true);
}
},
)
],
@ -191,8 +167,6 @@ class MapUtilityScreen extends StatelessWidget {
);
}
textPlaceInput(context) {
return Consumer<LocationViewModel>(builder: (_, vm, __) {
return SizedBox(
@ -201,8 +175,7 @@ class MapUtilityScreen extends StatelessWidget {
labelText: "Enter Pickup Location Manually".needTranslation,
hintText: "Enter Pickup Location".needTranslation,
controller: TextEditingController(
text: vm.geocodeResponse?.results.first.formattedAddress ??
vm.selectedPrediction?.description,
text: vm.geocodeResponse?.results.first.formattedAddress ?? vm.selectedPrediction?.description,
),
leadingIcon: AppAssets.location_pickup,
isAllowLeadingIcon: true,

Loading…
Cancel
Save