Water Intake Design completed

pull/135/head
faizatflutter 3 weeks ago
parent 94668997aa
commit fd2abbbc7b

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.85417 0C4.41186 0 0 4.41186 0 9.85417C0 15.2965 4.41186 19.7083 9.85417 19.7083C15.2965 19.7083 19.7083 15.2965 19.7083 9.85417C19.7083 4.41186 15.2965 0 9.85417 0ZM10.7708 6.1875C10.7708 5.68124 10.3604 5.27083 9.85417 5.27083C9.34791 5.27083 8.9375 5.68124 8.9375 6.1875V8.9375H6.1875C5.68124 8.9375 5.27083 9.34791 5.27083 9.85417C5.27083 10.3604 5.68124 10.7708 6.1875 10.7708H8.9375V13.5208C8.9375 14.0271 9.34791 14.4375 9.85417 14.4375C10.3604 14.4375 10.7708 14.0271 10.7708 13.5208V10.7708H13.5208C14.0271 10.7708 14.4375 10.3604 14.4375 9.85417C14.4375 9.34791 14.0271 8.9375 13.5208 8.9375H10.7708V6.1875Z" fill="#2B353E"/>
</svg>

After

Width:  |  Height:  |  Size: 790 B

@ -1,3 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
add_<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1905 5.77665C20.59 6.15799 20.6047 6.79098 20.2234 7.19048L9.72336 18.1905C9.53745 18.3852 9.28086 18.4968 9.01163 18.4999C8.7424 18.5031 8.48328 18.3975 8.29289 18.2071L4.79289 14.7071C4.40237 14.3166 4.40237 13.6834 4.79289 13.2929C5.18342 12.9024 5.81658 12.9024 6.20711 13.2929L8.98336 16.0692L18.7766 5.80953C19.158 5.41003 19.791 5.39531 20.1905 5.77665Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 537 B

@ -0,0 +1,5 @@
<svg width="44" height="32" viewBox="0 0 44 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1H31.2656L28.4282 31H7.6206L1 1Z" stroke="#8F9AA3" stroke-width="2" stroke-linejoin="round" stroke-dasharray="4 4"/>
<path d="M31.2656 3.14062H36.9404C40.1561 3.14062 42.6152 5.92634 42.6152 9.5692V15.9978C42.6152 19.6406 40.1561 22.4263 36.9404 22.4263H33.1572" stroke="#8F9AA3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4 4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9171 10.1057C20.1307 9.99999 19.1234 9.99999 17.8411 10H17.7741C16.4918 9.99999 15.4846 9.99999 14.6982 10.1057C13.8923 10.2141 13.253 10.4406 12.7506 10.943C12.2482 11.4453 12.0217 12.0847 11.9133 12.8906C11.8076 13.6769 11.8076 14.6842 11.8076 15.9665V16.0335C11.8076 17.3158 11.8076 18.3231 11.9133 19.1094C12.0217 19.9153 12.2482 20.5547 12.7506 21.057C13.253 21.5594 13.8923 21.7859 14.6982 21.8943C15.4846 22 16.4918 22 17.7741 22H17.8411C19.1234 22 20.1307 22 20.9171 21.8943C21.7229 21.7859 22.3623 21.5594 22.8646 21.057C23.367 20.5547 23.5936 19.9153 23.7019 19.1094C23.8076 18.3231 23.8076 17.3158 23.8076 16.0335V15.9665C23.8076 14.6842 23.8076 13.6769 23.7019 12.8906C23.5936 12.0847 23.367 11.4453 22.8646 10.943C22.3623 10.4406 21.7229 10.2141 20.9171 10.1057ZM18.393 13.6585C18.393 13.3352 18.1309 13.0732 17.8076 13.0732C17.4843 13.0732 17.2223 13.3352 17.2223 13.6585V15.4146H15.4662C15.1429 15.4146 14.8808 15.6767 14.8808 16C14.8808 16.3233 15.1429 16.5854 15.4662 16.5854H17.2223V18.3415C17.2223 18.6648 17.4843 18.9268 17.8076 18.9268C18.1309 18.9268 18.393 18.6648 18.393 18.3415V16.5854H20.1491C20.4724 16.5854 20.7344 16.3233 20.7344 16C20.7344 15.6767 20.4724 15.4146 20.1491 15.4146H18.393V13.6585Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,4 @@
<svg width="44" height="32" viewBox="0 0 44 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1H31.2656L28.4282 31H7.6206L1 1Z" stroke="#2E3039" stroke-width="2" stroke-linejoin="round"/>
<path d="M31.2656 3.14062H36.9404C40.1561 3.14062 42.6152 5.92634 42.6152 9.5692V15.9978C42.6152 19.6406 40.1561 22.4263 36.9404 22.4263H33.1572" stroke="#2E3039" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 438 B

@ -0,0 +1,5 @@
<svg width="44" height="32" viewBox="0 0 44 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1H31.2656L28.4282 31H7.6206L1 1Z" stroke="#2E3039" stroke-width="2" stroke-linejoin="round"/>
<path d="M31.2656 3.14062H36.9404C40.1561 3.14062 42.6152 5.92634 42.6152 9.5692V15.9978C42.6152 19.6406 40.1561 22.4263 36.9404 22.4263H33.1572" stroke="#2E3039" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.80762 14.561C14.141 11.4732 20.4743 22.2805 26.8076 19.1927V30H9.91873L7.80762 14.561Z" fill="#4EB5FF"/>
</svg>

After

Width:  |  Height:  |  Size: 555 B

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.582 1.25C18.1253 1.25 17.7623 1.44409 17.4564 1.6775C17.4426 1.68802 17.4288 1.69876 17.4149 1.70972C17.1988 1.87986 17.0908 1.96493 17.0829 2.09948C17.0749 2.23403 17.1799 2.33908 17.39 2.54917L21.4509 6.61006C21.661 6.82016 21.7661 6.92521 21.9006 6.91724C22.0352 6.90928 22.1202 6.80127 22.2904 6.58526C22.3013 6.57135 22.3121 6.5575 22.3226 6.54371C22.556 6.23778 22.7501 5.87482 22.7501 5.41811C22.7501 4.96139 22.556 4.59843 22.3226 4.2925C22.1683 4.0902 21.9664 3.87507 21.7389 3.64395L22.4645 2.91831C22.8461 2.53666 22.8461 1.91789 22.4645 1.53624C22.0828 1.15459 21.4641 1.15459 21.0824 1.53624L20.3568 2.26187C20.1254 2.03412 19.9101 1.83199 19.7076 1.6775C19.4017 1.44409 19.0387 1.25 18.582 1.25Z" fill="#8F9AA3"/>
<path d="M15.8025 2.55451C15.4544 2.41032 15.0732 2.37717 14.7099 2.45499C14.3791 2.52586 14.1459 2.7182 14.0063 2.85774L12.9207 3.94343C12.7811 4.08298 12.5888 4.31616 12.5179 4.64697C12.4401 5.01026 12.4732 5.39146 12.6174 5.73957C12.7033 5.94681 12.8276 6.11274 12.9534 6.25694L14.8296 8.13314C14.6603 8.37678 14.4295 8.56478 14.1399 8.69315C11.6325 9.80468 9.80541 11.6318 8.69388 14.1392C8.56556 14.4286 8.37765 14.6594 8.13414 14.8286L6.2565 12.951C6.2565 12.951 5.94637 12.7009 5.73913 12.6151C5.39102 12.4709 5.00982 12.4377 4.64653 12.5155C4.31572 12.5864 4.08254 12.7787 3.94299 12.9183L2.8573 14.004C2.71776 14.1435 2.52542 14.3767 2.45455 14.7075C2.37673 15.0708 2.40988 15.452 2.55407 15.8001C2.63991 16.0074 2.76424 16.1733 2.89002 16.3175L7.68129 21.1088C7.82549 21.2345 7.99142 21.3589 8.19866 21.4447C8.54677 21.5889 8.92797 21.6221 9.29126 21.5442C9.62207 21.4734 9.85525 21.281 9.9948 21.1415L11.0805 20.0558C11.22 19.9162 11.4124 19.6831 11.4832 19.3522C11.5611 18.989 11.5279 18.6078 11.3837 18.2597C11.2979 18.0524 11.1736 17.8865 11.0478 17.7423L9.55828 16.2528C9.96485 15.9054 10.2931 15.4666 10.5223 14.9497C11.4326 12.8962 12.8969 11.4319 14.9504 10.5215C15.4675 10.2923 15.9063 9.96398 16.2537 9.5573L17.7446 11.0482C17.7446 11.0482 18.0548 11.2983 18.262 11.3842C18.6101 11.5284 18.9913 11.5615 19.3546 11.4837C19.6854 11.4128 19.9186 11.2205 20.0582 11.0809L21.1438 9.99524C21.2834 9.85569 21.4757 9.62251 21.5466 9.2917C21.6244 8.92841 21.5913 8.54721 21.4471 8.1991C21.3612 7.99186 21.2369 7.82593 21.1111 7.68173L16.3199 2.89046C16.1757 2.76468 16.0097 2.64035 15.8025 2.55451Z" fill="#8F9AA3"/>
<path d="M5.41914 22.7492C5.87586 22.7492 6.23882 22.5551 6.54475 22.3217C6.55854 22.3112 6.57238 22.3005 6.58629 22.2895C6.80231 22.1194 6.91032 22.0343 6.91828 21.8997C6.92624 21.7652 6.8212 21.6601 6.6111 21.45L2.55022 17.3892C2.34012 17.1791 2.23508 17.074 2.10053 17.082C1.96598 17.0899 1.8809 17.1979 1.71076 17.414C1.69981 17.4279 1.68906 17.4417 1.67854 17.4555C1.44513 17.7614 1.25104 18.1244 1.25104 18.5811C1.25104 19.0378 1.44513 19.4008 1.67854 19.7067C1.83274 19.9088 2.03441 20.1237 2.26163 20.3546L1.53697 21.0793C1.15532 21.4609 1.15532 22.0797 1.53697 22.4613C1.91862 22.843 2.53739 22.843 2.91904 22.4613L3.64369 21.7367C3.8753 21.9647 4.09087 22.1671 4.29354 22.3217C4.59947 22.5551 4.96243 22.7492 5.41914 22.7492Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@ -0,0 +1,3 @@
<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00124 0C11.3713 0 13.5174 0.164601 15.0979 0.443605C15.8881 0.58307 16.5336 0.748865 17.0233 0.953187C17.2682 1.05523 17.4752 1.16174 17.6522 1.31444C17.795 1.43765 17.9265 1.61567 17.9745 1.83394C17.997 1.90181 18.0048 1.9739 17.9972 2.04501L17.0012 11.996L15.9975 22.043C15.9672 22.5395 15.5942 22.827 15.1929 23.0391C14.7917 23.2511 14.276 23.417 13.658 23.5565C12.4221 23.8362 10.7723 24.0027 8.95569 23.9999C7.13906 23.9971 5.49826 23.8251 4.27681 23.5411C3.6661 23.3993 3.16459 23.2334 2.77121 23.0177C2.57471 22.9099 2.40312 22.7875 2.26148 22.6271C2.11978 22.4665 2.00417 22.247 2.0038 21.998V22.0511L0.00409739 2.04501C-0.00616554 1.96529 0.00292169 1.88427 0.030564 1.80879C0.0830907 1.60168 0.215339 1.43369 0.352891 1.3149C0.529744 1.16221 0.734914 1.05546 0.979786 0.953417C1.46953 0.748945 2.11501 0.58315 2.90522 0.443836C4.48567 0.164907 6.63123 0 9.00124 0ZM9.00124 0.99817C6.67896 0.99817 4.57689 1.17015 3.08229 1.4337C2.33498 1.56561 1.73987 1.72046 1.36969 1.875C1.25708 1.92187 1.21402 1.95496 1.14902 1.99426C1.21402 2.03395 1.25557 2.07167 1.36969 2.11929C1.73965 2.27312 2.33498 2.4308 3.08229 2.56267C4.57689 2.82648 6.67896 2.9982 9.00124 2.9982C11.3236 2.9982 13.4256 2.82629 14.9202 2.56267C15.6675 2.43076 16.2705 2.27346 16.6406 2.11929C16.7547 2.07167 16.7959 2.03395 16.8613 1.99426C16.7963 1.95458 16.7532 1.92187 16.6406 1.875C16.2706 1.72042 15.6675 1.56557 14.9202 1.4337C13.4256 1.16989 11.3236 0.99817 9.00124 0.99817ZM16.8886 3.08794C16.4149 3.27049 15.8253 3.42253 15.0979 3.55092C13.5174 3.82985 11.3713 3.99429 9.00124 3.99429C6.63123 3.99429 4.48503 3.82992 2.90458 3.55092C2.17718 3.42242 1.58755 3.27037 1.11391 3.08794L1.90087 10.9552C2.34156 10.75 2.91315 10.5837 3.60563 10.4454C5.01028 10.1646 6.90406 9.99623 9.00124 9.99623C11.0985 9.99623 13 10.1644 14.4046 10.4454C15.0971 10.5841 15.6612 10.75 16.1017 10.9552L16.8886 3.08794ZM9.00124 11.0002C6.95818 11.0002 5.10756 11.1661 3.79897 11.4279C3.14469 11.5586 2.62265 11.7139 2.30704 11.8634C2.19594 11.9159 2.14793 11.9563 2.09427 11.996C2.14793 12.0357 2.19518 12.0754 2.30704 12.1287C2.62258 12.278 3.14469 12.4332 3.79897 12.5642C5.10756 12.8258 6.95818 12.994 9.00124 12.994C11.0443 12.994 12.8969 12.8261 14.2055 12.5642C14.8597 12.4334 15.3798 12.2778 15.6954 12.1287C15.8077 12.0758 15.8565 12.0361 15.9101 11.996C15.8561 11.9563 15.8062 11.9155 15.6954 11.8634C15.3799 11.7141 14.8597 11.5588 14.2055 11.4279C12.8969 11.1663 11.0443 11.0002 9.00124 11.0002ZM13.9145 18.4944C13.7965 18.493 13.6817 18.5333 13.5906 18.6084C13.4994 18.6834 13.4378 18.7882 13.4165 18.9043L12.9811 21.0901L11.2997 21.5099C11.2352 21.525 11.1744 21.5526 11.1206 21.5913C11.0669 21.63 11.0214 21.6789 10.9867 21.7353C10.952 21.7917 10.9288 21.8544 10.9184 21.9198C10.9081 21.9852 10.9108 22.052 10.9264 22.1164C10.942 22.1807 10.9702 22.2413 11.0093 22.2947C11.0484 22.3481 11.0978 22.3931 11.1544 22.4273C11.2111 22.4616 11.2739 22.4842 11.3394 22.494C11.4049 22.5038 11.4717 22.5007 11.5359 22.4845L13.5277 21.9826C13.6193 21.9598 13.7025 21.9116 13.7679 21.8435C13.8332 21.7754 13.8779 21.6902 13.8969 21.5978L14.3988 19.1036C14.414 19.0349 14.4145 18.9636 14.4004 18.8946C14.3862 18.8257 14.3576 18.7606 14.3165 18.7034C14.2754 18.6463 14.2227 18.5985 14.1618 18.5631C14.101 18.5278 14.0333 18.5055 13.9633 18.4981C13.9471 18.496 13.9308 18.4949 13.9145 18.4944Z" fill="#4EB5FF"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5378 19.5227C15.7479 19.3066 15.8082 18.9857 15.6909 18.7081C15.5736 18.4304 15.3014 18.25 15 18.25H13L13 5.75L15 5.75C15.3014 5.75 15.5736 5.56957 15.6909 5.29193C15.8082 5.01429 15.7479 4.69339 15.5378 4.47726C15.4246 4.36071 15.2441 4.13501 14.9904 3.81197C14.7621 3.52108 14.4399 3.11067 14.1661 2.78758C13.8723 2.44084 13.5478 2.08603 13.2266 1.81244C13.0657 1.67538 12.888 1.54343 12.6999 1.44275C12.5177 1.34525 12.2758 1.25 12 1.25C11.7242 1.25 11.4824 1.34525 11.3002 1.44275C11.112 1.54343 10.9344 1.67538 10.7734 1.81244C10.4522 2.08603 10.1277 2.44084 9.8339 2.78758C9.56013 3.11067 9.23797 3.52107 9.00963 3.81197C8.7559 4.13501 8.57549 4.36071 8.46221 4.47726C8.25213 4.69339 8.19185 5.01429 8.30917 5.29193C8.42649 5.56957 8.69861 5.75 9.00002 5.75L11 5.75L11 18.25H9.00002C8.69861 18.25 8.42649 18.4304 8.30917 18.7081C8.19185 18.9857 8.25213 19.3066 8.46221 19.5227C8.57549 19.6393 8.7559 19.865 9.00963 20.188C9.23797 20.4789 9.56014 20.8893 9.8339 21.2124C10.1277 21.5592 10.4522 21.914 10.7734 22.1876C10.9344 22.3246 11.112 22.4566 11.3002 22.5573C11.4824 22.6547 11.7242 22.75 12 22.75C12.2758 22.75 12.5177 22.6547 12.6999 22.5572C12.888 22.4566 13.0657 22.3246 13.2266 22.1876C13.5478 21.914 13.8723 21.5592 14.1661 21.2124C14.4399 20.8893 14.7621 20.4789 14.9904 20.188C15.2441 19.865 15.4246 19.6393 15.5378 19.5227Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,3 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.125 0C4.53312 0 0 4.53312 0 10.125C0 15.7169 4.53312 20.25 10.125 20.25C15.7169 20.25 20.25 15.7169 20.25 10.125C20.25 4.53312 15.7169 0 10.125 0ZM6.35756 9.18314C5.83738 9.18314 5.4157 9.60482 5.4157 10.125C5.4157 10.6452 5.83738 11.0669 6.35756 11.0669H13.8924C14.4126 11.0669 14.8343 10.6452 14.8343 10.125C14.8343 9.60482 14.4126 9.18314 13.8924 9.18314H6.35756Z" fill="#ED1C2B"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

@ -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.25C7.54394 1.25 3.92765 4.84151 3.92761 9.27697C3.92752 10.3087 3.85814 11.0873 3.38247 11.7872C3.31632 11.8831 3.22855 12.0032 3.13265 12.1345C2.9661 12.3625 2.77503 12.624 2.63009 12.8505C2.37428 13.2503 2.12435 13.7324 2.03843 14.2942C1.75812 16.127 3.05031 17.3136 4.33721 17.8454C8.86992 19.7182 15.1296 19.7182 19.6623 17.8454C20.9492 17.3136 22.2414 16.127 21.9611 14.2942C21.8752 13.7324 21.6252 13.2503 21.3694 12.8505C21.2245 12.624 21.0334 12.3625 20.8669 12.1345C20.771 12.0033 20.6832 11.8832 20.6171 11.7873C20.1414 11.0874 20.072 10.3088 20.0719 9.27703C20.0719 4.84155 16.4556 1.25 11.9998 1.25Z" fill="#8F9AA3"/>
<path d="M11.9982 22.7477C13.0185 22.7477 13.974 22.4563 14.7773 21.9511C15.4904 21.5027 15.8469 21.2785 15.7222 20.9057C15.5974 20.533 15.1054 20.5747 14.1215 20.6582C12.7143 20.7776 11.2821 20.7776 9.87493 20.6582C8.89101 20.5747 8.39905 20.533 8.27429 20.9057C8.14953 21.2784 8.50605 21.5027 9.2191 21.9511C10.0225 22.4563 10.978 22.7477 11.9982 22.7477Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,9 @@
<svg width="102" height="85" viewBox="0 0 102 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.6143 34.2284C37.6104 34.2284 39.2286 32.696 39.2286 30.8056C39.2286 28.9153 37.6104 27.3828 35.6143 27.3828C33.6182 27.3828 32 28.9153 32 30.8056C32 32.696 33.6182 34.2284 35.6143 34.2284Z" fill="#CBE7FF" fill-opacity="0.7"/>
<path d="M2.40952 61.6106C3.74026 61.6106 4.81904 60.589 4.81904 59.3287C4.81904 58.0685 3.74026 57.0469 2.40952 57.0469C1.07878 57.0469 0 58.0685 0 59.3287C0 60.589 1.07878 61.6106 2.40952 61.6106Z" fill="#CBE7FF" fill-opacity="0.6"/>
<path d="M11.4452 85.0015C13.1087 85.0015 14.4571 83.7245 14.4571 82.1492C14.4571 80.5739 13.1087 79.2969 11.4452 79.2969C9.78182 79.2969 8.43335 80.5739 8.43335 82.1492C8.43335 83.7245 9.78182 85.0015 11.4452 85.0015Z" fill="#CBE7FF" fill-opacity="0.5"/>
<path d="M72.6927 32.3844C74.3561 32.3844 75.7046 31.1073 75.7046 29.532C75.7046 27.9567 74.3561 26.6797 72.6927 26.6797C71.0293 26.6797 69.6808 27.9567 69.6808 29.532C69.6808 31.1073 71.0293 32.3844 72.6927 32.3844Z" fill="#CBE7FF" fill-opacity="0.7"/>
<path d="M99.4095 53.0559C100.74 53.0559 101.819 52.0343 101.819 50.7741C101.819 49.5138 100.74 48.4922 99.4095 48.4922C98.0788 48.4922 97 49.5138 97 50.7741C97 52.0343 98.0788 53.0559 99.4095 53.0559Z" fill="#CBE7FF" fill-opacity="0.6"/>
<path d="M88.8681 77.0097C90.8642 77.0097 92.4824 75.4772 92.4824 73.5869C92.4824 71.6965 90.8642 70.1641 88.8681 70.1641C86.872 70.1641 85.2538 71.6965 85.2538 73.5869C85.2538 75.4772 86.872 77.0097 88.8681 77.0097Z" fill="#CBE7FF" fill-opacity="0.5"/>
<path d="M13.4095 4.56373C14.7403 4.56373 15.819 3.54211 15.819 2.28187C15.819 1.02163 14.7403 0 13.4095 0C12.0788 0 11 1.02163 11 2.28187C11 3.54211 12.0788 4.56373 13.4095 4.56373Z" fill="#D7EEFF" fill-opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.75 0C4.81294 0 0 4.81294 0 10.75C0 16.6871 4.81294 21.5 10.75 21.5C16.6871 21.5 21.5 16.6871 21.5 10.75C21.5 4.81294 16.6871 0 10.75 0ZM10.7416 5C8.94576 5 7.48808 6.45407 7.48808 8.25C7.48808 10.0459 8.94576 11.5 10.7416 11.5C12.5375 11.5 13.9952 10.0459 13.9952 8.25C13.9952 6.45407 12.5375 5 10.7416 5ZM15.7909 15.2302C13.1235 12.3502 8.32472 12.4987 5.71382 15.2256L5.52631 15.4131C5.38104 15.5584 5.30172 15.7569 5.30688 15.9623C5.31204 16.1677 5.40122 16.362 5.5536 16.4998C6.92769 17.7423 8.7513 18.5 10.7501 18.5C12.7489 18.5 14.5725 17.7423 15.9466 16.4998C16.099 16.362 16.1882 16.1677 16.1933 15.9623C16.1985 15.7569 16.1192 15.5584 15.9739 15.4131L15.7909 15.2302Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

@ -0,0 +1,34 @@
<svg width="143" height="315" viewBox="0 0 143 315" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7567_21248)">
<path d="M91.32 30.3281H64.357C56.9114 30.3281 50.8755 35.6934 50.8755 42.3118V52.5836C50.8755 59.202 56.9114 64.5673 64.357 64.5673H91.32C98.7656 64.5673 104.801 59.202 104.801 52.5836V42.3118C104.801 35.6934 98.7656 30.3281 91.32 30.3281Z" fill="#F9FDFF"/>
<path d="M96.5104 12.7188H58.0881C51.7221 12.7188 46.5614 17.5366 46.5614 23.4796C46.5614 29.4227 51.7221 34.2405 58.0881 34.2405H96.5104C102.876 34.2405 108.037 29.4227 108.037 23.4796C108.037 17.5366 102.876 12.7188 96.5104 12.7188Z" fill="#CFEDFF"/>
<path d="M87.3176 64.5625H68.084C41.5279 64.5625 20 83.9067 20 107.769V280.595C20 304.457 41.5279 323.802 68.084 323.802H87.3176C113.874 323.802 135.402 304.457 135.402 280.595V107.769C135.402 83.9067 113.874 64.5625 87.3176 64.5625Z" fill="url(#paint0_linear_7567_21248)"/>
<mask id="mask0_7567_21248" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="22" y="91" width="112" height="229">
<path d="M91.626 91.9531H63.7756C40.7036 91.9531 22 109.616 22 131.403V280.438C22 302.225 40.7036 319.888 63.7756 319.888H91.626C114.698 319.888 133.402 302.225 133.402 280.438V131.403C133.402 109.616 114.698 91.9531 91.626 91.9531Z" fill="white"/>
</mask>
<g mask="url(#mask0_7567_21248)">
<path d="M133.402 92H20.9685V320H133.402V92Z" fill="url(#paint1_linear_7567_21248)"/>
</g>
<path d="M122.696 130.671C125.516 130.671 127.802 128.506 127.802 125.835C127.802 123.165 125.516 121 122.696 121C119.877 121 117.591 123.165 117.591 125.835C117.591 128.506 119.877 130.671 122.696 130.671Z" fill="#CBE7FF" fill-opacity="0.7"/>
<path d="M63.6143 207.228C65.6104 207.228 67.2286 205.696 67.2286 203.806C67.2286 201.915 65.6104 200.383 63.6143 200.383C61.6182 200.383 60 201.915 60 203.806C60 205.696 61.6182 207.228 63.6143 207.228Z" fill="#CBE7FF" fill-opacity="0.7"/>
<path d="M30.4095 234.611C31.7403 234.611 32.819 233.589 32.819 232.329C32.819 231.069 31.7403 230.047 30.4095 230.047C29.0788 230.047 28 231.069 28 232.329C28 233.589 29.0788 234.611 30.4095 234.611Z" fill="#CBE7FF" fill-opacity="0.6"/>
<path d="M39.4452 258.002C41.1087 258.002 42.4571 256.725 42.4571 255.149C42.4571 253.574 41.1087 252.297 39.4452 252.297C37.7818 252.297 36.4333 253.574 36.4333 255.149C36.4333 256.725 37.7818 258.002 39.4452 258.002Z" fill="#CBE7FF" fill-opacity="0.5"/>
<path d="M100.693 205.384C102.356 205.384 103.705 204.107 103.705 202.532C103.705 200.957 102.356 199.68 100.693 199.68C99.0293 199.68 97.6808 200.957 97.6808 202.532C97.6808 204.107 99.0293 205.384 100.693 205.384Z" fill="#CBE7FF" fill-opacity="0.7"/>
<path d="M127.41 226.056C128.74 226.056 129.819 225.034 129.819 223.774C129.819 222.514 128.74 221.492 127.41 221.492C126.079 221.492 125 222.514 125 223.774C125 225.034 126.079 226.056 127.41 226.056Z" fill="#CBE7FF" fill-opacity="0.6"/>
<path d="M116.868 250.01C118.864 250.01 120.482 248.477 120.482 246.587C120.482 244.697 118.864 243.164 116.868 243.164C114.872 243.164 113.254 244.697 113.254 246.587C113.254 248.477 114.872 250.01 116.868 250.01Z" fill="#CBE7FF" fill-opacity="0.5"/>
<path d="M41.4095 177.564C42.7403 177.564 43.819 176.542 43.819 175.282C43.819 174.022 42.7403 173 41.4095 173C40.0788 173 39 174.022 39 175.282C39 176.542 40.0788 177.564 41.4095 177.564Z" fill="#D7EEFF" fill-opacity="0.8"/>
</g>
<defs>
<linearGradient id="paint0_linear_7567_21248" x1="77.7008" y1="29.9973" x2="77.7008" y2="323.802" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9FDFF"/>
<stop offset="1" stop-color="#CBE7FF"/>
</linearGradient>
<linearGradient id="paint1_linear_7567_21248" x1="77.185" y1="65.1765" x2="77.185" y2="333.412" gradientUnits="userSpaceOnUse">
<stop stop-color="#DAF2FF"/>
<stop offset="1" stop-color="#AEDCFF"/>
</linearGradient>
<clipPath id="clip0_7567_21248">
<rect width="143" height="315" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

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

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -19,7 +19,7 @@ abstract class ApiClient {
Future<void> post(
String endPoint, {
required Map<String, dynamic> body,
required dynamic body,
required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess,
required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure,
bool isAllowAny,
@ -27,6 +27,8 @@ abstract class ApiClient {
bool isRCService,
bool isPaymentServices,
bool bypassConnectionCheck,
Map<String, String> apiHeaders,
bool isBodyPlainText,
});
Future<void> get(
@ -89,7 +91,7 @@ class ApiClientImp implements ApiClient {
@override
post(
String endPoint, {
required Map<String, dynamic> body,
required dynamic body,
required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess,
required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure,
bool isAllowAny = false,
@ -97,6 +99,8 @@ class ApiClientImp implements ApiClient {
bool isRCService = false,
bool isPaymentServices = false,
bool bypassConnectionCheck = true,
Map<String, String>? apiHeaders,
bool isBodyPlainText = false,
}) async {
String url;
if (isExternal) {
@ -110,80 +114,84 @@ class ApiClientImp implements ApiClient {
}
// try {
var user = _appState.getAuthenticatedUser();
Map<String, String> headers = {'Content-Type': 'application/json', 'Accept': 'application/json'};
if (!isExternal) {
String? token = _appState.appAuthToken;
Map<String, String> headers = apiHeaders ?? {'Content-Type': 'application/json', 'Accept': 'application/json'};
if (body.containsKey('SetupID')) {
body['SetupID'] = body.containsKey('SetupID') ? body['SetupID'] ?? body[''] : SETUP_ID;
} else {}
// When isBodyPlainText is true, skip all body manipulation and use body as-is
if (!isBodyPlainText) {
if (!isExternal) {
String? token = _appState.appAuthToken;
if (body.containsKey('isDentalAllowedBackend')) {
body['isDentalAllowedBackend'] =
body.containsKey('isDentalAllowedBackend') ? body['isDentalAllowedBackend'] ?? IS_DENTAL_ALLOWED_BACKEND : IS_DENTAL_ALLOWED_BACKEND;
}
if (body.containsKey('SetupID')) {
body['SetupID'] = body.containsKey('SetupID') ? body['SetupID'] ?? body[''] : SETUP_ID;
} else {}
if (!body.containsKey('IsPublicRequest')) {
// if (!body.containsKey('PatientType')) {
if (user != null && user.patientType != null) {
body['PatientType'] = user.patientType;
} else {
body['PatientType'] = PATIENT_TYPE.toString();
if (body.containsKey('isDentalAllowedBackend')) {
body['isDentalAllowedBackend'] =
body.containsKey('isDentalAllowedBackend') ? body['isDentalAllowedBackend'] ?? IS_DENTAL_ALLOWED_BACKEND : IS_DENTAL_ALLOWED_BACKEND;
}
if (user != null && user.patientType != null) {
body['PatientTypeID'] = user.patientType;
} else {
body['PatientType'] = PATIENT_TYPE_ID.toString();
}
if (!body.containsKey('IsPublicRequest')) {
// if (!body.containsKey('PatientType')) {
if (user != null && user.patientType != null) {
body['PatientType'] = user.patientType;
} else {
body['PatientType'] = PATIENT_TYPE.toString();
}
if (user != null && user.patientType != null) {
body['PatientTypeID'] = user.patientType;
} else {
body['PatientType'] = PATIENT_TYPE_ID.toString();
}
if (user != null) {
body['TokenID'] = body['TokenID'] ?? token;
if (user != null) {
body['TokenID'] = body['TokenID'] ?? token;
body['PatientID'] = body['PatientID'] ?? user.patientId;
body['PatientID'] = body['PatientID'] ?? user.patientId;
body['PatientOutSA'] = body.containsKey('PatientOutSA') ? body['PatientOutSA'] ?? user.outSa : user.outSa;
body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe
body['PatientOutSA'] = body.containsKey('PatientOutSA') ? body['PatientOutSA'] ?? user.outSa : user.outSa;
body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe
}
// else {
// body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe
//
// }
}
// else {
// body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe
//
// }
}
}
// request.versionID = VERSION_ID;
// request.channel = CHANNEL;
// request.iPAdress = IP_ADDRESS;
// request.generalid = GENERAL_ID;
// request.languageID = (languageID == 'ar' ? 1 : 2);
// request.patientOutSA = (request.zipCode == '966' || request.zipCode == '+966') ? 0 : 1;
// body['VersionID'] = ApiConsts.appVersionID.toString();
if (!isExternal) {
body['VersionID'] = ApiConsts.appVersionID.toString();
body['Channel'] = ApiConsts.appChannelId.toString();
body['IPAdress'] = ApiConsts.appIpAddress;
body['generalid'] = ApiConsts.appGeneralId;
body['LanguageID'] = _appState.getLanguageID().toString();
body['Latitude'] = _appState.userLat.toString();
body['Longitude'] = _appState.userLong.toString();
body['DeviceTypeID'] = _appState.deviceTypeID;
if (_appState.appAuthToken.isNotEmpty) {
body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken;
// request.versionID = VERSION_ID;
// request.channel = CHANNEL;
// request.iPAdress = IP_ADDRESS;
// request.generalid = GENERAL_ID;
// request.languageID = (languageID == 'ar' ? 1 : 2);
// request.patientOutSA = (request.zipCode == '966' || request.zipCode == '+966') ? 0 : 1;
// body['VersionID'] = ApiConsts.appVersionID.toString();
if (!isExternal) {
body['VersionID'] = ApiConsts.appVersionID.toString();
body['Channel'] = ApiConsts.appChannelId.toString();
body['IPAdress'] = ApiConsts.appIpAddress;
body['generalid'] = ApiConsts.appGeneralId;
body['LanguageID'] = _appState.getLanguageID().toString();
body['Latitude'] = _appState.userLat.toString();
body['Longitude'] = _appState.userLong.toString();
body['DeviceTypeID'] = _appState.deviceTypeID;
if (_appState.appAuthToken.isNotEmpty) {
body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken;
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 1018977;
// body['PatientTypeID'] = 1;
//
// body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q";
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 1018977;
// body['PatientTypeID'] = 1;
//
// body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q";
body.removeWhere((key, value) => value == null);
}
body.removeWhere((key, value) => value == null);
final bool networkStatus = await Utils.checkConnection(bypassConnectionCheck: bypassConnectionCheck);
if (!networkStatus) {
@ -196,12 +204,13 @@ class ApiClientImp implements ApiClient {
return;
}
final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers);
// Handle body encoding based on isBodyPlainText flag
final dynamic requestBody = isBodyPlainText ? body : json.encode(body);
final response = await http.post(Uri.parse(url.trim()), body: requestBody, headers: headers);
final int statusCode = response.statusCode;
log("uri: ${Uri.parse(url.trim())}");
log("body: ${json.encode(body)}");
// log("response.body: ${response.body}");
// log("response.body: ${response.body}");
if (statusCode < 200 || statusCode >= 400) {
onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data"));
logApiEndpointError(endPoint, 'Error While Fetching data', statusCode);

@ -853,12 +853,11 @@ class ApiConsts {
// SYMPTOMS CHECKER
static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName';
static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors';
static final String getGeneralSuggestion = '$symptomsCheckerApi/GetGeneralSggestion';
static final String getSuggestions = '$symptomsCheckerApi/GetSuggestion';
static final String diagnosis = '$symptomsCheckerApi/diagnosis';
static final String explain = '$symptomsCheckerApi/explain';
//E-REFERRAL SERVICES
static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes";
static final sendActivationCodeForEReferral = 'Services/Authentication.svc/REST/SendActivationCodeForEReferral';
static final checkActivationCodeForEReferral = 'Services/Authentication.svc/REST/CheckActivationCodeForEReferral';
@ -866,6 +865,13 @@ class ApiConsts {
static final createEReferral = "Services/Patients.svc/REST/CreateEReferral";
static final getEReferrals = "Services/Patients.svc/REST/GetEReferrals";
//WATER CONSUMPTION
static String h2oGetUserProgress = "Services/H2ORemainder.svc/REST/H2O_GetUserProgress";
static String h2oInsertUserActivity = "Services/H2ORemainder.svc/REST/H2O_InsertUserActivity";
static String h2oGetUserDetail = "Services/H2ORemainder.svc/REST/H2O_GetUserDetails_New";
static String h2oUpdateUserDetail = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New";
static String h2oUndoUserActivity = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity";
// ************ static values for Api ****************
static final double appVersionID = 50.3;
static final int appChannelId = 3;

@ -247,7 +247,22 @@ class AppAssets {
static const String refreshIcon = '$svgBasePath/refresh.svg';
static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg';
// PNGS //
// Water Monitor
static const String waterBottle = '$svgBasePath/water_bottle.svg';
static const String cupAdd = '$svgBasePath/cup_add.svg';
static const String cupFilled = '$svgBasePath/cup_filled.svg';
static const String waterBottleOuterBubbles = '$svgBasePath/outer_bubbles.svg';
static const String cupEmpty = '$svgBasePath/cup_empty.svg';
static const String dumbellIcon = '$svgBasePath/dumbell_icon.svg';
static const String weightScaleIcon = '$svgBasePath/weight_scale_icon.svg';
static const String heightIcon = '$svgBasePath/height_icon.svg';
static const String profileIcon = '$svgBasePath/profile_icon.svg';
static const String notificationIconGrey = '$svgBasePath/notification_icon_grey.svg';
static const String minimizeIcon = '$svgBasePath/minimize_icon.svg';
static const String addIconDark = '$svgBasePath/add_icon_dark.svg';
static const String glassIcon = '$svgBasePath/glass_icon.svg';
// PNGS
static const String hmgLogo = '$pngBasePath/hmg_logo.png';
static const String liveCareService = '$pngBasePath/livecare_service.png';

@ -45,6 +45,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_r
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/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
@ -124,7 +125,10 @@ class AppDependencies {
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()));
getIt.registerLazySingleton<BloodDonationRepo>(() => BloodDonationRepoImp(
loggerService: getIt<LoggerService>(),
apiClient: getIt(),
));
// ViewModels
// Global/shared VMs LazySingleton
@ -140,10 +144,11 @@ class AppDependencies {
getIt.registerLazySingleton<InsuranceViewModel>(() => InsuranceViewModel(insuranceRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<MyAppointmentsViewModel>(() => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
getIt.registerLazySingleton<MyAppointmentsViewModel>(
() => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
getIt.registerLazySingleton<AppointmentRatingViewModel>(
() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
getIt.registerLazySingleton<PayfortViewModel>(
() => PayfortViewModel(
@ -168,7 +173,12 @@ class AppDependencies {
getIt.registerLazySingleton<BookAppointmentsViewModel>(
() => BookAppointmentsViewModel(
bookAppointmentsRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt(), myAppointmentsViewModel: getIt(), locationUtils: getIt(), dialogService: getIt()),
bookAppointmentsRepo: getIt(),
errorHandlerService: getIt(),
navigationService: getIt(),
myAppointmentsViewModel: getIt(),
locationUtils: getIt(),
dialogService: getIt()),
);
getIt.registerLazySingleton<ImmediateLiveCareViewModel>(
@ -182,7 +192,13 @@ class AppDependencies {
getIt.registerLazySingleton<AuthenticationViewModel>(
() => AuthenticationViewModel(
authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()),
authenticationRepo: getIt(),
cacheService: getIt(),
navigationService: getIt(),
dialogService: getIt(),
appState: getIt(),
errorHandlerService: getIt(),
localAuthService: getIt()),
);
getIt.registerLazySingleton<ProfileSettingsViewModel>(() => ProfileSettingsViewModel());
@ -191,12 +207,21 @@ class AppDependencies {
getIt.registerLazySingleton<DoctorFilterViewModel>(() => DoctorFilterViewModel());
getIt.registerLazySingleton<AppointmentViaRegionViewmodel>(
() => AppointmentViaRegionViewmodel(navigationService: getIt(), appState: getIt()),
() => AppointmentViaRegionViewmodel(
navigationService: getIt(),
appState: getIt(),
),
);
getIt.registerLazySingleton<EmergencyServicesViewModel>(
() => EmergencyServicesViewModel(
locationUtils: getIt(), navServices: getIt(), emergencyServicesRepo: getIt(), appState: getIt(), errorHandlerService: getIt(), appointmentRepo: getIt(), dialogService: getIt()),
locationUtils: getIt(),
navServices: getIt(),
emergencyServicesRepo: getIt(),
appState: getIt(),
errorHandlerService: getIt(),
appointmentRepo: getIt(),
dialogService: getIt()),
);
getIt.registerLazySingleton<LocationViewModel>(
@ -213,19 +238,38 @@ class AppDependencies {
() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()),
);
getIt.registerLazySingleton<SymptomsCheckerViewModel>(() => SymptomsCheckerViewModel(errorHandlerService: getIt(), symptomsCheckerRepo: getIt()));
getIt.registerLazySingleton<SymptomsCheckerViewModel>(
() => SymptomsCheckerViewModel(
errorHandlerService: getIt(),
symptomsCheckerRepo: getIt(),
appState: getIt(),
),
);
getIt.registerLazySingleton<HmgServicesViewModel>(
() => HmgServicesViewModel(bookAppointmentsRepo: getIt(), hmgServicesRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()),
() => HmgServicesViewModel(
bookAppointmentsRepo: getIt(),
hmgServicesRepo: getIt(),
errorHandlerService: getIt(),
navigationService: getIt(),
),
);
getIt.registerLazySingleton<BloodDonationViewModel>(
() => BloodDonationViewModel(bloodDonationRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt()),
() => BloodDonationViewModel(
bloodDonationRepo: getIt(),
errorHandlerService: getIt(),
navigationService: getIt(),
dialogService: getIt(),
appState: getIt(),
),
);
getIt.registerLazySingleton<HealthProvider>(
() => HealthProvider(),
);
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel());
// Screen-specific VMs Factory
// getIt.registerFactory<BookAppointmentsViewModel>(
// () => BookAppointmentsViewModel(

@ -41,7 +41,7 @@ extension EmailValidator on String {
FontWeight? weight,
bool isBold = false,
bool isUnderLine = false,
bool isCenter = false,
bool isCenter = false,
int? maxlines,
FontStyle? fontStyle,
TextOverflow? textOverflow,
@ -214,39 +214,38 @@ extension EmailValidator on String {
decoration: isUnderLine ? TextDecoration.underline : null),
);
Widget toText16({
Color? color,
bool isUnderLine = false,
bool isBold = false,
bool isCenter = false,
int? maxlines,
double? height,
TextAlign? textAlign,
FontWeight? weight,
TextOverflow? textOverflow,
double? letterSpacing = -0.4,
Color decorationColor =AppColors.errorColor
}) =>
Widget toText16(
{Color? color,
bool isUnderLine = false,
bool isBold = false,
bool isCenter = false,
int? maxlines,
double? height,
TextAlign? textAlign,
FontWeight? weight,
TextOverflow? textOverflow,
double? letterSpacing = -0.4,
Color decorationColor = AppColors.errorColor}) =>
Text(
this,
maxLines: maxlines,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
color: color ?? AppColors.blackColor,
fontSize: 16.f,
letterSpacing: letterSpacing,
height: height,
overflow: textOverflow,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
decoration: isUnderLine ? TextDecoration.underline : null,
decorationColor: decorationColor
),
color: color ?? AppColors.blackColor,
fontSize: 16.f,
letterSpacing: letterSpacing,
height: height,
overflow: textOverflow,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
decoration: isUnderLine ? TextDecoration.underline : null,
decorationColor: decorationColor),
);
Widget toText17({Color? color, bool isBold = false, bool isCenter = false}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
style: TextStyle(
color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
);
Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow}) => Text(
@ -255,39 +254,62 @@ extension EmailValidator on String {
this,
overflow: textOverflow,
style: TextStyle(
fontSize: 18.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4),
fontSize: 18.f,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
color: color ?? AppColors.blackColor,
letterSpacing: -0.4),
);
Widget toText19({Color? color, bool isBold = false}) => Text(
this,
style: TextStyle(fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4),
style: TextStyle(
fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4),
);
Widget toText20({Color? color, FontWeight? weight, bool isBold = false, }) => Text(
Widget toText20({
Color? color,
FontWeight? weight,
bool isBold = false,
}) =>
Text(
this,
style: TextStyle(
fontSize: 20.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4),
fontSize: 20.f,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
color: color ?? AppColors.blackColor,
letterSpacing: -0.4),
);
Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text(
this,
maxLines: maxlines,
style: TextStyle(
color: color ?? AppColors.blackColor, fontSize: 21.f, letterSpacing: -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)),
color: color ?? AppColors.blackColor,
fontSize: 21.f,
letterSpacing: -1,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)),
);
Widget toText22({Color? color, bool isBold = false, bool isCenter = false}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
height: 1, color: color ?? AppColors.blackColor, fontSize: 22.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
height: 1,
color: color ?? AppColors.blackColor,
fontSize: 22.f,
letterSpacing: -1,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
);
Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing??-1, fontWeight: isBold ? FontWeight.bold : fontWeight??FontWeight.normal),
height: 23 / 24,
color: color ?? AppColors.blackColor,
fontSize: 24.f,
letterSpacing: letterSpacing ?? -1,
fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal),
);
Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing}) => Text(
@ -312,17 +334,25 @@ extension EmailValidator on String {
fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
);
Widget toText32({Color? color, bool isBold = false, bool isCenter = false}) => Text(
Widget toText32({FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
height: 32 / 32,
color: color ?? AppColors.blackColor,
fontSize: 32.f,
letterSpacing: -1,
fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal),
);
Widget toText44({Color? color, bool isBold = false}) => Text(
this,
style: TextStyle(
height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
height: 32 / 32,
color: color ?? AppColors.blackColor,
fontSize: 44.f,
letterSpacing: -1,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
);
Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) {

@ -0,0 +1,59 @@
class RiskAndSuggestionsResponseModel {
final List<RiskAndSuggestionsItemModel>? dataDetails;
RiskAndSuggestionsResponseModel({this.dataDetails});
factory RiskAndSuggestionsResponseModel.fromJson(Map<String, dynamic> json) {
return RiskAndSuggestionsResponseModel(
dataDetails:
json['dataDetails'] != null ? (json['dataDetails'] as List).map((item) => RiskAndSuggestionsItemModel.fromJson(item)).toList() : null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.map((item) => item.toJson()).toList(),
};
}
}
class RiskAndSuggestionsItemModel {
final String? id;
final String? type;
final String? name;
final String? commonName;
final String? language;
RiskAndSuggestionsItemModel({
this.id,
this.type,
this.name,
this.commonName,
this.language,
});
factory RiskAndSuggestionsItemModel.fromJson(Map<String, dynamic> json) {
return RiskAndSuggestionsItemModel(
id: json['id'],
type: json['type'],
name: json['name'],
commonName: json['common_name'],
language: json['language'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'name': name,
'common_name': commonName,
'language': language,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -7,77 +7,214 @@ 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/features/symptoms_checker/models/resp_models/risk_and_suggestions_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,
});
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getRiskFactors({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
});
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
});
}
class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
final ApiClient apiClient;
final LoggerService loggerService;
SymptomsCheckerRepoImp({
required this.apiClient,
required this.loggerService,
});
SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService});
@override
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
required List<String> organNames,
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({required List<String> organNames}) async {
log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}");
log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}");
Map<String, String> headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'};
try {
GenericApiModel<BodySymptomResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getBodySymptomsByName,
apiHeaders: headers,
body: jsonEncode(organNames),
isExternal: true,
isAllowAny: true,
isBodyPlainText: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetBodySymptomsByName API Failed: $error");
log("GetBodySymptomsByName Failed: $error, Status: $statusCode");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("GetBodySymptomsByName Response Status: $statusCode");
loggerService.logInfo("GetBodySymptomsByName API Success: $response");
log("GetBodySymptomsByName Response: $response");
BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response);
apiResponse = GenericApiModel<BodySymptomResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: bodySymptomResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetBodySymptomsByName response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getBodySymptomsByName: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getRiskFactors({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"sex": sex,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"language": language,
};
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,
GenericApiModel<RiskAndSuggestionsResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRiskFactors,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
log("GetRiskFactors Failed: $error, Status: $statusCode");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("GetRiskFactors Response: $response");
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<RiskAndSuggestionsResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: riskFactorsResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetRiskFactors response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
failure = DataParsingFailure(e.toString());
}
},
);
final int statusCode = response.statusCode;
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getRiskFactors: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"sex": sex,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"language": language,
};
log("GetBodySymptomsByName Response Status: $statusCode");
loggerService.logInfo("GetBodySymptomsByName Response Status: $statusCode");
try {
GenericApiModel<RiskAndSuggestionsResponseModel>? apiResponse;
Failure? failure;
try {
// Parse the response
final responseBody = jsonDecode(response.body);
await apiClient.post(
ApiConsts.getSuggestions,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
log("getSuggestions Failed: $error, Status: $statusCode");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("getSuggestions Response: $response");
loggerService.logInfo("GetBodySymptomsByName API Success: $responseBody");
log("GetBodySymptomsByName Response: $responseBody");
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(responseBody);
RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData);
GenericApiModel<BodySymptomResponseModel> apiResponse = GenericApiModel<BodySymptomResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: bodySymptomResponse,
);
apiResponse = GenericApiModel<RiskAndSuggestionsResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: riskFactorsResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing getSuggestions response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
failure = DataParsingFailure(e.toString());
}
},
);
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()));
}
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getBodySymptomsByName: $e");
loggerService.logError("Exception in getSuggestions: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));

@ -1,20 +1,24 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/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/models/resp_models/risk_and_suggestions_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;
final AppState appState;
SymptomsCheckerViewModel({
required this.symptomsCheckerRepo,
required this.errorHandlerService,
required this.appState,
});
// State variables
@ -29,9 +33,19 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
// API loading states
bool isBodySymptomsLoading = false;
bool isRiskFactorsLoading = false;
bool isSuggestionsLoading = false;
// API data storage - using API models directly
BodySymptomResponseModel? bodySymptomResponse;
RiskAndSuggestionsResponseModel? riskFactorsResponse;
RiskAndSuggestionsResponseModel? suggestionsResponse;
// Selected risk factors tracking
final Set<String> _selectedRiskFactorIds = {};
// Selected Suggestions tracking
final Set<String> _selectedSuggestionsIds = {};
// Selected symptoms tracking (organId -> Set of symptom IDs)
final Map<String, Set<String>> _selectedSymptomsByOrgan = {};
@ -111,6 +125,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty);
}
/// Get risk factors list
List<RiskAndSuggestionsItemModel> get riskFactorsList {
return riskFactorsResponse?.dataDetails ?? [];
}
/// Check if any risk factors are selected
bool get hasSelectedRiskFactors => _selectedRiskFactorIds.isNotEmpty;
/// Get selected risk factors count
int get selectedRiskFactorsCount => _selectedRiskFactorIds.length;
/// Check if any risk factors are selected
bool get hasSelectedSuggestions => _selectedSuggestionsIds.isNotEmpty;
/// Get selected risk factors count
int get selectedSuggestionsCount => _selectedSuggestionsIds.length;
/// Get risk factors list
List<RiskAndSuggestionsItemModel> get suggestionsList {
return suggestionsResponse?.dataDetails ?? [];
}
void toggleView() {
_currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front;
notifyListeners();
@ -122,6 +158,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
}
void toggleOrganSelection(String organId) {
if (selectedOrganIds.isEmpty && _isBottomSheetExpanded == false) {
toggleBottomSheet();
}
if (_selectedOrganIds.contains(organId)) {
_selectedOrganIds.remove(organId);
} else {
@ -131,6 +171,11 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
// Show tooltip
_showTooltip(organId);
if (_selectedOrganIds.isEmpty) {
_isBottomSheetExpanded = false;
notifyListeners();
}
notifyListeners();
}
@ -157,6 +202,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
void removeOrgan(String organId) {
_selectedOrganIds.remove(organId);
notifyListeners();
if (_selectedOrganIds.isEmpty) {
_isBottomSheetExpanded = false;
notifyListeners();
}
}
void clearAllSelections() {
@ -164,7 +213,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
void toggleBottomSheet() {
void toggleBottomSheet({bool? value}) {
if (value != null) {
_isBottomSheetExpanded = value;
notifyListeners();
return;
}
_isBottomSheetExpanded = !_isBottomSheetExpanded;
notifyListeners();
}
@ -268,11 +322,305 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
// Risk Factors Methods
/// Toggle risk factor selection
void toggleRiskFactorSelection(String riskFactorId) {
if (riskFactorId == "not_applicable") {
// "Not applicable" is mutually exclusive: if selected, clear all others
if (_selectedRiskFactorIds.contains(riskFactorId)) {
_selectedRiskFactorIds.remove(riskFactorId);
} else {
_selectedRiskFactorIds
..clear()
..add(riskFactorId);
}
} else {
_selectedRiskFactorIds.remove("not_applicable");
if (!_selectedRiskFactorIds.add(riskFactorId)) {
_selectedRiskFactorIds.remove(riskFactorId);
}
}
notifyListeners();
}
/// Check if risk factor is selected
bool isRiskFactorSelected(String riskFactorId) {
return _selectedRiskFactorIds.contains(riskFactorId);
}
/// Get all selected risk factors
List<RiskAndSuggestionsItemModel> getAllSelectedRiskFactors() {
return riskFactorsList.where((factor) => factor.id != null && _selectedRiskFactorIds.contains(factor.id)).toList();
}
/// Clear all risk factor selections
void clearAllRiskFactorSelections() {
_selectedRiskFactorIds.clear();
notifyListeners();
}
/// Fetch risk factors based on selected symptoms
Future<void> fetchRiskFactors({
Function()? onSuccess,
Function(String)? onError,
}) async {
// Get all selected symptoms
final selectedSymptoms = getAllSelectedSymptoms();
if (selectedSymptoms.isEmpty) {
if (onError != null) {
onError('No symptoms selected');
}
return;
}
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
// Extract symptom IDs
List<String> evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList();
await getRiskFactors(
age: _selectedAge!,
sex: _selectedGender!.toLowerCase(),
evidenceIds: evidenceIds,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
onSuccess();
}
},
onError: (error) {
if (onError != null) {
onError(error);
}
},
);
}
/// Call Risk Factors API
Future<void> getRiskFactors({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
Function(RiskAndSuggestionsResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isRiskFactorsLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getRiskFactors(
age: age,
sex: sex,
evidenceIds: evidenceIds,
language: language,
);
result.fold(
(failure) async {
isRiskFactorsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isRiskFactorsLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
riskFactorsResponse = apiResponse.data;
if (riskFactorsResponse != null && riskFactorsResponse!.dataDetails != null) {
RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel(
id: "not_applicable",
commonName: "Not Applicable",
name: "Not Applicable",
language: appState.isArabic() ? 'ar' : 'en',
type: null,
);
riskFactorsResponse!.dataDetails!.add(riskFactorItem);
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch risk factors');
}
}
},
);
}
// Suggestions Methods
/// Toggle suggestions selection
void toggleSuggestionsSelection(String suggestionsId) {
if (suggestionsId == "not_applicable") {
// "Not applicable" is mutually exclusive: if selected, clear all others
if (_selectedSuggestionsIds.contains(suggestionsId)) {
_selectedSuggestionsIds.remove(suggestionsId);
} else {
_selectedSuggestionsIds
..clear()
..add(suggestionsId);
}
} else {
_selectedSuggestionsIds.remove("not_applicable");
if (!_selectedSuggestionsIds.add(suggestionsId)) {
_selectedSuggestionsIds.remove(suggestionsId);
}
}
notifyListeners();
}
/// Check if risk factor is selected
bool isSuggestionsSelected(String riskFactorId) {
return _selectedSuggestionsIds.contains(riskFactorId);
}
/// Get all selected risk factors
List<RiskAndSuggestionsItemModel> getAllSelectedSuggestions() {
return suggestionsList.where((factor) => factor.id != null && _selectedSuggestionsIds.contains(factor.id)).toList();
}
/// Clear all risk factor selections
void clearAllSuggestionsSelections() {
_selectedSuggestionsIds.clear();
notifyListeners();
}
/// Fetch risk factors based on selected symptoms
Future<void> fetchSuggestions({
Function()? onSuccess,
Function(String)? onError,
}) async {
// Get all selected symptoms
final selectedSymptoms = getAllSelectedSymptoms();
if (selectedSymptoms.isEmpty) {
if (onError != null) {
onError('No symptoms selected');
}
return;
}
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
// Extract symptom IDs
List<String> evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList();
// Get all selected symptoms
final selectedRisks = getAllSelectedRiskFactors();
if (selectedRisks.isNotEmpty) {
List<String> evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList();
evidenceIds.addAll(evidenceRisksIds);
}
await getSuggestions(
age: _selectedAge!,
sex: _selectedGender!.toLowerCase(),
evidenceIds: evidenceIds,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
onSuccess();
}
},
onError: (error) {
if (onError != null) {
onError(error);
}
},
);
}
/// Call Suggestions API
Future<void> getSuggestions({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
Function(RiskAndSuggestionsResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isSuggestionsLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getSuggestions(
age: age,
sex: sex,
evidenceIds: evidenceIds,
language: language,
);
result.fold(
(failure) async {
isSuggestionsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isSuggestionsLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
suggestionsResponse = apiResponse.data;
if (suggestionsResponse != null && suggestionsResponse!.dataDetails != null) {
RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel(
id: "not_applicable",
commonName: "Not Applicable",
name: "Not Applicable",
language: appState.isArabic() ? 'ar' : 'en',
type: null,
);
suggestionsResponse!.dataDetails!.add(riskFactorItem);
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch risk factors');
}
}
},
);
}
void reset() {
_currentView = BodyView.front;
_selectedOrganIds.clear();
_selectedSymptomsByOrgan.clear();
_selectedRiskFactorIds.clear();
_selectedSuggestionsIds.clear();
bodySymptomResponse = null;
riskFactorsResponse = null;
suggestionsResponse = null;
_isBottomSheetExpanded = false;
_tooltipTimer?.cancel();
_tooltipOrganId = null;

@ -0,0 +1,47 @@
class WaterCupModel {
final String id;
final String name;
final int capacityMl;
final String iconPath; // or use IconData if you prefer
final bool isDefault;
WaterCupModel({
required this.id,
required this.name,
required this.capacityMl,
required this.iconPath,
this.isDefault = false,
});
WaterCupModel copyWith({
String? id,
String? name,
int? capacityMl,
String? iconPath,
bool? isDefault,
}) {
return WaterCupModel(
id: id ?? this.id,
name: name ?? this.name,
capacityMl: capacityMl ?? this.capacityMl,
iconPath: iconPath ?? this.iconPath,
isDefault: isDefault ?? this.isDefault,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'capacityMl': capacityMl,
'iconPath': iconPath,
'isDefault': isDefault,
};
factory WaterCupModel.fromJson(Map<String, dynamic> json) => WaterCupModel(
id: json['id'],
name: json['name'],
capacityMl: json['capacityMl'],
iconPath: json['iconPath'],
isDefault: json['isDefault'] ?? false,
);
}

@ -0,0 +1,408 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart';
class WaterMonitorViewModel extends ChangeNotifier {
// Controllers
final TextEditingController nameController = TextEditingController();
final TextEditingController heightController = TextEditingController();
final TextEditingController weightController = TextEditingController();
final TextEditingController ageController = TextEditingController();
// Units
final List<String> heightUnits = ['cm', 'm', 'ft', 'in'];
final List<String> weightUnits = ['kg', 'lb'];
// Selected values
String _selectedHeightUnit = 'cm';
String _selectedWeightUnit = 'kg';
String _selectedActivityLevel = 'Lightly active';
String _selectedNumberOfReminders = '3 Time';
String _selectedGender = "Male";
// Validation error message
String? _validationError;
// Getters
String? get validationError => _validationError;
String get selectedHeightUnit => _selectedHeightUnit;
String get selectedWeightUnit => _selectedWeightUnit;
String get selectedActivityLevel => _selectedActivityLevel;
String get selectedNumberOfReminders => _selectedNumberOfReminders;
String get selectedGender => _selectedGender;
// Activity level options
List<String> get activityLevels =>
["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"];
// Reminder options
List<String> get reminderOptions => ["1 Time", "2 Time", "3 Time", "4 Time", "5 Time", "6 Time"];
// Gender options
List<String> get genderOptions => ["Male", "Female"];
// Initialize method to be called when needed
void initialize() {
_loadSettings();
_initializeDefaultCups();
}
// Reset all fields to default
void resetFields() {
nameController.clear();
heightController.clear();
weightController.clear();
ageController.clear();
_selectedHeightUnit = 'cm';
_selectedWeightUnit = 'kg';
_selectedActivityLevel = 'Lightly active';
_selectedNumberOfReminders = '3 Time';
_selectedGender = "Male";
notifyListeners();
}
// Setters with notification
void setGender(String gender) {
_selectedGender = gender;
notifyListeners();
}
void setHeightUnit(String unit) {
_selectedHeightUnit = unit;
notifyListeners();
}
void setWeightUnit(String unit) {
_selectedWeightUnit = unit;
notifyListeners();
}
void setActivityLevel(String level) {
_selectedActivityLevel = level;
notifyListeners();
}
void setNumberOfReminders(String number) {
_selectedNumberOfReminders = number;
notifyListeners();
}
// Validation methods
bool get isValid {
_validationError = null; // Clear previous error
if (nameController.text.trim().isEmpty) {
_validationError = 'Name is required';
return false;
}
if (ageController.text.trim().isEmpty) {
_validationError = 'Age is required';
return false;
}
if (!_isAgeValid()) {
_validationError = validateAge();
return false;
}
if (heightController.text.trim().isEmpty) {
_validationError = 'Height is required';
return false;
}
if (!_isHeightValid()) {
_validationError = validateHeight();
return false;
}
if (weightController.text.trim().isEmpty) {
_validationError = 'Weight is required';
return false;
}
if (!_isWeightValid()) {
_validationError = validateWeight();
return false;
}
return true;
}
bool _isAgeValid() {
final age = int.tryParse(ageController.text.trim());
return age != null && age >= 11 && age <= 120;
}
bool _isHeightValid() {
final height = double.tryParse(heightController.text.trim());
return height != null && height > 0;
}
bool _isWeightValid() {
final weight = double.tryParse(weightController.text.trim());
return weight != null && weight > 0;
}
String? validateAge() {
if (ageController.text.trim().isEmpty) {
return 'Age is required'.needTranslation;
}
final age = int.tryParse(ageController.text.trim());
if (age == null) {
return 'Invalid age'.needTranslation;
}
if (age < 11 || age > 120) {
return 'Age must be between 11 and 120'.needTranslation;
}
return null;
}
String? validateHeight() {
if (heightController.text.trim().isEmpty) {
return 'Height is required'.needTranslation;
}
final height = double.tryParse(heightController.text.trim());
if (height == null || height <= 0) {
return 'Invalid height'.needTranslation;
}
return null;
}
String? validateWeight() {
if (weightController.text.trim().isEmpty) {
return 'Weight is required'.needTranslation;
}
final weight = double.tryParse(weightController.text.trim());
if (weight == null || weight <= 0) {
return 'Invalid weight'.needTranslation;
}
return null;
}
// Calculate water intake based on user data
double calculateDailyWaterIntake() {
if (!isValid) return 0;
final weight = double.parse(weightController.text.trim());
final weightInKg = _convertWeightToKg(weight, _selectedWeightUnit);
// Base calculation: 30-35ml per kg of body weight
double baseIntake = weightInKg * 33;
// Adjust for activity level
double activityMultiplier = _getActivityMultiplier();
double totalIntake = baseIntake * activityMultiplier;
return totalIntake; // in ml
}
double _convertWeightToKg(double weight, String unit) {
switch (unit) {
case 'kg':
return weight;
case 'lb':
return weight * 0.453592;
default:
return weight;
}
}
double _getActivityMultiplier() {
switch (_selectedActivityLevel) {
case "Almost Inactive (no exercise)":
return 1.0;
case "Lightly active":
return 1.1;
case "Lightly active (1-3) days per week":
return 1.2;
case "Super active (very hard exercise)":
return 1.4;
default:
return 1.1;
}
}
// Save settings
Future<bool> saveSettings() async {
if (!isValid) {
notifyListeners(); // Notify so error can be read
return false;
}
_validationError = null; // Clear error on success
notifyListeners();
return true;
}
// Load settings
Future<void> _loadSettings() async {
notifyListeners();
}
@override
void dispose() {
nameController.dispose();
heightController.dispose();
weightController.dispose();
ageController.dispose();
super.dispose();
}
List<WaterCupModel> _cups = [];
String? _selectedCupId;
int _totalConsumedMl = 1000;
int _dailyGoalMl = 2000;
// Calibration: portion of the bottle SVG height that is fillable (0.0 - 1.0)
double _fillableHeightPercent = 0.823;
List<WaterCupModel> get cups => _cups;
WaterCupModel? get selectedCup {
if (_cups.isEmpty) return null;
return _cups.firstWhere((c) => c.id == _selectedCupId, orElse: () => _cups.first);
}
int get totalConsumedMl => _totalConsumedMl;
int get dailyGoalMl => _dailyGoalMl;
// Portion of bottle drawable height that is fillable.
double get fillableHeightPercent => _fillableHeightPercent;
void setFillableHeightPercent(double v) {
_fillableHeightPercent = v.clamp(0.0, 1.0);
notifyListeners();
}
// Normalized progress in 0.0 - 1.0 (ensure double)
double get progress {
if (_dailyGoalMl == 0) return 0.0;
final p = _totalConsumedMl / _dailyGoalMl;
return p.clamp(0.0, 1.0).toDouble();
}
// Convenience percent (0 - 100)
double get progressPercent => (progress * 100.0).clamp(0.0, 100.0).toDouble();
// Allow updating consumed and goal through the vm so UI doesn't manipulate internal fields directly
void setTotalConsumedMl(int ml) {
_totalConsumedMl = ml;
notifyListeners();
}
void addConsumedMl(int ml) {
_totalConsumedMl = (_totalConsumedMl + ml).clamp(0, 1000000);
notifyListeners();
}
void subtractConsumedMl(int ml) {
_totalConsumedMl = (_totalConsumedMl - ml).clamp(0, 1000000);
notifyListeners();
}
void setDailyGoal(int ml) {
_dailyGoalMl = ml;
notifyListeners();
}
void _initializeDefaultCups() {
_cups = [
WaterCupModel(
id: 'default_125',
name: '125ml',
capacityMl: 125,
iconPath: AppAssets.cupEmpty,
isDefault: true,
),
WaterCupModel(
id: 'default_150',
name: '150ml',
capacityMl: 150,
iconPath: AppAssets.cupEmpty,
isDefault: true,
),
WaterCupModel(
id: 'default_175',
name: '175ml',
capacityMl: 175,
iconPath: AppAssets.cupEmpty,
isDefault: true,
),
WaterCupModel(
id: 'default_200',
name: '200ml',
capacityMl: 200,
iconPath: AppAssets.cupEmpty,
isDefault: true,
),
WaterCupModel(
id: 'default_250',
name: '250ml',
capacityMl: 250,
iconPath: AppAssets.cupEmpty,
isDefault: true,
),
WaterCupModel(
id: 'default_300',
name: '300ml',
capacityMl: 300,
iconPath: AppAssets.cupEmpty,
isDefault: true,
),
];
_selectedCupId = _cups.first.id;
notifyListeners();
}
void selectCup(String cupId) {
_selectedCupId = cupId;
notifyListeners();
}
void addCup(WaterCupModel cup) {
_cups.add(cup);
notifyListeners();
}
void updateCup(WaterCupModel updatedCup) {
final index = _cups.indexWhere((c) => c.id == updatedCup.id);
if (index != -1) {
_cups[index] = updatedCup;
notifyListeners();
}
}
// Public alias for deleting/removing a cup. Keeps API intention clear in UI code.
void removeCup(String cupId) {
deleteCup(cupId);
}
void deleteCup(String cupId) {
final cup = _cups.firstWhere((c) => c.id == cupId);
if (cup.isDefault) return; // can't delete default cups
_cups.removeWhere((c) => c.id == cupId);
if (_selectedCupId == cupId) {
_selectedCupId = _cups.first.id;
}
notifyListeners();
}
void addWaterIntake() {
if (selectedCup != null) {
_totalConsumedMl += selectedCup!.capacityMl;
notifyListeners();
}
}
void resetDaily() {
_totalConsumedMl = 0;
notifyListeners();
}
}

@ -30,8 +30,9 @@ 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/smartwatch_health_data/health_provider.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/features/todo_section/todo_section_view_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -165,6 +166,9 @@ void main() async {
),
ChangeNotifierProvider<HealthCalcualtorViewModel>(
create: (_) => getIt.get<HealthCalcualtorViewModel>(),
),
ChangeNotifierProvider<WaterMonitorViewModel>(
create: (_) => getIt.get<WaterMonitorViewModel>(),
)
], child: MyApp()),
),

@ -42,8 +42,11 @@ class AppointmentQueuePage extends StatelessWidget {
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
side:
BorderSide(color: myAppointmentsVM.isAppointmentQueueDetailsLoading ? AppColors.whiteColor : Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), width: 2.w),
side: BorderSide(
color: myAppointmentsVM.isAppointmentQueueDetailsLoading
? AppColors.whiteColor
: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus),
width: 2.w),
),
child: Padding(
padding: EdgeInsets.all(16.h),
@ -62,17 +65,23 @@ class AppointmentQueuePage extends StatelessWidget {
],
).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
SizedBox(height: 10.h),
"Hala ${appState!.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
"Hala ${appState!.getAuthenticatedUser()!.firstName}!!!"
.needTranslation
.toText16(isBold: true)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
SizedBox(height: 8.h),
"Thank you for your patience, here is your queue number."
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
SizedBox(height: 8.h),
myAppointmentsVM.currentPatientQueueDetails.queueNo!.toText32(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
myAppointmentsVM.currentPatientQueueDetails.queueNo!
.toText32(isBold: true)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
SizedBox(height: 8.h),
CustomButton(
text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""),
text: Utils.getCardButtonText(
myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""),
onPressed: () {},
backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus),
borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01),
@ -94,61 +103,72 @@ class AppointmentQueuePage extends StatelessWidget {
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Serving Now".needTranslation.toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Serving Now"
.needTranslation
.toText16(isBold: true)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
SizedBox(height: 18.h),
ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: myAppointmentsVM.patientQueueDetailsList.length,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
myAppointmentsVM.patientQueueDetailsList[index].queueNo!.toText17(isBold: true),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
"Room: ${myAppointmentsVM.patientQueueDetailsList[index].roomNo}".toText12(fontWeight: FontWeight.w500),
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
myAppointmentsVM.patientQueueDetailsList[index].queueNo!.toText17(isBold: true),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
"Room: ${myAppointmentsVM.patientQueueDetailsList[index].roomNo}"
.toText12(fontWeight: FontWeight.w500),
SizedBox(width: 8.w),
AppCustomChipWidget(
deleteIcon: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? AppAssets.call_for_vitals : AppAssets.call_for_doctor,
labelText:
myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? "Call for vital signs".needTranslation : "Call for Doctor".needTranslation,
iconColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? AppColors.primaryRedColor : AppColors.successColor,
textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? AppColors.primaryRedColor : AppColors.successColor,
deleteIcon: myAppointmentsVM.patientQueueDetailsList[index].callType == 1
? AppAssets.call_for_vitals
: AppAssets.call_for_doctor,
labelText: myAppointmentsVM.patientQueueDetailsList[index].callType == 1
? "Call for vital signs".needTranslation
: "Call for Doctor".needTranslation,
iconColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1
? AppColors.primaryRedColor
: AppColors.successColor,
textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1
? AppColors.primaryRedColor
: AppColors.successColor,
iconSize: 14.w,
backgroundColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1
? AppColors.primaryRedColor.withValues(alpha: 0.1)
: AppColors.successColor.withValues(alpha: 0.1),
labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h),
),
],
),
],
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h),
],
),
],
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h),
).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading),
],
),
),
),
)
: SizedBox.shrink(),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
borderRadius: 24.4,
hasShadow: true,
),
child: Padding(
@ -171,15 +191,25 @@ class AppointmentQueuePage extends StatelessWidget {
// Are there any side effects I should know about?
// When should I come back for a follow-up?
"${"What can I do to improve my overall health?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
"${"What can I do to improve my overall health?"}"
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"Are there any routine screenings I should get?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
"${"Are there any routine screenings I should get?"}"
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"What is this medication for?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
"${"What is this medication for?"}"
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"Are there any side effects I should know about?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
"${"Are there any side effects I should know about?"}"
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"When should I come back for a follow-up?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
"${"When should I come back for a follow-up?"}"
.needTranslation
.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 16.h),
],

@ -24,6 +24,7 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart';
import 'package:provider/provider.dart';
import 'e-referral_validator.dart';
import 'e_referral_form_manager.dart';
@ -43,7 +44,6 @@ class _NewReferralPageState extends State<NewReferralPage> {
double widthOfOneState = ((ResponsiveExtension.screenWidth) / 3) - (20.h);
@override
void initState() {
super.initState();
@ -116,7 +116,7 @@ class _NewReferralPageState extends State<NewReferralPage> {
cityCode: _formManager.formData.patientCity!.iD!.toString(),
cityName: _formManager.formData.patientCity!.description,
requesterName: _formManager.formData.requesterName,
requesterContactNo: _formManager.formData.countryEnum.countryCode + _formManager.formData.requesterPhone,
requesterContactNo: _formManager.formData.countryEnum.countryCode + _formManager.formData.requesterPhone,
requesterRelationship: _formManager.formData.relationship?.iD,
otherRelationship: _formManager.formData.relationship!.iD.toString(),
fullName: _formManager.formData.patientName,
@ -133,10 +133,8 @@ class _NewReferralPageState extends State<NewReferralPage> {
hmgServicesVM.createEReferral(
requestModel: createReferralRequestModel,
onSuccess: (GenericApiModel response) {
showSuccessBottomSheet(int.parse(response.data), hmgServicesVM);
LoaderBottomSheet.hideLoader();
},
onError: (errorMessage) {
// Handle error (e.g., show error message)
@ -146,9 +144,6 @@ class _NewReferralPageState extends State<NewReferralPage> {
}
void _loadData() {
final authVM = context.read<AuthenticationViewModel>();
final habibWalletVM = context.read<HabibWalletViewModel>();
final hmgServicesVM = context.read<HmgServicesViewModel>();
@ -179,7 +174,7 @@ class _NewReferralPageState extends State<NewReferralPage> {
color: Colors.white,
padding: EdgeInsets.all(ResponsiveExtension(20).h),
child: CustomButton(
text: _currentStep <=1 ? LocaleKeys.next.tr() : LocaleKeys.submit.tr(),
text: _currentStep <= 1 ? LocaleKeys.next.tr() : LocaleKeys.submit.tr(),
// icon: AppAssets.search_icon,
iconColor: Colors.white,
onPressed: () => {_handleNextStep()},
@ -188,7 +183,7 @@ class _NewReferralPageState extends State<NewReferralPage> {
child: ChangeNotifierProvider.value(
value: _formManager,
child: SizedBox(
height: ResponsiveExtension.screenHeight * 0.65,
height: ResponsiveExtension.screenHeight * 0.65,
child: Column(
children: [
const SizedBox(height: 8),
@ -220,7 +215,6 @@ class _NewReferralPageState extends State<NewReferralPage> {
// );
}
showSuccessBottomSheet(int requestId, HmgServicesViewModel hmgServicesViewModel) {
return showCommonBottomSheetWithoutHeight(
context,
@ -232,9 +226,9 @@ class _NewReferralPageState extends State<NewReferralPage> {
Row(
children: [
"Here is your Referral #: ".needTranslation.toText14(
color: AppColors.textColorLight,
weight: FontWeight.w500,
),
color: AppColors.textColorLight,
weight: FontWeight.w500,
),
SizedBox(width: 4.w),
("$requestId").toText16(isBold: true),
],
@ -249,7 +243,7 @@ class _NewReferralPageState extends State<NewReferralPage> {
onPressed: () {
context.pop();
context.pop();
_currentStep =0;
_currentStep = 0;
},
textColor: AppColors.whiteColor,
),

@ -131,7 +131,7 @@ class ServicesPage extends StatelessWidget {
AppAssets.daily_water_monitor_icon,
bgColor: AppColors.whiteColor,
true,
route: AppRoutes.eReferralPage,
route: AppRoutes.waterConsumptionScreen,
),
HmgServicesComponentModel(
11,
@ -374,7 +374,7 @@ class ServicesPage extends StatelessWidget {
SizedBox(height: 16.h),
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:(isFoldable || isTablet) ? 6 : 4, // 4 icons per row
crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row
crossAxisSpacing: 12.w,
mainAxisSpacing: 18.h,
childAspectRatio: 0.8,

@ -446,68 +446,62 @@ class _HhcProceduresPageState extends State<HhcProceduresPage> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: "Home Health Care".needTranslation,
history: () => Navigator.of(context).push(CustomPageRoute(page: HhcOrderDetailPage(), direction: AxisDirection.up)),
child: Consumer<HmgServicesViewModel>(
builder: (context, hmgServicesViewModel, child) {
if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) {
return _buildLoadingShimmer();
}
final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList);
if (pendingOrder != null) {
return _buildPendingOrderCard(pendingOrder);
} else {
return Column(
children: [
Center(
child: Utils.getNoDataWidget(
context,
noDataText: "You have no pending requests.".needTranslation,
),
),
],
);
}
},
),
),
),
Consumer(
builder: (BuildContext context, HmgServicesViewModel hmgServicesViewModel, Widget? child) {
if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) {
return SizedBox.shrink();
}
final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList);
if (pendingOrder == null) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
body: CollapsingListView(
title: "Home Health Care".needTranslation,
history: () => Navigator.of(context).push(CustomPageRoute(page: HhcOrderDetailPage(), direction: AxisDirection.up)),
bottomChild: Consumer(
builder: (BuildContext context, HmgServicesViewModel hmgServicesViewModel, Widget? child) {
if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) {
return SizedBox.shrink();
}
final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList);
if (pendingOrder == null) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: CustomButton(
borderWidth: 0,
text: "Create new request".needTranslation,
onPressed: () => _buildServicesListBottomsSheet(hmgServicesViewModel.hhcServicesList),
textColor: AppColors.whiteColor,
borderRadius: 12.r,
borderColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
child: Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 24.h, top: 24.h),
child: CustomButton(
borderWidth: 0,
text: "Create new request".needTranslation,
onPressed: () => _buildServicesListBottomsSheet(hmgServicesViewModel.hhcServicesList),
textColor: AppColors.whiteColor,
borderRadius: 12.r,
borderColor: Colors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
);
}
return SizedBox.shrink();
},
),
child: Consumer<HmgServicesViewModel>(
builder: (context, hmgServicesViewModel, child) {
if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) {
return _buildLoadingShimmer();
}
final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList);
if (pendingOrder != null) {
return _buildPendingOrderCard(pendingOrder);
} else {
return Column(
children: [
Center(
child: Utils.getNoDataWidget(
context,
noDataText: "You have no pending requests.".needTranslation,
),
),
);
}
return SizedBox.shrink();
},
),
],
],
);
}
},
),
),
);
}

@ -267,14 +267,15 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
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),
);
labelText: organ.description,
backgroundColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
deleteIcon: AppAssets.cancel,
deleteIconColor: AppColors.primaryRedColor,
deleteIconHasColor: false,
onDeleteTap: () {
viewModel.removeOrgan(organ.id);
});
}).toList(),
),
),

@ -1,16 +1,15 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_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 {
@ -24,23 +23,24 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
@override
void initState() {
super.initState();
// Initialize symptom groups based on selected organs
// Fetch risk factors based on selected symptoms
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.initializeSymptomGroups();
viewModel.fetchRiskFactors();
});
}
void _onOptionSelected(int optionIndex) {}
void _onRiskFactorSelected(SymptomsCheckerViewModel viewModel, String riskFactorId) {
viewModel.toggleRiskFactorSelection(riskFactorId);
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (viewModel.hasSelectedSymptoms) {
// Navigate to triage screen
if (viewModel.hasSelectedRiskFactors) {
context.navigateWithName(AppRoutes.suggestionsScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one option before proceeding'.needTranslation),
content: Text('Please select at least one risk before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
@ -51,27 +51,13 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
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 _buildRiskFactorItem(SymptomsCheckerViewModel viewModel, String riskFactorId, String optionText) {
final bool selected = viewModel.isRiskFactorSelected(riskFactorId);
Widget _buildOptionItem(int index, bool selected, String optionText) {
return GestureDetector(
onTap: () => _onOptionSelected(index),
onTap: () => _onRiskFactorSelected(viewModel, riskFactorId),
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
margin: EdgeInsets.only(bottom: 16.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -83,56 +69,137 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
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),
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, 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),
),
),
child: optionText.toText14(
color: riskFactorId == "not_applicable" ? AppColors.errorColor : AppColors.textColor,
weight: 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,
Widget _buildRiskFactorsList(SymptomsCheckerViewModel viewModel) {
return Container(
key: ValueKey<int>(viewModel.riskFactorsList.length),
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.only(top: 24.h, left: 16.w, right: 16.w, bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...viewModel.riskFactorsList.map((factor) {
return _buildRiskFactorItem(viewModel, factor.id ?? '', factor.getDisplayName());
}),
SizedBox(height: 12.w),
Row(
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.alertSquare,
height: 24.h,
width: 24.h,
iconColor: AppColors.textColor,
),
SizedBox(width: 12.w),
Expanded(
child: RichText(
text: TextSpan(
style: TextStyle(
height: 1.3,
fontSize: 13.f,
fontWeight: FontWeight.w500,
color: AppColors.greyInfoTextColor,
),
children: [
TextSpan(
text: "Above you see the most common risk factors. Although /diagnosis may return questions about risk factors, "
.needTranslation,
),
TextSpan(
text: "read more".needTranslation,
style: TextStyle(
color: AppColors.primaryRedColor,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// handle tap - navigate or show bottom sheet
debugPrint('Read more tapped');
// Example: Navigator.push(context, MaterialPageRoute(builder: (_) => RiskFactorsDetailScreen()));
},
),
],
),
),
)
],
),
);
},
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");
],
),
);
}
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(
'Not Applicable Risk Factor',
style: TextStyle(fontSize: 14.f, color: AppColors.textColor),
),
).toShimmer2(isShow: true, radius: 24.r);
}),
],
),
),
],
),
);
}
@ -147,68 +214,19 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
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(),
// ),
// ],
// ),
// ),
// );
// }),
// ],
// ),
title: "Risk Factors".needTranslation,
leadingCallback: () => context.pop(),
child: viewModel.isRiskFactorsLoading
? _buildLoadingShimmer()
: viewModel.riskFactorsList.isEmpty
? _buildEmptyState()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
_buildRiskFactorsList(viewModel),
],
),
),
),
_buildStickyBottomCard(context, viewModel),
@ -229,7 +247,7 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor),
SizedBox(height: 16.h),
Text(
'No organs selected'.needTranslation,
'No risk factors found'.needTranslation,
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w600,
@ -238,7 +256,7 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
),
SizedBox(height: 8.h),
Text(
'Please go back and select organs first'.needTranslation,
'Based on your selected symptoms, no additional risk factors were identified.'.needTranslation,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.f,

@ -1,16 +1,14 @@
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/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 {
@ -27,14 +25,18 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
// Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.initializeSymptomGroups();
viewModel.fetchSuggestions();
});
}
void _onOptionSelected(int optionIndex) {}
void _onSuggestionSelected(SymptomsCheckerViewModel viewModel, String suggestionId) {
viewModel.toggleSuggestionsSelection(suggestionId);
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (viewModel.hasSelectedSymptoms) {
if (viewModel.hasSelectedSuggestions) {
// Navigate to triage screen
context.navigateWithName(AppRoutes.triageScreen);
} else {
@ -51,27 +53,13 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
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 _buildSuggestionItem(SymptomsCheckerViewModel viewModel, String suggestionId, String optionText) {
final bool selected = viewModel.isSuggestionsSelected(suggestionId);
Widget _buildOptionItem(int index, bool selected, String optionText) {
return GestureDetector(
onTap: () => _onOptionSelected(index),
onTap: () => _onSuggestionSelected(viewModel, suggestionId),
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
margin: EdgeInsets.only(bottom: 16.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -83,56 +71,111 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
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),
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, 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),
),
),
child: optionText.toText14(
color: suggestionId == "not_applicable" ? AppColors.errorColor : AppColors.textColor,
weight: 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,
Widget _buildSuggestionsList(SymptomsCheckerViewModel viewModel) {
return Container(
key: ValueKey<int>(viewModel.suggestionsList.length),
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.only(top: 24.h, left: 16.w, right: 16.w, bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...viewModel.suggestionsList.map((factor) {
return _buildSuggestionItem(viewModel, factor.id ?? '', factor.getDisplayName());
}),
SizedBox(height: 12.w),
Row(
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.alertSquare,
height: 24.h,
width: 24.h,
iconColor: AppColors.textColor,
),
SizedBox(width: 12.w),
Expanded(
child: "This is a list of symptoms suggested by our AI, based on the information gathered so far during the interview".toText12(
color: AppColors.greyInfoTextColor,
),
)
],
),
);
},
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");
],
),
);
}
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(
'Not Applicable Suggestion',
style: TextStyle(fontSize: 14.f, color: AppColors.textColor),
),
).toShimmer2(isShow: true, radius: 24.r);
}),
],
),
),
],
),
);
}
@ -148,68 +191,18 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
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(),
// ),
// ],
// ),
// ),
// );
// }),
// ],
// ),
leadingCallback: () => context.pop(),
child: viewModel.isSuggestionsLoading
? _buildLoadingShimmer()
: viewModel.suggestionsList.isEmpty
? _buildEmptyState()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
_buildSuggestionsList(viewModel),
],
),
),
),
_buildStickyBottomCard(context, viewModel),

@ -148,10 +148,7 @@ class _TriageScreenState extends State<TriageScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentQuestion.question,
style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.w500, color: AppColors.textColor),
),
currentQuestion.question.toText16(weight: FontWeight.w500),
SizedBox(height: 24.h),
...List.generate(currentQuestion.options.length, (index) {
bool selected = currentQuestion.selectedOptionIndex == index;
@ -179,17 +176,12 @@ class _TriageScreenState extends State<TriageScreen> {
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),
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, 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),
),
),
Expanded(child: optionText.toText14(weight: FontWeight.w500)),
],
),
),

@ -0,0 +1,269 @@
import 'dart:developer';
import 'dart:math' as math;
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/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/bottle_shape_clipper.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_consumption_progress_widget.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 WaterConsumptionScreen extends StatefulWidget {
const WaterConsumptionScreen({super.key});
@override
State<WaterConsumptionScreen> createState() => _WaterConsumptionScreenState();
}
class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
@override
void initState() {
super.initState();
// Initialize VM once the widget is mounted
WidgetsBinding.instance.addPostFrameCallback((_) {
final vm = Provider.of<WaterMonitorViewModel>(context, listen: false);
vm.initialize();
// optional: log current progress for debugging
// ignore: avoid_print
log('WaterMonitor initialized: consumed=${vm.totalConsumedMl}, goal=${vm.dailyGoalMl}, progress=${vm.progressPercent}');
});
}
_buildHydrationTipsWidget() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.h),
padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.bulb_icon, width: 24.w, height: 24.h),
SizedBox(width: 8.w),
"Tips to stay hydrated".needTranslation.toText16(isBold: true),
],
),
SizedBox(height: 8.h),
"${"Drink before you feel thirsty"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"Keep a refillable bottle next to you"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"Track your daily intake to stay motivated"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 4.h),
"${"Choose sparkling water instead of soda"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
SizedBox(height: 8.h),
],
),
);
}
_buildStatsColumn() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Next Drink Time".toText18(weight: FontWeight.w600, color: AppColors.textColor),
"03:00 AM".toText32(weight: FontWeight.w600, color: AppColors.blueColor),
SizedBox(height: 12.h),
_buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "2000ml"),
SizedBox(height: 8.h),
_buildStatusColumn(title: "Remaining".needTranslation, subTitle: "300ml"),
SizedBox(height: 8.h),
_buildStatusColumn(title: "Completed".needTranslation, subTitle: "90%", subTitleColor: AppColors.successColor),
SizedBox(height: 8.h),
_buildStatusColumn(title: "Hydration Status".needTranslation, subTitle: "Hydrated", subTitleColor: AppColors.blueColor),
SizedBox(height: 8.h),
],
);
}
_buildWaterIntakeSummaryWidget() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.blueGradientColorOne,
AppColors.blueGradientColorTwo,
],
),
),
child: LayoutBuilder(
builder: (context, constraints) {
// Responsive: stack vertically on small screens
final isSmallScreen = constraints.maxWidth < 600;
if (isSmallScreen) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(flex: 3, child: _buildStatsColumn()),
SizedBox(width: 16.w),
Expanded(flex: 2, child: _buildWaterBottleWidget()),
],
);
} else {
// Larger screens: more space for bottle
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(flex: 2, child: _buildStatsColumn()),
SizedBox(width: 32.w),
Flexible(flex: 1, child: _buildWaterBottleWidget()),
],
);
}
},
),
);
}
_buildStatusColumn({required String title, required String subTitle, Color? subTitleColor}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"$title:".toText16(weight: FontWeight.w500, color: AppColors.textColor),
subTitle.toText12(fontWeight: FontWeight.w600, color: subTitleColor ?? AppColors.greyTextColor),
],
);
}
Widget _buildWaterBottleWidget() {
return Consumer<WaterMonitorViewModel>(
builder: (context, vm, _) {
// final progressPercent = 100.0;
final progressPercent = (vm.progress * 100).clamp(0.0, 100.0);
log("progressPercent: ${progressPercent}");
// SVG original dimensions
final svgOriginalWidth = 143.w;
final svgOriginalHeight = 315.h;
// Responsive sizing - scale based on screen width but maintain aspect ratio
final bottleWidth = 120.w; // Adjust this for your layout
final aspectRatio = svgOriginalHeight / svgOriginalWidth; // 2. 2027
final bottleHeight = bottleWidth * aspectRatio;
// Exact fillable area based on SVG paths (provided by VM calibration)
final fillableHeightPercent = vm.fillableHeightPercent;
final fillableWidthPercent = 0.8; // 80.7% is body width
final bottomPaddingPercent = 0; // 0% padding from bottom (8px of 315px)
final fillableHeight = bottleHeight * fillableHeightPercent;
final fillableWidth = bottleWidth * fillableWidthPercent;
final bottomPadding = bottleHeight * bottomPaddingPercent;
return SizedBox(
height: bottleHeight,
width: bottleWidth,
child: Stack(
alignment: Alignment.center,
children: [
// Bottle SVG
Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.waterBottle,
height: bottleHeight,
width: bottleWidth,
fit: BoxFit.contain, // Important for scaling
),
),
// Water wave - positioned from bottom
Positioned(
bottom: bottomPadding,
right: 10.w,
child: ClipPath(
clipper: BottleShapeClipper(), // Use custom clipper for exact bottle shape
child: SizedBox(
height: fillableHeight,
width: fillableWidth,
child: Align(
alignment: Alignment.bottomCenter,
child: WaterConsumptionProgressWidget(
progress: progressPercent,
// Pass a square size so the painter's vertical range matches the clip area.
// Use the larger of width/height so the waves render correctly and won't be squashed.
size: math.max(fillableWidth, fillableHeight),
// Provide exact container dimensions so the painter draws waves using the bottle's rect
containerWidth: fillableWidth,
containerHeight: fillableHeight,
waveDuration: const Duration(milliseconds: 3000),
waveColor: AppColors.blueColor,
),
),
),
),
),
// Bubbles positioned dynamically based on water level
if (progressPercent > 10)
Positioned(
bottom: 40.h,
child: Utils.buildSvgWithAssets(
icon: AppAssets.waterBottleOuterBubbles,
height: 60.h,
width: fillableWidth * 0.7,
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "Water Consumption".needTranslation,
bottomChild: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: CustomButton(
text: "Set Reminder".needTranslation,
textColor: AppColors.successColor,
backgroundColor: AppColors.successLightBgColor,
onPressed: () => {},
icon: AppAssets.bell,
iconColor: AppColors.successColor,
borderRadius: 12.r,
borderColor: AppColors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
),
),
child: Column(
children: [
SizedBox(height: 16.h),
_buildWaterIntakeSummaryWidget(),
SizedBox(height: 16.h),
_buildHydrationTipsWidget(),
],
),
),
);
// );
}
}

@ -0,0 +1,360 @@
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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:provider/provider.dart';
class WaterMonitorSettingsScreen extends StatefulWidget {
const WaterMonitorSettingsScreen({super.key});
@override
State<WaterMonitorSettingsScreen> createState() => _WaterMonitorSettingsScreenState();
}
class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<WaterMonitorViewModel>().initialize();
});
}
void _showSnackbar(String text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(text),
backgroundColor: AppColors.errorColor,
),
);
}
// Reusable method to build selection row widget
Widget _buildSelectionRow({
required String value,
required String groupValue,
required VoidCallback onTap,
bool useUpperCase = false,
}) {
return SizedBox(
height: 70.h,
child: Row(
spacing: 8.h,
children: [
Radio<String>(
value: value,
groupValue: groupValue,
activeColor: AppColors.errorColor,
onChanged: (_) => onTap(),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
(useUpperCase ? value.toUpperCase() : value.toCamelCase)
.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1)
.expanded,
],
).onPress(onTap),
);
}
// Reusable method to show selection bottom sheet
void _showSelectionBottomSheet({
required BuildContext context,
required String title,
required List<String> items,
required String selectedValue,
required Function(String) onSelected,
bool useUpperCase = false,
}) {
final dialogService = getIt.get<DialogService>();
dialogService.showFamilyBottomSheetWithoutHWithChild(
label: title.needTranslation,
message: "",
child: Container(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)),
child: ListView.separated(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return _buildSelectionRow(
value: item,
groupValue: selectedValue,
useUpperCase: useUpperCase,
onTap: () {
onSelected(item);
Navigator.pop(context);
},
);
},
separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor),
),
),
onOkPressed: () {},
);
}
void _showGenderSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
_showSelectionBottomSheet(
context: context,
title: "Select Gender".needTranslation,
items: viewModel.genderOptions,
selectedValue: viewModel.selectedGender,
onSelected: viewModel.setGender,
);
}
void _showHeightUnitSelectionBottomSheet(BuildContext context, WaterMonitorViewModel viewModel) {
_showSelectionBottomSheet(
context: context,
title: "Select Unit".needTranslation,
items: viewModel.heightUnits,
selectedValue: viewModel.selectedHeightUnit,
onSelected: viewModel.setHeightUnit,
useUpperCase: true,
);
}
void _showWeightUnitSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
_showSelectionBottomSheet(
context: context,
title: "Select Unit".needTranslation,
items: viewModel.weightUnits,
selectedValue: viewModel.selectedWeightUnit,
onSelected: viewModel.setWeightUnit,
useUpperCase: true,
);
}
void _showActivityLevelSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
_showSelectionBottomSheet(
context: context,
title: "Select Activity Level".needTranslation,
items: viewModel.activityLevels,
selectedValue: viewModel.selectedActivityLevel,
onSelected: viewModel.setActivityLevel,
);
}
void _showNumberOfRemindersSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
_showSelectionBottomSheet(
context: context,
title: "Select Number of Reminders".needTranslation,
items: viewModel.reminderOptions,
selectedValue: viewModel.selectedNumberOfReminders,
onSelected: viewModel.setNumberOfReminders,
);
}
// Reusable method to build text field
Widget _buildTextField(TextEditingController controller, String hintText) {
return TextField(
controller: controller,
keyboardType: TextInputType.number,
maxLines: 1,
cursorHeight: 14.h,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isCollapsed: true,
hintText: hintText,
hintStyle: const TextStyle(color: Colors.grey),
),
style: TextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w500,
color: AppColors.textColor,
),
);
}
// Reusable method to build settings row
Widget _buildSettingsRow({
required String icon,
required String label,
String? value,
Widget? inputField,
String? unit,
VoidCallback? onUnitTap,
VoidCallback? onRowTap,
bool showDivider = true,
}) {
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 40.w,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.greyColor,
borderRadius: 10.r,
hasShadow: false,
),
child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w)),
),
SizedBox(width: 12.w),
Expanded(
flex: unit != null ? 3 : 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor),
if (inputField != null)
Container(
height: 20.w,
alignment: Alignment.centerLeft,
child: inputField,
)
else if (value != null)
value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor),
],
),
),
if (unit != null) ...[
Container(
width: 1.w,
height: 30.w,
color: AppColors.dividerColor,
).paddingOnly(right: 10.w),
Expanded(
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
"Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor),
unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor),
],
),
SizedBox(width: 12.w),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down)
],
).onPress(onUnitTap ?? () {}),
),
] else if (onRowTap != null) ...[
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down),
SizedBox(width: 4.w),
],
],
).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}),
if (showDivider) Divider(height: 1, color: AppColors.dividerColor),
],
);
}
@override
Widget build(BuildContext context) {
final viewModel = context.watch<WaterMonitorViewModel>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "H20 Settings".needTranslation,
bottomChild: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: CustomButton(
text: "Save".needTranslation,
onPressed: () async {
showSwitchCupBottomSheet(context);
return;
final success = await viewModel.saveSettings();
if (!success && viewModel.validationError != null) {
_showSnackBar(context, viewModel.validationError!);
} else if (success) {
_showSnackBar(context, "Saved Successfully".needTranslation);
}
},
borderRadius: 12.r,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
),
),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Column(
children: [
_buildSettingsRow(
icon: AppAssets.profileIcon,
label: "Your Name".needTranslation,
inputField: _buildTextField(viewModel.nameController, 'Guest'),
),
_buildSettingsRow(
icon: AppAssets.genderIcon,
label: "Select Gender".needTranslation,
value: viewModel.selectedGender,
onRowTap: () => _showGenderSelectionBottomsheet(context, viewModel),
),
_buildSettingsRow(
icon: AppAssets.calendarGrey,
label: "Age (11-120) yrs".needTranslation,
inputField: _buildTextField(viewModel.ageController, '20'),
),
_buildSettingsRow(
icon: AppAssets.heightIcon,
label: "Height".needTranslation,
inputField: _buildTextField(viewModel.heightController, '175'),
unit: viewModel.selectedHeightUnit,
onUnitTap: () => _showHeightUnitSelectionBottomSheet(context, viewModel),
),
_buildSettingsRow(
icon: AppAssets.weightScaleIcon,
label: "Weight".needTranslation,
inputField: _buildTextField(viewModel.weightController, '75'),
unit: viewModel.selectedWeightUnit,
onUnitTap: () => _showWeightUnitSelectionBottomsheet(context, viewModel),
),
_buildSettingsRow(
icon: AppAssets.dumbellIcon,
label: "Activity Level".needTranslation,
value: viewModel.selectedActivityLevel,
onRowTap: () => _showActivityLevelSelectionBottomsheet(context, viewModel),
),
_buildSettingsRow(
icon: AppAssets.notificationIconGrey,
label: "Number of reminders in a day".needTranslation,
value: viewModel.selectedNumberOfReminders,
onRowTap: () => _showNumberOfRemindersSelectionBottomsheet(context, viewModel),
showDivider: false,
),
],
),
),
),
);
}
// Show snackbar for validation errors and success messages
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor,
),
);
}
}

@ -0,0 +1,25 @@
// Add this class at the bottom of your file (outside the main class)
import 'package:flutter/material.dart';
class BottleShapeClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
// Create rounded rectangle matching the bottle body shape
// The bottle has rounded corners with radius ~30-40 based on SVG
final borderRadius = size.width * 0.25; // 25% of width for rounded corners
path.addRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(borderRadius),
),
);
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

@ -0,0 +1,324 @@
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/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/water_monitor/models/water_cup_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.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/common_bottom_sheet.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
/// Bottom sheet to switch between existing cups or add new custom cup
void showSwitchCupBottomSheet(BuildContext context) {
return showCommonBottomSheetWithoutHeight(
context,
titleWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Switch Cups".toText20(weight: FontWeight.w600),
"Select your preferred cup size".toText16(weight: FontWeight.w500, color: AppColors.greyTextColor),
],
),
child: SwitchCupBottomSheet(),
callBackFunc: () {},
);
}
class SwitchCupBottomSheet extends StatelessWidget {
const SwitchCupBottomSheet({super.key});
@override
Widget build(BuildContext context) {
final viewModel = context.watch<WaterMonitorViewModel>();
final selectedId = viewModel.selectedCup?.id;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 16.w,
childAspectRatio: 0.85,
),
itemCount: viewModel.cups.length + 1,
itemBuilder: (context, index) {
if (index == viewModel.cups.length) {
return _buildAddCupItem(context);
}
final cup = viewModel.cups[index];
final isSelected = selectedId == cup.id;
return _buildCupItem(
cup: cup,
isSelected: isSelected,
onTap: () {
viewModel.selectCup(cup.id);
context.pop();
},
);
},
),
],
);
}
Widget _buildCupItem({required WaterCupModel cup, required bool isSelected, required VoidCallback onTap}) {
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor,
width: 1,
),
),
child: Center(
child: Utils.buildSvgWithAssets(
icon: cup.iconPath,
height: 30.h,
width: 42.w,
iconColor: isSelected ? AppColors.primaryRedColor : null,
),
),
),
// Red badge for custom cups (delete)
if (!cup.isDefault)
Positioned(
top: -6.h,
right: -6.w,
child: Builder(builder: (ctx) {
return InkWell(
onTap: () {
// call viewmodel remove
final vm = ctx.read<WaterMonitorViewModel>();
vm.removeCup(cup.id);
},
child: Container(
color: AppColors.whiteColor,
child: Utils.buildSvgWithAssets(icon: AppAssets.minimizeIcon, height: 20.w, width: 20.w),
),
);
}),
),
],
),
SizedBox(height: 2.h),
'${cup.capacityMl}ml'.toText10(weight: FontWeight.w500),
],
),
);
}
Widget _buildAddCupItem(BuildContext context) {
return GestureDetector(
onTap: () {
showCustomizeCupBottomSheet(context);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 60.w,
height: 60.w,
child: Center(child: Utils.buildSvgWithAssets(icon: AppAssets.cupAdd, height: 30.h, width: 42.w)),
),
SizedBox(height: 4.h),
'Add'.needTranslation.toText10(weight: FontWeight.w500),
],
),
);
}
}
/// Bottom sheet to customize cup capacity with slider
void showCustomizeCupBottomSheet(BuildContext context, {WaterCupModel? cupToEdit}) {
return showCommonBottomSheetWithoutHeight(
context,
titleWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Customize your drink cup".needTranslation.toText20(weight: FontWeight.w600),
],
),
child: CustomizeCupBottomSheet(cupToEdit: cupToEdit),
callBackFunc: () {},
);
}
class CustomizeCupBottomSheet extends StatefulWidget {
final WaterCupModel? cupToEdit;
const CustomizeCupBottomSheet({super.key, this.cupToEdit});
@override
State<CustomizeCupBottomSheet> createState() => _CustomizeCupBottomSheetState();
}
class _CustomizeCupBottomSheetState extends State<CustomizeCupBottomSheet> {
static const int minCapacity = 50;
static const int maxCapacity = 500;
late double _currentCapacity;
@override
void initState() {
super.initState();
_currentCapacity = (widget.cupToEdit?.capacityMl ?? 150).toDouble();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Cup icon with fill level visualization
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 60.w,
height: 80.h,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
// Cup image with dynamic fill
SizedBox(
width: 60.w,
height: 80.h,
child: Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.cupEmpty,
width: 60.w,
height: 80.h,
fit: BoxFit.contain,
),
),
),
ClipRect(
child: Align(
alignment: Alignment.bottomCenter,
heightFactor: (_currentCapacity / maxCapacity).clamp(0.0, 1.0),
child: Utils.buildSvgWithAssets(
icon: AppAssets.cupFilled,
width: 60.w,
height: 80.h,
fit: BoxFit.contain,
)),
),
],
),
),
SizedBox(width: 12.w),
// Slider and labels
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Current value
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
'${_currentCapacity.round()}'.toText32(isBold: true),
SizedBox(width: 4.w),
Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Text(
'ml',
style: TextStyle(
fontSize: 16.f,
fontWeight: FontWeight.w500,
color: AppColors.greyTextColor,
height: 1.0,
),
),
),
],
).paddingOnly(left: 12.w),
SizedBox(height: 16.h),
// Slider
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: AppColors.primaryRedColor,
inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2),
thumbColor: AppColors.primaryRedColor,
overlayColor: AppColors.primaryRedColor.withOpacity(0.2),
trackHeight: 4.h,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w),
),
child: Slider(
value: _currentCapacity,
min: minCapacity.toDouble(),
max: maxCapacity.toDouble(),
divisions: (maxCapacity - minCapacity) ~/ 10,
onChanged: (value) => setState(() => _currentCapacity = value),
),
),
Align(
alignment: Alignment.centerRight,
child: 'Max: $maxCapacity ml'.toText14(
color: AppColors.greyTextColor,
),
),
],
),
),
],
),
SizedBox(height: 24.h),
CustomButton(
text: 'Select'.needTranslation,
onPressed: () {
final newCup = WaterCupModel(
id: widget.cupToEdit?.id ?? Uuid().v4(),
name: '${_currentCapacity.round()}ml',
capacityMl: _currentCapacity.round(),
iconPath: AppAssets.cupEmpty,
isDefault: false,
);
final viewModel = context.read<WaterMonitorViewModel>();
if (widget.cupToEdit != null) {
viewModel.updateCup(newCup);
} else {
viewModel.addCup(newCup);
}
viewModel.selectCup(newCup.id);
Navigator.pop(context);
},
backgroundColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
],
);
}
}

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_splash_progress_widget.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class WaterConsumptionProgressWidget extends StatefulWidget {
/// progress: 0.0 - 100.0
final double progress;
final double size;
final Color? waveColor;
final double? containerWidth;
final double? containerHeight;
final Duration? waveDuration;
const WaterConsumptionProgressWidget({
super.key,
required this.progress,
this.size = 100,
this.waveColor,
this.containerWidth,
this.containerHeight,
this.waveDuration,
});
@override
State<WaterConsumptionProgressWidget> createState() => _WaterConsumptionProgressWidgetState();
}
class _WaterConsumptionProgressWidgetState extends State<WaterConsumptionProgressWidget> with SingleTickerProviderStateMixin {
late AnimationController _progressController;
late Animation<double> _progressAnimation;
double _lastTarget = 0.0;
@override
void initState() {
super.initState();
_lastTarget = widget.progress;
_progressController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200));
_progressAnimation = Tween<double>(begin: 0, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut));
_progressController.forward();
}
@override
void didUpdateWidget(covariant WaterConsumptionProgressWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.progress - _lastTarget).abs() > 0.01) {
// animate from current value to new target
final begin = _progressAnimation.value;
_progressAnimation =
Tween<double>(begin: begin, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut));
_progressController
..reset()
..forward();
_lastTarget = widget.progress;
}
}
@override
void dispose() {
_progressController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// default to app blue color if none provided
final blue = widget.waveColor ?? AppColors.blueColor;
return AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return WaterWaveProgress(
progress: _progressAnimation.value,
size: widget.size,
showPercentage: false,
useCircleClip: false,
waveColor: blue,
containerWidth: widget.containerWidth,
containerHeight: widget.containerHeight,
waveDuration: widget.waveDuration,
backgroundColor: Colors.transparent,
progressColor: Colors.transparent,
textColor: Colors.transparent,
);
},
);
}
}

@ -0,0 +1,351 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
/// Spherical water wave progress bar with animated ripple effect
/// Based on https://github.com/meeziest/spherical_water_wavy_progress_bar
///
/// Usage:
/// WaterWaveProgress(
/// progress: 75.0,
/// size: 200.w,
/// showPercentage: true,
/// )
class WaterWaveProgress extends StatefulWidget {
final double progress; // 0.0 to 100.0
final double size;
final bool showPercentage;
final bool useCircleClip;
final Color? waveColor;
final Color? backgroundColor;
final Color? progressColor;
final Color? textColor;
final Duration? waveDuration;
// When drawing inside a non-square clip (like bottle area), provide the actual available
// width and height so the painter can compute vertical fill correctly.
final double? containerWidth;
final double? containerHeight;
const WaterWaveProgress({
super.key,
required this.progress,
this.size = 200,
this.showPercentage = true,
this.useCircleClip = true,
this.waveColor,
this.backgroundColor,
this.progressColor,
this.textColor,
this.waveDuration,
this.containerWidth,
this.containerHeight,
});
@override
State<WaterWaveProgress> createState() => _WaterWaveProgressState();
}
class _WaterWaveProgressState extends State<WaterWaveProgress> with SingleTickerProviderStateMixin {
late AnimationController _waveController;
@override
void initState() {
super.initState();
_waveController = AnimationController(
vsync: this,
duration: widget.waveDuration ?? const Duration(milliseconds: 2000),
)..repeat();
}
@override
void dispose() {
_waveController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final radius = widget.size / 2;
// When container dimensions are provided (non-square bottle area), use them so
// the CustomPaint canvas matches the available clip rect. Otherwise fall back
// to the square size value.
final paintWidth = widget.containerWidth ?? widget.size;
final paintHeight = widget.containerHeight ?? widget.size;
return SizedBox(
width: paintWidth,
height: paintHeight,
child: CustomPaint(
painter: _WavePainter(
progress: widget.progress,
waveAnimation: _waveController,
// For circle-based painters we still provide circleRadius; when using
// rectangular painting the circleRadius is unused.
circleRadius: radius,
waveColor: widget.waveColor ?? AppColors.primaryRedColor,
backgroundColor: widget.backgroundColor ?? AppColors.bgScaffoldColor,
useCircleClip: widget.useCircleClip,
containerWidth: widget.containerWidth,
containerHeight: widget.containerHeight,
),
// Only draw the circular progress/percentage when explicitly requested.
foregroundPainter: widget.showPercentage
? _ProgressPainter(
progress: widget.progress,
circleRadius: radius,
progressColor: widget.progressColor ?? AppColors.primaryRedColor,
textColor: widget.textColor ?? AppColors.textColor,
showPercentage: widget.showPercentage,
)
: null,
),
);
}
}
/// Paints the animated water waves inside a circular clip
class _WavePainter extends CustomPainter {
final double progress;
final Animation<double> waveAnimation;
final double circleRadius;
final Color waveColor;
final Color backgroundColor;
final bool useCircleClip;
final double? containerWidth;
final double? containerHeight;
_WavePainter({
required this.progress,
required this.waveAnimation,
required this.circleRadius,
required this.waveColor,
required this.backgroundColor,
this.useCircleClip = true,
this.containerWidth,
this.containerHeight,
}) : super(repaint: waveAnimation);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
canvas.translate(center.dx, center.dy);
if (useCircleClip) {
// Clip to circle
canvas.clipPath(
Path()
..addOval(
Rect.fromCircle(
center: Offset.zero,
radius: circleRadius,
),
),
);
// Fill background circle if specified
if (backgroundColor != Colors.transparent) {
canvas.drawCircle(
Offset.zero,
circleRadius,
Paint()..color = backgroundColor,
);
}
// Draw two sine waves clipped to circle for spherical style
_drawSineWave(canvas, waveColor.withAlpha((0.5 * 255).round()), shift: 0);
_drawSineWave(canvas, waveColor, shift: circleRadius / 2, mirror: true);
} else {
// No circular clipping: draw waves over the full rectangular area. Use provided container
// dimensions if available; otherwise fall back to square bounds derived from circleRadius.
final w = containerWidth ?? (circleRadius * 2);
final h = containerHeight ?? (circleRadius * 2);
// use local transforms to center at origin and compute waves relative to the provided rect
canvas.save();
canvas.translate(-w / 2, -h / 2); // move origin to top-left of the rect
_drawSineWaveRect(canvas, waveColor.withAlpha((0.5 * 255).round()), width: w, height: h, shift: 0);
_drawSineWaveRect(canvas, waveColor, width: w, height: h, shift: w / 4, mirror: true);
canvas.restore();
}
}
void _drawSineWaveRect(Canvas canvas, Color color, {required double width, required double height, double shift = 0.0, bool mirror = false}) {
if (mirror) {
canvas.save();
canvas.translate(width, 0);
canvas.scale(-1, 1);
}
final amplitude = height * 0.04; // smaller amplitude for rectangular waves
final angularVelocity = pi / (width / 2);
final delta = Curves.easeInOut.transform(progress / 100);
final offsetX = 2 * width * waveAnimation.value + shift;
final offsetY = height * (1.0 - delta);
final path = Path();
for (double x = 0; x <= width; x += 1) {
final y = amplitude * sin(angularVelocity * (x + offsetX));
if (x == 0) {
path.moveTo(x, y + offsetY);
} else {
path.lineTo(x, y + offsetY);
}
}
path.lineTo(width, height);
path.lineTo(0, height);
path.close();
final wavePaint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
canvas.drawPath(path, wavePaint);
if (mirror) canvas.restore();
}
void _drawSineWave(Canvas canvas, Color color, {double shift = 0.0, bool mirror = false}) {
if (mirror) {
canvas.save();
canvas.transform(Matrix4.rotationY(pi).storage);
}
// original circular/spherical style: compute bounds based on circleRadius
final startX = -circleRadius;
final endX = circleRadius;
final startY = circleRadius;
final endY = -circleRadius;
final amplitude = circleRadius * 0.15;
final angularVelocity = pi / circleRadius;
final delta = Curves.easeInOut.transform(progress / 100);
final offsetX = 2 * circleRadius * waveAnimation.value + shift;
final offsetY = startY + (endY - startY - amplitude) * delta;
final wavePaint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final path = Path();
for (double x = startX; x <= endX; x++) {
// Sine wave function: y = A * sin(ωx + φ)
final y = amplitude * sin(angularVelocity * (x + offsetX));
if (x == startX) {
path.moveTo(x, y + offsetY);
} else {
path.lineTo(x, y + offsetY);
}
}
path.lineTo(endX, startY);
path.lineTo(startX, startY);
path.close();
canvas.drawPath(path, wavePaint);
if (mirror) canvas.restore();
}
@override
bool shouldRepaint(covariant _WavePainter oldDelegate) => oldDelegate.progress != progress;
}
/// Paints the circular progress arc and percentage text
class _ProgressPainter extends CustomPainter {
final double progress;
final double circleRadius;
final Color progressColor;
final Color textColor;
final bool showPercentage;
_ProgressPainter({
required this.progress,
required this.circleRadius,
required this.progressColor,
required this.textColor,
required this.showPercentage,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
canvas.translate(center.dx, center.dy);
_drawCircleProgress(canvas);
if (showPercentage) {
_drawProgressText(canvas);
}
}
void _drawCircleProgress(Canvas canvas) {
final strokeWidth = circleRadius * 0.077;
// Background circle with shadow
final bgPaint = Paint()
..color = progressColor.withAlpha((0.2 * 255).round())
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..isAntiAlias = true;
final shadowPaint = Paint()
..color = Colors.black.withAlpha((0.1 * 255).round())
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 10);
canvas.drawCircle(Offset.zero, circleRadius, bgPaint);
canvas.drawCircle(Offset.zero, circleRadius, shadowPaint);
// Progress arc
final progressPaint = Paint()
..color = progressColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..isAntiAlias = true;
canvas.drawArc(
Rect.fromCircle(center: Offset.zero, radius: circleRadius),
-0.5 * pi,
2 * pi * (progress / 100),
false,
progressPaint,
);
}
void _drawProgressText(Canvas canvas) {
final textSpan = TextSpan(
text: "${progress.toInt()}%",
style: TextStyle(
color: textColor,
fontSize: circleRadius * 0.35,
fontWeight: FontWeight.w700,
height: 1.0,
),
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(
canvas,
Offset(-textPainter.width / 2, -textPainter.height / 2),
);
}
@override
bool shouldRepaint(covariant _ProgressPainter oldDelegate) => oldDelegate.progress != progress;
}

@ -21,6 +21,8 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_screen.
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/presentation/water_monitor/water_consumption_screen.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_screen.dart';
import 'package:hmg_patient_app_new/splashPage.dart';
class AppRoutes {
@ -42,6 +44,10 @@ class AppRoutes {
//appointments
static const String bookAppointmentPage = '/bookAppointmentPage';
// Water Monitor
static const String waterConsumptionScreen = '/waterConsumptionScreen';
static const String waterMonitorSettingsScreen = '/waterMonitorSettingsScreen';
// Symptoms Checker
static const String organSelectorPage = '/organSelectorPage';
static const String symptomsSelectorScreen = '/symptomsCheckerScreen';
@ -77,9 +83,8 @@ class AppRoutes {
userInfoFlowManager: (context) => UserInfoFlowManager(),
smartWatches: (context) => SmartwatchInstructionsPage(),
huaweiHealthExample: (context) => HuaweiHealthExample(),
//
healthCalculatorsPage: (context) => HealthCalculatorsPage()
healthCalculatorsPage: (context) => HealthCalculatorsPage(),
waterConsumptionScreen: (context) => WaterConsumptionScreen(),
waterMonitorSettingsScreen: (context) => WaterMonitorSettingsScreen(),
};
}

@ -8,6 +8,7 @@ class AppColors {
static const bottomSheetBgColor = Color(0xFFF8F8FA);
static const lightGreyEFColor = Color(0xffeaeaff);
static const greyF7Color = Color(0xffF7F7F7);
static const greyInfoTextColor = Color(0xff777777);
static const lightGrayColor = Color(0xff808080);
static const greyTextColorLight = Color(0xFFA2A2A2);
@ -32,6 +33,7 @@ class AppColors {
static const Color inputLabelTextColor = Color(0xff898A8D);
static const Color greyTextColor = Color(0xFF8F9AA3);
static const Color lightGrayBGColor = Color(0x142E3039);
static const Color checkBoxBorderColor = Color(0xffD2D2D2);
static const Color pharmacyBGColor = Color(0xFF359846);
@ -42,6 +44,7 @@ class AppColors {
//Chips
static const Color successColor = Color(0xff18C273);
static const Color successLightBgColor = Color(0xffDDF6EA);
static const Color errorColor = Color(0xFFED1C2B);
static const Color alertColor = Color(0xFFD48D05);
static const Color infoColor = Color(0xFF0B85F7);
@ -96,4 +99,9 @@ class AppColors {
static const Color eReferralCardColor = Color(0xFFFF8012);
static const Color bloodDonationCardColor = Color(0xFFFF5662);
static const Color bookAppointment = Color(0xFF415364);
// Water Monitor
static const Color blueColor = Color(0xFF4EB5FF);
static const Color blueGradientColorOne = Color(0xFFF1F7FD);
static const Color blueGradientColorTwo = Color(0xFFD9EFFF);
}

@ -75,7 +75,7 @@ class CustomButton extends StatelessWidget {
children: [
if (icon != null)
Padding(
padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
padding: text.isNotEmpty ? EdgeInsets.only(right: 8.w, left: 8.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled, width: iconS, height: iconS),
),
Visibility(

Loading…
Cancel
Save