Merge branch 'refs/heads/faiz_dev' into dev_sultan

# Conflicts:
#	lib/presentation/book_appointment/laser/laser_appointment.dart
pull/137/head
Sultan khan 2 weeks ago
commit 449c693fdb

@ -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,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 2C3.55229 2 4 2.44772 4 3L4 14C4 15.6782 4.00213 16.8362 4.11923 17.7072C4.17092 18.0917 4.23383 18.5232 4.45131 18.8563C4.60954 18.5142 4.76255 18.1696 4.91553 17.8252C5.29664 16.967 5.67762 16.1092 6.13888 15.2891C6.58328 14.499 7.11953 13.7058 7.77477 13.1011C8.43871 12.4882 9.28219 12.0189 10.3058 12.0189C11.6975 12.0189 12.5705 12.9129 13.1308 13.4867L13.1883 13.5455C13.8415 14.2124 14.1538 14.4717 14.6132 14.4717C15.0859 14.4717 15.4115 14.2943 15.733 13.9424C16.1205 13.5181 16.396 12.9986 16.6658 12.49C16.7134 12.4003 16.7608 12.3109 16.8086 12.2224C17.5056 10.9328 18.5501 9 20.9995 9C21.5518 9 21.9995 9.44772 21.9995 10C21.9995 10.5523 21.5518 11 20.9995 11C19.8601 11 19.3296 11.7648 18.5475 13.2114L18.4738 13.348C18.1403 13.9674 17.7401 14.7105 17.2097 15.2911C16.5941 15.9651 15.7622 16.4717 14.6132 16.4717C13.2476 16.4717 12.3831 15.5847 11.8309 15.0182C11.7865 14.9725 11.7412 14.925 11.6952 14.8766C11.3159 14.4783 10.8784 14.0189 10.3058 14.0189C9.93769 14.0189 9.55589 14.1788 9.13128 14.5707C8.69797 14.9707 8.28242 15.5577 7.88206 16.2696C7.45308 17.0322 7.09983 17.8308 6.7465 18.6295C6.56362 19.043 6.38072 19.4565 6.18729 19.865C6.22215 19.8708 6.25731 19.876 6.2928 19.8808C7.16378 19.9979 8.32182 20 10 20H21C21.5523 20 22 20.4477 22 21C22 21.5523 21.5523 22 21 22H9.928C8.33933 22 7.04616 22.0001 6.0263 21.8629C4.96232 21.7199 4.04736 21.4113 3.31802 20.682C2.58869 19.9526 2.28011 19.0377 2.13706 17.9737C1.99995 16.9539 1.99997 15.6607 2 14.0721L2 3C2 2.44772 2.44772 2 3 2Z" fill="#2E3039"/>
<path d="M8 4C8.55229 4 9 3.55228 9 3C9 2.44772 8.55229 2 8 2L7 2C6.44772 2 6 2.44772 6 3C6 3.55228 6.44772 4 7 4L8 4Z" fill="#2E3039"/>
<path d="M11 8C11.5523 8 12 7.55228 12 7C12 6.44772 11.5523 6 11 6H7C6.44772 6 6 6.44772 6 7C6 7.55228 6.44772 8 7 8L11 8Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 8.0625C0 12.5153 3.60975 16.125 8.0625 16.125C12.5153 16.125 16.125 12.5153 16.125 8.0625C16.125 3.60975 12.5153 0 8.0625 0C3.60975 0 0 3.60975 0 8.0625ZM11.5695 5.25974C11.8748 5.53949 11.895 6.01423 11.6153 6.31948L7.49026 10.8195C7.35226 10.9702 7.158 11.058 6.954 11.0625C6.74925 11.067 6.552 10.9875 6.40725 10.8428L4.53225 8.96777C4.239 8.67527 4.239 8.19973 4.53225 7.90723C4.82475 7.61398 5.30026 7.61398 5.59276 7.90723L6.91425 9.228L10.5097 5.30552C10.7895 5.00027 11.2643 4.97999 11.5695 5.25974Z" fill="#18C273"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path 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,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4C2.44772 4 2 4.44772 2 5C2 5.55228 2.44772 6 3 6L5 6C5.55228 6 6 5.55229 6 5C6 4.44772 5.55228 4 5 4L3 4Z" fill="#2E3039"/>
<path d="M9 4C8.44772 4 8 4.44772 8 5C8 5.55228 8.44772 6 9 6L21 6C21.5523 6 22 5.55229 22 5C22 4.44772 21.5523 4 21 4L9 4Z" fill="#2E3039"/>
<path d="M8 12C8 11.4477 8.44772 11 9 11L21 11C21.5523 11 22 11.4477 22 12C22 12.5523 21.5523 13 21 13L9 13C8.44772 13 8 12.5523 8 12Z" fill="#2E3039"/>
<path d="M3 11C2.44772 11 2 11.4477 2 12C2 12.5523 2.44772 13 3 13H5C5.55228 13 6 12.5523 6 12C6 11.4477 5.55228 11 5 11L3 11Z" fill="#2E3039"/>
<path d="M8 19C8 18.4477 8.44772 18 9 18L21 18C21.5523 18 22 18.4477 22 19C22 19.5523 21.5523 20 21 20L9 20C8.44772 20 8 19.5523 8 19Z" fill="#2E3039"/>
<path d="M3 18C2.44772 18 2 18.4477 2 19C2 19.5523 2.44772 20 3 20H5C5.55228 20 6 19.5523 6 19C6 18.4477 5.55228 18 5 18H3Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 975 B

@ -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

@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.0625 16.125C3.6097 16.125 0 12.5153 0 8.0625C0 3.6097 3.6097 0 8.0625 0C12.5153 0 16.125 3.6097 16.125 8.0625C16.125 12.5153 12.5153 16.125 8.0625 16.125ZM8.0625 4.3125C8.47671 4.3125 8.8125 4.64829 8.8125 5.0625V8.43749H8.93401C9.15181 8.43742 9.38226 8.43734 9.56413 8.45912C9.69673 8.47499 10.1846 8.53481 10.4097 8.99153C10.635 9.4486 10.3763 9.85561 10.3061 9.96553C10.2094 10.1169 10.0646 10.2914 9.92764 10.4564L9.90413 10.4848C9.68853 10.7449 9.43178 11.0409 9.1743 11.2764C9.04572 11.394 8.89699 11.5151 8.73553 11.6109C8.58584 11.6996 8.34945 11.8125 8.0625 11.8125C7.77556 11.8125 7.53916 11.6996 7.38947 11.6109C7.22801 11.5151 7.07929 11.394 6.95071 11.2764C6.69322 11.0409 6.43647 10.7449 6.22087 10.4848L6.19736 10.4564C6.06041 10.2914 5.91565 10.1169 5.81894 9.96552C5.74871 9.85561 5.49001 9.4486 5.71529 8.99153C5.94038 8.53481 6.42827 8.47499 6.56087 8.45912C6.74274 8.43734 6.9732 8.43742 7.191 8.43749H7.3125L7.3125 5.0625C7.3125 4.64829 7.64829 4.3125 8.0625 4.3125Z" fill="#FF9E15"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 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}");
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);

@ -334,6 +334,8 @@ var GET_PATIENT_SHARE_FOR_WALKIN_APPOINTMENT = 'Services/Doctors.svc/REST/GetChe
var CAN_PAY_FOR_FOR_WALKIN_APPOINTMENT = 'Services/Doctors.svc/REST/CanPayForWalkinAppointment';
var GET_APPOINTMENT_NEAREST_GATE = 'Services/OUTPs.svc/REST/getGateByProjectIDandClinicID';
//URL to get medicine and pharmacies list
var CHANNEL = 3;
var GENERAL_ID = 'Cs2020@2016\$2958';
@ -437,14 +439,6 @@ var RATE_DOCTOR_RESPONSE = 'Services/OUTPs.svc/REST/insertAppointmentQuestionRat
var GET_PATIENT_ALLERGIES = 'Services/Patients.svc/REST/GetPatientAllergies';
// H2O
var H2O_GET_USER_PROGRESS = "Services/H2ORemainder.svc/REST/H2O_GetUserProgress";
var H2O_INSERT_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2O_InsertUserActivity";
var H2O_GET_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_GetUserDetails_New";
var H2O_UPDATE_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New";
var H2O_UNDO_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity";
//E_Referral Services
// Encillary Orders
var GET_ANCILLARY_ORDERS = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList';
@ -670,25 +664,6 @@ var addPayFortApplePayResponse = "Services/PayFort_Serv.svc/REST/AddResponse";
// Auth Provider Consts
const String INSERT_DEVICE_IMEI = 'Services/Patients.svc/REST/Patient_INSERTDeviceIMEI';
const String SELECT_DEVICE_IMEI = 'Services/Patients.svc/REST/Patient_SELECTDeviceIMEIbyIMEI';
const String CHECK_PATIENT_AUTH = 'Services/Authentication.svc/REST/CheckPatientAuthentication';
const GET_MOBILE_INFO = 'Services/Authentication.svc/REST/GetMobileLoginInfo';
const FORGOT_PASSWORD = 'Services/Authentication.svc/REST/CheckActivationCodeForSendFileNo';
const CHECK_PATIENT_FOR_REGISTRATION = "Services/Authentication.svc/REST/CheckPatientForRegisteration";
const CHECK_USER_STATUS = "Services/NHIC.svc/REST/GetPatientInfo";
const REGISTER_USER = 'Services/Authentication.svc/REST/PatientRegistration';
const LOGGED_IN_USER_URL = 'Services/MobileNotifications.svc/REST/Insert_PatientMobileDeviceInfo';
const FORGOT_PATIENT_ID = 'Services/Authentication.svc/REST/SendPatientIDSMSByMobileNumber';
const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard';
const PROFILE_SETTING = 'Services/Patients.svc/REST/GetPateintInfoForUpdate';
const SAVE_SETTING = 'Services/Patients.svc/REST/UpdatePateintInfo';
const DEACTIVATE_ACCOUNT = 'Services/Patients.svc/REST/PatientAppleActivation_InsertUpdate';
var ER_CREATE_ADVANCE_PAYMENT = "services/Doctors.svc/REST/ER_CreateAdvancePaymentForClinic";
//family Files
@ -853,12 +828,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 +840,14 @@ 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 h2oInsertUserDetailsNew = "Services/H2ORemainder.svc/REST/H2O_InsertUserDetails_New";
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;
@ -877,3 +859,34 @@ class ApiConsts {
class ApiKeyConstants {
static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng';
}
//flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_InsertUserActivity
// flutter: {"IdentificationNo":"2530976584","MobileNumber":"504278212","QuantityIntake":200,"VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":200.00,"PercentageConsumed":9.41,"PercentageLeft":90.59,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[{"Quantity":200.000,"CreatedDate":"\/Date(1766911222217+0300)\/"}],"VerificationCode":null,"isSMSSent":false}
// URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2o_UndoUserActivity
// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
// Progress":2 means weekly data
// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress
// flutter: {"Progress":2,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":null,"UserProgressForWeekData":[{"DayNumber":1,"DayDate":null,"DayName":"Sunday","PercentageConsumed":0},{"DayNumber":7,"DayDate":null,"DayName":"Saturday","PercentageConsumed":0},{"DayNumber":6,"DayDate":null,"DayName":"Friday","PercentageConsumed":0},{"DayNumber":5,"DayDate":null,"DayName":"Thursday","PercentageConsumed":0},{"DayNumber":4,"DayDate":null,"DayName":"Wednesday","PercentageConsumed":0},{"DayNumber":3,"DayDate":null,"DayName":"Tuesday","PercentageConsumed":0},{"DayNumber":2,"DayDate":null,"DayName":"Monday","PercentageConsumed":0}],"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
// Progress":1 means daily data
//URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress
// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
// Progress":1 means monthly data
// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress
// flutter: {"Progress":3,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":[{"MonthNumber":1,"MonthName":"January","PercentageConsumed":0},{"MonthNumber":2,"MonthName":"February","PercentageConsumed":0},{"MonthNumber":3,"MonthName":"March","PercentageConsumed":0},{"MonthNumber":4,"MonthName":"April","PercentageConsumed":0},{"MonthNumber":5,"MonthName":"May","PercentageConsumed":0},{"MonthNumber":6,"MonthName":"June","PercentageConsumed":0},{"MonthNumber":7,"MonthName":"July","PercentageConsumed":0},{"MonthNumber":8,"MonthName":"August","PercentageConsumed":0},{"MonthNumber":9,"MonthName":"September","PercentageConsumed":0},{"MonthNumber":10,"MonthName":"October","PercentageConsumed":0},{"MonthNumber":11,"MonthName":"November","PercentageConsumed":0},{"MonthNumber":12,"MonthName":"December","PercentageConsumed":0}],"UserProgressForTodayData":null,"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}

@ -247,6 +247,26 @@ class AppAssets {
static const String refreshIcon = '$svgBasePath/refresh.svg';
static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg';
// 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';
static const String graphIcon = '$svgBasePath/graph_icon.svg';
static const String listIcon = '$svgBasePath/list_icon.svg';
static const String yellowArrowDownIcon = '$svgBasePath/yellow_arrow_down_icon.svg';
static const String greenTickIcon = '$svgBasePath/green_tick_icon.svg';
// PNGS
static const String bloodSugar = '$svgBasePath/bloodsugar.svg';

@ -63,6 +63,7 @@ class CacheConst {
static const String pharmacyAutorzieToken = 'PHARMACY_AUTORZIE_TOKEN';
static const String h2oUnit = 'H2O_UNIT';
static const String h2oReminder = 'H2O_REMINDER';
static const String waterReminderEnabled = 'WATER_REMINDER_ENABLED';
static const String livecareClinicData = 'LIVECARE_CLINIC_DATA';
static const String doctorScheduleDateSel = 'DOCTOR_SCHEDULE_DATE_SEL';
static const String appointmentHistoryMedical = 'APPOINTMENT_HISTORY_MEDICAL';

@ -1,26 +1,26 @@
///class used to provide value for the [DynamicResultChart] to plot the values
class DataPoint {
///values that is displayed on the graph and dot is plotted on this
final double value;
///label shown on the bottom of the graph
String label;
String referenceValue;
String actualValue;
String? unitOfMeasurement ;
String? unitOfMeasurement;
DateTime time;
String displayTime;
DataPoint(
{required this.value,
required this.label,
required this.referenceValue,
required this.actualValue,
required this.time,
required this.displayTime,
this.unitOfMeasurement
});
DataPoint({
required this.value,
required this.label,
required this.actualValue,
required this.time,
required this.displayTime,
this.unitOfMeasurement,
this.referenceValue = '',
});
@override
String toString() {

@ -1,4 +1,5 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
@ -45,6 +46,8 @@ 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_repo.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
@ -53,6 +56,7 @@ import 'package:hmg_patient_app_new/services/firebase_service.dart';
import 'package:hmg_patient_app_new/services/localauth_service.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/notification_service.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:local_auth/local_auth.dart';
import 'package:logger/web.dart';
@ -101,6 +105,13 @@ class AppDependencies {
final sharedPreferences = await SharedPreferences.getInstance();
getIt.registerLazySingleton<CacheService>(() => CacheServiceImp(sharedPreferences: sharedPreferences, loggerService: getIt()));
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
getIt.registerLazySingleton<NotificationService>(() => NotificationServiceImp(
flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin,
loggerService: getIt(),
));
getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt()));
getIt.registerLazySingleton<LocalAuthService>(
() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()),
@ -126,6 +137,7 @@ class AppDependencies {
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<WaterMonitorRepo>(() => WaterMonitorRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
// ViewModels
// Global/shared VMs LazySingleton
@ -136,25 +148,25 @@ class AppDependencies {
() => RadiologyViewModel(radiologyRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()),
);
getIt.registerLazySingleton<PrescriptionsViewModel>(() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<PrescriptionsViewModel>(
() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<InsuranceViewModel>(() => InsuranceViewModel(insuranceRepo: getIt(), errorHandlerService: getIt()));
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()));
getIt.registerLazySingleton<AppointmentRatingViewModel>(
() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
getIt.registerLazySingleton<PayfortViewModel>(
() => PayfortViewModel(
payfortRepo: getIt(),
errorHandlerService: getIt(),
),
() => PayfortViewModel(payfortRepo: getIt(), errorHandlerService: getIt()),
);
getIt.registerLazySingleton<HabibWalletViewModel>(
() => HabibWalletViewModel(
habibWalletRepo: getIt(),
errorHandlerService: getIt(),
errorHandlerService: getIt()
),
);
@ -167,7 +179,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>(
@ -181,8 +198,15 @@ 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());
getIt.registerLazySingleton<DateRangeSelectorRangeViewModel>(() => DateRangeSelectorRangeViewModel());
@ -195,7 +219,14 @@ class AppDependencies {
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>(
@ -208,30 +239,37 @@ class AppDependencies {
getIt.registerLazySingleton<HealthCalcualtorViewModel>(() => HealthCalcualtorViewModel());
getIt.registerLazySingleton<TodoSectionViewModel>(
() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()),
getIt.registerLazySingleton<TodoSectionViewModel>(() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<SymptomsCheckerViewModel>(
() => SymptomsCheckerViewModel(
errorHandlerService: getIt(),
symptomsCheckerRepo: getIt(),
appState: getIt(),
),
);
getIt.registerLazySingleton<SymptomsCheckerViewModel>(() => SymptomsCheckerViewModel(errorHandlerService: getIt(), symptomsCheckerRepo: 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<HealthProvider>(() => HealthProvider());
// Screen-specific VMs Factory
// getIt.registerFactory<BookAppointmentsViewModel>(
// () => BookAppointmentsViewModel(
// bookAppointmentsRepo: getIt(),
// dialogService: getIt(),
// errorHandlerService: getIt(),
// ),
// );
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt()));
}
}

@ -12,8 +12,9 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:huawei_location/huawei_location.dart' as HmsLocation show FusedLocationProviderClient, Location, LocationSettingsRequest, LocationRequest;
import 'package:location/location.dart' show Location, PermissionStatus, LocationData;
import 'package:huawei_location/huawei_location.dart' as HmsLocation
show FusedLocationProviderClient, Location, LocationSettingsRequest, LocationRequest;
import 'package:location/location.dart' show Location;
import 'package:permission_handler/permission_handler.dart' show Permission, PermissionListActions, PermissionStatusGetters, openAppSettings;
class LocationUtils {
@ -59,37 +60,22 @@ class LocationUtils {
// }
void getLocation(
{Function(LatLng)? onSuccess,
VoidCallback? onFailure,
bool isShowConfirmDialog = false,
VoidCallback? onLocationDeniedForever}) async {
{Function(LatLng)? onSuccess, VoidCallback? onFailure, bool isShowConfirmDialog = false, VoidCallback? onLocationDeniedForever}) async {
this.isShowConfirmDialog = isShowConfirmDialog;
if (Platform.isIOS) {
getCurrentLocation(
onFailure: onFailure,
onSuccess: onSuccess,
onLocationDeniedForever: onLocationDeniedForever);
getCurrentLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever);
return;
}
if (await isGMSDevice ?? true) {
getCurrentLocation(
onFailure: onFailure,
onSuccess: onSuccess,
onLocationDeniedForever: onLocationDeniedForever);
getCurrentLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever);
return;
}
getHMSLocation(
onFailure: onFailure,
onSuccess: onSuccess,
onLocationDeniedForever: onLocationDeniedForever);
getHMSLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever);
}
void getCurrentLocation(
{Function(LatLng)? onSuccess,
VoidCallback? onFailure,
VoidCallback? onLocationDeniedForever}) async {
void getCurrentLocation({Function(LatLng)? onSuccess, VoidCallback? onFailure, VoidCallback? onLocationDeniedForever}) async {
var location = Location();
bool isLocationEnabled = await location.serviceEnabled();
@ -113,14 +99,12 @@ class LocationUtils {
}
} else if (permissionGranted == LocationPermission.deniedForever) {
appState.resetLocation();
if(onLocationDeniedForever == null && isShowConfirmDialog){
if (onLocationDeniedForever == null && isShowConfirmDialog) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!),
navigationService.navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText:
"Please grant location permission from app settings to see better results"
.needTranslation,
loadingText: "Please grant location permission from app settings to see better results".needTranslation,
isShowActionButtons: true,
onCancelTap: () {
navigationService.pop();
@ -253,10 +237,7 @@ class LocationUtils {
appState.userLong = locationData.longitude;
}
void getHMSLocation(
{VoidCallback? onFailure,
Function(LatLng p1)? onSuccess,
VoidCallback? onLocationDeniedForever}) async {
void getHMSLocation({VoidCallback? onFailure, Function(LatLng p1)? onSuccess, VoidCallback? onLocationDeniedForever}) async {
try {
var location = Location();
HmsLocation.FusedLocationProviderClient locationService = HmsLocation.FusedLocationProviderClient()..initFusedLocationService();
@ -279,14 +260,12 @@ class LocationUtils {
permissionGranted = await Geolocator.requestPermission();
if (permissionGranted == LocationPermission.deniedForever) {
appState.resetLocation();
if(onLocationDeniedForever == null && isShowConfirmDialog){
if (onLocationDeniedForever == null && isShowConfirmDialog) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!),
navigationService.navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText:
"Please grant location permission from app settings to see better results"
.needTranslation,
loadingText: "Please grant location permission from app settings to see better results".needTranslation,
isShowActionButtons: true,
onCancelTap: () {
navigationService.pop();
@ -311,7 +290,7 @@ class LocationUtils {
HmsLocation.Location data = await locationService.getLastLocation();
if (data.latitude == null || data.longitude == null) {
appState.resetLocation();
appState.resetLocation();
HmsLocation.LocationRequest request = HmsLocation.LocationRequest()
..priority = HmsLocation.LocationRequest.PRIORITY_HIGH_ACCURACY
..interval = 1000 // 1 second

@ -14,19 +14,20 @@ class PostParamsModel {
String? sessionID;
String? setupID;
PostParamsModel(
{this.versionID,
this.channel,
this.languageID,
this.logInTokenID,
this.tokenID,
this.language,
this.ipAddress,
this.generalId,
this.latitude,
this.longitude,
this.deviceTypeID,
this.sessionID});
PostParamsModel({
this.versionID,
this.channel,
this.languageID,
this.logInTokenID,
this.tokenID,
this.language,
this.ipAddress,
this.generalId,
this.latitude,
this.longitude,
this.deviceTypeID,
this.sessionID,
});
PostParamsModel.fromJson(Map<String, dynamic> json) {
versionID = json['VersionID'];

@ -6,8 +6,6 @@ class DateUtil {
/// convert String To Date function
/// [date] String we want to convert
static DateTime convertStringToDate(String? date) {
if (date == null) return DateTime.now();
if (date.isEmpty) return DateTime.now();
@ -522,6 +520,64 @@ class DateUtil {
}
return "";
}
/// Get short month name from full month name
/// [monthName] Full month name like "January"
/// Returns short form like "Jan"
static String getShortMonthName(String monthName) {
switch (monthName.toLowerCase()) {
case 'january':
return 'Jan';
case 'february':
return 'Feb';
case 'march':
return 'Mar';
case 'april':
return 'Apr';
case 'may':
return 'May';
case 'june':
return 'Jun';
case 'july':
return 'Jul';
case 'august':
return 'Aug';
case 'september':
return 'Sep';
case 'october':
return 'Oct';
case 'november':
return 'Nov';
case 'december':
return 'Dec';
default:
return monthName; // Return as-is if not recognized
}
}
/// Get short weekday name from full weekday name
/// [weekDayName] Full weekday name like "Monday"
/// Returns short form like "Mon"
static String getShortWeekDayName(String weekDayName) {
switch (weekDayName.toLowerCase().trim()) {
case 'monday':
return 'Mon';
case 'tuesday':
return 'Tue';
case 'wednesday':
return 'Wed';
case 'thursday':
return 'Thu';
case 'friday':
return 'Fri';
case 'saturday':
return 'Sat';
case 'sunday':
return 'Sun';
default:
return weekDayName; // Return as-is if not recognized
}
}
}
extension OnlyDate on DateTime {

@ -1,191 +0,0 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
class LocalNotification {
Function(String payload)? _onNotificationClick;
static LocalNotification? _instance;
static LocalNotification? getInstance() {
return _instance;
}
static init({required Function(String payload) onNotificationClick}) {
if (_instance == null) {
_instance = LocalNotification();
_instance?._onNotificationClick = onNotificationClick;
_instance?._initialize();
} else {
// assert(false,(){
// //TODO fix it
// "LocalNotification Already Initialized";
// });
}
}
_initialize() async {
try {
var initializationSettingsAndroid = new AndroidInitializationSettings('app_icon');
var initializationSettingsIOS = DarwinInitializationSettings();
var initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) {
switch (notificationResponse.notificationResponseType) {
case NotificationResponseType.selectedNotification:
// selectNotificationStream.add(notificationResponse.payload);
break;
case NotificationResponseType.selectedNotificationAction:
// if (notificationResponse.actionId == navigationActionId) {
// selectNotificationStream.add(notificationResponse.payload);
// }
break;
}
},
// onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
} catch (ex) {
print(ex.toString());
}
// flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse)
// {
// switch (notificationResponse.notificationResponseType) {
// case NotificationResponseType.selectedNotification:
// // selectNotificationStream.add(notificationResponse.payload);
// break;
// case NotificationResponseType.selectedNotificationAction:
// // if (notificationResponse.actionId == navigationActionId) {
// // selectNotificationStream.add(notificationResponse.payload);
// }
// // break;
// },}
//
// ,
//
// );
}
// void notificationTapBackground(NotificationResponse notificationResponse) {
// // ignore: avoid_print
// print('notification(${notificationResponse.id}) action tapped: '
// '${notificationResponse.actionId} with'
// ' payload: ${notificationResponse.payload}');
// if (notificationResponse.input?.isNotEmpty ?? false) {
// // ignore: avoid_print
// print('notification action tapped with input: ${notificationResponse.input}');
// }
// }
var _random = new Random();
_randomNumber({int from = 100000}) {
return _random.nextInt(from);
}
_vibrationPattern() {
var vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
vibrationPattern[1] = 1000;
vibrationPattern[2] = 5000;
vibrationPattern[3] = 2000;
return vibrationPattern;
}
Future? showNow({required String title, required String subtitle, required String payload}) {
Future.delayed(Duration(seconds: 1)).then((result) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'com.hmg.local_notification',
'HMG',
channelDescription: 'HMG',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
vibrationPattern: _vibrationPattern(),
ongoing: true,
autoCancel: false,
usesChronometer: true,
when: DateTime.now().millisecondsSinceEpoch - 120 * 1000,
);
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(25613, title, subtitle, platformChannelSpecifics, payload: payload).catchError((err) {
print(err);
});
});
}
Future scheduleNotification({required DateTime scheduledNotificationDateTime, required String title, required String description}) async {
///vibrationPattern
var vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
vibrationPattern[1] = 1000;
vibrationPattern[2] = 5000;
vibrationPattern[3] = 2000;
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('active-prescriptions', 'ActivePrescriptions',
// channelDescription: 'ActivePrescriptionsDescription',
// // icon: 'secondary_icon',
// sound: RawResourceAndroidNotificationSound('slow_spring_board'),
//
// ///change it to be as ionic
// // largeIcon: DrawableResourceAndroidBitmap('sample_large_icon'),///change it to be as ionic
// vibrationPattern: vibrationPattern,
// enableLights: true,
// color: const Color.fromARGB(255, 255, 0, 0),
// ledColor: const Color.fromARGB(255, 255, 0, 0),
// ledOnMs: 1000,
// ledOffMs: 500);
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails(sound: 'slow_spring_board.aiff');
// /change it to be as ionic
// var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.schedule(0, title, description, scheduledNotificationDateTime, platformChannelSpecifics);
}
///Repeat notification every day at approximately 10:00:00 am
Future showDailyAtTime() async {
// var time = Time(10, 0, 0);
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('repeatDailyAtTime channel id', 'repeatDailyAtTime channel name', channelDescription: 'repeatDailyAtTime description');
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
// var platformChannelSpecifics = NotificationDetails(
// androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.showDailyAtTime(
// 0,
// 'show daily title',
// 'Daily notification shown at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}',
// time,
// platformChannelSpecifics);
}
///Repeat notification weekly on Monday at approximately 10:00:00 am
Future showWeeklyAtDayAndTime() async {
// var time = Time(10, 0, 0);
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('show weekly channel id', 'show weekly channel name', channelDescription: 'show weekly description');
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
// var platformChannelSpecifics = NotificationDetails(
// androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.showWeeklyAtDayAndTime(
// 0,
// 'show weekly title',
// 'Weekly notification shown on Monday at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}',
// Day.Monday,
// time,
// platformChannelSpecifics);
}
String _toTwoDigitString(int value) {
return value.toString().padLeft(2, '0');
}
Future cancelNotification() async {
await flutterLocalNotificationsPlugin.cancel(0);
}
Future cancelAllNotifications() async {
await flutterLocalNotificationsPlugin.cancelAll();
}
}

@ -15,16 +15,11 @@ import 'package:flutter_callkit_incoming/entities/notification_params.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_ios_voip_kit_karmm/call_state_type.dart';
import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart';
// import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hmg_patient_app_new/core/utils/local_notifications.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
import '../cache_consts.dart';
// |--> Push Notification Background
@pragma('vm:entry-point')
Future<dynamic> backgroundMessageHandler(dynamic message) async {
@ -36,7 +31,7 @@ Future<dynamic> backgroundMessageHandler(dynamic message) async {
// showCallkitIncoming(message);
_incomingCall(message.data);
return;
} else {}
}
}
callPage(String sessionID, String token) async {}
@ -323,7 +318,7 @@ class PushNotificationHandler {
if (fcmToken != null) onToken(fcmToken);
// }
} catch (ex) {
print("Notification Exception: " + ex.toString());
print("Notification Exception: $ex");
}
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
}
@ -331,7 +326,7 @@ class PushNotificationHandler {
if (Platform.isIOS) {
final permission = await FirebaseMessaging.instance.requestPermission();
await FirebaseMessaging.instance.getAPNSToken().then((value) async {
log("APNS token: " + value.toString());
log("APNS token: $value");
await Utils.saveStringFromPrefs(CacheConst.apnsToken, value.toString());
});
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
@ -378,14 +373,14 @@ class PushNotificationHandler {
});
FirebaseMessaging.instance.getToken().then((String? token) {
print("Push Notification getToken: " + token!);
print("Push Notification getToken: ${token!}");
onToken(token!);
}).catchError((err) {
print(err);
});
FirebaseMessaging.instance.onTokenRefresh.listen((fcm_token) {
print("Push Notification onTokenRefresh: " + fcm_token);
print("Push Notification onTokenRefresh: $fcm_token");
onToken(fcm_token);
});
@ -401,7 +396,7 @@ class PushNotificationHandler {
}
newMessage(RemoteMessage remoteMessage) async {
print("Remote Message: " + remoteMessage.data.toString());
print("Remote Message: ${remoteMessage.data}");
if (remoteMessage.data.isEmpty) {
return;
}
@ -427,7 +422,7 @@ class PushNotificationHandler {
}
onToken(String token) async {
print("Push Notification Token: " + token);
print("Push Notification Token: $token");
await Utils.saveStringFromPrefs(CacheConst.pushToken, token);
}
@ -441,9 +436,7 @@ class PushNotificationHandler {
Future<void> requestPermissions() async {
try {
if (Platform.isIOS) {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(alert: true, badge: true, sound: true);
await FirebaseMessaging.instance.requestPermission(alert: true, badge: true, sound: true);
} else if (Platform.isAndroid) {
Map<Permission, PermissionStatus> statuses = await [
Permission.notification,

@ -1,4 +1,5 @@
import 'dart:developer';
import 'dart:math' as math;
import 'package:flutter/material.dart'; // These are the Viewport values of your Figma Design.
@ -6,6 +7,16 @@ import 'package:flutter/material.dart'; // These are the Viewport values of your
const num figmaDesignWidth = 375; // iPhone X / 12 base width
const num figmaDesignHeight = 812; // iPhone X / 12 base height
extension ConstrainedResponsive on num {
/// Width with max cap for tablets
double get wCapped => isTablet ? math.min( w, this * 1.3) : w;
/// Height with max cap for tablets
double get hCapped => isTablet ? math.min(h, this * 1.3) : h;
}
extension ResponsiveExtension on num {
double get _screenWidth => SizeUtils.width;

@ -351,10 +351,10 @@ class Utils {
).center;
}
static Widget getSuccessWidget({String? loadingText}) {
static Widget getSuccessWidget({String? loadingText, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: crossAxisAlignment,
children: [
Lottie.asset(AppAnimations.checkmark, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
@ -885,7 +885,6 @@ class Utils {
launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
static Color getCardBorderColor(int currentQueueStatus) {
switch (currentQueueStatus) {
case 0:

@ -23,14 +23,15 @@ extension CapExtension on String {
extension EmailValidator on String {
Widget get toWidget => Text(this);
Widget toText8({Color? color, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text(
Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) =>
Text(
this,
maxLines: maxlines,
overflow: textOverflow,
style: TextStyle(
fontSize: 8.f,
fontStyle: fontStyle ?? FontStyle.normal,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal),
color: color ?? AppColors.blackColor,
letterSpacing: 0,
),
@ -41,7 +42,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 +215,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 +255,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 +335,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 = ""}) {

@ -5,6 +5,7 @@ import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
@ -102,6 +103,8 @@ abstract class BookAppointmentsRepo {
required int userAge,
Function(dynamic)? onSuccess,
Function(String)? onError});
Future<Either<Failure, GenericApiModel<AppointmentNearestGateResponseModel>>> getAppointmentNearestGate({required int projectID, required int clinicID});
}
class BookAppointmentsRepoImp implements BookAppointmentsRepo {
@ -1046,4 +1049,40 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<AppointmentNearestGateResponseModel>>> getAppointmentNearestGate({required int projectID, required int clinicID}) async {
Map<String, dynamic> mapRequest = {"ProjectID": projectID, "ClinicID": clinicID};
try {
GenericApiModel<AppointmentNearestGateResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
GET_APPOINTMENT_NEAREST_GATE,
body: mapRequest,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final nearestGateResponse = AppointmentNearestGateResponseModel.fromJson(response['getGateByProjectIDandClinicIDList'][0]);
apiResponse = GenericApiModel<AppointmentNearestGateResponseModel>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: nearestGateResponse,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}

@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/LaserCategoryType.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
@ -44,6 +45,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
bool isDoctorsListLoading = false;
bool isDoctorProfileLoading = false;
bool isDoctorSearchByNameStarted = false;
bool isAppointmentNearestGateLoading = false;
bool isLiveCareSchedule = false;
bool isGetDocForHealthCal = false;
@ -132,6 +134,8 @@ class BookAppointmentsViewModel extends ChangeNotifier {
PatientAppointmentShareResponseModel? patientWalkInAppointmentShareResponseModel;
AppointmentNearestGateResponseModel? appointmentNearestGateResponseModel;
///variables for laser clinic
List<LaserCategoryType> femaleLaserCategory = [
LaserCategoryType(1, 'bodyString'),
@ -1343,4 +1347,32 @@ class BookAppointmentsViewModel extends ChangeNotifier {
},
);
}
Future<void> getAppointmentNearestGate({required int projectID, required int clinicID, Function(dynamic)? onSuccess, Function(String)? onError}) async {
isAppointmentNearestGateLoading = true;
notifyListeners();
final result = await bookAppointmentsRepo.getAppointmentNearestGate(projectID: projectID, clinicID: clinicID);
result.fold(
(failure) async {
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage!);
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
appointmentNearestGateResponseModel = apiResponse.data!;
isAppointmentNearestGateLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
}

@ -0,0 +1,64 @@
class AppointmentNearestGateResponseModel {
String? clinicDescription;
String? clinicDescriptionN;
int? clinicID;
String? clinicLocation;
String? clinicLocationN;
int? gender;
int? iD;
String? nearestGateNumber;
String? nearestGateNumberN;
int? projectID;
String? projectName;
String? projectNameN;
int? rowID;
AppointmentNearestGateResponseModel(
{this.clinicDescription,
this.clinicDescriptionN,
this.clinicID,
this.clinicLocation,
this.clinicLocationN,
this.gender,
this.iD,
this.nearestGateNumber,
this.nearestGateNumberN,
this.projectID,
this.projectName,
this.projectNameN,
this.rowID});
AppointmentNearestGateResponseModel.fromJson(Map<String, dynamic> json) {
clinicDescription = json['ClinicDescription'];
clinicDescriptionN = json['ClinicDescriptionN'];
clinicID = json['ClinicID'];
clinicLocation = json['ClinicLocation'];
clinicLocationN = json['ClinicLocationN'];
gender = json['Gender'];
iD = json['ID'];
nearestGateNumber = json['NearestGateNumber'];
nearestGateNumberN = json['NearestGateNumberN'];
projectID = json['ProjectID'];
projectName = json['ProjectName'];
projectNameN = json['ProjectNameN'];
rowID = json['RowID'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = Map<String, dynamic>();
data['ClinicDescription'] = clinicDescription;
data['ClinicDescriptionN'] = clinicDescriptionN;
data['ClinicID'] = clinicID;
data['ClinicLocation'] = clinicLocation;
data['ClinicLocationN'] = clinicLocationN;
data['Gender'] = gender;
data['ID'] = iD;
data['NearestGateNumber'] = nearestGateNumber;
data['NearestGateNumberN'] = nearestGateNumberN;
data['ProjectID'] = projectID;
data['ProjectName'] = projectName;
data['ProjectNameN'] = projectNameN;
data['RowID'] = rowID;
return data;
}
}

@ -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,26 @@
class InsertUserActivityRequestModel {
String? identificationNo;
String? mobileNumber;
num? quantityIntake;
InsertUserActivityRequestModel({
this.identificationNo,
this.mobileNumber,
this.quantityIntake,
});
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (identificationNo != null) map['IdentificationNo'] = identificationNo;
if (mobileNumber != null) map['MobileNumber'] = mobileNumber;
if (quantityIntake != null) map['QuantityIntake'] = quantityIntake;
return map;
}
factory InsertUserActivityRequestModel.fromJson(Map<String, dynamic> json) => InsertUserActivityRequestModel(
identificationNo: json['IdentificationNo']?.toString(),
mobileNumber: json['MobileNumber']?.toString(),
quantityIntake: json['QuantityIntake'],
);
}

@ -0,0 +1,26 @@
class UndoUserActivityRequestModel {
num? progress;
String? mobileNumber;
String? identificationNo;
UndoUserActivityRequestModel({
this.progress,
this.mobileNumber,
this.identificationNo,
});
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (progress != null) map['Progress'] = progress;
if (mobileNumber != null) map['MobileNumber'] = mobileNumber;
if (identificationNo != null) map['IdentificationNo'] = identificationNo;
return map;
}
factory UndoUserActivityRequestModel.fromJson(Map<String, dynamic> json) => UndoUserActivityRequestModel(
progress: json['Progress'],
mobileNumber: json['MobileNumber']?.toString(),
identificationNo: json['IdentificationNo']?.toString(),
);
}

@ -0,0 +1,88 @@
class UpdateUserDetailRequestModel {
// User detail fields
String? activityID;
String? dOB;
String? email;
String? firstName;
String? lastName;
String? firstNameN;
String? middleName;
String? middleNameN;
String? lastNameN;
String? gender;
num? height;
bool? isHeightInCM;
bool? isWeightInKG;
String? zipCode;
num? weight;
bool? isNotificationOn;
String? mobileNumber;
String? identificationNo;
UpdateUserDetailRequestModel({
this.activityID,
this.dOB,
this.email,
this.firstName,
this.lastName,
this.firstNameN,
this.middleName,
this.middleNameN,
this.lastNameN,
this.gender,
this.height,
this.isHeightInCM,
this.isWeightInKG,
this.zipCode,
this.weight,
this.isNotificationOn,
this.mobileNumber,
this.identificationNo,
});
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
// User detail fields
if (activityID != null) map['ActivityID'] = activityID;
if (dOB != null) map['DOB'] = dOB;
if (email != null) map['Email'] = email;
if (firstName != null) map['FirstName'] = firstName;
if (lastName != null) map['LastName'] = lastName;
if (firstNameN != null) map['FirstNameN'] = firstNameN;
if (middleName != null) map['MiddleName'] = middleName;
if (middleNameN != null) map['MiddleNameN'] = middleNameN;
if (lastNameN != null) map['LastNameN'] = lastNameN;
if (gender != null) map['Gender'] = gender;
if (height != null) map['Height'] = height;
if (isHeightInCM != null) map['IsHeightInCM'] = isHeightInCM;
if (isWeightInKG != null) map['IsWeightInKG'] = isWeightInKG;
if (zipCode != null) map['ZipCode'] = zipCode;
if (weight != null) map['Weight'] = weight;
if (isNotificationOn != null) map['IsNotificationOn'] = isNotificationOn;
if (mobileNumber != null) map['MobileNumber'] = mobileNumber;
if (identificationNo != null) map['IdentificationNo'] = identificationNo;
return map;
}
factory UpdateUserDetailRequestModel.fromJson(Map<String, dynamic> json) => UpdateUserDetailRequestModel(
activityID: json['ActivityID']?.toString(),
dOB: json['DOB']?.toString(),
email: json['Email']?.toString(),
firstName: json['FirstName']?.toString(),
lastName: json['LastName']?.toString(),
firstNameN: json['FirstNameN']?.toString(),
middleName: json['MiddleName']?.toString(),
middleNameN: json['MiddleNameN']?.toString(),
lastNameN: json['LastNameN']?.toString(),
gender: json['Gender']?.toString(),
height: json['Height'],
isHeightInCM: json['IsHeightInCM'] as bool?,
isWeightInKG: json['IsWeightInKG'] as bool?,
zipCode: json['ZipCode']?.toString(),
weight: json['Weight'],
isNotificationOn: json['IsNotificationOn'] as bool?,
mobileNumber: json['MobileNumber']?.toString(),
identificationNo: json['IdentificationNo']?.toString(),
);
}

@ -0,0 +1,111 @@
/// Model for today's water progress data
class UserProgressForTodayModel {
num? quantityConsumed;
num? percentageConsumed;
num? percentageLeft;
num? quantityLimit;
UserProgressForTodayModel({
this.quantityConsumed,
this.percentageConsumed,
this.percentageLeft,
this.quantityLimit,
});
factory UserProgressForTodayModel.fromJson(Map<String, dynamic> json) => UserProgressForTodayModel(
quantityConsumed: json['QuantityConsumed'],
percentageConsumed: json['PercentageConsumed'],
percentageLeft: json['PercentageLeft'],
quantityLimit: json['QuantityLimit'],
);
Map<String, dynamic> toJson() {
return {
'QuantityConsumed': quantityConsumed,
'PercentageConsumed': percentageConsumed,
'PercentageLeft': percentageLeft,
'QuantityLimit': quantityLimit,
};
}
}
/// Model for weekly water progress data
class UserProgressForWeekModel {
int? dayNumber;
String? dayDate;
String? dayName;
num? percentageConsumed;
UserProgressForWeekModel({
this.dayNumber,
this.dayDate,
this.dayName,
this.percentageConsumed,
});
factory UserProgressForWeekModel.fromJson(Map<String, dynamic> json) => UserProgressForWeekModel(
dayNumber: json['DayNumber'] as int?,
dayDate: json['DayDate']?.toString(),
dayName: json['DayName']?.toString(),
percentageConsumed: json['PercentageConsumed'],
);
Map<String, dynamic> toJson() {
return {
'DayNumber': dayNumber,
'DayDate': dayDate,
'DayName': dayName,
'PercentageConsumed': percentageConsumed,
};
}
}
/// Model for monthly water progress data
class UserProgressForMonthModel {
int? monthNumber;
String? monthName;
num? percentageConsumed;
UserProgressForMonthModel({
this.monthNumber,
this.monthName,
this.percentageConsumed,
});
factory UserProgressForMonthModel.fromJson(Map<String, dynamic> json) => UserProgressForMonthModel(
monthNumber: json['MonthNumber'] as int?,
monthName: json['MonthName']?.toString(),
percentageConsumed: json['PercentageConsumed'],
);
Map<String, dynamic> toJson() {
return {
'MonthNumber': monthNumber,
'MonthName': monthName,
'PercentageConsumed': percentageConsumed,
};
}
}
/// Model for user progress history data
class UserProgressHistoryModel {
num? quantity;
String? createdDate;
UserProgressHistoryModel({
this.quantity,
this.createdDate,
});
factory UserProgressHistoryModel.fromJson(Map<String, dynamic> json) => UserProgressHistoryModel(
quantity: json['Quantity'],
createdDate: json['CreatedDate']?.toString(),
);
Map<String, dynamic> toJson() {
return {
'Quantity': quantity,
'CreatedDate': createdDate,
};
}
}

@ -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,333 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
/// Progress types to request different ranges from the progress API.
enum ProgressType { today, week, month }
abstract class WaterMonitorRepo {
/// Fetch user details for water monitoring.
/// The request will include the standard payload injected by ApiClient and
/// additionally these three parameters: Progress, MobileNumber, IdentificationNo.
Future<Either<Failure, GenericApiModel<dynamic>>> getUserDetailsForWaterMonitoring({
required num progress,
required String mobileNumber,
required String identificationNo,
});
/// Fetch user progress for water monitoring (H2O_GetUserProgress).
Future<Either<Failure, GenericApiModel<dynamic>>> getUserProgressForWaterMonitoring({
required ProgressType progressType,
required String mobileNumber,
required String identificationNo,
});
/// Update user details for water monitoring.
Future<Either<Failure, GenericApiModel<dynamic>>> updateOrInsertUserDetailForWaterMonitoring(
{required UpdateUserDetailRequestModel requestModel, bool isUpdate = false});
/// Insert user activity (water intake).
Future<Either<Failure, GenericApiModel<dynamic>>> insertUserActivity({
required InsertUserActivityRequestModel requestModel,
});
/// Undo last user activity.
Future<Either<Failure, GenericApiModel<dynamic>>> undoUserActivity({
required UndoUserActivityRequestModel requestModel,
});
}
class WaterMonitorRepoImp implements WaterMonitorRepo {
final ApiClient apiClient;
final LoggerService loggerService;
WaterMonitorRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<dynamic>>> getUserDetailsForWaterMonitoring({
required num progress,
required String mobileNumber,
required String identificationNo,
}) async {
Map<String, dynamic> request = {
"Progress": progress,
"MobileNumber": mobileNumber,
"IdentificationNo": identificationNo,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.h2oGetUserDetail,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// Extract only the specific key from the API response as requested
dynamic extracted;
if (response is Map && response.containsKey('UserDetailData_New')) {
extracted = response['UserDetailData_New'];
} else {
extracted = null;
}
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: extracted,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> getUserProgressForWaterMonitoring({
required ProgressType progressType,
required String mobileNumber,
required String identificationNo,
}) async {
final progressValue = progressType == ProgressType.today ? 1 : (progressType == ProgressType.week ? 2 : 3);
Map<String, dynamic> request = {
"Progress": progressValue,
"MobileNumber": mobileNumber,
"IdentificationNo": identificationNo,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.h2oGetUserProgress,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// Extract progress data and history data
dynamic extracted;
dynamic historyData;
if (response is Map) {
// Extract history data (available for all progress types)
if (response.containsKey('UserProgressHistoryData')) {
historyData = response['UserProgressHistoryData'];
}
// Extract progress data based on type
switch (progressType) {
case ProgressType.today:
if (response.containsKey('UserProgressForTodayData')) {
extracted = response['UserProgressForTodayData'];
}
break;
case ProgressType.week:
if (response.containsKey('UserProgressForWeekData')) {
extracted = response['UserProgressForWeekData'];
}
break;
case ProgressType.month:
if (response.containsKey('UserProgressForMonthData')) {
extracted = response['UserProgressForMonthData'];
}
break;
}
// fallbacks
if (extracted == null) {
if (response.containsKey('UserProgress')) {
extracted = response['UserProgress'];
} else if (response.containsKey('UserProgressData_New')) {
extracted = response['UserProgressData_New'];
} else {
extracted = response;
}
}
} else {
extracted = response;
}
// Package both progress data and history data
final combinedData = {
'progressData': extracted,
'historyData': historyData,
};
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: combinedData,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> updateOrInsertUserDetailForWaterMonitoring({
required UpdateUserDetailRequestModel requestModel,
bool isUpdate = false,
}) async {
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
// Use different endpoint based on isUpdate flag
final endpoint = isUpdate ? ApiConsts.h2oUpdateUserDetail : ApiConsts.h2oInsertUserDetailsNew;
await apiClient.post(
endpoint,
body: requestModel.toJson(),
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
dynamic extracted;
if (response is Map && response.containsKey('UserDetailData_New')) {
extracted = response['UserDetailData_New'];
} else {
extracted = null;
}
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: extracted,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> insertUserActivity({
required InsertUserActivityRequestModel requestModel,
}) async {
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.h2oInsertUserActivity,
body: requestModel.toJson(),
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// Extract UserProgressForTodayData from the response
dynamic extracted;
if (response is Map && response.containsKey('UserProgressForTodayData')) {
extracted = response['UserProgressForTodayData'];
} else {
extracted = response;
}
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: extracted,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> undoUserActivity({
required UndoUserActivityRequestModel requestModel,
}) async {
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.h2oUndoUserActivity,
body: requestModel.toJson(),
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("response h2oUndoUserActivity: ${response.toString()}");
// Extract UserProgressForTodayData from the response
dynamic extracted;
if (response is Map && response.containsKey('UserProgressForTodayData')) {
extracted = response['UserProgressForTodayData'];
} else {
extracted = response;
}
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: extracted,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}

File diff suppressed because it is too large Load Diff

@ -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_and_converts/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';
@ -71,7 +72,7 @@ Future<void> callInitializations() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
AppDependencies.addDependencies();
await AppDependencies.addDependencies();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
HttpOverrides.global = MyHttpOverrides();
await callAppStateInitializations();
@ -165,6 +166,9 @@ void main() async {
),
ChangeNotifierProvider<HealthCalcualtorViewModel>(
create: (_) => getIt.get<HealthCalcualtorViewModel>(),
),
ChangeNotifierProvider<WaterMonitorViewModel>(
create: (_) => getIt.get<WaterMonitorViewModel>(),
)
], child: MyApp()),
),
@ -187,11 +191,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Dr. AlHabib',
builder: (context, mchild) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(1.0),
),
child: mchild!);
return MediaQuery(data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)), child: mchild!);
},
showSemanticsDebugger: false,
debugShowCheckedModeBanner: false,

@ -4,6 +4,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
@ -31,6 +33,7 @@ import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_deta
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
@ -132,10 +135,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
},
onCancelTap: () async {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", );
setState(() {
myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel);
});
LoaderBottomSheet.showLoader(loadingText: "Cancelling Appointment, Please Wait...".needTranslation);
await myAppointmentsViewModel.cancelAppointment(
patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel,
@ -155,6 +155,10 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
isFullScreen: false,
);
});
var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", );
setState(() {
myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel);
});
},
onRescheduleTap: () async {
openDoctorScheduleCalendar();
@ -164,90 +168,105 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? Column(
children: [
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
"Appointment Status".needTranslation.toText16(isBold: true),
],
),
SizedBox(height: 4.h),
(!AppointmentType.isConfirmed(widget.patientAppointmentHistoryResponseModel)
? "Not Confirmed".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500)
: "Confirmed".needTranslation.toText12(color: AppColors.successColor, fontWeight: FontWeight.w500)),
SizedBox(height: 16.h),
//TODO Add countdown timer in case of LiveCare Appointment
widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false
? Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 40.h, height: 40.h),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"The doctor will call you once the appointment time approaches."
.needTranslation
.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500),
],
Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
"Appointment Status".needTranslation.toText16(isBold: true),
],
),
SizedBox(height: 4.h),
(!AppointmentType.isConfirmed(widget.patientAppointmentHistoryResponseModel)
? "Not Confirmed".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500)
: "Confirmed".needTranslation.toText12(color: AppColors.successColor, fontWeight: FontWeight.w500)),
SizedBox(height: 16.h),
//TODO Add countdown timer in case of LiveCare Appointment
widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false
? Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 40.h, height: 40.h),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"The doctor will call you once the appointment time approaches."
.needTranslation
.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500),
],
),
),
),
],
)
: Stack(
children: [
ClipRRect(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(24.r),
// Todo: what is this???? Api Key??? 😲
child: Image.network(
"https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
fit: BoxFit.contain,
],
)
: Stack(
children: [
ClipRRect(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(24.r),
// Todo: what is this???? Api Key??? 😲
child: Image.network(
"https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
fit: BoxFit.contain,
),
),
),
Positioned(
bottom: 0,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.785,
child: CustomButton(
text: "Get Directions".needTranslation,
onPressed: () {
MapsLauncher.launchCoordinates(
double.parse(widget.patientAppointmentHistoryResponseModel.latitude!),
double.parse(widget.patientAppointmentHistoryResponseModel.longitude!),
widget.patientAppointmentHistoryResponseModel.projectName);
},
backgroundColor: AppColors.textColor.withValues(alpha: 0.8),
borderColor: AppointmentType.getNextActionButtonColor(
widget.patientAppointmentHistoryResponseModel.nextAction)
.withValues(alpha: 0.01),
textColor: AppColors.whiteColor,
fontSize: 14.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppAssets.directions_icon,
iconColor: AppColors.whiteColor,
iconSize: 14.h,
).paddingAll(12.h),
Positioned(
bottom: 0,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.785,
child: CustomButton(
text: "Get Directions".needTranslation,
onPressed: () {
MapsLauncher.launchCoordinates(double.parse(widget.patientAppointmentHistoryResponseModel.latitude!),
double.parse(widget.patientAppointmentHistoryResponseModel.longitude!), widget.patientAppointmentHistoryResponseModel.projectName);
},
backgroundColor: AppColors.textColor.withValues(alpha: 0.8),
borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: AppColors.whiteColor,
fontSize: 14.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppAssets.directions_icon,
iconColor: AppColors.whiteColor,
iconSize: 14.h,
).paddingAll(12.h),
),
),
),
],
),
],
],
),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: bookAppointmentsVM.isAppointmentNearestGateLoading
? "Floor: Ground Floor"
: "Floor: ${getIt.get<AppState>().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}",
).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
AppCustomChipWidget(
labelText:
"Nearest Gate: ${getIt.get<AppState>().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}")
.toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
],
),
],
),
),
),
),
);
}),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(

@ -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),
],

@ -22,6 +22,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointme
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
@ -187,18 +188,9 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
AppCustomChipWidget(
labelText:
"${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments"),
Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
@ -224,7 +216,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
child: isExpanded
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 0.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -232,63 +224,6 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
appointment.doctorImageURL!,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,
).circle(100),
SizedBox(width: 8.h),
Expanded(child: appointment.doctorNameObj!.toText14(weight: FontWeight.w500)),
],
),
SizedBox(height: 8.h),
Row(
children: [
CustomButton(
text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointment.appointmentDate), false),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: myAppointmentsVM.isAppointmentsSortByClinic ? appointment.projectName! : appointment.clinicName!,
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: appointment.statusDesc ?? "",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
],
),
SizedBox(height: 8.h),
AppointmentCard(
patientAppointmentHistoryResponseModel: appointment,
myAppointmentsViewModel: myAppointmentsViewModel,
@ -296,9 +231,9 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
isLoading: false,
isFromHomePage: false,
),
SizedBox(height: 12.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h),
SizedBox(height: 12.h),
SizedBox(height: 8.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h).paddingSymmetrical(16.w, 0.h),
SizedBox(height: 8.h),
],
);
}),

@ -372,6 +372,7 @@ class AppointmentCard extends StatelessWidget {
),
);
} else {
bookAppointmentsViewModel.getAppointmentNearestGate(projectID: patientAppointmentHistoryResponseModel.projectID, clinicID: patientAppointmentHistoryResponseModel.clinicID);
Navigator.of(context)
.push(
CustomPageRoute(

@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart' show CapExtension;
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/laser_body_parts.dart';
@ -82,7 +83,7 @@ class LaserAppointment extends StatelessWidget {
activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
tabs: [
CustomTabBarModel(null,LocaleKeys.malE.tr()),
CustomTabBarModel(null,"Female".needTranslation.tr()),
CustomTabBarModel(null, "Female".needTranslation),
],
onTabChange: (index) {
var viewmodel = context.read<BookAppointmentsViewModel>();

@ -50,146 +50,168 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
Expanded(
child: CollapsingListView(
title: LocaleKeys.reviewAppointment.tr(context: context),
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
LocaleKeys.docInfo.tr(context: context).toText16(isBold: true),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.network(
bookAppointmentsViewModel.selectedDoctor.doctorImageURL!,
width: 50.h,
height: 50.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.49,
child:
"${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}".toString().toText16(isBold: true, maxlines: 1),
),
Image.network(
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!,
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
),
],
),
SizedBox(height: 2.h),
(bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "")
.toString()
.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1),
],
),
],
),
SizedBox(height: 12.h),
Wrap(
direction: Axis.horizontal,
spacing: 8.h,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation,
),
AppCustomChipWidget(
labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation,
),
AppCustomChipWidget(
labelText:
"${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? DateUtil.formatDateToDate(DateTime.now(), false) : bookAppointmentsViewModel.selectedAppointmentDate}"
.needTranslation,
),
AppCustomChipWidget(
labelText:
"${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? "Waiting Appointment".needTranslation : bookAppointmentsViewModel.selectedAppointmentTime}"
.needTranslation,
),
],
),
],
child: Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) {
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
LocaleKeys.docInfo.tr(context: context).toText16(isBold: true),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
),
),
SizedBox(height: 24.h),
LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Row(
children: [
Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h,
height: 52.h,
),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true),
SizedBox(height: 8.h),
AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"),
],
),
],
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.network(
bookAppointmentsViewModel.selectedDoctor.doctorImageURL!,
width: 50.h,
height: 50.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.49,
child: "${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}"
.toString()
.toText16(isBold: true, maxlines: 1),
),
Image.network(
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!,
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
),
],
),
SizedBox(height: 2.h),
(bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "")
.toString()
.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1),
],
),
],
),
SizedBox(height: 12.h),
Wrap(
direction: Axis.horizontal,
spacing: 8.h,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation,
),
AppCustomChipWidget(
labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation,
),
AppCustomChipWidget(
labelText:
"${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? DateUtil.formatDateToDate(DateTime.now(), false) : bookAppointmentsViewModel.selectedAppointmentDate}"
.needTranslation,
),
AppCustomChipWidget(
labelText:
"${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? "Waiting Appointment".needTranslation : bookAppointmentsViewModel.selectedAppointmentTime}"
.needTranslation,
),
],
),
],
),
),
),
),
SizedBox(height: 24.h),
"Hospital Information".needTranslation.toText16(isBold: true),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.h,
hasShadow: false,
SizedBox(height: 24.h),
LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Row(
children: [
Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h,
height: 52.h,
),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true),
SizedBox(height: 8.h),
AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"),
],
),
],
),
),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Row(
children: [
bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true),
],
SizedBox(height: 24.h),
"Hospital Information".needTranslation.toText16(isBold: true),
SizedBox(height: 16.h),
Container(
width: double.infinity,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: bookAppointmentsVM.isAppointmentNearestGateLoading
? "Floor: Ground Floor"
: "Floor: ${getIt.get<AppState>().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}",
).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
AppCustomChipWidget(
labelText:
"Nearest Gate: ${getIt.get<AppState>().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}")
.toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading),
],
),
],
),
),
),
),
],
),
),
],
),
);
}),
),
),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
borderRadius: 24.r,
hasShadow: true,
),
child: CustomButton(

@ -142,6 +142,7 @@ class WaitingAppointmentOnlineCheckinSheet extends StatelessWidget {
bookAppointmentsViewModel.waitingAppointmentProjectID,
onSuccess: (value) {
LoaderBottomSheet.hideLoader();
bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.waitingAppointmentProjectID, clinicID: bookAppointmentsViewModel.waitingAppointmentDoctor!.clinicID!);
bookAppointmentsViewModel.setIsWaitingAppointmentSelected(true);
Navigator.of(context).push(
CustomPageRoute(

@ -185,6 +185,7 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
),
);
} else {
bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!);
bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime);
Navigator.of(context).pop();
Navigator.of(context).push(

@ -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,
),

@ -143,7 +143,7 @@ class ServicesPage extends StatelessWidget {
AppAssets.daily_water_monitor_icon,
bgColor: AppColors.whiteColor,
true,
route: AppRoutes.eReferralPage,
route: AppRoutes.waterConsumptionScreen,
),
HmgServicesComponentModel(
11,
@ -386,7 +386,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();
},
),
],
],
);
}
},
),
),
);
}

File diff suppressed because one or more lines are too long

@ -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,829 @@
import 'package:fl_chart/fl_chart.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/common_models/data_points.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/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/hydration_tips_widget.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_intake_summary_widget.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:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class WaterConsumptionScreen extends StatefulWidget {
const WaterConsumptionScreen({super.key});
@override
State<WaterConsumptionScreen> createState() => _WaterConsumptionScreenState();
}
class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _refreshData();
});
}
/// Refresh data by calling initialize on the view model
Future<void> _refreshData() async {
final vm = context.read<WaterMonitorViewModel>();
await vm.initialize();
}
Widget _buildLoadingShimmer({bool isForHistory = true}) {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(0.w),
itemCount: 4,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
height: isForHistory ? 60.h : 40.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(10.r),
),
),
);
},
);
}
Widget buildHistoryListTile({required String title, required String subTitle}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title.toText14(weight: FontWeight.w500, color: AppColors.labelTextColor),
subTitle.toText18(weight: FontWeight.w600),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon)
],
).paddingSymmetrical(0, 8.h);
}
Widget _buildHistoryGraphOrList() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.w),
padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Consumer(builder: (BuildContext context, WaterMonitorViewModel viewModel, Widget? child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"History".needTranslation.toText16(isBold: true),
Row(
children: [
InkWell(
onTap: () => viewModel.setGraphView(!viewModel.isGraphView),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
},
child: Container(
key: ValueKey<bool>(viewModel.isGraphView),
child: Utils.buildSvgWithAssets(
icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon,
height: 24.h,
width: 24.h,
),
),
),
),
SizedBox(width: 8.w),
InkWell(
onTap: () => _showHistoryDurationBottomsheet(context, viewModel),
child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_calendar_icon, height: 24.h, width: 24.h))
],
),
],
),
SizedBox(height: 12.h),
if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else _buildHistoryGraph()
],
);
}),
);
}
Widget _buildHistoryListView(WaterMonitorViewModel viewModel) {
final selectedDuration = viewModel.selectedDurationFilter;
// Build list items based on duration
List<Widget> listItems = [];
if (selectedDuration == 'Daily') {
if (viewModel.todayProgressList.isNotEmpty) {
final todayData = viewModel.todayProgressList.first;
listItems.add(
buildHistoryListTile(
title: "Today's Progress",
subTitle: "${todayData.quantityConsumed?.toStringAsFixed(0) ?? '0'} ml / ${todayData.quantityLimit?.toStringAsFixed(0) ?? '0'} ml",
),
);
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
listItems.add(
buildHistoryListTile(
title: "Percentage Completed",
subTitle: "${todayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
),
);
// Add history data if available (show ALL entries)
if (viewModel.historyList.isNotEmpty) {
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
listItems.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: "Water Intake History".toText14(
weight: FontWeight.w600,
color: AppColors.textColor,
),
),
);
// Show all history entries
for (var history in viewModel.historyList) {
final quantity = "${history.quantity?.toStringAsFixed(0) ?? '0'} ml";
final time = _formatHistoryDate(history.createdDate ?? '');
listItems.add(
buildHistoryListTile(
title: quantity,
subTitle: time,
),
);
if (history != viewModel.historyList.last) {
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
}
}
}
} else {
listItems.add(
Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: "No data available for today".toText14(color: AppColors.greyTextColor),
),
),
);
}
} else if (selectedDuration == 'Weekly') {
if (viewModel.weekProgressList.isNotEmpty) {
// Show previous 6 days + today (total 7 days)
// API returns data in reverse order (today first), so we reverse it to show oldest to newest (top to bottom)
// This ensures today appears at the end (bottom)
final totalDays = viewModel.weekProgressList.length;
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList();
for (var dayData in weekDataToShow) {
listItems.add(
buildHistoryListTile(
title: dayData.dayName ?? 'Unknown',
subTitle: "${dayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
),
);
if (dayData != weekDataToShow.last) {
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
}
}
} else {
listItems.add(
Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: "No data available for this week".toText14(color: AppColors.greyTextColor),
),
),
);
}
} else if (selectedDuration == 'Monthly') {
if (viewModel.monthProgressList.isNotEmpty) {
// Show last 6 months + current month (total 7 months)
// Show in chronological order: oldest to newest (top to bottom)
final totalMonths = viewModel.monthProgressList.length;
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList();
for (var monthData in monthDataToShow) {
listItems.add(
buildHistoryListTile(
title: monthData.monthName ?? 'Unknown',
subTitle: "${monthData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
),
);
if (monthData != monthDataToShow.last) {
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
}
}
} else {
listItems.add(
Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: "No data available for this year".toText14(color: AppColors.greyTextColor),
),
),
);
}
}
// Return scrollable list with min and max height constraints
return ConstrainedBox(
constraints: BoxConstraints(minHeight: 80.h, maxHeight: 270.h),
child: viewModel.isLoading
? _buildLoadingShimmer().paddingOnly(top: 16.h)
: listItems.isEmpty
? Center(
child: "No history data available".toText14(color: AppColors.greyTextColor),
)
: ListView.separated(
padding: EdgeInsets.only(top: 16.h),
shrinkWrap: true,
itemCount: listItems.length,
separatorBuilder: (context, index) => SizedBox.shrink(),
itemBuilder: (context, index) => listItems[index],
),
);
}
Widget _buildHistoryGraph() {
return Consumer<WaterMonitorViewModel>(
builder: (context, viewModel, _) {
final selectedDuration = viewModel.selectedDurationFilter;
// Build dynamic data points based on selected duration
List<DataPoint> dataPoints = [];
if (selectedDuration == 'Daily') {
// For daily, show last 7 history entries with at least 5 minutes difference
if (viewModel.historyList.isNotEmpty) {
// Filter entries with at least 5 minutes difference
List<DataPoint> filteredPoints = [];
DateTime? lastTime;
for (var historyItem in viewModel.historyList) {
final currentTime = _parseHistoryDate(historyItem.createdDate ?? '');
// Add if first entry OR if more than 5 minutes difference from last added entry
if (lastTime == null || currentTime.difference(lastTime).inMinutes.abs() >= 5) {
final quantity = historyItem.quantity?.toDouble() ?? 0.0;
final time = _formatHistoryDate(historyItem.createdDate ?? '');
filteredPoints.add(
DataPoint(
value: quantity,
actualValue: quantity.toStringAsFixed(0),
label: time,
displayTime: time,
unitOfMeasurement: 'ml',
time: currentTime,
),
);
lastTime = currentTime;
}
}
// Take only last 7 filtered entries
final totalFiltered = filteredPoints.length;
final startIndex = totalFiltered > 7 ? totalFiltered - 7 : 0;
dataPoints = filteredPoints.skip(startIndex).toList();
} else if (viewModel.todayProgressList.isNotEmpty) {
// Fallback: show today's percentage if no history
final todayData = viewModel.todayProgressList.first;
final percentage = todayData.percentageConsumed?.toDouble() ?? 0.0;
dataPoints.add(
DataPoint(
value: percentage,
actualValue: percentage.toStringAsFixed(1),
label: 'Today',
displayTime: 'Today',
unitOfMeasurement: '%',
time: DateTime.now(),
),
);
}
} else if (selectedDuration == 'Weekly') {
// For weekly, show previous 6 days + today (total 7 days)
// API returns data in reverse order (today first), so we reverse it to show oldest to newest (left to right)
// This ensures today appears at the end (right side)
if (viewModel.weekProgressList.isNotEmpty) {
final totalDays = viewModel.weekProgressList.length;
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList();
for (var dayData in weekDataToShow) {
final percentage = dayData.percentageConsumed?.toDouble() ?? 0.0;
final dayName = dayData.dayName ?? 'Day ${dayData.dayNumber}';
dataPoints.add(
DataPoint(
value: percentage,
actualValue: percentage.toStringAsFixed(1),
label: DateUtil.getShortWeekDayName(dayName),
displayTime: dayName,
unitOfMeasurement: '%',
time: DateTime.now(),
),
);
}
}
} else if (selectedDuration == 'Monthly') {
// For monthly, show last 6 months + current month (total 7 months)
// Show in chronological order: oldest to newest (left to right)
if (viewModel.monthProgressList.isNotEmpty) {
final totalMonths = viewModel.monthProgressList.length;
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList();
for (var monthData in monthDataToShow) {
final percentage = monthData.percentageConsumed?.toDouble() ?? 0.0;
final monthName = monthData.monthName ?? 'Month ${monthData.monthNumber}';
dataPoints.add(
DataPoint(
value: percentage,
actualValue: percentage.toStringAsFixed(1),
label: DateUtil.getShortMonthName(monthName),
displayTime: monthName,
unitOfMeasurement: '%',
time: DateTime.now(),
),
);
}
}
}
// If no data, show empty state
if (dataPoints.isEmpty) {
return Container(
padding: EdgeInsets.symmetric(vertical: 80.h),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.graphIcon,
iconColor: AppColors.greyTextColor.withValues(alpha: 0.5),
height: 32.w,
width: 32.w,
),
SizedBox(height: 12.h),
"No graph data available".toText14(color: AppColors.greyTextColor),
],
),
),
);
}
// Show loading shimmer while fetching data
if (viewModel.isLoading) {
return Container(
padding: EdgeInsets.symmetric(vertical: 40.h),
child: _buildLoadingShimmer(),
);
}
// Configure graph based on selected duration
double maxY;
double minY;
double horizontalInterval;
double leftLabelInterval;
if (selectedDuration == 'Daily') {
// For daily (quantity in ml), use max available cup size
// Get the biggest cup from available cups
final maxCupSize = viewModel.cups.isEmpty ? 500.0 : viewModel.cups.map((cup) => cup.capacityMl.toDouble()).reduce((a, b) => a > b ? a : b);
maxY = maxCupSize;
minY = 0;
// Divide into 4 intervals (5 labels: 0, 1/4, 1/2, 3/4, max)
horizontalInterval = maxY / 4;
leftLabelInterval = maxY / 4;
} else {
// For weekly/monthly (percentage), use 0-100%
maxY = 100.0;
minY = 0;
horizontalInterval = 25;
leftLabelInterval = 25;
}
return CustomGraph(
bottomLabelReservedSize: 30,
dataPoints: dataPoints,
makeGraphBasedOnActualValue: true,
leftLabelReservedSize: 50.h,
showGridLines: true,
maxY: maxY,
minY: minY,
maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0,
horizontalInterval: horizontalInterval,
leftLabelInterval: leftLabelInterval,
showShadow: true,
getDrawingHorizontalLine: (value) {
// Draw dashed lines at intervals
if (selectedDuration == 'Daily') {
// For daily, draw lines every 50 or 100 ml
if (value % horizontalInterval == 0 && value > 0) {
return FlLine(
color: AppColors.greyTextColor.withValues(alpha: 0.3),
strokeWidth: 1.5,
dashArray: [8, 4],
);
}
} else {
// For weekly/monthly, draw lines at 25%, 50%, 75%
if (value == 25 || value == 50 || value == 75) {
return FlLine(
color: AppColors.successColor.withValues(alpha: 0.3),
strokeWidth: 1.5,
dashArray: [8, 4],
);
}
}
return FlLine(color: AppColors.transparent, strokeWidth: 0);
},
leftLabelFormatter: (value) {
if (selectedDuration == 'Daily') {
// Show exactly 5 labels: 0, 1/4, 1/2, 3/4, max
// Check if value matches one of the 5 positions
final interval = maxY / 4;
final positions = [0.0, interval, interval * 2, interval * 3, maxY];
for (var position in positions) {
if ((value - position).abs() < 1) {
return '${value.toInt()}ml'.toText10(weight: FontWeight.w600);
}
}
} else {
// Show percentage labels
if (value == 0) return '0%'.toText10(weight: FontWeight.w600);
if (value == 25) return '25%'.toText10(weight: FontWeight.w600);
if (value == 50) return '50%'.toText10(weight: FontWeight.w600);
if (value == 75) return '75%'.toText10(weight: FontWeight.w600);
if (value == 100) return '100%'.toText10(weight: FontWeight.w600);
}
return SizedBox.shrink();
},
graphColor: AppColors.successColor,
graphShadowColor: AppColors.successColor.withValues(alpha: 0.15),
bottomLabelFormatter: (value, data) {
if (data.isEmpty) return SizedBox.shrink();
// Only show labels for whole number positions (not fractional)
if ((value - value.round()).abs() > 0.01) {
return SizedBox.shrink();
}
int index = value.round();
if (index < 0 || index >= data.length) return SizedBox.shrink();
// For daily, show all 7 time labels (last 7 entries)
if (selectedDuration == 'Daily' && index < 7) {
return Padding(
padding: EdgeInsets.only(top: 5.h),
child: data[index].label.toText8(
fontWeight: FontWeight.w600,
color: AppColors.labelTextColor,
),
);
}
// For weekly, show all 7 days (today + last 6 days)
if (selectedDuration == 'Weekly' && index < 7) {
return Padding(
padding: EdgeInsets.only(top: 5.h),
child: data[index].label.toText10(
weight: FontWeight.w600,
color: AppColors.labelTextColor,
),
);
}
// For monthly, show all 7 months (current month + last 6 months)
if (selectedDuration == 'Monthly' && index < 7) {
return Padding(
padding: EdgeInsets.only(top: 5.h),
child: data[index].label.toText10(
weight: FontWeight.w600,
color: AppColors.labelTextColor,
),
);
}
return SizedBox.shrink();
},
scrollDirection: selectedDuration == 'Monthly' ? Axis.horizontal : Axis.vertical,
height: 250.h,
spotColor: AppColors.successColor,
);
},
);
}
// 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),
);
}
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: AppColors.whiteColor, 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 _showHistoryDurationBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
_showSelectionBottomSheet(
context: context,
title: "Select Duration".needTranslation,
items: viewModel.durationFilters,
selectedValue: viewModel.selectedDurationFilter,
onSelected: viewModel.setFilterDuration,
);
}
/// Handle reminder button tap (Set or Cancel)
Future<void> _handleReminderButtonTap(WaterMonitorViewModel viewModel) async {
if (viewModel.isWaterReminderEnabled) {
// Cancel reminders
_showCancelReminderConfirmation(viewModel);
} else {
// Set reminders
await _setReminders(viewModel);
}
}
/// Show confirmation bottom sheet before cancelling reminders
void _showCancelReminderConfirmation(WaterMonitorViewModel viewModel) {
showCommonBottomSheetWithoutHeight(
title: 'Notice'.needTranslation,
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to cancel all water reminders?".needTranslation,
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
Navigator.pop(context);
await _cancelReminders(viewModel);
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
isDismissible: true,
);
}
/// Set water reminders
Future<void> _setReminders(WaterMonitorViewModel viewModel) async {
// Schedule reminders
final success = await viewModel.scheduleWaterReminders();
if (success) {
final times = await viewModel.getScheduledReminderTimes();
_showReminderScheduledDialog(times);
}
}
/// Cancel water reminders
Future<void> _cancelReminders(WaterMonitorViewModel viewModel) async {
final success = await viewModel.cancelWaterReminders();
}
/// Show bottom sheet with scheduled reminder times
void _showReminderScheduledDialog(List<DateTime> times) {
showCommonBottomSheetWithoutHeight(
title: 'Reminders Set!'.needTranslation,
context,
isCloseButtonVisible: false,
isDismissible: false,
child: Padding(
padding: EdgeInsets.only(top: 16.w, left: 16.w, right: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.getSuccessWidget(loadingText: 'Daily water reminders scheduled at:'.needTranslation),
SizedBox(height: 16.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: times
.map(
(time) => AppCustomChipWidget(
icon: AppAssets.bell,
iconColor: AppColors.quickLoginColor,
richText: _formatTime(time).toText14(),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
),
)
.toList(),
),
SizedBox(height: 24.h),
// OK button
Row(
children: [
Expanded(
child: CustomButton(
height: 56.h,
text: 'OK'.needTranslation,
onPressed: () => Navigator.of(context).pop(),
textColor: AppColors.whiteColor,
),
),
],
),
],
),
),
callBackFunc: () {},
);
}
/// Format DateTime to readable time string
String _formatTime(DateTime time) {
final hour = time.hour;
final minute = time.minute;
final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour);
final period = hour >= 12 ? 'PM' : 'AM';
return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period';
}
/// Format history date from /Date(milliseconds+0300)/ format
String _formatHistoryDate(String dateString) {
try {
// Parse the /Date(milliseconds+0300)/ format
final regex = RegExp(r'\/Date\((\d+)');
final match = regex.firstMatch(dateString);
if (match != null) {
final milliseconds = int.tryParse(match.group(1)!);
if (milliseconds != null) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds);
return _formatTime(dateTime);
}
}
} catch (e) {
return dateString;
}
return dateString;
}
/// Parse history date from /Date(milliseconds+0300)/ format to DateTime
DateTime _parseHistoryDate(String dateString) {
try {
final regex = RegExp(r'\/Date\((\d+)');
final match = regex.firstMatch(dateString);
if (match != null) {
final milliseconds = int.tryParse(match.group(1)!);
if (milliseconds != null) {
return DateTime.fromMillisecondsSinceEpoch(milliseconds);
}
}
} catch (e) {
// Return current time as fallback
}
return DateTime.now();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "Water Consumption".needTranslation,
bottomChild: Consumer<WaterMonitorViewModel>(
builder: (context, viewModel, child) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: CustomButton(
text: viewModel.isWaterReminderEnabled ? "Cancel Reminders".needTranslation : "Set Reminder".needTranslation,
textColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor,
backgroundColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor.withValues(alpha: 0.1) : AppColors.successLightBgColor,
onPressed: () => _handleReminderButtonTap(viewModel),
icon: viewModel.isWaterReminderEnabled ? null : AppAssets.bell,
iconColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor,
borderRadius: 12.r,
borderColor: AppColors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
),
);
},
),
child: RefreshIndicator(
onRefresh: _refreshData,
color: AppColors.blueColor,
backgroundColor: AppColors.whiteColor,
child: Column(
children: [
SizedBox(height: 16.h),
const WaterIntakeSummaryWidget(),
SizedBox(height: 16.h),
_buildHistoryGraphOrList(),
SizedBox(height: 16.h),
const HydrationTipsWidget(),
SizedBox(height: 16.h),
],
),
),
),
);
}
}

@ -0,0 +1,359 @@
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/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 {
final success = await viewModel.saveSettings();
if (!success && viewModel.validationError != null) {
_showSnackBar(context, viewModel.validationError!);
} else if (success) {
_showSnackBar(context, "Settings saved successfully");
}
},
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,62 @@
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/theme/colors.dart';
class HydrationTipsWidget extends StatelessWidget {
const HydrationTipsWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.w),
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),
],
),
);
}
}

@ -0,0 +1,174 @@
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/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/theme/colors.dart';
import 'package:provider/provider.dart';
class WaterActionButtonsWidget extends StatelessWidget {
const WaterActionButtonsWidget({super.key});
@override
Widget build(BuildContext context) {
return Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
final cupAmount = vm.selectedCupCapacityMl;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () async {
if (cupAmount > 0) {
await vm.undoUserActivity();
}
},
child: Utils.buildSvgWithAssets(
icon: AppAssets.minimizeIcon,
height: 20.h,
width: 20.h,
iconColor: AppColors.textColor,
),
),
Container(
margin: EdgeInsets.symmetric(horizontal: 4.w),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.blueColor,
borderRadius: 99.r,
hasShadow: true,
),
child: (cupAmount > 0 ? "+ $cupAmount ml" : "+ 0ml").toText12(
fontWeight: FontWeight.w600,
color: AppColors.whiteColor,
),
),
InkWell(
onTap: () async {
if (cupAmount > 0) {
await vm.insertUserActivity(quantityIntake: cupAmount);
}
},
child: Utils.buildSvgWithAssets(
icon: AppAssets.addIconDark,
height: 20.h,
width: 20.h,
),
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildActionButton(
context: context,
onTap: () => showSwitchCupBottomSheet(context),
overlayWidget: AppAssets.refreshIcon,
title: "Switch Cup".needTranslation,
icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w),
),
_buildActionButton(
context: context,
onTap: () async {
final success = await vm.scheduleTestNotification();
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Test notification will appear in 5 seconds!'.needTranslation),
backgroundColor: AppColors.blueColor,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(16.w),
duration: const Duration(seconds: 2),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to schedule test notification'.needTranslation),
backgroundColor: AppColors.errorColor,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(16.w),
),
);
}
},
title: "Plain Water".needTranslation,
icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w),
),
_buildActionButton(
context: context,
onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen),
title: "Settings".needTranslation,
icon: Icon(
Icons.settings,
color: AppColors.blueColor,
size: 24.w,
),
),
],
),
],
);
});
}
Widget _buildActionButton({
required BuildContext context,
String? overlayWidget,
required String title,
required Widget icon,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Column(
children: [
Stack(
children: [
Container(
height: 46.w,
width: 46.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.blueColor.withValues(alpha: 0.14),
borderRadius: 12.r,
hasShadow: true,
),
child: Center(child: icon),
),
if (overlayWidget != null) ...[
Positioned(
top: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(2.w),
height: 16.w,
width: 16.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.blueColor,
borderRadius: 100.r,
hasShadow: true,
),
child: Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.refreshIcon,
iconColor: AppColors.whiteColor,
),
),
),
),
]
],
),
SizedBox(height: 4.h),
title.toText10(),
],
),
);
}
}

@ -0,0 +1,112 @@
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/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:provider/provider.dart';
class WaterBottleWidget extends StatelessWidget {
const WaterBottleWidget({super.key});
@override
Widget build(BuildContext context) {
return Consumer<WaterMonitorViewModel>(
builder: (context, vm, _) {
final progressPercent = (vm.progress * 100).clamp(0.0, 100.0);
// SVG aspect ratio
const svgAspectRatio = 315.0 / 143.0; // ~2.2
// Responsive bottle sizing with device-specific constraints
double bottleWidth;
if (isTablet) {
bottleWidth = math.min(SizeUtils.width * 0.15, 180.0);
} else if (isFoldable) {
bottleWidth = math.min(100.w, 160.0);
} else {
bottleWidth = math.min(120.w, 140.0);
}
final bottleHeight = bottleWidth * svgAspectRatio;
// Fillable area percentages
final fillableHeightPercent = 0.7;
const fillableWidthPercent = 0.8;
final fillableHeight = bottleHeight * fillableHeightPercent;
final fillableWidth = bottleWidth * fillableWidthPercent;
// Device-specific positioning offsets
final double leftOffset = isTablet ? 4.w : 8.w;
final double bottomOffset = isTablet ? -65.h : -78.h;
return SizedBox(
height: bottleHeight,
width: bottleWidth,
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
// Bottle SVG outline
Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.waterBottle,
height: bottleHeight,
width: bottleWidth,
fit: BoxFit.contain,
),
),
// Wave and bubbles clipped to bottle shape
Positioned.fill(
left: leftOffset,
bottom: bottomOffset,
child: Center(
child: SizedBox(
width: fillableWidth,
height: fillableHeight,
child: ClipPath(
clipper: BottleShapeClipper(),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
// Animated wave
Positioned(
child: WaterConsumptionProgressWidget(
progress: progressPercent,
size: math.min(fillableWidth, fillableHeight),
containerWidth: fillableWidth,
containerHeight: fillableHeight,
waveDuration: const Duration(milliseconds: 3000),
waveColor: AppColors.blueColor,
),
),
// Bubbles (only show if progress > 10%)
if (progressPercent > 10)
Positioned(
bottom: fillableHeight * 0.12,
child: Utils.buildSvgWithAssets(
icon: AppAssets.waterBottleOuterBubbles,
height: isTablet ? math.min(45.0, fillableHeight * 0.2) : math.min(55.0, fillableHeight * 0.22),
width: fillableWidth * 0.65,
),
),
],
),
),
),
),
),
],
),
);
},
);
}
}

@ -0,0 +1,78 @@
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,116 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_action_buttons_widget.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_bottle_widget.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class WaterIntakeSummaryWidget extends StatelessWidget {
const WaterIntakeSummaryWidget({super.key});
@override
Widget build(BuildContext context) {
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: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: isTablet ? 2 : 3,
child: Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
if (vm.isLoading) {
return _buildLoadingShimmer();
}
final goalMl = vm.dailyGoalMl;
final consumed = vm.totalConsumedMl;
final remaining = (goalMl - consumed) > 0 ? (goalMl - consumed) : 0;
final completedPercent = "${(vm.progress * 100).clamp(0.0, 100.0).toStringAsFixed(0)}%";
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Next Drink Time".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor),
vm.nextDrinkTime.toText32(weight: FontWeight.w600, color: AppColors.blueColor),
SizedBox(height: 12.h),
_buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "${goalMl}ml"),
SizedBox(height: 8.h),
_buildStatusColumn(title: "Remaining".needTranslation, subTitle: "${remaining}ml"),
SizedBox(height: 8.h),
_buildStatusColumn(title: "Completed".needTranslation, subTitle: completedPercent, subTitleColor: AppColors.successColor),
SizedBox(height: 8.h),
_buildStatusColumn(
title: "Hydration Status".needTranslation,
subTitle: vm.hydrationStatus,
subTitleColor: vm.hydrationStatusColor,
),
],
);
}),
),
SizedBox(width: isTablet ? 32 : 16.w),
Expanded(
flex: isTablet ? 1 : 2,
child: const WaterBottleWidget(),
),
],
),
const WaterActionButtonsWidget(),
],
),
);
}
Widget _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 _buildLoadingShimmer() {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(0.w),
itemCount: 4,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: AppColors.shimmerBaseColor,
highlightColor: AppColors.shimmerHighlightColor,
child: Container(
height: 40.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(10.r),
),
),
);
},
);
}
}

@ -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;
}

@ -22,6 +22,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';
import '../presentation/covid19test/covid19_landing_page.dart';
@ -46,6 +48,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';
@ -83,6 +89,8 @@ class AppRoutes {
huaweiHealthExample: (context) => HuaweiHealthExample(),
covid19Test: (context) => Covid19LandingPage(),
//
waterConsumptionScreen: (context) => WaterConsumptionScreen(),
waterMonitorSettingsScreen: (context) => WaterMonitorSettingsScreen(),
healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator),
healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter)
};

@ -0,0 +1,373 @@
import 'dart:typed_data';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz show TZDateTime, local, setLocalLocation, getLocation;
/// Abstract class defining the notification service interface
abstract class NotificationService {
/// Initialize the notification service
Future<void> initialize({Function(String payload)? onNotificationClick});
/// Request notification permissions (mainly for iOS)
Future<bool> requestPermissions();
/// Show an immediate notification
Future<void> showNotification({
required String title,
required String body,
String? payload,
});
/// Schedule a notification at a specific date and time
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
});
/// Schedule daily notifications at specific times
Future<void> scheduleDailyNotifications({
required List<DateTime> times,
required String title,
required String body,
String? payload,
});
/// Schedule water reminder notifications
Future<void> scheduleWaterReminders({
required List<DateTime> reminderTimes,
required String title,
required String body,
});
/// Cancel a specific notification by id
Future<void> cancelNotification(int id);
/// Cancel all scheduled notifications
Future<void> cancelAllNotifications();
/// Get list of pending notifications
Future<List<PendingNotificationRequest>> getPendingNotifications();
}
/// Implementation of NotificationService following the project architecture
class NotificationServiceImp implements NotificationService {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
final LoggerService loggerService;
NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService});
// Channel IDs for different notification types
static const String _waterReminderChannelId = 'water_reminder_channel';
static const String _waterReminderChannelName = 'Water Reminders';
static const String _waterReminderChannelDescription = 'Daily water intake reminders';
static const String _generalChannelId = 'hmg_general_channel';
static const String _generalChannelName = 'HMG Notifications';
static const String _generalChannelDescription = 'General notifications from HMG';
Function(String payload)? _onNotificationClick;
@override
Future<void> initialize({Function(String payload)? onNotificationClick}) async {
try {
// Initialize timezone database
tz.initializeTimeZones();
// Set local timezone (you can also use a specific timezone if needed)
// For example: tz.setLocalLocation(tz.getLocation('Asia/Riyadh'));
final locationName = DateTime.now().timeZoneName;
try {
tz.setLocalLocation(tz.getLocation(locationName));
} catch (e) {
// Fallback to UTC if specific timezone not found
loggerService.logInfo('Could not set timezone $locationName, using UTC');
tz.setLocalLocation(tz.getLocation('UTC'));
}
_onNotificationClick = onNotificationClick;
const androidSettings = AndroidInitializationSettings('app_icon');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initializationSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: _handleNotificationResponse,
);
loggerService.logInfo('NotificationService initialized successfully');
} catch (ex) {
loggerService.logError('Failed to initialize NotificationService: $ex');
}
}
/// Handle notification tap
void _handleNotificationResponse(NotificationResponse response) {
try {
if (response.payload != null && _onNotificationClick != null) {
_onNotificationClick!(response.payload!);
}
loggerService.logInfo('Notification tapped: ${response.payload}');
} catch (ex) {
loggerService.logError('Error handling notification response: $ex');
}
}
@override
Future<bool> requestPermissions() async {
try {
// Request permissions for iOS
final result =
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
// For Android 13+, permissions are requested at runtime
final androidResult = await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
loggerService.logInfo('Notification permissions: iOS=${result ?? true}, Android=${androidResult ?? true}');
return result ?? androidResult ?? true;
} catch (ex) {
loggerService.logError('Error requesting notification permissions: $ex');
return false;
}
}
@override
Future<void> showNotification({
required String title,
required String body,
String? payload,
}) async {
try {
final androidDetails = AndroidNotificationDetails(
_generalChannelId,
_generalChannelName,
channelDescription: _generalChannelDescription,
importance: Importance.high,
priority: Priority.high,
vibrationPattern: _getVibrationPattern(),
);
const iosDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await flutterLocalNotificationsPlugin.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
notificationDetails,
payload: payload,
);
loggerService.logInfo('Notification shown: $title');
} catch (ex) {
loggerService.logError('Error showing notification: $ex');
}
}
@override
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
}) async {
try {
final androidDetails = AndroidNotificationDetails(
_generalChannelId,
_generalChannelName,
channelDescription: _generalChannelDescription,
importance: Importance.high,
priority: Priority.high,
vibrationPattern: _getVibrationPattern(),
);
const iosDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await flutterLocalNotificationsPlugin.zonedSchedule(
id,
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
payload: payload,
);
loggerService.logInfo('Notification scheduled for: $scheduledDate');
} catch (ex) {
loggerService.logError('Error scheduling notification: $ex');
}
}
@override
Future<void> scheduleDailyNotifications({
required List<DateTime> times,
required String title,
required String body,
String? payload,
}) async {
try {
for (int i = 0; i < times.length; i++) {
final time = times[i];
await scheduleNotification(
id: i + 1000,
// Offset ID to avoid conflicts
title: title,
body: body,
scheduledDate: time,
payload: payload,
);
}
loggerService.logInfo('Scheduled ${times.length} daily notifications');
} catch (ex) {
loggerService.logError('Error scheduling daily notifications: $ex');
}
}
@override
Future<void> scheduleWaterReminders({
required List<DateTime> reminderTimes,
required String title,
required String body,
}) async {
try {
// Cancel existing water reminders first
await _cancelWaterReminders();
final androidDetails = AndroidNotificationDetails(
_waterReminderChannelId,
_waterReminderChannelName,
channelDescription: _waterReminderChannelDescription,
importance: Importance.high,
priority: Priority.high,
vibrationPattern: _getVibrationPattern(),
icon: 'app_icon',
styleInformation: const BigTextStyleInformation(''),
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
for (int i = 0; i < reminderTimes.length; i++) {
final reminderTime = reminderTimes[i];
final notificationId = 5000 + i; // Use 5000+ range for water reminders
// Schedule for today if time hasn't passed, otherwise schedule for tomorrow
DateTime scheduledDate = reminderTime;
if (scheduledDate.isBefore(DateTime.now())) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
await flutterLocalNotificationsPlugin.zonedSchedule(
notificationId,
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the same time
payload: 'water_reminder_$i',
);
}
loggerService.logInfo('Scheduled ${reminderTimes.length} water reminders');
} catch (ex) {
loggerService.logError('Error scheduling water reminders: $ex');
}
}
/// Cancel all water reminders (IDs 5000-5999)
Future<void> _cancelWaterReminders() async {
try {
final pendingNotifications = await getPendingNotifications();
for (final notification in pendingNotifications) {
if (notification.id >= 5000 && notification.id < 6000) {
await cancelNotification(notification.id);
}
}
loggerService.logInfo('Cancelled all water reminders');
} catch (ex) {
loggerService.logError('Error cancelling water reminders: $ex');
}
}
@override
Future<void> cancelNotification(int id) async {
try {
await flutterLocalNotificationsPlugin.cancel(id);
loggerService.logInfo('Cancelled notification with ID: $id');
} catch (ex) {
loggerService.logError('Error cancelling notification: $ex');
}
}
@override
Future<void> cancelAllNotifications() async {
try {
await flutterLocalNotificationsPlugin.cancelAll();
loggerService.logInfo('Cancelled all notifications');
} catch (ex) {
loggerService.logError('Error cancelling all notifications: $ex');
}
}
@override
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
try {
final pending = await flutterLocalNotificationsPlugin.pendingNotificationRequests();
loggerService.logInfo('Found ${pending.length} pending notifications');
return pending;
} catch (ex) {
loggerService.logError('Error getting pending notifications: $ex');
return [];
}
}
/// Get vibration pattern for notifications
Int64List _getVibrationPattern() {
final vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
vibrationPattern[1] = 500;
vibrationPattern[2] = 500;
vibrationPattern[3] = 500;
return vibrationPattern;
}
}

@ -5,30 +5,30 @@ import 'package:flutter_callkit_incoming/entities/call_event.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_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/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
// import 'package:hmg_patient_app_new/presentation/authantication/login.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/notification_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
import 'package:lottie/lottie.dart';
import 'package:provider/provider.dart';
import 'core/cache_consts.dart';
import 'core/utils/local_notifications.dart';
import 'core/utils/push_notification_handler.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
_SplashScreenState createState() => _SplashScreenState();
}
@ -47,9 +47,13 @@ class _SplashScreenState extends State<SplashPage> {
);
await authVm.getServicePrivilege();
Timer(Duration(seconds: 2, milliseconds: 500), () async {
bool isAppOpenedFromCall = await GetIt.instance<CacheService>().getBool(key: CacheConst.isAppOpenedFromCall) ?? false;
bool isAppOpenedFromCall = getIt.get<CacheService>().getBool(key: CacheConst.isAppOpenedFromCall) ?? false;
LocalNotification.init(onNotificationClick: (payload) {});
// Initialize NotificationService using dependency injection
final notificationService = getIt.get<NotificationService>();
await notificationService.initialize(onNotificationClick: (payload) {
// Handle notification click here
});
if (isAppOpenedFromCall) {
navigateToTeleConsult();
@ -78,7 +82,8 @@ class _SplashScreenState extends State<SplashPage> {
// GetIt.instance<CacheService>().remove(key: CacheConst.isAppOpenedFromCall);
Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall);
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation())));
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!)
.pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation())));
Navigator.pushReplacementNamed(
// context,
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
@ -216,7 +221,7 @@ class _SplashScreenState extends State<SplashPage> {
@override
void initState() {
authVm = context.read<AuthenticationViewModel>();
authVm = getIt<AuthenticationViewModel>();
super.initState();
initializeStuff();
}
@ -225,6 +230,8 @@ class _SplashScreenState extends State<SplashPage> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.whiteColor,
body: Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill).center);
body: Lottie.asset(AppAnimations.loadingAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill)
.center);
}
}

@ -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,5 +99,14 @@ 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);
// Shimmer
static const Color shimmerBaseColor = Color(0xFFE0E0E0);
static const Color shimmerHighlightColor = Color(0xFFF5F5F5);
static const Color covid29Color = Color(0xff2563EB);
}

@ -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(

@ -72,11 +72,11 @@ class AppCustomChipWidget extends StatelessWidget {
? Image.asset(icon, width: iconS, height: iconS)
: Utils.buildSvgWithAssets(
icon: icon,
width: iconS,
height: iconS,
iconColor: iconHasColor ? iconColor : null,
fit: BoxFit.contain,
)
width: iconS,
height: iconS,
iconColor: iconHasColor ? iconColor : null,
fit: BoxFit.contain,
)
: SizedBox.shrink(),
label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor),
padding: padding,

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/common_models/data_points.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
/// A customizable line graph widget using `fl_chart`.
///
/// Displays a line chart with configurable axis labels, colors, and data points.
@ -56,67 +57,69 @@ class CustomGraph extends StatelessWidget {
final FontWeight? bottomLabelFontWeight;
final double? leftLabelInterval;
final double? leftLabelReservedSize;
final double? bottomLabelReservedSize;
final bool? showGridLines;
final GetDrawingGridLine? getDrawingHorizontalLine;
final double? horizontalInterval;
final double? minY;
final bool showShadow;
final double? cutOffY;
final RangeAnnotations? rangeAnnotations;
///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart
final Widget Function(double) leftLabelFormatter;
final Widget Function(double , List<DataPoint>) bottomLabelFormatter;
final Widget Function(double, List<DataPoint>) bottomLabelFormatter;
final Axis scrollDirection;
final bool showBottomTitleDates;
final bool isFullScreeGraph;
final bool makeGraphBasedOnActualValue;
const CustomGraph({
super.key,
required this.dataPoints,
required this.leftLabelFormatter,
this.width,
required this.scrollDirection,
required this.height,
this.maxY,
this.maxX,
this.showBottomTitleDates = true,
this.isFullScreeGraph = false,
this.spotColor = AppColors.bgGreenColor,
this.graphColor = AppColors.bgGreenColor,
this.graphShadowColor = AppColors.graphGridColor,
this.graphGridColor = AppColors.graphGridColor,
this.bottomLabelColor = AppColors.textColor,
this.bottomLabelFontWeight = FontWeight.w500,
this.bottomLabelSize,
this.leftLabelInterval,
this.leftLabelReservedSize,
this.makeGraphBasedOnActualValue = false,
required this.bottomLabelFormatter,
this.minX,
this.showGridLines = false,
this.getDrawingHorizontalLine,
this.horizontalInterval,
this.minY,
this.showShadow = false,
this.rangeAnnotations
});
const CustomGraph(
{super.key,
required this.dataPoints,
required this.leftLabelFormatter,
this.width,
required this.scrollDirection,
required this.height,
this.maxY,
this.maxX,
this.showBottomTitleDates = true,
this.isFullScreeGraph = false,
this.spotColor = AppColors.bgGreenColor,
this.graphColor = AppColors.bgGreenColor,
this.graphShadowColor = AppColors.graphGridColor,
this.graphGridColor = AppColors.graphGridColor,
this.bottomLabelColor = AppColors.textColor,
this.bottomLabelFontWeight = FontWeight.w500,
this.bottomLabelSize,
this.leftLabelInterval,
this.leftLabelReservedSize,
this.bottomLabelReservedSize,
this.makeGraphBasedOnActualValue = false,
required this.bottomLabelFormatter,
this.minX,
this.showGridLines = false,
this.getDrawingHorizontalLine,
this.horizontalInterval,
this.minY,
this.showShadow = false,
this.cutOffY = 0,
this.rangeAnnotations});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
child: SizedBox(
width: width,
height: height,
child: LineChart(
LineChartData(
minY: minY??0,
color: Colors.white,
child: SizedBox(
width: width,
height: height,
child: LineChart(
LineChartData(
minY: minY ?? 0,
maxY: maxY,
maxX: maxX,
minX: minX ,
minX: minX,
lineTouchData: LineTouchData(
getTouchLineEnd: (_, __) => 0,
getTouchedSpotIndicator: (barData, indicators) {
@ -149,11 +152,8 @@ class CustomGraph extends StatelessWidget {
final dataPoint = dataPoints[spot.x.toInt()];
return LineTooltipItem(
'${dataPoint.actualValue} ${dataPoint.unitOfMeasurement??""} - ${dataPoint.displayTime}',
TextStyle(
color: Colors.black,
fontSize: 12.f,
fontWeight: FontWeight.w500),
'${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}',
TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500),
);
}
return null; // hides the rest
@ -165,7 +165,7 @@ class CustomGraph extends StatelessWidget {
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: leftLabelReservedSize??80,
reservedSize: leftLabelReservedSize ?? 80,
interval: leftLabelInterval ?? .1, // Let fl_chart handle it
getTitlesWidget: (value, _) {
return leftLabelFormatter(value);
@ -176,9 +176,9 @@ class CustomGraph extends StatelessWidget {
axisNameSize: 20,
sideTitles: SideTitles(
showTitles: showBottomTitleDates,
reservedSize: 20,
reservedSize: bottomLabelReservedSize ?? 20,
getTitlesWidget: (value, _) {
return bottomLabelFormatter(value, dataPoints, );
return bottomLabelFormatter(value, dataPoints);
},
interval: 1, // ensures 1:1 mapping with spots
),
@ -197,30 +197,29 @@ class CustomGraph extends StatelessWidget {
),
lineBarsData: _buildColoredLineSegments(dataPoints),
gridData: FlGridData(
show: showGridLines??true,
show: showGridLines ?? true,
drawVerticalLine: false,
horizontalInterval:horizontalInterval,
horizontalInterval: horizontalInterval,
// checkToShowHorizontalLine: (value) =>
// value >= 0 && value <= 100,
getDrawingHorizontalLine: getDrawingHorizontalLine??(value) {
return FlLine(
color: graphGridColor,
strokeWidth: 1,
dashArray: [5, 5],
);
},
getDrawingHorizontalLine: getDrawingHorizontalLine ??
(value) {
return FlLine(
color: graphGridColor,
strokeWidth: 1,
dashArray: [5, 5],
);
},
),
rangeAnnotations: rangeAnnotations
),
),
rangeAnnotations: rangeAnnotations),
),
),
);
}
List<LineChartBarData> _buildColoredLineSegments(List<DataPoint> dataPoints) {
final List<FlSpot> allSpots = dataPoints.asMap().entries.map((entry) {
double value = (makeGraphBasedOnActualValue)?double.tryParse(entry.value.actualValue)??0.0:entry.value.value;
double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value;
return FlSpot(entry.key.toDouble(), value);
}).toList();
@ -241,6 +240,8 @@ class CustomGraph extends StatelessWidget {
),
belowBarData: BarAreaData(
show: showShadow,
applyCutOffY: cutOffY != null,
cutOffY: cutOffY ?? 0,
gradient: LinearGradient(
colors: [
graphShadowColor,
@ -255,4 +256,4 @@ class CustomGraph extends StatelessWidget {
return data;
}
}
}

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/int_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@ -11,9 +12,7 @@ class CommonShimmerWidget extends StatelessWidget {
return SizedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
borderRadius: BorderRadius.all(Radius.circular(10.r)),
border: Border.all(color: AppColors.lightGreyEFColor, width: 1),
boxShadow: [
BoxShadow(
@ -27,7 +26,11 @@ class CommonShimmerWidget extends StatelessWidget {
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Container(height: 100).toShimmer(),
Container(
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24.r)),
)).toShimmer(),
16.height,
Container(height: 24).toShimmer(),
16.height,

@ -30,6 +30,7 @@ dependencies:
# firebase_core: ^3.13.1
permission_handler: ^12.0.1
flutter_local_notifications: ^19.4.1
timezone: ^0.10.0
provider: ^6.1.5+1
get_it: ^8.2.0
just_audio: ^0.10.4

Loading…
Cancel
Save