Completed Refund Request Module for Invoice and Withdraw from Wallet #322

Merged
Haroon6138 merged 8 commits from faiz_dev into master 4 weeks ago

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0002 9.25C19.2712 9.25 18.6011 9.43823 17.8597 9.76207C17.1407 10.0761 16.307 10.5401 15.2582 11.1238L11.1161 13.4289C10.5397 13.7497 10.0573 14.1526 9.72587 14.6959C9.39281 15.2418 9.25016 15.8673 9.25016 16.5691L9.25008 16.6352C9.24962 16.8906 9.24915 17.1497 9.31072 17.3815C9.46568 17.9648 9.83495 18.3408 10.2872 18.5422C10.6946 18.7238 11.1372 18.75 11.4694 18.75L28.5309 18.75C28.8631 18.75 29.3057 18.7238 29.7131 18.5422C30.1654 18.3408 30.5346 17.9648 30.6896 17.3815C30.7512 17.1497 30.7507 16.8906 30.7502 16.6352L30.7502 16.5691C30.7502 15.8673 30.6075 15.2418 30.2745 14.6959C29.9431 14.1526 29.4607 13.7497 28.8842 13.4289L24.7421 11.1238C23.6934 10.5401 22.8596 10.0761 22.1406 9.76206C21.3992 9.43823 20.7292 9.25 20.0002 9.25ZM19.9961 14C19.4438 14 18.9961 14.4477 18.9961 15C18.9961 15.5523 19.4438 16 19.9961 16H20.0051C20.5574 16 21.0051 15.5523 21.0051 15C21.0051 14.4477 20.5574 14 20.0051 14H19.9961Z" fill="#8F9AA3"/>
<path d="M9.25 29.75C9.25 28.0931 10.9289 26.75 13 26.75L27 26.75C29.0711 26.75 30.75 28.0931 30.75 29.75C30.75 30.3023 30.1904 30.75 29.5 30.75L10.5 30.75C9.80964 30.75 9.25 30.3023 9.25 29.75Z" fill="#8F9AA3"/>
<path d="M16.3501 25.5H13C12.5865 25.5 12.1848 25.5502 11.8006 25.6448C11.5358 25.71 11.4034 25.7426 11.3268 25.6825C11.2501 25.6224 11.2501 25.4944 11.2501 25.2383V20.216C11.2501 20.2021 11.2501 20.1952 11.2504 20.1904C11.2554 20.0851 11.3352 20.0053 11.4405 20.0002C11.4453 20 11.4534 20 11.4696 20H16.3501C16.5387 20 16.633 20 16.6915 20.0586C16.7501 20.1172 16.7501 20.2114 16.7501 20.4V25.1C16.7501 25.2886 16.7501 25.3828 16.6915 25.4414C16.633 25.5 16.5387 25.5 16.3501 25.5Z" fill="#8F9AA3"/>
<path d="M28.1996 25.6448C27.8153 25.5502 27.4135 25.5 27 25.5H23.6501C23.4616 25.5 23.3673 25.5 23.3087 25.4414C23.2501 25.3828 23.2501 25.2886 23.2501 25.1V20.4C23.2501 20.2114 23.2501 20.1172 23.3087 20.0586C23.3673 20 23.4616 20 23.6501 20L28.531 20C28.5469 20 28.5548 20 28.5595 20.0002C28.665 20.0052 28.745 20.0851 28.7499 20.1906L28.7501 20.1994L28.7501 20.2157V25.2384C28.7501 25.4945 28.7501 25.6226 28.6735 25.6826C28.5968 25.7427 28.4644 25.7101 28.1996 25.6448Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,6 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27093 0C5.84568 0 5.45483 0.1098 5.02235 0.298705C4.60293 0.481904 4.11656 0.75258 3.50481 1.09303L1.08855 2.43771C0.752306 2.62483 0.470904 2.85985 0.277588 3.17675C0.0833075 3.49522 9.35953e-05 3.8601 9.35953e-05 4.26946L4.69e-05 4.30801C-0.00022378 4.45702 -0.000498354 4.60817 0.0354227 4.74338C0.125815 5.08363 0.341219 5.30294 0.605033 5.42048C0.842686 5.52636 1.10087 5.54167 1.29468 5.54167L11.2472 5.54167C11.441 5.54167 11.6992 5.52636 11.9368 5.42048C12.2006 5.30294 12.416 5.08363 12.5064 4.74338C12.5424 4.60817 12.5421 4.45702 12.5418 4.308L12.5418 4.26946C12.5418 3.8601 12.4585 3.49522 12.2643 3.17675C12.071 2.85985 11.7895 2.62483 11.4533 2.43771L9.03706 1.09304C8.4253 0.752584 7.93892 0.481906 7.51951 0.298705C7.08703 0.1098 6.69617 0 6.27093 0ZM6.26856 2.77083C5.94639 2.77083 5.68522 3.032 5.68522 3.35417C5.68522 3.67633 5.94639 3.9375 6.26856 3.9375H6.27379C6.59596 3.9375 6.85713 3.67633 6.85713 3.35417C6.85713 3.032 6.59596 2.77083 6.27379 2.77083H6.26856Z" fill="#2E3039"/>
<path d="M0 11.9583C0 10.9918 0.979377 10.2083 2.1875 10.2083L10.3542 10.2083C11.5623 10.2083 12.5417 10.9918 12.5417 11.9583C12.5417 12.2805 12.2152 12.5417 11.8125 12.5417L0.729166 12.5417C0.326458 12.5417 0 12.2805 0 11.9583Z" fill="#2E3039"/>
<path d="M4.14174 9.47917L2.18748 9.47917C1.94632 9.47917 1.71199 9.50844 1.48786 9.56362C1.33338 9.60165 1.25615 9.62066 1.21144 9.58563C1.16674 9.5506 1.16674 9.4759 1.16674 9.3265L1.16674 6.39686C1.16674 6.38875 1.16674 6.3847 1.16687 6.3819C1.16982 6.32046 1.21636 6.27392 1.27781 6.27097C1.2806 6.27083 1.28532 6.27083 1.29475 6.27083L4.14174 6.27083C4.25173 6.27083 4.30673 6.27083 4.3409 6.305C4.37507 6.33918 4.37507 6.39417 4.37507 6.50417L4.37507 9.24583C4.37507 9.35583 4.37507 9.41083 4.3409 9.445C4.30673 9.47917 4.25173 9.47917 4.14174 9.47917Z" fill="#2E3039"/>
<path d="M11.0539 9.56366C10.8298 9.50845 10.5954 9.47917 10.3541 9.47917H8.40007C8.29008 9.47917 8.23508 9.47917 8.20091 9.445C8.16674 9.41083 8.16674 9.35583 8.16674 9.24583V6.50417C8.16674 6.39417 8.16674 6.33918 8.20091 6.305C8.23508 6.27083 8.29008 6.27083 8.40007 6.27083L11.2473 6.27083C11.2565 6.27083 11.2611 6.27083 11.2639 6.27096C11.3254 6.27385 11.3721 6.32048 11.3749 6.38204L11.3751 6.38713L11.3751 6.39668V9.32654C11.3751 9.47595 11.3751 9.55066 11.3304 9.58569C11.2857 9.62072 11.2084 9.6017 11.0539 9.56366Z" fill="#2E3039"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -1,3 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3335 9C9.30849 9 7.66686 10.6416 7.66686 12.6667C7.66686 14.6917 9.30849 16.3333 11.3335 16.3333C13.3586 16.3333 15.0002 14.6917 15.0002 12.6667C15.0002 10.6416 13.3586 9 11.3335 9ZM9.66686 12.6667C9.66686 11.7462 10.4131 11 11.3335 11C12.254 11 13.0002 11.7462 13.0002 12.6667C13.0002 13.5871 12.254 14.3333 11.3335 14.3333C10.4131 14.3333 9.66686 13.5871 9.66686 12.6667Z" fill="#2E3039"/>
<path d="M7.57253 22.424C8.17053 21.1466 9.65415 20.3583 11.3085 20.3339C12.9508 20.3097 14.4477 21.0424 15.0945 22.424C15.3287 22.9242 15.924 23.1398 16.4242 22.9057C16.9244 22.6715 17.14 22.0762 16.9059 21.576C15.8356 19.2897 13.4714 18.3018 11.279 18.3341C9.09878 18.3663 6.77427 19.412 5.76119 21.576C5.52703 22.0762 5.74269 22.6715 6.24288 22.9057C6.74307 23.1398 7.33838 22.9242 7.57253 22.424Z" fill="#2E3039"/>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M11 10C10.4477 10 10 10.4477 10 11C10 11.5523 10.4477 12 11 12L19 12C19.5523 12 20 11.5523 20 11C20 10.4477 19.5523 10 19 10L11 10Z" fill="#8F9AA3"/>
<path d="M11 16C10.4477 16 10 16.4477 10 17C10 17.5523 10.4477 18 11 18H19C19.5523 18 20 17.5523 20 17C20 16.4477 19.5523 16 19 16L11 16Z" fill="#8F9AA3"/>
<path d="M10 23C10 22.4477 10.4477 22 11 22L29 22C29.5523 22 30 22.4477 30 23C30 23.5523 29.5523 24 29 24L11 24C10.4477 24 10 23.5523 10 23Z" fill="#8F9AA3"/>
<path d="M11 28C10.4477 28 10 28.4477 10 29C10 29.5523 10.4477 30 11 30L29 30C29.5523 30 30 29.5523 30 29C30 28.4477 29.5523 28 29 28L11 28Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 790 B

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="14" height="14" rx="3" fill="#ED1C2B"/>
<path d="M2.54806 11.4479C2.54806 11.4479 2.98492 11.4447 3.29241 11.4447L5.6242 11.456C5.90252 11.456 6.13431 11.1644 6.14737 10.7432L6.15002 8.82325C6.15002 8.41389 6.3357 8.09359 6.6236 8.0753L7.33877 8.06967C7.62708 8.08716 7.825 8.41147 7.825 8.81822L7.82316 10.7484C7.83581 11.1695 8.03006 11.4443 8.30981 11.4443L11.4488 11.4527L11.4545 9.08021C11.4545 8.67205 11.2554 8.35497 10.9648 8.33628L9.70382 8.33004C9.4249 8.33004 9.20555 8.03991 9.19372 7.61828L9.18882 6.97649C9.20269 6.55546 9.42326 6.26593 9.7028 6.26593L10.9707 6.26955C11.2588 6.25126 11.4465 5.91608 11.4465 5.50853L11.4496 2.54687C11.346 2.71407 11.2043 2.85519 11.0357 2.95905C10.8671 3.06291 10.6763 3.12666 10.4784 3.14524L8.34613 3.14343C8.06843 3.14343 7.83602 3.44181 7.82296 3.86283L7.82296 5.76468C7.8099 6.1849 7.59178 6.48228 7.31285 6.48228L6.66767 6.47745C6.38793 6.47745 6.1643 6.18973 6.15165 5.77072L6.15328 3.84795C6.14063 3.42673 5.91904 3.14423 5.63991 3.14423L2.54562 3.14564L2.54562 5.51094C2.54562 5.9195 2.73925 6.24663 3.02817 6.26553L4.29609 6.2603C4.58277 6.2788 4.7858 6.61578 4.7858 7.02213L4.78192 7.56962C4.78192 7.97798 4.58604 8.30471 4.29793 8.32321L3.02797 8.32019C2.73905 8.3403 2.54541 8.662 2.54541 9.07036L2.54806 11.4479Z" fill="white"/>
<path d="M2.54806 11.4479C2.54806 11.4479 2.98492 11.4447 3.29241 11.4447L5.6242 11.456C5.90252 11.456 6.13431 11.1644 6.14737 10.7432L6.15002 8.82325C6.15002 8.41389 6.3357 8.09359 6.6236 8.0753L7.33877 8.06967C7.62708 8.08716 7.825 8.41147 7.825 8.81822L7.82316 10.7484C7.83581 11.1695 8.03006 11.4443 8.30981 11.4443L11.4488 11.4527L11.4545 9.08021C11.4545 8.67205 11.2554 8.35497 10.9648 8.33628L9.70382 8.33004C9.4249 8.33004 9.20555 8.03991 9.19372 7.61828L9.18882 6.97649C9.20269 6.55546 9.42326 6.26593 9.7028 6.26593L10.9707 6.26955C11.2588 6.25126 11.4465 5.91608 11.4465 5.50853L11.4496 2.54687C11.346 2.71407 11.2043 2.85519 11.0357 2.95905C10.8671 3.06291 10.6763 3.12666 10.4784 3.14524L8.34613 3.14343C8.06843 3.14343 7.83602 3.44181 7.82296 3.86283L7.82296 5.76468C7.8099 6.1849 7.59178 6.48228 7.31285 6.48228L6.66767 6.47745C6.38793 6.47745 6.1643 6.18973 6.15165 5.77072L6.15328 3.84795C6.14063 3.42673 5.91904 3.14423 5.63991 3.14423L2.54562 3.14564L2.54562 5.51094C2.54562 5.9195 2.73925 6.24663 3.02817 6.26553L4.29609 6.2603C4.58277 6.2788 4.7858 6.61578 4.7858 7.02213L4.78192 7.56962C4.78192 7.97798 4.58604 8.30471 4.29793 8.32321L3.02797 8.32019C2.73905 8.3403 2.54541 8.662 2.54541 9.07036L2.54806 11.4479Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 7.5C18.5 8.60457 17.6046 9.5 16.5 9.5C15.3954 9.5 14.5 8.60457 14.5 7.5C14.5 6.39543 15.3954 5.5 16.5 5.5C17.6046 5.5 18.5 6.39543 18.5 7.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9255 1.5H12.0745C14.2504 1.49998 15.9852 1.49996 17.3453 1.68282C18.7497 1.87164 19.9035 2.27175 20.8159 3.18414C21.7283 4.09653 22.1284 5.25033 22.3172 6.65471C22.5 8.01485 22.5 9.74959 22.5 11.9256V12.0744C22.5 14.2504 22.5 15.9851 22.3172 17.3453C22.1284 18.7497 21.7283 19.9035 20.8159 20.8159C19.9035 21.7283 18.7497 22.1284 17.3453 22.3172C15.9851 22.5 14.2504 22.5 12.0744 22.5H11.9256C9.74959 22.5 8.01485 22.5 6.65471 22.3172C5.25033 22.1284 4.09653 21.7283 3.18414 20.8159C2.27175 19.9035 1.87164 18.7497 1.68282 17.3453C1.49996 15.9852 1.49998 14.2504 1.5 12.0745V11.9255C1.49998 9.74958 1.49996 8.01484 1.68282 6.65471C1.87164 5.25033 2.27175 4.09653 3.18414 3.18414C4.09653 2.27175 5.25033 1.87164 6.65471 1.68282C8.01485 1.49996 9.74958 1.49998 11.9255 1.5ZM6.92121 3.66499C5.73861 3.82399 5.0772 4.1195 4.59835 4.59835C4.1195 5.0772 3.82399 5.73861 3.66499 6.92121C3.50213 8.13258 3.5 9.73256 3.5 12C3.5 12.2688 3.50003 12.5282 3.50036 12.7786C3.78795 12.7608 4.07624 12.7521 4.36474 12.7527C7.17775 12.6952 9.92747 13.572 12.124 15.2364C13.9124 16.5915 15.2416 18.3991 15.9596 20.4377C16.3674 20.4138 16.7385 20.3808 17.0788 20.335C17.2221 20.3157 17.3578 20.2945 17.4864 20.2711C16.9795 18.6882 16.1544 17.2258 15.0687 15.9582C14.8393 15.6904 14.9291 15.27 15.2648 15.1622C16.122 14.887 16.9998 14.7468 17.8878 14.7501C18.7607 14.7494 19.6259 14.889 20.4723 15.162C20.4994 14.2767 20.5 13.2378 20.5 12C20.5 9.73256 20.4979 8.13258 20.335 6.92121C20.176 5.73861 19.8805 5.0772 19.4017 4.59835C18.9228 4.1195 18.2614 3.82399 17.0788 3.66499C15.8674 3.50213 14.2674 3.5 12 3.5C9.73256 3.5 8.13258 3.50213 6.92121 3.66499Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,4 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.93712 2.78621e-06C4.05309 -5.24077e-05 3.28757 -0.00010021 2.67381 0.0856193C2.01162 0.178101 1.38072 0.387107 0.873027 0.91449C0.370475 1.43654 0.176225 2.07619 0.0895854 2.74561C0.00806761 3.37545 0.00810474 4.16382 0.00814813 5.08738L0.00815016 5.15177C0.00815016 5.40552 0.00591272 5.64931 0.00378245 5.88142C-0.00173354 6.48244 -0.00653088 7.00515 0.0300965 7.41981C0.0870866 8.06499 0.254658 8.66564 0.762836 9.19353C1.14586 9.59142 1.77891 9.60346 2.17679 9.22044C2.57468 8.83741 2.58672 8.20436 2.2037 7.80648C2.12657 7.72636 2.05658 7.63147 2.02234 7.24383C1.99474 6.93135 1.99819 6.55852 2.00318 6.02087C2.0055 5.77045 2.00815 5.48426 2.00815 5.15177C2.00815 4.14595 2.01012 3.48847 2.07304 3.00232C2.13273 2.54115 2.23113 2.38751 2.31389 2.30154C2.39151 2.22092 2.52427 2.12592 2.95045 2.06639C3.40942 2.00229 4.0335 2.00001 5.00488 2.00001L16.9951 2.00001C17.9664 2.00001 18.5905 2.00229 19.0495 2.0664C19.4757 2.12592 19.6084 2.22092 19.686 2.30155C19.7688 2.38751 19.8672 2.54115 19.9269 3.00232C19.9898 3.48847 19.9918 4.14595 19.9918 5.15177C19.9918 5.48233 19.9944 5.76713 19.9967 6.01653C20.0016 6.55612 20.005 6.93046 19.9773 7.2435C19.9429 7.63104 19.8729 7.72616 19.7956 7.80648C19.4125 8.20436 19.4246 8.83741 19.8225 9.22044C20.2203 9.60346 20.8534 9.59142 21.2364 9.19353C21.7444 8.66584 21.9123 8.06542 21.9695 7.42014C22.0063 7.00455 22.0016 6.48073 21.9961 5.87825C21.994 5.64711 21.9918 5.40439 21.9918 5.15177L21.9918 5.08739C21.9918 4.16387 21.9919 3.37543 21.9103 2.74561C21.8237 2.07619 21.6295 1.43654 21.1269 0.914491C20.6192 0.387108 19.9883 0.178102 19.3261 0.0856201C18.7124 -9.94951e-05 17.9468 -5.18116e-05 17.0628 3.38225e-06L4.93712 2.78621e-06Z" fill="#0B85F7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1978 4C13.9586 3.99998 14.598 3.99996 15.1143 4.06403C15.6617 4.13197 16.1602 4.2806 16.5912 4.64146C17.0222 5.00232 17.2562 5.46697 17.4193 5.99389C17.5731 6.49084 17.6855 7.1203 17.8192 7.8693L18.4895 11.6226C18.7725 13.2078 18.9993 14.4775 19.0348 15.4829C19.0715 16.5216 18.9118 17.4176 18.2925 18.1573C17.6731 18.8971 16.8191 19.2118 15.7902 19.3582C14.7942 19.5 13.5044 19.5 11.8942 19.5H10.1065C8.49624 19.5 7.20645 19.5 6.21049 19.3582C5.18153 19.2118 4.32758 18.8971 3.70822 18.1573C3.08886 17.4176 2.92921 16.5216 2.96588 15.4829C3.00137 14.4775 3.22812 13.2078 3.51121 11.6227L4.18145 7.86934C4.31517 7.12034 4.42756 6.49084 4.58139 5.99389C4.74449 5.46697 4.97845 5.00232 5.40946 4.64146C5.84047 4.2806 6.339 4.13197 6.8864 4.06403C7.40264 3.99996 8.04206 3.99998 8.80289 4L13.1978 4ZM11 7.5C11.4142 7.5 11.75 7.83579 11.75 8.25L11.75 13.4393L12.9697 12.2197C13.2626 11.9268 13.7374 11.9268 14.0303 12.2197C14.3232 12.5126 14.3232 12.9874 14.0303 13.2803L11.5303 15.7803C11.2374 16.0732 10.7626 16.0732 10.4697 15.7803L7.96967 13.2803C7.67678 12.9874 7.67678 12.5126 7.96967 12.2197C8.26256 11.9268 8.73744 11.9268 9.03033 12.2197L10.25 13.4393L10.25 8.25C10.25 7.83579 10.5858 7.5 11 7.5Z" fill="#0B85F7"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M26.5 15.5C26.5 16.6046 25.6046 17.5 24.5 17.5C23.3954 17.5 22.5 16.6046 22.5 15.5C22.5 14.3954 23.3954 13.5 24.5 13.5C25.6046 13.5 26.5 14.3954 26.5 15.5Z" fill="#8F9AA3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9255 9.5H20.0745C22.2504 9.49998 23.9852 9.49996 25.3453 9.68282C26.7497 9.87164 27.9035 10.2717 28.8159 11.1841C29.7283 12.0965 30.1284 13.2503 30.3172 14.6547C30.5 16.0149 30.5 17.7496 30.5 19.9256V20.0744C30.5 22.2504 30.5 23.9851 30.3172 25.3453C30.1284 26.7497 29.7283 27.9035 28.8159 28.8159C27.9035 29.7283 26.7497 30.1284 25.3453 30.3172C23.9851 30.5 22.2504 30.5 20.0744 30.5H19.9256C17.7496 30.5 16.0149 30.5 14.6547 30.3172C13.2503 30.1284 12.0965 29.7283 11.1841 28.8159C10.2717 27.9035 9.87164 26.7497 9.68282 25.3453C9.49996 23.9852 9.49998 22.2504 9.5 20.0745V19.9255C9.49998 17.7496 9.49996 16.0148 9.68282 14.6547C9.87164 13.2503 10.2717 12.0965 11.1841 11.1841C12.0965 10.2717 13.2503 9.87164 14.6547 9.68282C16.0148 9.49996 17.7496 9.49998 19.9255 9.5ZM14.9212 11.665C13.7386 11.824 13.0772 12.1195 12.5984 12.5984C12.1195 13.0772 11.824 13.7386 11.665 14.9212C11.5021 16.1326 11.5 17.7326 11.5 20C11.5 20.2688 11.5 20.5282 11.5004 20.7786C11.7879 20.7608 12.0762 20.7521 12.3647 20.7527C15.1778 20.6952 17.9275 21.572 20.124 23.2364C21.9124 24.5915 23.2416 26.3991 23.9596 28.4377C24.3674 28.4138 24.7385 28.3808 25.0788 28.335C25.2221 28.3157 25.3578 28.2945 25.4864 28.2711C24.9795 26.6882 24.1544 25.2258 23.0687 23.9582C22.8393 23.6904 22.9291 23.27 23.2648 23.1622C24.122 22.887 24.9998 22.7468 25.8878 22.7501C26.7607 22.7494 27.6259 22.889 28.4723 23.162C28.4994 22.2767 28.5 21.2378 28.5 20C28.5 17.7326 28.4979 16.1326 28.335 14.9212C28.176 13.7386 27.8805 13.0772 27.4017 12.5984C26.9228 12.1195 26.2614 11.824 25.0788 11.665C23.8674 11.5021 22.2674 11.5 20 11.5C17.7326 11.5 16.1326 11.5021 14.9212 11.665Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -1158,6 +1158,7 @@
"transportOptions": "خيارات النقل",
"selectHospitalForAdvancePayment": "يرجى تحديد المستشفى أولاً.",
"recharge": "شحن المحفظة",
"withdraw": "سحب",
"activityLevel": "مستوى النشاط",
"selectActivityLevel": "حدد مستوى النشاط",
"almostInactive": "غير نشط تقريبًا (بدون تمارين)",
@ -1780,11 +1781,11 @@
"illurgyInfomation": "الحساسية",
"vaccineInfomation": "التطعيمات",
"approvals1": "موافقات",
"convertBloodcholesterolInfo":"حول قيمة كوليسترول الدم بين ملجم/ديسيلتر و مليمول/لتر",
"convertBloodcholesterolInfo": "حول قيمة كوليسترول الدم بين ملجم/ديسيلتر و مليمول/لتر",
"carbsAlert": "حساب نسب المغذيات (الكربوهيدرات ،البروتين والدهون) بالسعرات الحرارية والقرامات وفقا لنسب محددة مسبقا",
"bodyFatAlert": "يحسب بناءً على الحجم",
"ibwAlert": "حساب الوزن المثالي بناءً على الطول، الوزن، وبنية الجسم",
"glucoseAlert":"تغيير وحدات القياس بين مليجرام لكل ديسيلتر و ملي مول لكل لتر",
"glucoseAlert": "تغيير وحدات القياس بين مليجرام لكل ديسيلتر و ملي مول لكل لتر",
"selectBloodGroup": "اختر الفصيلة",
"routePrescription": "طريقة الاستخدام",
"noNearestER": "لا يوجد طوارئ قريب منك",
@ -1820,9 +1821,9 @@
"dragToMoveAround": "اسحب للتحرك",
"startExploring": "ابدأ الاستكشاف",
"symptomsCheckSuggestions": "الاحتمالات",
"appointmentArrived": "تم الحضور",
"appointmentArrived": "تم الحضور",
"calendarPermissionAlertForAppointment": "يرجى منح إذن الوصول إلى التقويم من إعدادات التطبيق لضبط تذكير الموعد",
"rechargePageKey": "شحن المحفظة",
"rechargePageKey": "شحن المحفظة",
"selectFamilyFile": "اختيار ",
"ancillaryOrdersListNew": "طلبات غير مكتملة",
"medicalReportNew": "التقارير الطبية",
@ -1846,9 +1847,50 @@
"fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت",
"completedPrescriptionOrder": "تمت الخدمة",
"submitEReferral": "تقديم الطلب",
"requestRefund": "طلب استرداد",
"uploadImage": "رفع صورة",
"selectImage": "اختر صورة",
"selectOptions": "اختر الخيارات",
"iban": "رقم الآيبان",
"addIbanDetails": "أضف تفاصيل الآيبان",
"description": "الوصف",
"enterDescription": "أدخل الوصف",
"submitRequest": "تقديم الطلب",
"ibanIsRequired": "حقل رقم الآيبان مطلوب",
"descriptionIsRequired": "الوصف مطلوب",
"invalidIbanFormat": "رقم الآيبان غير صالح. يرجى إدخال رقم آيبان صالح (مثال: SA0380000000608010167519)",
"refundHistory": "سجل الاسترداد",
"noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.",
"invoiceNo": "رقم الفاتورة",
"totalAmount": "المبلغ الإجمالي",
"patientShare": "نصيب المريض",
"selectProcedure": "حدد العمليات",
"noProceduresAvailableForRefund": "لا توجد عمليات متاحة للاسترداد",
"selectAll": "تحديد الكل",
"selectRefundMethod": "اختر طريقة الاسترداد",
"hmgWallet": "محفظة HMG",
"bankTransfer": "تحويل بنكي",
"selectRefundReason": "اختر سبب الاسترداد",
"refundReason": "سبب الاسترداد",
"done": "تم",
"refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته",
"refundReason2": "المريض لا يريد إجراء سريرياً",
"refundReason3": "المريض لم ينتظر الاستشارة / يريد طبيباً آخر",
"refundReason4": "المريض أوقف الاشتراك في الباقة",
"refundReason5": "الخدمة لم تُنجز / غير متاحة بسبب عطل الجهاز / رفض المريض",
"refundReason6": "تم إلغاء الإجراء السريري أو تغييره من قِبل الطبيب المحيل",
"refundReason7": "تم إلغاء إجراء وحدة الخدمة / المختبر / الأشعة أو تغييره من قِبل الطبيب المحيل",
"selectProcedures": "اختر العمليات",
"submitEReferral": "تقديم الطلب",
"redeem": "استبدال",
"points": "نقاط",
"transactions": "المعاملات",
"pharmacy": "الصيدلية",
"visitsOrders": "الزيارات/الطلبات"
"visitsOrders": "الزيارات/الطلبات",
"cannotOpenThisFile": "لا يمكن فتح هذا الملف",
"refundDetails": "تفاصيل الاسترداد",
"requestNo": "رقم الطلب.",
"referenceNo": "رقم المرجع",
"advanceNumber": "رقم الدفعة المقدمة",
"amountOnly": "المبلغ"
}

@ -883,7 +883,7 @@
"selectDate": "Select Date",
"historyFlowchart": "History FlowChart",
"to": "to",
"startDate" : "Start Date",
"startDate": "Start Date",
"endDate": "End Date",
"hmgHospitals": "HMG Hospitals",
"hmcMedicalClinic": "HMC Medical Centers",
@ -896,7 +896,7 @@
"walkin": "Booked",
"continueString": "Continue",
"laserClinic": "Laser Clinic",
"covid_info" :"Dr. Sulaiman Al Habib hospitals are conducting a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes, and obtain the result within several hours. Corona Virus Covid 19 testing service with PCR technology to detect the virus according to the highest international standards and with the latest high-precision RT-PCR devices (American GeneXpert and others), That is approved by the Food and Drug Authority as well as by the Saudi Center for Infectious Diseases Prevention.",
"covid_info": "Dr. Sulaiman Al Habib hospitals are conducting a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes, and obtain the result within several hours. Corona Virus Covid 19 testing service with PCR technology to detect the virus according to the highest international standards and with the latest high-precision RT-PCR devices (American GeneXpert and others), That is approved by the Food and Drug Authority as well as by the Saudi Center for Infectious Diseases Prevention.",
"appointmentDetails": "Appointment Details",
"checkingDoctorAvailability": "Checking doctor availability...",
"cancellingAppointmentPleaseWait": "Cancelling Appointment, Please Wait...",
@ -1149,6 +1149,7 @@
"transportOptions": "Transport Options",
"selectHospitalForAdvancePayment": "Please select the hospital you want to make an advance payment for.",
"recharge": "Add Funds",
"withdraw": "Withdraw",
"activityLevel": "Activity Level",
"selectActivityLevel": "Select Activity Level",
"almostInactive": "Almost Inactive (no exercise)",
@ -1262,6 +1263,8 @@
"selectLocationPrescriptionDelivery": "Please select the location for prescription delivery",
"noRadiologyOrders": "No Radiology Orders Found",
"ageIsRequired": "Age is required",
"ibanIsRequired": "IBAN field is required",
"descriptionIsRequired": "Description is required",
"invalidAge": "Invalid age",
"ageMustBeBetween11And120": "Age must be between 11 and 120",
"heightIsRequired": "Height is required",
@ -1747,7 +1750,7 @@
"onlineCheckIn24HoursOnly": "Online Check-In is only available within 4 hours of the appointment time.",
"searchFor": "Search For",
"m": "m",
"inch": "in",
"inch": "in",
"updateEmailAddressTitle": "Enter the new email address to be updated: ",
"enterNewNewContactNumber": "Enter the new emergency contact number to be updated: ",
"nationalIdFileNumber": "National ID/File Number",
@ -1765,10 +1768,10 @@
"selectCriteria": "Select Criteria",
"locationServicesDisabled": "Weather is unavailable because location services are disabled. Please enable location services to see the weather information.",
"selectDateTimeKey": "Select",
"carbsAlert":"Calculate carbohydrate based protein and fat ration in calories and grams according to a pre set ratio.",
"carbsAlert": "Calculate carbohydrate based protein and fat ration in calories and grams according to a pre set ratio.",
"bodyFatAlert": "Estimate the total body fat based on the size.",
"ibwAlert": "Calculates the ideal body weight based on height, weight and body size.",
"glucoseAlert":"Convert blood glucose values between mg/dL and mmol/L. (1 mmol/L ≈ 18.0182 mg/dL)",
"glucoseAlert": "Convert blood glucose values between mg/dL and mmol/L. (1 mmol/L ≈ 18.0182 mg/dL)",
"selectBloodGroup": "Select Blood Group",
"routePrescription": "Route",
"noNearestER": "No nearest Er Around you",
@ -1810,9 +1813,9 @@
"dragToMoveAround": "Drag to move around",
"startExploring": "Start exploring",
"symptomsCheckSuggestions": "Suggestions",
"appointmentArrived": "Arrived",
"appointmentArrived": "Arrived",
"calendarPermissionAlertForAppointment": "Please grant calendar access permission from app settings to set appointment reminder.",
"rechargePageKey": "Recharge Wallet",
"rechargePageKey": "Recharge Wallet",
"selectFamilyFile": "Select",
"ancillaryOrdersListNew": "Ancillary Orders List",
"medicalReportNew": "Medical Report",
@ -1836,11 +1839,54 @@
"fileSizeExceedsLimit": "File size must not exceed 1 MB",
"completedPrescriptionOrder": "Completed",
"submitEReferral": "Submit",
"requestRefund": "Request Refund",
"uploadImage": "Upload Image",
"selectImage": "Select Image",
"selectOptions": "Select Options",
"iban": "IBAN",
"addIbanDetails": "Add IBAN Details",
"description": "Description",
"enterDescription": "Enter Description",
"submitRequest": "Submit Request",
"ibanIsRequired": "IBAN is required",
"descriptionIsRequired": "Description is required",
"invalidIbanFormat": "Invalid IBAN. Please enter a valid IBAN (e.g. SA0380000000608010167519)",
"refundHistory": "Refund History",
"noRefundHistoryFound": "You don't have any refund history yet.",
"invoiceNo": "Invoice No.",
"totalAmount": "Total Amount",
"patientShare": "Patient Share",
"selectProcedure": "Select Procedure",
"noProceduresAvailableForRefund": "No procedures available for refund",
"selectAll": "Select all",
"selectRefundMethod": "Select Refund Method",
"hmgWallet": "HMG Wallet",
"bankTransfer": "Bank Transfer",
"selectRefundReason": "Select Refund Reason",
"refundReason": "Refund Reason",
"done": "Done",
"refundReason1": "Company patient treated as self-paying and re-invoiced",
"refundReason2": "Patient does not want a clinical procedure",
"refundReason3": "Patient does not wait for consultation/wants another doctor",
"refundReason4": "Patient discontinued the package",
"refundReason5": "Service not done/not available due to machine breakdown/patient refusal",
"refundReason6": "Clinical procedure cancelled or changed by requesting doctor",
"refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor",
"selectProcedures": "Select Procedures",
"redeem": "Redeem",
"points": "Points",
"transactions": "Transactions",
"pharmacy": "Pharmacy",
"visitsOrders": "Visits/Orders"
"visitsOrders": "Visits/Orders",
"cannotOpenThisFile": "Cannot open this file",
"thisInvoiceIsNotEligibleForRefund": "This invoice is not eligible for refund",
"refundDetails": "Refund Details",
"requestNo": "Request No.",
"referenceNo": "Reference No.",
"refund": "Refund",
"advanceNumber": "Advance Number",
"amountOnly": "Amount"
}

@ -601,7 +601,7 @@ SPEC CHECKSUMS:
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
PayFortSDK: 233eabe9a45601fdbeac67fa6e5aae46ed8faf82
PayFortSDK: e12f79051ae15b2e29536771a45354900180bfcb
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
Polyline: 2a1f29f87f8d9b7de868940f4f76deb8c678a5b1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
@ -612,7 +612,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
Solar-dev: 4612dc9878b9fed2667d23b327f1d4e54e16e8d0
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftProtobuf: e1b437c8e31a4c5577b643249a0bb62ed4f02153
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1587365701FF0D2AAAFED0D8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
478CFA942E638C8E0064F3D7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */; };
@ -25,7 +26,6 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -52,11 +52,13 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
05E1B2A0E6844572ADA81174 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3EAE1F31D8D0AA94CA6C11EB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
478CFA952E6E20A60064F3D7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
61243B3D2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HMGPenguinInPlatformBridge.swift; sourceTree = "<group>"; };
@ -70,11 +72,10 @@
687F8A672F828A6300B0FFC1 /* PenNavUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = PenNavUI.xcframework; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
769C9BF82E6F106D009F68A9 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
76AA18AC2F3B2A4D00DC8DFC /* ring_30Sec.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ring_30Sec.caf; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7BEF44ABFEF585D64D3F266C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -82,8 +83,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -91,10 +91,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */,
687F8A682F828A6300B0FFC1 /* PenguinINRenderer.xcframework in Frameworks */,
687F8A692F828A6300B0FFC1 /* Penguin.xcframework in Frameworks */,
687F8A6A2F828A6300B0FFC1 /* PenNavUI.xcframework in Frameworks */,
1587365701FF0D2AAAFED0D8 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -135,7 +135,7 @@
687F8A652F828A6300B0FFC1 /* Penguin.xcframework */,
687F8A662F828A6300B0FFC1 /* PenguinINRenderer.xcframework */,
687F8A672F828A6300B0FFC1 /* PenNavUI.xcframework */,
D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */,
A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -143,9 +143,9 @@
79DD2093A1D9674C94359FC8 /* Pods */ = {
isa = PBXGroup;
children = (
8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */,
7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */,
D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */,
3EAE1F31D8D0AA94CA6C11EB /* Pods-Runner.debug.xcconfig */,
05E1B2A0E6844572ADA81174 /* Pods-Runner.release.xcconfig */,
7BEF44ABFEF585D64D3F266C /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -227,15 +227,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
BFED6CCFE59BB148875A533B /* [CP] Check Pods Manifest.lock */,
EA10FC42B68E41E75A052FAA /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
8372B02399CDF54531650AD4 /* [CP] Embed Pods Frameworks */,
81DE7C26F41956799E954FCE /* [CP] Copy Pods Resources */,
765A5A922F35CD8B0003FF7D /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5D78F615DF2D966539B34DE2 /* [CP] Embed Pods Frameworks */,
4AA9AE39AD56A64A1F16E548 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -325,7 +325,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
81DE7C26F41956799E954FCE /* [CP] Copy Pods Resources */ = {
4AA9AE39AD56A64A1F16E548 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -342,7 +342,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
8372B02399CDF54531650AD4 /* [CP] Embed Pods Frameworks */ = {
5D78F615DF2D966539B34DE2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -374,7 +374,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BFED6CCFE59BB148875A533B /* [CP] Check Pods Manifest.lock */ = {
EA10FC42B68E41E75A052FAA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
@ -95,8 +96,7 @@ class ApiClientImp implements ApiClient {
required AppState appState,
HttpClientManager? httpClient,
}) : _appState = appState,
_httpClient = httpClient ??
HttpClientManager(lifecycleService: getIt<AppLifecycleService>());
_httpClient = httpClient ?? HttpClientManager(lifecycleService: getIt<AppLifecycleService>());
@override
post(
@ -209,7 +209,8 @@ class ApiClientImp implements ApiClient {
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 1018977;
// body['PatientID'] = 4770714;
// body['PatientID'] = 89754444;
// body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q";
@ -244,7 +245,8 @@ class ApiClientImp implements ApiClient {
final dynamic requestBody = isBodyPlainText ? body : json.encode(body);
debugPrint("uri: ${Uri.parse(url.trim())}");
var requestBodyJSON = json.encode(body);
debugPrint("body: $requestBodyJSON", wrapWidth: 2048);
// debugPrint("body: $requestBodyJSON", wrapWidth: 2048);
log("body: $requestBodyJSON");
http.Response response;
try {
@ -253,7 +255,7 @@ class ApiClientImp implements ApiClient {
body: requestBody,
headers: headers,
);
// debugPrint("response: ${response.body}", wrapWidth: 2048);
log("response: ${response.body.toString()}");
} on SocketException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));

@ -269,6 +269,15 @@ class ApiConsts {
// static String getAiOverViewLabOrders = "Services/Patients.svc/REST/HMGAI_Lab_Analyze_Orders_API";
// static String getAiOverViewLabOrder = "Services/Patients.svc/REST/HMGAI_Lab_Analyzer_API";
//Refund Request
static String getRefundableInvoices = "Services/Outps.svc/REST/getRefundableInvoices";
static String getRefundableInvoicesDetails = "Services/Outps.svc/REST/getRefundableInvoicesDetails";
static String getRefundableAdvances = "Services/Outps.svc/REST/getRefundableAdvances";
static String getRefundStatus = "Services/COCWS.svc/REST/GetRefundStatus";
static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices";
static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund";
static String isRefundableInvoice = "Services/Outps.svc/REST/isRefundableInvoice";
static String getAiAnalysisURL = "https://api.labpredict.com";
static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology";
static String getAiOverViewLabOrdersNew = "$getAiAnalysisURL/analyze_lab_orders";

@ -260,16 +260,15 @@ class AppAssets {
//smartwatch
static const String watchActivity = '$svgBasePath/watch_activity.svg';
static const String watchActivityTrailing = '$svgBasePath/watch_activity_trailing.svg';
static const String watchSteps= '$svgBasePath/watch_steps.svg';
static const String watchStepsTrailing= '$svgBasePath/watch_steps_trailing.svg';
static const String watchSleep= '$svgBasePath/watch_sleep.svg';
static const String watchSleepTrailing= '$svgBasePath/watch_sleep_trailing.svg';
static const String watchBmi= '$svgBasePath/watch_bmi.svg';
static const String watchBmiTrailing= '$svgBasePath/watch_bmi_trailing.svg';
static const String watchWeight= '$svgBasePath/watch_weight.svg';
static const String watchWeightTrailing= '$svgBasePath/watch_weight_trailing.svg';
static const String watchHeight= '$svgBasePath/watch_height.svg';
static const String watchSteps = '$svgBasePath/watch_steps.svg';
static const String watchStepsTrailing = '$svgBasePath/watch_steps_trailing.svg';
static const String watchSleep = '$svgBasePath/watch_sleep.svg';
static const String watchSleepTrailing = '$svgBasePath/watch_sleep_trailing.svg';
static const String watchBmi = '$svgBasePath/watch_bmi.svg';
static const String watchBmiTrailing = '$svgBasePath/watch_bmi_trailing.svg';
static const String watchWeight = '$svgBasePath/watch_weight.svg';
static const String watchWeightTrailing = '$svgBasePath/watch_weight_trailing.svg';
static const String watchHeight = '$svgBasePath/watch_height.svg';
//bottom navigation//
static const String homeBottom = '$svgBasePath/home_bottom.svg';
@ -392,6 +391,13 @@ class AppAssets {
static const String bmicalc = '$svgBasePath/bmi1.svg';
static const String bmrcalc = '$svgBasePath/bmr1.svg';
static const String refundIcon = '$svgBasePath/refund_icon.svg';
static const String bankIcon = '$svgBasePath/bank_icon.svg';
static const String bankIconOnly = '$svgBasePath/bank_icon_only.svg';
static const String habibLogoOnly = '$svgBasePath/habib logo_only.svg';
static const String descriptionIcon = '$svgBasePath/description_icon.svg';
static const String selectImageIcon = '$svgBasePath/select_image_icon.svg';
// PNGS //
static const String hmgLogo = '$pngBasePath/hmg_logo.png';
static const String liveCareService = '$pngBasePath/livecare_service.png';

@ -4,6 +4,7 @@ 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';
import 'package:hmg_patient_app_new/core/location_util.dart';
import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_repo.dart';
import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart';
@ -12,8 +13,6 @@ import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_repo.
import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart';
import 'package:hmg_patient_app_new/features/common/common_repo.dart';
import 'package:hmg_patient_app_new/features/contact_us/contact_us_repo.dart';
import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart';
@ -23,6 +22,7 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_repo.dart';
import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart';
import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_repo.dart';
@ -59,6 +59,8 @@ import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_v
import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_repo.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart';
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart';
@ -70,7 +72,6 @@ import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.da
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_repo.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
@ -88,13 +89,11 @@ import 'package:local_auth/local_auth.dart';
import 'package:logger/web.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../features/monthly_reports/monthly_reports_repo.dart';
import '../features/qr_parking/qr_parking_view_model.dart';
import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart';
import '../features/active_prescriptions/active_prescriptions_repo.dart';
import '../features/monthly_reports/terms_conditions_repo.dart';
import '../features/monthly_reports/terms_conditions_view_model.dart';
import '../features/qr_parking/qr_parking_view_model.dart';
import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart';
GetIt getIt = GetIt.instance;
@ -183,18 +182,14 @@ class AppDependencies {
getIt.registerLazySingleton<MonthlyReportRepo>(() => MonthlyReportRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ActivePrescriptionsRepo>(() => ActivePrescriptionsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<TermsConditionsRepo>(() => TermsConditionsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerFactory<TermsConditionsViewModel>(
() => TermsConditionsViewModel(
termsConditionsRepo: getIt<TermsConditionsRepo>(),
errorHandlerService: getIt<ErrorHandlerService>(),
),
);
getIt.registerFactory<TermsConditionsViewModel>(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<MonthlyReportsRepo>(() => MonthlyReportsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<QrParkingRepo>(() => QrParkingRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<NotificationsRepo>(() => NotificationsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<AskDoctorRepo>(() => AskDoctorRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ServicesPriceListRepo>(() => ServicesPriceListRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ProfileSettingsRepo>(() => ProfileSettingsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<RefundRequestRepo>(() => RefundRequestRepoImp(apiClient: getIt(), loggerService: getIt()));
getIt.registerLazySingleton<OffersAndDiscountsRepo>(() => OffersAndDiscountsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
// ViewModels
@ -206,20 +201,23 @@ 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()),
);
getIt.registerLazySingleton<HabibWalletViewModel>(
() => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()),
() => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt(), refundRequestRepo: getIt()),
);
getIt.registerLazySingleton<MedicalFileViewModel>(
@ -231,7 +229,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>(
@ -245,7 +248,13 @@ class AppDependencies {
getIt.registerLazySingleton<AuthenticationViewModel>(
() => AuthenticationViewModel(
authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()),
authenticationRepo: getIt(),
cacheService: getIt(),
navigationService: getIt(),
dialogService: getIt(),
appState: getIt(),
errorHandlerService: getIt(),
localAuthService: getIt()),
);
getIt.registerLazySingleton<ProfileSettingsViewModel>(() => ProfileSettingsViewModel(
@ -323,26 +332,38 @@ class AppDependencies {
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<WeatherMonitorViewModel>(() => WeatherMonitorViewModel(weatherRepo: getIt(), errorHandlerService: getIt(), locationUtils: getIt()));
getIt.registerLazySingleton<WeatherMonitorViewModel>(
() => WeatherMonitorViewModel(weatherRepo: getIt(), errorHandlerService: getIt(), locationUtils: getIt()));
//commenting this because its already define there was on run time error because of this.
getIt.registerLazySingleton<MyInvoicesViewModel>(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<MyInvoicesViewModel>(
() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<MonthlyReportViewModel>(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt()));
getIt.registerLazySingleton<NotificationsViewModel>(() => NotificationsViewModel(notificationsRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<HealthTrackersViewModel>(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ActivePrescriptionsViewModel>(() => ActivePrescriptionsViewModel(errorHandlerService: getIt(), activePrescriptionsRepo: getIt()));
getIt.registerFactory<QrParkingViewModel>(() => QrParkingViewModel(qrParkingRepo: getIt<QrParkingRepo>(), errorHandlerService: getIt<ErrorHandlerService>(), cacheService: getIt<CacheService>()));
getIt.registerLazySingleton<ActivePrescriptionsViewModel>(
() => ActivePrescriptionsViewModel(errorHandlerService: getIt(), activePrescriptionsRepo: getIt()));
getIt.registerFactory<QrParkingViewModel>(() => QrParkingViewModel(
qrParkingRepo: getIt<QrParkingRepo>(), errorHandlerService: getIt<ErrorHandlerService>(), cacheService: getIt<CacheService>()));
getIt.registerLazySingleton<AskDoctorViewModel>(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(
() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<OffersAndDiscountsViewModel>(() => OffersAndDiscountsViewModel(offersAndDiscountsRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<OffersAndDiscountsViewModel>(
() => OffersAndDiscountsViewModel(offersAndDiscountsRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(
() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton<RefundRequestViewModel>(() => RefundRequestViewModel(
appState: getIt(),
refundRequestRepo: getIt(),
errorHandlerService: getIt(),
));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt()));
}

@ -33,7 +33,7 @@ class ValidationUtils {
isCorrectID = false;
}
if(nationalId!.length == 10) {
if (nationalId!.length == 10) {
if (Utils.isSAUDIIDValid(nationalId!) == false) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidNationalId.tr(), onOkPressed: onOkPress);
isCorrectID = false;
@ -77,10 +77,7 @@ class ValidationUtils {
// Check if "Others" country is selected and phone number starts with restricted codes
if (selectedCountry == CountryEnum.others) {
if (phoneNumber.startsWith('00966') || phoneNumber.startsWith('00971')) {
_dialogService.showExceptionBottomSheet(
message: LocaleKeys.cannotEnterSaudiOrUAENumber.tr(),
onOkPressed: onOkPress
);
_dialogService.showExceptionBottomSheet(message: LocaleKeys.cannotEnterSaudiOrUAENumber.tr(), onOkPressed: onOkPress);
return false;
}
}
@ -121,7 +118,8 @@ class ValidationUtils {
return regex.hasMatch(id);
}
static bool validateUaeRegistration({String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) {
static bool validateUaeRegistration(
{String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) {
if (name == null || name.isEmpty) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAValidName.tr(), onOkPressed: onOkPress);
return false;
@ -160,7 +158,8 @@ class ValidationUtils {
return true;
}
static bool isValidatedIdAndPhoneWithCountryValidation({String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) {
static bool isValidatedIdAndPhoneWithCountryValidation(
{String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) {
bool isCorrectID = true;
if (nationalId == null || nationalId.isEmpty) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAnationalID.tr(), onOkPressed: onOkPress);
@ -191,8 +190,40 @@ class ValidationUtils {
}
static bool isNullOrEmpty(String? value) {
return value == null || value
.trim()
.isEmpty;
return value == null || value.trim().isEmpty;
}
}
static bool validateIban(String iban) {
final normalized = iban.replaceAll(RegExp(r'\s+'), '').toUpperCase();
// General IBAN structure: country code + check digits + BBAN
if (!RegExp(r'^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$').hasMatch(normalized)) {
return false;
}
// IBAN total length must be between 15 and 34 characters
if (normalized.length < 15 || normalized.length > 34) {
return false;
}
// Move first 4 chars to the end
final rearranged = normalized.substring(4) + normalized.substring(0, 4);
// Convert letters to numbers: A=10, B=11, ..., Z=35
final numeric = rearranged.split('').map((char) {
final code = char.codeUnitAt(0);
if (code >= 65 && code <= 90) {
return (code - 55).toString();
}
return char;
}).join();
// MOD-97 check without parsing huge integer at once
int remainder = 0;
for (final ch in numeric.split('')) {
remainder = (remainder * 10 + int.parse(ch)) % 97;
}
return remainder == 1;
}
}

@ -18,13 +18,20 @@ extension CapExtension on String {
String get needTranslation => this;
String get capitalizeFirstofEach => trim().isNotEmpty ? trim().toLowerCase().split(" ").map((str) => str.inCaps).join(" ") : "";
}
extension EmailValidator on String {
Widget get toWidget => Text(this);
Widget toText8({bool isEnglishOnly = false, Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text(
Widget toText8(
{bool isEnglishOnly = false,
Color? color,
FontWeight? fontWeight,
bool isBold = false,
int? maxlines,
FontStyle? fontStyle,
TextOverflow? textOverflow}) =>
Text(
this,
maxLines: maxlines,
overflow: textOverflow,
@ -74,7 +81,8 @@ extension EmailValidator on String {
int? maxlines,
FontStyle? fontStyle,
TextOverflow? textOverflow,
double letterSpacing = 0, bool isEnglishOnly = false}) =>
double letterSpacing = 0,
bool isEnglishOnly = false}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
@ -91,7 +99,17 @@ extension EmailValidator on String {
decorationColor: color ?? AppColors.blackColor),
);
Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isCenter = false, bool isBold = false, int maxLine = 0, double letterSpacing = 0, bool isEnglishOnly = false,}) => Text(
Widget toText11({
Color? color,
FontWeight? weight,
bool isUnderLine = false,
bool isCenter = false,
bool isBold = false,
int maxLine = 0,
double letterSpacing = 0,
bool isEnglishOnly = false,
}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
maxLines: (maxLine > 0) ? maxLine : null,
@ -107,7 +125,8 @@ extension EmailValidator on String {
);
Widget toText12(
{bool isEnglishOnly = false, Color? color,
{bool isEnglishOnly = false,
Color? color,
bool isUnderLine = false,
TextAlign textAlignment = TextAlign.start,
bool isBold = false,
@ -176,7 +195,15 @@ extension EmailValidator on String {
),
);
Widget toText13({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0, FontWeight? weight, double? letterSpacing = 0}) => Text(
Widget toText13(
{Color? color,
bool isUnderLine = false,
bool isBold = false,
bool isCenter = false,
int maxLine = 0,
FontWeight? weight,
double? letterSpacing = 0}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
maxLines: (maxLine > 0) ? maxLine : null,
@ -213,11 +240,24 @@ extension EmailValidator on String {
height: height,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
decoration: isUnderLine ? TextDecoration.underline : null,
fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
fontFamily: fontFamily ??
(isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
decorationColor: color ?? AppColors.blackColor),
);
Widget toText15({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, FontWeight? weight, int? maxlines, double? letterSpacing = -1}) => Text(
Widget toText15(
{Color? color,
bool isUnderLine = false,
bool isBold = false,
bool isCenter = false,
FontWeight? weight,
int? maxlines,
double? letterSpacing = -1}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
maxLines: maxlines,
@ -229,7 +269,9 @@ extension EmailValidator on String {
decoration: isUnderLine ? TextDecoration.underline : null),
);
Widget toText16({bool isEnglishOnly = false, Color? color,
Widget toText16(
{bool isEnglishOnly = false,
Color? color,
bool isUnderLine = false,
bool isBold = false,
bool isCenter = false,
@ -254,85 +296,194 @@ extension EmailValidator on String {
overflow: textOverflow,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
decoration: isUnderLine ? TextDecoration.underline : null,
fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
fontFamily: fontFamily ??
(isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
decorationColor: decorationColor ?? AppColors.errorColor),
);
Widget toText17({bool isEnglishOnly = false, 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, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
style: TextStyle(
color: color ?? AppColors.blackColor,
fontSize: 17.f,
letterSpacing: -1,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
);
Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow, bool isEnglishOnly = false,}) => Text(
Widget toText18({
Color? color,
FontWeight? weight,
bool isBold = false,
bool isCenter = false,
int? maxlines,
TextOverflow? textOverflow,
bool isEnglishOnly = false,
}) =>
Text(
maxLines: maxlines,
textAlign: isCenter ? TextAlign.center : null,
this,
overflow: textOverflow,
style: TextStyle(fontSize: 18.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4, fontFamily: (isEnglishOnly ? "Poppins" : getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),),
style: TextStyle(
fontSize: 18.f,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal),
color: color ?? AppColors.blackColor,
letterSpacing: -0.4,
fontFamily: (isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
),
);
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,
bool isEnglishOnly = false,
}) =>
Text(
this,
style: TextStyle(fontSize: 20.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4),
style: TextStyle(
fontFamily: (isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
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)),
style: TextStyle(
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),
style: TextStyle(
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, bool isEnglishOnly = false,}) => Text(
Widget toText24({
Color? color,
bool isBold = false,
bool isCenter = false,
FontWeight? fontWeight,
double? letterSpacing,
bool isEnglishOnly = false,
}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
fontFamily: (isEnglishOnly ? "Poppins" : getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal),
fontFamily: (isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
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, bool isEnglishOnly = false,}) => Text(
Widget toText26({
Color? color,
bool isBold = false,
double? height,
bool isCenter = false,
FontWeight? weight,
double? letterSpacing,
bool isEnglishOnly = false,
}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
fontFamily: (isEnglishOnly ? "Poppins" : getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
height: height ?? 23 / 26, color: color ?? AppColors.blackColor, fontSize: 26.f, letterSpacing: letterSpacing ?? -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)),
fontFamily: (isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
height: height ?? 23 / 26,
color: color ?? AppColors.blackColor,
fontSize: 26.f,
letterSpacing: letterSpacing ?? -1,
fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)),
);
Widget toText28({bool isEnglishOnly = false, Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => Text(
Widget toText28({bool isEnglishOnly = false, Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) =>
Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(height: height ?? 23 / 28, color: color ?? AppColors.blackColor, fontSize: 28.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
style: TextStyle(
height: height ?? 23 / 28,
color: color ?? AppColors.blackColor,
fontSize: 28.f,
letterSpacing: letterSpacing ?? -1,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
);
Widget toText32({bool isEnglishOnly = false, FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
style: TextStyle(
height: getIt.get<AppState>().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,),
height: getIt.get<AppState>().getLanguageCode() == "ar" ? 1.2 : 32 / 32,
color: color ?? AppColors.blackColor,
fontSize: 32.f,
letterSpacing: -1,
fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins',
fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,
),
);
Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text(
Widget toText44({
Color? color,
bool isBold = false,
bool isEnglishOnly = false,
}) =>
Text(
this,
style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: (isEnglishOnly ? "Poppins" : getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),),
style: TextStyle(
height: 32 / 32,
color: color ?? AppColors.blackColor,
fontSize: 44.f,
letterSpacing: -1,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontFamily: (isEnglishOnly
? "Poppins"
: getIt.get<AppState>().getLanguageCode() == "ar"
? 'CairoArabic'
: 'Poppins'),
),
);
Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) {
@ -368,7 +519,9 @@ extension EmailValidator on String {
}
bool isValidEmail() {
return RegExp(r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$').hasMatch(this);
return RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(this);
}
String toFormattedDate() {

@ -3,14 +3,29 @@ import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/lakum_inquiry_information_response_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/patient_advance_balance_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class HabibWalletViewModel extends ChangeNotifier {
HabibWalletRepo habibWalletRepo;
RefundRequestRepo refundRequestRepo;
ErrorHandlerService errorHandlerService;
HabibWalletViewModel({
required this.habibWalletRepo,
required this.errorHandlerService,
required this.refundRequestRepo,
});
bool isWalletAmountLoading = false;
num habibWalletAmount = 0;
num walletRechargeAmount = 0;
@ -18,6 +33,12 @@ class HabibWalletViewModel extends ChangeNotifier {
bool isBottomSheetContentLoading = false;
// Refundable Advances
bool isAdvancesLoading = false;
double totalAdvancesBalance = 0.0;
List<RefundableAdvancesItem> refundableAdvancesList = [];
bool isSubmittingWithdrawal = false;
bool isSearchedFileNumberDataShown = false;
int currentIndex = 0;
@ -26,9 +47,6 @@ class HabibWalletViewModel extends ChangeNotifier {
HospitalsModel? selectedHospital;
HabibWalletRepo habibWalletRepo;
ErrorHandlerService errorHandlerService;
String fileNumber = '';
String depositorName = '';
String mobileNumber = '';
@ -44,6 +62,7 @@ class HabibWalletViewModel extends ChangeNotifier {
// Getters for field errors
String? get amountError => _amountError;
String? get hospitalError => _hospitalError;
String yahalaAccountNumber = '';
@ -77,13 +96,13 @@ class HabibWalletViewModel extends ChangeNotifier {
allTransactions.sort((a, b) {
// Null dates go to the end
if (a.transactionDate == null && b.transactionDate == null) return 0;
if (a.transactionDate == null) return 1; // a goes after b
if (a.transactionDate == null) return 1; // a goes after b
if (b.transactionDate == null) return -1; // a goes before b
try {
DateTime dateA = DateUtil.convertStringToDate(a.transactionDate!);
DateTime dateB = DateUtil.convertStringToDate(b.transactionDate!);
// Compare dateB to dateA (not dateA to dateB) for DESCENDING order
// This puts newer dates (larger values) at the top of the list
return dateB.compareTo(dateA);
@ -115,6 +134,48 @@ class HabibWalletViewModel extends ChangeNotifier {
notifyListeners();
}
// ==================== WITHDRAW VALIDATION ERROR TRACKING ====================
String? _ibanError;
String? _withdrawDescriptionError;
String? get ibanError => _ibanError;
String? get withdrawDescriptionError => _withdrawDescriptionError;
void clearIbanError() {
_ibanError = null;
notifyListeners();
}
void clearWithdrawDescriptionError() {
_withdrawDescriptionError = null;
notifyListeners();
}
bool validateWithdrawForm({required String iban, required String description}) {
bool isValid = true;
_ibanError = null;
_withdrawDescriptionError = null;
if (iban.trim().isEmpty) {
_ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false;
} else {
if (!ValidationUtils.validateIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false;
}
}
if (description.trim().isEmpty) {
_withdrawDescriptionError = LocaleKeys.descriptionIsRequired.tr();
isValid = false;
}
notifyListeners();
return isValid;
}
// Validate recharge form - shows only first error
bool validateRechargeForm(String amount) {
// Clear previous errors
@ -150,7 +211,114 @@ class HabibWalletViewModel extends ChangeNotifier {
return true;
}
HabibWalletViewModel({required this.habibWalletRepo, required this.errorHandlerService});
Future<void> fetchAdvancesForHospital() async {
isAdvancesLoading = true;
totalAdvancesBalance = 0.0;
refundableAdvancesList = [];
notifyListeners();
final result = await refundRequestRepo.getRefundableAdvances();
result.fold(
(failure) {
isAdvancesLoading = false;
notifyListeners();
errorHandlerService.handleError(failure: failure);
},
(apiResponse) {
isAdvancesLoading = false;
if (apiResponse.data != null) {
refundableAdvancesList = apiResponse.data!.refundableAdvancesItems;
totalAdvancesBalance = refundableAdvancesList.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0));
}
notifyListeners();
},
);
}
/// Converts a WCF /Date(ms+timezone)/ string to "YYYY-MM-DD" format expected by the API.
String _formatAdvanceDate(String? rawDate) {
if (rawDate == null || rawDate.isEmpty) return '';
try {
final date = DateUtil.convertStringToDate(rawDate);
return DateFormat('yyyy-MM-dd').format(date);
} catch (_) {
return rawDate;
}
}
Future<void> submitWalletWithdrawalRequest({
required String iban,
required String remarks,
required String patientName,
required String patientId,
required String patientMobileNumber,
required String patientNationalId,
required int projectId,
required String projectName,
Function(SubmitRefundResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isSubmittingWithdrawal = true;
notifyListeners();
final advances = refundableAdvancesList
.map((a) => SubmitRefundAdvanceItem(
advanceNo: (a.advanceNo ?? 0).toString(),
paidAmount: a.paidAmount ?? 0.0,
balanceAmount: a.balanceAmount ?? 0.0,
advanceDate: a.advanceDate ?? "",
// advanceDate: _formatAdvanceDate(a.advanceDate),
))
.toList();
final request = SubmitRefundRequestModel(
projectId: projectId,
refundType: '2',
// wallet withdrawal
refundProject: projectName,
refundProjectId: projectId,
refundPatientId: patientId,
patientMobileNumber: patientMobileNumber,
refundPatientName: patientName,
refundNationalId: patientNationalId,
refundInvoiceNo: '',
refundAppointmentNo: '',
refundDoctorId: 0,
refundDoctorName: '',
refundClinicId: 0,
refundClinicName: '',
refundDestination: '2', // only bank transfer
refundIban: iban,
refundRemarks: remarks,
refundTotalAmount: totalAdvancesBalance,
refundProceduresList: const [],
refundAdvancesList: advances,
);
final result = await refundRequestRepo.submitRefund(request: request);
result.fold(
(failure) {
isSubmittingWithdrawal = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isSubmittingWithdrawal = false;
notifyListeners();
if (apiResponse.data != null) {
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
if (onError != null) {
onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr());
}
}
},
);
}
initHabibWalletProvider() {
isWalletAmountLoading = true;
@ -180,6 +348,20 @@ class HabibWalletViewModel extends ChangeNotifier {
notifyListeners();
}
/// Resets all state related to the wallet withdrawal form.
/// Pass [silent] = true when calling from dispose() to avoid notifyListeners
/// while the widget tree is locked.
void resetWithdrawForm({bool silent = false}) {
selectedHospital = null;
totalAdvancesBalance = 0.0;
refundableAdvancesList = [];
isAdvancesLoading = false;
isSubmittingWithdrawal = false;
_ibanError = null;
_withdrawDescriptionError = null;
if (!silent) notifyListeners();
}
setWalletRechargeAmount(num amount) {
walletRechargeAmount = amount;
notifyListeners();
@ -277,12 +459,17 @@ class HabibWalletViewModel extends ChangeNotifier {
Function(dynamic)? onSuccess,
Function(String)? onError}) async {
final result = await habibWalletRepo.HISCreateAdvancePayment(
paymentMethodName: paymentMethodName, paidAmount: paidAmount, paymentReference: paymentReference, patientID: patientID, projectID: projectID, depositorName: depositorName);
paymentMethodName: paymentMethodName,
paidAmount: paidAmount,
paymentReference: paymentReference,
patientID: patientID,
projectID: projectID,
depositorName: depositorName);
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
if(onError != null) {
if (onError != null) {
onError(failure.message);
}
},
@ -299,7 +486,8 @@ class HabibWalletViewModel extends ChangeNotifier {
);
}
Future<void> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Future<void> addAdvanceNumberRequest(
{required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference);
result.fold(
@ -347,7 +535,8 @@ class HabibWalletViewModel extends ChangeNotifier {
lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners();
final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
final result = await habibWalletRepo.getLakumAccountInformation(
identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),

@ -18,6 +18,7 @@ class GetInvoicesListResponseModel {
double? decimalDoctorRate;
String? doctorImageUrl;
int? doctorRate;
bool? isAllowRefund;
int? patientNumber;
String? projectName;
@ -39,6 +40,7 @@ class GetInvoicesListResponseModel {
this.decimalDoctorRate,
this.doctorImageUrl,
this.doctorRate,
this.isAllowRefund,
this.patientNumber,
this.projectName,
});
@ -48,46 +50,48 @@ class GetInvoicesListResponseModel {
String toRawJson() => json.encode(toJson());
factory GetInvoicesListResponseModel.fromJson(Map<String, dynamic> json) => GetInvoicesListResponseModel(
setupId: json["SetupId"],
projectId: json["ProjectID"],
patientId: json["PatientID"],
appointmentNo: json["AppointmentNo"],
appointmentDate: json["AppointmentDate"],
appointmentDateN: json["AppointmentDateN"],
clinicId: json["ClinicID"],
doctorId: json["DoctorID"],
invoiceNo: json["InvoiceNo"],
status: json["Status"],
arrivedOn: json["ArrivedOn"],
doctorName: json["DoctorName"],
doctorNameN: json["DoctorNameN"],
clinicName: json["ClinicName"],
decimalDoctorRate: json["DecimalDoctorRate"]?.toDouble(),
doctorImageUrl: json["DoctorImageURL"],
doctorRate: json["DoctorRate"],
patientNumber: json["PatientNumber"],
projectName: json["ProjectName"],
);
setupId: json["SetupId"],
projectId: json["ProjectID"],
patientId: json["PatientID"],
appointmentNo: json["AppointmentNo"],
appointmentDate: json["AppointmentDate"],
appointmentDateN: json["AppointmentDateN"],
clinicId: json["ClinicID"],
doctorId: json["DoctorID"],
invoiceNo: json["InvoiceNo"],
status: json["Status"],
arrivedOn: json["ArrivedOn"],
doctorName: json["DoctorName"],
doctorNameN: json["DoctorNameN"],
clinicName: json["ClinicName"],
decimalDoctorRate: json["DecimalDoctorRate"]?.toDouble(),
doctorImageUrl: json["DoctorImageURL"],
doctorRate: json["DoctorRate"],
isAllowRefund: json["IsAllowRefund"],
patientNumber: json["PatientNumber"],
projectName: json["ProjectName"],
);
Map<String, dynamic> toJson() => {
"SetupId": setupId,
"ProjectID": projectId,
"PatientID": patientId,
"AppointmentNo": appointmentNo,
"AppointmentDate": appointmentDate,
"AppointmentDateN": appointmentDateN,
"ClinicID": clinicId,
"DoctorID": doctorId,
"InvoiceNo": invoiceNo,
"Status": status,
"ArrivedOn": arrivedOn,
"DoctorName": doctorName,
"DoctorNameN": doctorNameN,
"ClinicName": clinicName,
"DecimalDoctorRate": decimalDoctorRate,
"DoctorImageURL": doctorImageUrl,
"DoctorRate": doctorRate,
"PatientNumber": patientNumber,
"ProjectName": projectName,
};
"SetupId": setupId,
"ProjectID": projectId,
"PatientID": patientId,
"AppointmentNo": appointmentNo,
"AppointmentDate": appointmentDate,
"AppointmentDateN": appointmentDateN,
"ClinicID": clinicId,
"DoctorID": doctorId,
"InvoiceNo": invoiceNo,
"Status": status,
"ArrivedOn": arrivedOn,
"DoctorName": doctorName,
"DoctorNameN": doctorNameN,
"ClinicName": clinicName,
"DecimalDoctorRate": decimalDoctorRate,
"DoctorImageURL": doctorImageUrl,
"DoctorRate": doctorRate,
"IsAllowRefund": isAllowRefund,
"PatientNumber": patientNumber,
"ProjectName": projectName,
};
}

@ -11,7 +11,6 @@ import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/paytabs_config.dart';
import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class PayTabsViewModel extends ChangeNotifier {
late PaymentSdkConfigurationDetails paymentConfiguration;
@ -33,8 +32,9 @@ class PayTabsViewModel extends ChangeNotifier {
cartDescription: paymentDescription,
merchantName: PaymentSdkDefaultConfig.defaultMerchantName,
locale: appState.isArabic() ? PaymentSdkLocale.AR : PaymentSdkLocale.EN,
merchantApplePayIndentifier:
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? PaymentSdkDefaultConfig.defaultMerchantAppleBundleIDUAT : PaymentSdkDefaultConfig.defaultMerchantAppleBundleID,
merchantApplePayIndentifier: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat
? PaymentSdkDefaultConfig.defaultMerchantAppleBundleIDUAT
: PaymentSdkDefaultConfig.defaultMerchantAppleBundleID,
// merchantApplePayIndentifier: PaymentSdkDefaultConfig.defaultMerchantAppleBundleIDUAT,
screentTitle: paymentDescription,
amount: amount.toDouble(),
@ -43,10 +43,23 @@ class PayTabsViewModel extends ChangeNotifier {
currencyCode: PaymentSdkDefaultConfig.defaultCurrency,
merchantCountryCode: PaymentSdkDefaultConfig.defaultMerchantCountryCode,
billingDetails: BillingDetails(
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"),
shippingDetails: ShippingDetails("${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000",
"Riyadh",
"SA",
"Riyadh",
"Riyadh",
"12626"),
shippingDetails: ShippingDetails(
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000",
"Riyadh",
"SA",
"Riyadh",
"Riyadh",
"12626"),
alternativePaymentMethods: [],
linkBillingNameWithCardHolderName: true,
simplifyApplePayValidation: true);
@ -89,9 +102,11 @@ class PayTabsViewModel extends ChangeNotifier {
onSuccess!(paytabsTransactionResponseModel);
} else {
// Transaction was processed but failed
final reason = transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error";
final reason =
transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error";
final responseCode = transactionDetails?["responseCode"] ?? "";
final errorMessage = responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason";
final errorMessage =
responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason";
onError!(errorMessage);
}
} else if (event["status"] == "error") {
@ -117,9 +132,11 @@ class PayTabsViewModel extends ChangeNotifier {
onSuccess!(paytabsTransactionResponseModel);
} else {
// Transaction was processed but failed
final reason = transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error";
final reason =
transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error";
final responseCode = transactionDetails?["responseCode"] ?? "";
final errorMessage = responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason";
final errorMessage =
responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason";
onError!(errorMessage);
}
} else if (event["status"] == "error") {

@ -0,0 +1,142 @@
class SubmitRefundProcedureItem {
final int lineItemNo;
final String procedureId;
final String procedureName;
final double patientPaidAmount;
final String refundReason;
const SubmitRefundProcedureItem({
required this.lineItemNo,
required this.procedureId,
required this.procedureName,
required this.patientPaidAmount,
required this.refundReason,
});
Map<String, dynamic> toJson() => {
"LineItemNo": lineItemNo,
"ProcedureID": procedureId,
"ProcedureName": procedureName,
"PatientPaidAmount": patientPaidAmount,
"RefundReason": refundReason,
};
@override
String toString() {
return 'SubmitRefundProcedureItem{lineItemNo: $lineItemNo, procedureId: $procedureId, procedureName: $procedureName, patientPaidAmount: $patientPaidAmount, refundReason: $refundReason}';
}
}
/// Represents a single advance item in the wallet withdrawal refund payload.
class SubmitRefundAdvanceItem {
final String advanceNo;
final double paidAmount;
final double balanceAmount;
final String advanceDate;
const SubmitRefundAdvanceItem({
required this.advanceNo,
required this.paidAmount,
required this.balanceAmount,
required this.advanceDate,
});
Map<String, dynamic> toJson() => {
"AdvanceNo": advanceNo,
"PaidAmount": paidAmount,
"BalanceAmount": balanceAmount,
"AdvanceDate": advanceDate,
};
@override
String toString() {
return 'SubmitRefundAdvanceItem{advanceNo: $advanceNo, paidAmount: $paidAmount, balanceAmount: $balanceAmount, advanceDate: $advanceDate}';
}
}
class SubmitRefundRequestModel {
final int projectId;
final String refundType;
final String refundProject;
final int refundProjectId;
final String refundPatientId;
final String patientMobileNumber;
final String refundPatientName;
final String refundNationalId;
final String refundInvoiceNo;
final String refundAppointmentNo;
final int refundDoctorId;
final String refundDoctorName;
final int refundClinicId;
final String refundClinicName;
final String refundDestination;
final String refundIban;
final String refundRemarks;
final double refundTotalAmount;
final List<SubmitRefundProcedureItem> refundProceduresList;
/// Populated only for wallet withdrawal refunds (refundType == '2').
final List<SubmitRefundAdvanceItem> refundAdvancesList;
const SubmitRefundRequestModel({
required this.projectId,
required this.refundType,
required this.refundProject,
required this.refundProjectId,
required this.refundPatientId,
required this.patientMobileNumber,
required this.refundPatientName,
required this.refundNationalId,
required this.refundInvoiceNo,
required this.refundAppointmentNo,
required this.refundDoctorId,
required this.refundDoctorName,
required this.refundClinicId,
required this.refundClinicName,
required this.refundDestination,
required this.refundIban,
required this.refundRemarks,
required this.refundTotalAmount,
required this.refundProceduresList,
this.refundAdvancesList = const [],
});
/// refundType '1' = invoice refund sends Refund_ProceduresList
/// refundType '2' = wallet withdrawal sends Refund_AdvancesList
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = {
"ProjectID": projectId,
"Refund_RefundType": refundType,
"Refund_Project": refundProject,
"Refund_ProjectId": refundProjectId,
"Refund_PatientId": refundPatientId,
"PatientMobileNumber": patientMobileNumber,
"Refund_PatientName": refundPatientName,
"Refund_NationalId": refundNationalId,
"Refund_DoctorId": refundDoctorId,
"Refund_DoctorName": refundDoctorName,
"Refund_RefundDestination": refundDestination,
"Refund_IBAN": refundIban,
"Refund_Remarks": refundRemarks,
"Refund_TotalRefundAmount": refundTotalAmount,
};
if (refundType == '2') {
// Wallet withdrawal include advances list, omit invoice-specific fields
json["Refund_AdvancesList"] = refundAdvancesList.map((a) => a.toJson()).toList();
} else {
// Invoice refund include invoice-specific fields and procedures list
json["Refund_InvoiceNo"] = refundInvoiceNo;
json["Refund_AppointmentNo"] = refundAppointmentNo;
json["Refund_ClinicId"] = refundClinicId;
json["Refund_ClinicName"] = refundClinicName;
json["Refund_ProceduresList"] = refundProceduresList.map((p) => p.toJson()).toList();
}
return json;
}
@override
String toString() {
return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundAdvancesList: $refundAdvancesList, refundProceduresList: $refundProceduresList}';
}
}

@ -0,0 +1,23 @@
class IsRefundableInvoiceObject {
final String? errorCode;
final bool? isRefundable;
final String? message;
final int? statusCode;
IsRefundableInvoiceObject({
this.errorCode,
this.isRefundable,
this.message,
this.statusCode,
});
factory IsRefundableInvoiceObject.fromJson(Map<String, dynamic> json) {
return IsRefundableInvoiceObject(
errorCode: json['ErrorCode'] as String?,
isRefundable: json['IsRefundable'] as bool?,
message: json['Message'] as String?,
statusCode: json['StatusCode'] as int?,
);
}
}

@ -0,0 +1,312 @@
class SubmitRefundResponseModel {
final String? message;
final String? refundType;
final String? patientId;
final String? refNo;
SubmitRefundResponseModel({
this.message,
this.refundType,
this.patientId,
this.refNo,
});
factory SubmitRefundResponseModel.fromJson(Map<String, dynamic> json) {
return SubmitRefundResponseModel(
message: (json['Message'] ?? json['message']) as String?,
refundType: (json['Refund_RefundTypeDisplay'] ?? json['refundType'])?.toString(),
patientId: (json['Refund_PatientIdDisplay'] ?? json['patientId'])?.toString(),
refNo: (json['Refund_RefNo'] ?? json['ReferenceNumber'] ?? json['refNo'])?.toString(),
);
}
}
class RefundStatusModel {
final String? errorMessage;
final String? lastUpdated;
final bool? pendingVerification;
final String? referenceNumber;
final String? requestStatusName;
final String? status;
final bool? verified;
RefundStatusModel({
this.errorMessage,
this.lastUpdated,
this.pendingVerification,
this.referenceNumber,
this.requestStatusName,
this.status,
this.verified,
});
factory RefundStatusModel.fromJson(Map<String, dynamic> json) {
return RefundStatusModel(
errorMessage: json['ErrorMessage'] as String?,
lastUpdated: json['LastUpdated'] as String?,
pendingVerification: json['PendingVerification'] as bool?,
referenceNumber: json['ReferenceNumber'] as String?,
requestStatusName: json['RequestStatusName'] as String?,
status: json['Status'] as String?,
verified: json['Verified'] as bool?,
);
}
}
class WalletAdvanceItem {
final String? advanceDate;
final String? advanceNo;
final double? balanceAmount;
final double? paidAmount;
WalletAdvanceItem({
this.advanceDate,
this.advanceNo,
this.balanceAmount,
this.paidAmount,
});
factory WalletAdvanceItem.fromJson(Map<String, dynamic> json) {
return WalletAdvanceItem(
advanceDate: json['AdvanceDate'] as String?,
advanceNo: json['AdvanceNo'] as String?,
balanceAmount: (json['BalanceAmount'] as num?)?.toDouble(),
paidAmount: (json['PaidAmount'] as num?)?.toDouble(),
);
}
}
class WalletRequestPayload {
final List<WalletAdvanceItem>? advancesList;
final int? doctorId;
final String? doctorName;
final String? iban;
final String? nationalId;
final String? patientId;
final String? patientName;
final String? project;
final int? projectId;
final String? refundAmountTypeName;
final String? refundDestination;
final String? refundDestinationName;
final String? refundType;
final String? refundTypeName;
final String? remarks;
final double? totalRefundAmount;
WalletRequestPayload({
this.advancesList,
this.doctorId,
this.doctorName,
this.iban,
this.nationalId,
this.patientId,
this.patientName,
this.project,
this.projectId,
this.refundAmountTypeName,
this.refundDestination,
this.refundDestinationName,
this.refundType,
this.refundTypeName,
this.remarks,
this.totalRefundAmount,
});
factory WalletRequestPayload.fromJson(Map<String, dynamic> json) {
return WalletRequestPayload(
advancesList: (json['AdvancesList'] as List<dynamic>?)?.map((e) => WalletAdvanceItem.fromJson(e as Map<String, dynamic>)).toList(),
doctorId: (json['DoctorId'] as num?)?.toInt(),
doctorName: json['DoctorName'] as String?,
iban: json['IBAN'] as String?,
nationalId: json['NationalId'] as String?,
patientId: json['PatientId'] as String?,
patientName: json['PatientName'] as String?,
project: json['Project'] as String?,
projectId: (json['ProjectId'] as num?)?.toInt(),
refundAmountTypeName: json['RefundAmountTypeName'] as String?,
refundDestination: json['RefundDestination'] as String?,
refundDestinationName: json['RefundDestinationName'] as String?,
refundType: json['RefundType'] as String?,
refundTypeName: json['RefundTypeName'] as String?,
remarks: json['Remarks'] as String?,
totalRefundAmount: (json['TotalRefundAmount'] as num?)?.toDouble(),
);
}
}
class InvoiceRefundProcedure {
final bool? isLocked;
final int? lineItemNo;
final double? patientPaidAmount;
final String? procedureId;
final String? procedureName;
final String? refundReason;
InvoiceRefundProcedure({
this.isLocked,
this.lineItemNo,
this.patientPaidAmount,
this.procedureId,
this.procedureName,
this.refundReason,
});
factory InvoiceRefundProcedure.fromJson(Map<String, dynamic> json) {
return InvoiceRefundProcedure(
isLocked: json['IsLocked'] as bool?,
lineItemNo: (json['LineItemNo'] as num?)?.toInt(),
patientPaidAmount: (json['PatientPaidAmount'] as num?)?.toDouble(),
procedureId: json['ProcedureID'] as String?,
procedureName: json['ProcedureName'] as String?,
refundReason: json['RefundReason'] as String?,
);
}
}
class InvoiceRequestPayload {
final List<WalletAdvanceItem>? advancesList;
final String? appointmentNo;
final int? clinicId;
final String? clinicName;
final int? doctorId;
final String? doctorName;
final String? iban;
final String? invoiceDate;
final String? invoiceNo;
final bool? isFastTrack;
final String? nationalId;
final String? patientId;
final String? patientName;
final List<InvoiceRefundProcedure>? proceduresList;
final String? project;
final int? projectId;
final String? refundAmountType;
final String? refundAmountTypeName;
final String? refundDestination;
final String? refundDestinationName;
final double? refundTaxAmount;
final String? refundType;
final String? refundTypeName;
final String? remarks;
final double? totalRefundAmount;
InvoiceRequestPayload({
this.advancesList,
this.appointmentNo,
this.clinicId,
this.clinicName,
this.doctorId,
this.doctorName,
this.iban,
this.invoiceDate,
this.invoiceNo,
this.isFastTrack,
this.nationalId,
this.patientId,
this.patientName,
this.proceduresList,
this.project,
this.projectId,
this.refundAmountType,
this.refundAmountTypeName,
this.refundDestination,
this.refundDestinationName,
this.refundTaxAmount,
this.refundType,
this.refundTypeName,
this.remarks,
this.totalRefundAmount,
});
factory InvoiceRequestPayload.fromJson(Map<String, dynamic> json) {
return InvoiceRequestPayload(
advancesList: (json['AdvancesList'] as List<dynamic>?)?.map((e) => WalletAdvanceItem.fromJson(e as Map<String, dynamic>)).toList(),
appointmentNo: json['AppointmentNo']?.toString(),
clinicId: (json['ClinicId'] as num?)?.toInt(),
clinicName: json['ClinicName'] as String?,
doctorId: (json['DoctorId'] as num?)?.toInt(),
doctorName: json['DoctorName'] as String?,
iban: json['IBAN'] as String?,
invoiceDate: json['InvoiceDate'] as String?,
invoiceNo: json['InvoiceNo'] as String?,
isFastTrack: json['IsFastTrack'] as bool?,
nationalId: json['NationalId'] as String?,
patientId: json['PatientId'] as String?,
patientName: json['PatientName'] as String?,
proceduresList: (json['ProceduresList'] as List<dynamic>?)?.map((e) => InvoiceRefundProcedure.fromJson(e as Map<String, dynamic>)).toList(),
project: json['Project'] as String?,
projectId: (json['ProjectId'] as num?)?.toInt(),
refundAmountType: json['RefundAmountType'] as String?,
refundAmountTypeName: json['RefundAmountTypeName'] as String?,
refundDestination: json['RefundDestination'] as String?,
refundDestinationName: json['RefundDestinationName'] as String?,
refundTaxAmount: (json['RefundTaxAmount'] as num?)?.toDouble(),
refundType: json['RefundType'] as String?,
refundTypeName: json['RefundTypeName'] as String?,
remarks: json['Remarks'] as String?,
totalRefundAmount: (json['TotalRefundAmount'] as num?)?.toDouble(),
);
}
}
class RefundInvoiceModel {
final String? invoiceNumber;
final String? lastUpdated;
final String? patientId;
final bool? pendingVerification;
final String? referenceNumber;
final String? refundDestination;
final int? refundDestinationId;
final String? refundType;
final int? refundTypeId;
final String? requestStatusName;
final int? requestStatusId;
final String? verificationInitiatedAt;
final bool? verified;
final String? errorMessage;
final WalletRequestPayload? walletRequestPayload;
final InvoiceRequestPayload? invoiceRequestPayload;
RefundInvoiceModel({
this.invoiceNumber,
this.lastUpdated,
this.patientId,
this.pendingVerification,
this.referenceNumber,
this.refundDestination,
this.refundDestinationId,
this.refundType,
this.refundTypeId,
this.requestStatusName,
this.requestStatusId,
this.verificationInitiatedAt,
this.verified,
this.errorMessage,
this.walletRequestPayload,
this.invoiceRequestPayload,
});
factory RefundInvoiceModel.fromJson(Map<String, dynamic> json) {
return RefundInvoiceModel(
invoiceNumber: json['InvoiceNumber'] as String?,
lastUpdated: json['LastUpdated'] as String?,
patientId: json['PatientID'] as String?,
pendingVerification: json['PendingVerification'] as bool?,
referenceNumber: json['ReferenceNumber'] as String?,
refundDestination: json['RefundDestination'] as String?,
refundDestinationId: json['RefundDestinationId'] as int?,
refundType: json['RefundType'] as String?,
refundTypeId: json['RefundTypeId'] as int?,
requestStatusName: json['StatusName'] as String?,
requestStatusId: json['StatusId'] as int?,
verificationInitiatedAt: json['VerificationInitiatedAt'] as String?,
verified: json['Verified'] as bool?,
errorMessage: json['ErrorMessage'] as String?,
walletRequestPayload:
json['WalletRequestPayload'] != null ? WalletRequestPayload.fromJson(json['WalletRequestPayload'] as Map<String, dynamic>) : null,
invoiceRequestPayload:
json['InvoiceRequestPayload'] != null ? InvoiceRequestPayload.fromJson(json['InvoiceRequestPayload'] as Map<String, dynamic>) : null,
);
}
}

@ -0,0 +1,199 @@
import 'dart:convert';
class RefundableInvoicesResponseModel {
final String? errorCode;
final String? message;
final List<RefundableInvoiceItem>? refundableInvoiceItems;
final int? statusCode;
RefundableInvoicesResponseModel({
this.errorCode,
this.message,
this.refundableInvoiceItems,
this.statusCode,
});
factory RefundableInvoicesResponseModel.fromJson(Map<String, dynamic> json) {
return RefundableInvoicesResponseModel(
errorCode: json['ErrorCode'] as String?,
message: json['Message'] as String?,
refundableInvoiceItems: json['RefundableInvoiceItems'] != null
? (json['RefundableInvoiceItems'] as List).map((item) => RefundableInvoiceItem.fromJson(item as Map<String, dynamic>)).toList()
: null,
statusCode: json['StatusCode'] as int?,
);
}
}
class RefundableInvoiceItem {
final int? appointmentNo;
final int? clinicID;
final String? clinicName;
final int? doctorID;
final String? doctorName;
final String? invoiceDate;
final int? invoiceNo;
final String? nationalID;
final int? patientID;
final int? projectID;
RefundableInvoiceItem({
this.appointmentNo,
this.clinicID,
this.clinicName,
this.doctorID,
this.doctorName,
this.invoiceDate,
this.invoiceNo,
this.nationalID,
this.patientID,
this.projectID,
});
factory RefundableInvoiceItem.fromRawJson(String str) => RefundableInvoiceItem.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory RefundableInvoiceItem.fromJson(Map<String, dynamic> json) {
return RefundableInvoiceItem(
appointmentNo: json['AppointmentNo'] as int?,
clinicID: json['ClinicID'] as int?,
clinicName: json['ClinicName'] as String?,
doctorID: json['DoctorID'] as int?,
doctorName: json['DoctorName'] as String?,
invoiceDate: json['InvoiceDate'] as String?,
invoiceNo: json['InvoiceNo'] as int?,
nationalID: json['NationalID'] as String?,
patientID: json['PatientID'] as int?,
projectID: json['ProjectID'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'AppointmentNo': appointmentNo,
'ClinicID': clinicID,
'ClinicName': clinicName,
'DoctorID': doctorID,
'DoctorName': doctorName,
'InvoiceDate': invoiceDate,
'InvoiceNo': invoiceNo,
'NationalID': nationalID,
'PatientID': patientID,
'ProjectID': projectID,
};
}
}
class RefundableInvoiceDetailsItem {
final int? lineItemNo;
final double? patientPaidAmount;
final String? procedureId;
final String? procedureName;
RefundableInvoiceDetailsItem({
this.lineItemNo,
this.patientPaidAmount,
this.procedureId,
this.procedureName,
});
factory RefundableInvoiceDetailsItem.fromJson(Map<String, dynamic> json) {
return RefundableInvoiceDetailsItem(
lineItemNo: json['LineItemNo'] as int?,
patientPaidAmount: (json['PatientPaidAmount'] as num?)?.toDouble(),
procedureId: json['ProcedureID'] as String?,
procedureName: json['ProcedureName'] as String?,
);
}
}
class RefundableInvoicesDetailsObject {
final String? errorCode;
final int? invoiceNo;
final String? message;
final int? projectId;
final List<RefundableInvoiceDetailsItem> refundableInvoiceDetailsItems;
final int? statusCode;
RefundableInvoicesDetailsObject({
this.errorCode,
this.invoiceNo,
this.message,
this.projectId,
this.refundableInvoiceDetailsItems = const [],
this.statusCode,
});
factory RefundableInvoicesDetailsObject.fromJson(Map<String, dynamic> json) {
return RefundableInvoicesDetailsObject(
errorCode: json['ErrorCode'] as String?,
invoiceNo: json['InvoiceNo'] as int?,
message: json['Message'] as String?,
projectId: json['ProjectID'] as int?,
refundableInvoiceDetailsItems: json['RefundableInvoiceDetailsItems'] != null
? (json['RefundableInvoiceDetailsItems'] as List)
.map((item) => RefundableInvoiceDetailsItem.fromJson(item as Map<String, dynamic>))
.toList()
: [],
statusCode: json['StatusCode'] as int?,
);
}
}
class RefundableAdvancesItem {
final String? advanceDate;
final int? advanceNo;
final double? balanceAmount;
final double? paidAmount;
final int? patientId;
final String? patientName;
final int? projectId;
RefundableAdvancesItem({
this.advanceDate,
this.advanceNo,
this.balanceAmount,
this.paidAmount,
this.patientId,
this.patientName,
this.projectId,
});
factory RefundableAdvancesItem.fromJson(Map<String, dynamic> json) {
return RefundableAdvancesItem(
advanceDate: json['AdvanceDate'] as String?,
advanceNo: json['AdvanceNo'] as int?,
balanceAmount: (json['BalanceAmount'] as num?)?.toDouble(),
paidAmount: (json['PaidAmount'] as num?)?.toDouble(),
patientId: json['PatientID'] as int?,
patientName: json['PatientName'] as String?,
projectId: json['ProjectID'] as int?,
);
}
}
class RefundableAdvancesObject {
final String? errorCode;
final String? message;
final List<RefundableAdvancesItem> refundableAdvancesItems;
final int? statusCode;
RefundableAdvancesObject({
this.errorCode,
this.message,
this.refundableAdvancesItems = const [],
this.statusCode,
});
factory RefundableAdvancesObject.fromJson(Map<String, dynamic> json) {
return RefundableAdvancesObject(
errorCode: json['ErrorCode'] as String?,
message: json['Message'] as String?,
refundableAdvancesItems: json['RefundableAdvancesItems'] != null
? (json['RefundableAdvancesItems'] as List).map((item) => RefundableAdvancesItem.fromJson(item as Map<String, dynamic>)).toList()
: [],
statusCode: json['StatusCode'] as int?,
);
}
}

@ -0,0 +1,360 @@
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/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class RefundRequestRepo {
Future<Either<Failure, GenericApiModel<List<RefundableInvoiceItem>>>> getRefundableInvoices({required int projectID});
Future<Either<Failure, GenericApiModel<RefundableInvoicesDetailsObject>>> getRefundableInvoicesDetails({
required int invoiceNo,
required int projectId,
});
Future<Either<Failure, GenericApiModel<RefundableAdvancesObject>>> getRefundableAdvances();
Future<Either<Failure, GenericApiModel<RefundStatusModel>>> getRefundStatus({required String referenceNumber});
Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({
required int refundType,
required String patientId,
String invoiceNumberQuery,
});
Future<Either<Failure, GenericApiModel<SubmitRefundResponseModel>>> submitRefund({required SubmitRefundRequestModel request});
Future<Either<Failure, GenericApiModel<IsRefundableInvoiceObject>>> checkIsRefundableInvoice({
required int invoiceNo,
required int projectId,
});
}
class RefundRequestRepoImp implements RefundRequestRepo {
final ApiClient apiClient;
final LoggerService loggerService;
RefundRequestRepoImp({required this.apiClient, required this.loggerService});
@override
Future<Either<Failure, GenericApiModel<List<RefundableInvoiceItem>>>> getRefundableInvoices({required int projectID}) async {
Map<String, dynamic> body = {"ProjectID": projectID};
try {
GenericApiModel<List<RefundableInvoiceItem>>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRefundableInvoices,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final refundableInvoicesObject = response['RefundableInvoicesObject'];
if (refundableInvoicesObject != null) {
final invoiceItemsJson = refundableInvoicesObject['RefundableInvoiceItems'];
final List<RefundableInvoiceItem> invoicesList = invoiceItemsJson != null
? (invoiceItemsJson as List).map((item) => RefundableInvoiceItem.fromJson(item as Map<String, dynamic>)).toList()
: [];
apiResponse = GenericApiModel<List<RefundableInvoiceItem>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: invoicesList,
);
} else {
failure = DataParsingFailure("RefundableInvoicesObject is null");
}
} catch (e) {
loggerService.logError("getRefundableInvoices parsing 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) {
loggerService.logError("getRefundableInvoices exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RefundableInvoicesDetailsObject>>> getRefundableInvoicesDetails({
required int invoiceNo,
required int projectId,
}) async {
final Map<String, dynamic> body = {
"InvoiceNo": invoiceNo,
"ProjectID": projectId,
};
try {
GenericApiModel<RefundableInvoicesDetailsObject>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRefundableInvoicesDetails,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final detailsJson = response['RefundableInvoicesDetailsObject'];
final RefundableInvoicesDetailsObject details = detailsJson != null
? RefundableInvoicesDetailsObject.fromJson(detailsJson as Map<String, dynamic>)
: RefundableInvoicesDetailsObject();
apiResponse = GenericApiModel<RefundableInvoicesDetailsObject>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: details,
);
} catch (e) {
loggerService.logError("getRefundableInvoicesDetails parsing 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) {
loggerService.logError("getRefundableInvoicesDetails exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RefundableAdvancesObject>>> getRefundableAdvances() async {
try {
GenericApiModel<RefundableAdvancesObject>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRefundableAdvances,
body: {},
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final advancesJson = response['RefundableAdvancesObject'];
final RefundableAdvancesObject advances =
advancesJson != null ? RefundableAdvancesObject.fromJson(advancesJson as Map<String, dynamic>) : RefundableAdvancesObject();
apiResponse = GenericApiModel<RefundableAdvancesObject>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: advances,
);
} catch (e) {
loggerService.logError("getRefundableAdvances parsing 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) {
loggerService.logError("getRefundableAdvances exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RefundStatusModel>>> getRefundStatus({required String referenceNumber}) async {
final Map<String, dynamic> body = {
"Refund_ReferenceNumber": referenceNumber,
};
try {
GenericApiModel<RefundStatusModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRefundStatus,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final dataJson = response['Refund_StatusItem'];
final RefundStatusModel model = dataJson != null ? RefundStatusModel.fromJson(dataJson as Map<String, dynamic>) : RefundStatusModel();
apiResponse = GenericApiModel<RefundStatusModel>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: model,
);
} catch (e) {
loggerService.logError("getRefundStatus parsing 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) {
loggerService.logError("getRefundStatus exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({
required int refundType,
required String patientId,
String invoiceNumberQuery = "",
}) async {
final Map<String, dynamic> body = {
"Refund_RefundType": refundType,
"Refund_PatientId": patientId,
"Refund_InvoiceNumberQuery": invoiceNumberQuery,
};
try {
GenericApiModel<List<RefundInvoiceModel>>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRefundInvoices,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final listJson = response['Refund_InvoicesList'];
final List<RefundInvoiceModel> invoices =
listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map<String, dynamic>)).toList() : [];
apiResponse = GenericApiModel<List<RefundInvoiceModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: invoices,
);
} catch (e) {
loggerService.logError("getRefundInvoices parsing 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) {
loggerService.logError("getRefundInvoices exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<SubmitRefundResponseModel>>> submitRefund({required SubmitRefundRequestModel request}) async {
try {
GenericApiModel<SubmitRefundResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.submitRefund,
body: request.toJson(),
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final model = SubmitRefundResponseModel.fromJson(response as Map<String, dynamic>);
apiResponse = GenericApiModel<SubmitRefundResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: model,
);
} catch (e) {
loggerService.logError("submitRefund parsing 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) {
loggerService.logError("submitRefund exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<IsRefundableInvoiceObject>>> checkIsRefundableInvoice({
required int invoiceNo,
required int projectId,
}) async {
final Map<String, dynamic> body = {
"InvoiceNo": invoiceNo.toString(),
"ProjectID": projectId,
};
try {
GenericApiModel<IsRefundableInvoiceObject>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.isRefundableInvoice,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final dataJson = response['IsRefundableInvoiceObject'];
final IsRefundableInvoiceObject model =
dataJson != null ? IsRefundableInvoiceObject.fromJson(dataJson as Map<String, dynamic>) : IsRefundableInvoiceObject();
apiResponse = GenericApiModel<IsRefundableInvoiceObject>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: model,
);
} catch (e) {
loggerService.logError("checkIsRefundableInvoice parsing 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) {
loggerService.logError("checkIsRefundableInvoice exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
}

@ -0,0 +1,569 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
enum RefundTabType { invoices, hmgWallet }
extension RefundTabTypeExtension on RefundTabType {
String toDisplayString() {
switch (this) {
case RefundTabType.hmgWallet:
return LocaleKeys.hmgWallet.tr();
case RefundTabType.invoices:
return LocaleKeys.bankTransfer.tr();
}
}
}
/// Lightweight model representing a refundable procedure line item.
class RefundableProcedure {
final int procedureId;
final String procedureName;
final String? procedureNameN;
final double patientPaidAmount;
bool isSelected;
String? refundReason;
/// Raw values from the API used when building the SubmitRefund payload
final int? apiLineItemNo;
final String? apiProcedureId;
RefundableProcedure({
required this.procedureId,
required this.procedureName,
this.procedureNameN,
required this.patientPaidAmount,
this.isSelected = false,
this.refundReason,
this.apiLineItemNo,
this.apiProcedureId,
});
}
/// Lightweight model for an invoice dropdown item.
class RefundInvoiceItem {
final int invoiceNo;
final int appointmentNo;
final int projectId;
final String displayLabel;
const RefundInvoiceItem({
required this.invoiceNo,
required this.appointmentNo,
required this.projectId,
required this.displayLabel,
});
}
class RefundRequestViewModel extends ChangeNotifier {
final AppState appState;
final RefundRequestRepo refundRequestRepo;
final ErrorHandlerService errorHandlerService;
RefundRequestViewModel({
required this.appState,
required this.refundRequestRepo,
required this.errorHandlerService,
});
// Tab
RefundTabType selectedTab = RefundTabType.invoices;
void setTab(RefundTabType tab) {
selectedTab = tab;
notifyListeners();
}
// Step (0 = select invoice+hospital, 1 = select procedures)
int currentStep = 0;
void goToStep(int step) {
currentStep = step;
notifyListeners();
}
void nextStep() {
if (currentStep < 1) {
currentStep++;
notifyListeners();
}
}
void previousStep() {
if (currentStep > 0) {
currentStep--;
notifyListeners();
}
}
void resetState() {
currentStep = 0;
procedures.clear();
selectedPaymentMethod = null;
selectedRefundReason = null;
isProceduresLoading = false;
isInvoiceDetailsLoading = false;
invoiceDetails = null;
_ibanError = null;
_descriptionError = null;
// Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose)
Future.microtask(() {
if (hasListeners) notifyListeners();
});
}
// Hospital selection
HospitalsModel? selectedHospital;
void setSelectedHospital(HospitalsModel? hospital) {
selectedHospital = hospital;
// Reset invoice and procedures whenever hospital changes
selectedInvoice = null;
procedures.clear();
refundableInvoicesList.clear();
notifyListeners();
// Fetch refundable invoices for the selected hospital's project
if (hospital != null && hospital.iD != null) {
final projectID = hospital.iD is int ? hospital.iD as int : int.tryParse(hospital.iD.toString());
if (projectID != null) {
getRefundableInvoices(projectID: projectID);
}
}
}
void clearHospital() {
selectedHospital = null;
selectedInvoice = null;
procedures.clear();
notifyListeners();
}
// Invoice selection
RefundInvoiceItem? selectedInvoice;
List<RefundableInvoiceItem> refundableInvoicesList = [];
bool isRefundableInvoicesLoading = false;
Future<void> getRefundableInvoices({
required int projectID,
Function(dynamic)? onSuccess,
Function(String)? onError,
}) async {
isRefundableInvoicesLoading = true;
refundableInvoicesList.clear();
notifyListeners();
final result = await refundRequestRepo.getRefundableInvoices(projectID: projectID);
result.fold(
(failure) {
isRefundableInvoicesLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isRefundableInvoicesLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
refundableInvoicesList = apiResponse.data!;
notifyListeners();
if (onSuccess != null) onSuccess(apiResponse);
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to load invoices");
}
}
},
);
}
void setSelectedInvoice(RefundInvoiceItem invoice) {
selectedInvoice = invoice;
procedures.clear();
notifyListeners();
}
void clearInvoice() {
selectedInvoice = null;
procedures.clear();
notifyListeners();
}
// Procedures
List<RefundableProcedure> procedures = [];
bool isProceduresLoading = false;
void setProcedures(List<RefundableProcedure> list) {
procedures = list;
isProceduresLoading = false;
notifyListeners();
}
void toggleProcedureSelection(RefundableProcedure procedure) {
procedure.isSelected = !procedure.isSelected;
notifyListeners();
}
void setRefundReasonForProcedure(RefundableProcedure procedure, String reason) {
procedure.refundReason = reason;
notifyListeners();
}
bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected);
void toggleSelectAll() {
final selectAll = !isAllSelected;
for (final p in procedures) {
p.isSelected = selectAll;
}
notifyListeners();
}
List<RefundableProcedure> get selectedProcedures => procedures.where((p) => p.isSelected).toList();
bool get allSelectedProceduresHaveReasons =>
selectedProcedures.isNotEmpty && selectedProcedures.every((p) => p.refundReason != null && p.refundReason!.isNotEmpty);
// Calculation
double get totalRefundableAmount {
double total = 0.0;
for (final p in selectedProcedures) {
total += p.patientPaidAmount;
}
return total;
}
// Bank Details & Description
String bankDetails = '';
String description = '';
// Bank Transfer Error Tracking
String? _ibanError;
String? _descriptionError;
String? get ibanError => _ibanError;
String? get descriptionError => _descriptionError;
void clearIbanError() {
_ibanError = null;
notifyListeners();
}
void clearDescriptionError() {
_descriptionError = null;
notifyListeners();
}
bool validateBankTransferForm({required String iban, required String description}) {
bool isValid = true;
_ibanError = null;
_descriptionError = null;
if (iban.trim().isEmpty) {
_ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false;
} else {
if (!ValidationUtils.validateIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false;
}
}
if (description.trim().isEmpty) {
_descriptionError = LocaleKeys.descriptionIsRequired.tr();
isValid = false;
}
notifyListeners();
return isValid;
}
// Payment Method
RefundTabType? selectedPaymentMethod;
void setPaymentMethod(RefundTabType method) {
selectedPaymentMethod = method;
notifyListeners();
}
// Refund Reason
String? selectedRefundReason;
void setRefundReason(String? reason) {
selectedRefundReason = reason;
notifyListeners();
}
/// Used by text controllers in the UI layer to trigger a rebuild.
void triggerRebuild() => notifyListeners();
void setBankDetails(String value) {
bankDetails = value;
notifyListeners();
}
void setDescription(String value) {
description = value;
notifyListeners();
}
// Validation
bool get isStep1Valid => selectedHospital != null && selectedInvoice != null;
bool get isStep2Valid => selectedProcedures.isNotEmpty && selectedPaymentMethod != null;
// Invoice Details
RefundableInvoicesDetailsObject? invoiceDetails;
bool isInvoiceDetailsLoading = false;
Future<void> getRefundableInvoicesDetails({
required int invoiceNo,
required int projectId,
Function(RefundableInvoicesDetailsObject)? onSuccess,
Function(String)? onError,
}) async {
isInvoiceDetailsLoading = true;
isProceduresLoading = true;
invoiceDetails = null;
procedures.clear();
notifyListeners();
final result = await refundRequestRepo.getRefundableInvoicesDetails(invoiceNo: invoiceNo, projectId: projectId);
result.fold(
(failure) {
isInvoiceDetailsLoading = false;
isProceduresLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isInvoiceDetailsLoading = false;
isProceduresLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
invoiceDetails = apiResponse.data;
notifyListeners();
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr());
}
}
},
);
}
// Refundable Advances
RefundableAdvancesObject? refundableAdvances;
bool isRefundableAdvancesLoading = false;
Future<void> getRefundableAdvances({
Function(RefundableAdvancesObject)? onSuccess,
Function(String)? onError,
}) async {
isRefundableAdvancesLoading = true;
refundableAdvances = null;
notifyListeners();
final result = await refundRequestRepo.getRefundableAdvances();
result.fold(
(failure) {
isRefundableAdvancesLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isRefundableAdvancesLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
refundableAdvances = apiResponse.data;
notifyListeners();
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to load refundable advances");
}
}
},
);
}
// Refund Status
RefundStatusModel? refundStatus;
bool isRefundStatusLoading = false;
Future<void> getRefundStatus({
required String referenceNumber,
Function(RefundStatusModel)? onSuccess,
Function(String)? onError,
}) async {
isRefundStatusLoading = true;
refundStatus = null;
notifyListeners();
final result = await refundRequestRepo.getRefundStatus(referenceNumber: referenceNumber);
result.fold(
(failure) {
isRefundStatusLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isRefundStatusLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
refundStatus = apiResponse.data;
notifyListeners();
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to load refund status");
}
}
},
);
}
// Refund Invoices
List<RefundInvoiceModel> refundInvoicesList = [];
bool isRefundInvoicesLoading = false;
Future<void> getRefundInvoices({
required int refundType,
String invoiceNumberQuery = "",
Function(List<RefundInvoiceModel>)? onSuccess,
Function(String)? onError,
}) async {
isRefundInvoicesLoading = true;
refundInvoicesList.clear();
notifyListeners();
final resolvedPatientId = appState.getAuthenticatedUser()?.patientId?.toString() ?? "";
final result = await refundRequestRepo.getRefundInvoices(
refundType: refundType,
patientId: resolvedPatientId,
invoiceNumberQuery: invoiceNumberQuery,
);
result.fold(
(failure) {
isRefundInvoicesLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isRefundInvoicesLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
refundInvoicesList = apiResponse.data!;
notifyListeners();
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to load refund invoices");
}
}
},
);
}
// Submit Refund
bool isSubmitting = false;
Future<void> submitInvoiceRefundRequest({
required SubmitRefundRequestModel request,
Function(SubmitRefundResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isSubmitting = true;
notifyListeners();
final result = await refundRequestRepo.submitRefund(request: request);
result.fold(
(failure) {
isSubmitting = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isSubmitting = false;
notifyListeners();
if (apiResponse.data != null) {
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to submit refund");
}
}
},
);
}
// Check Is Refundable Invoice
Future<void> checkIsRefundableInvoice({
required int invoiceNo,
required int projectId,
Function(IsRefundableInvoiceObject)? onSuccess,
Function(String)? onError,
}) async {
final result = await refundRequestRepo.checkIsRefundableInvoice(invoiceNo: invoiceNo, projectId: projectId);
result.fold(
(failure) {
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
if (apiResponse.data != null) {
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to check refundable status");
}
}
},
);
}
}

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names
abstract class LocaleKeys {
abstract class LocaleKeys {
static const english = 'english';
static const arabic = 'arabic';
static const login = 'login';
@ -1156,6 +1156,7 @@ abstract class LocaleKeys {
static const transportOptions = 'transportOptions';
static const selectHospitalForAdvancePayment = 'selectHospitalForAdvancePayment';
static const recharge = 'recharge';
static const withdraw = 'withdraw';
static const activityLevel = 'activityLevel';
static const selectActivityLevel = 'selectActivityLevel';
static const almostInactive = 'almostInactive';
@ -1838,10 +1839,44 @@ abstract class LocaleKeys {
static const fileSizeExceedsLimit = 'fileSizeExceedsLimit';
static const completedPrescriptionOrder = 'completedPrescriptionOrder';
static const submitEReferral = 'submitEReferral';
static const requestRefund = 'requestRefund';
static const uploadImage = 'uploadImage';
static const selectImage = 'selectImage';
static const selectOptions = 'selectOptions';
static const iban = 'iban';
static const addIbanDetails = 'addIbanDetails';
static const enterDescription = 'enterDescription';
static const ibanIsRequired = 'ibanIsRequired';
static const descriptionIsRequired = 'descriptionIsRequired';
static const invalidIbanFormat = 'invalidIbanFormat';
static const refundHistory = 'refundHistory';
static const noRefundHistoryFound = 'noRefundHistoryFound';
static const selectProcedures = 'selectProcedures';
static const noProceduresAvailableForRefund = 'noProceduresAvailableForRefund';
static const selectAll = 'selectAll';
static const selectRefundMethod = 'selectRefundMethod';
static const hmgWallet = 'hmgWallet';
static const bankTransfer = 'bankTransfer';
static const selectRefundReason = 'selectRefundReason';
static const refundReason = 'refundReason';
static const refundReason1 = 'refundReason1';
static const refundReason2 = 'refundReason2';
static const refundReason3 = 'refundReason3';
static const refundReason4 = 'refundReason4';
static const refundReason5 = 'refundReason5';
static const refundReason6 = 'refundReason6';
static const refundReason7 = 'refundReason7';
static const redeem = 'redeem';
static const points = 'points';
static const transactions = 'transactions';
static const pharmacy = 'pharmacy';
static const visitsOrders = 'visitsOrders';
static const cannotOpenThisFile = 'cannotOpenThisFile';
static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund';
static const refundDetails = 'refundDetails';
static const requestNo = 'requestNo';
static const referenceNo = 'referenceNo';
static const refund = 'refund';
static const advanceNumber = 'advanceNumber';
static const amountOnly = 'amountOnly';
}

@ -10,18 +10,16 @@ import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart';
import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart';
import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart';
import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart';
import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart';
import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
@ -37,27 +35,25 @@ import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_picture/profile_picture_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart';
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.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/translations/custom_asset_loader.dart';
import 'package:hmg_patient_app_new/features/translations/translation_service.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_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/theme/app_theme.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel;
import 'package:provider/provider.dart';
@ -75,7 +71,6 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// flutter3_32 pub run easy_localization:generate -O ./lib/generated -f keys -o locale_keys.g.dart --source-dir ./assets/langs
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
@ -269,10 +264,15 @@ void main() async {
),
ChangeNotifierProvider<WeatherMonitorViewModel>(
create: (_) => getIt.get<WeatherMonitorViewModel>(),
), ChangeNotifierProvider<DateRangCalenderModel>(
),
ChangeNotifierProvider<DateRangCalenderModel>(
create: (_) => getIt.get<DateRangCalenderModel>(),
), ChangeNotifierProvider<PayTabsViewModel>(
),
ChangeNotifierProvider<PayTabsViewModel>(
create: (_) => getIt.get<PayTabsViewModel>(),
),
ChangeNotifierProvider<RefundRequestViewModel>(
create: (_) => getIt.get<RefundRequestViewModel>(),
)
], child: MyApp()),
), // Wrap your app

@ -376,7 +376,7 @@ class _ComprehensiveCheckupPageState extends State<ComprehensiveCheckupPage> {
borderRadius: 24.r,
hasShadow: true,
),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h, bottom: 16.h),
child: CustomButton(
borderWidth: 0,
text: LocaleKeys.next.tr(context: context),

@ -3,17 +3,18 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
@ -42,7 +43,7 @@ class _HabibWalletState extends State<HabibWalletPage> {
@override
Widget build(BuildContext context) {
habibWalletVM = Provider.of<HabibWalletViewModel>(context, listen: false);
AppState _appState = getIt.get<AppState>();
AppState appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
@ -57,11 +58,10 @@ class _HabibWalletState extends State<HabibWalletPage> {
width: double.infinity,
height: 180.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
// color: AppColors.blackBgColor,
color: Color(0xFF2E3039),
borderRadius: 24.r,
hasShadow: true
),
// color: AppColors.blackBgColor,
color: Color(0xFF2E3039),
borderRadius: 24.r,
hasShadow: true),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
@ -74,8 +74,9 @@ class _HabibWalletState extends State<HabibWalletPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}".toText19(isBold: true, color: Colors.white),
"${LocaleKeys.fileno.tr(context: context)}: ${_appState.getAuthenticatedUser()!.patientId!}"
"${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}"
.toText19(isBold: true, color: Colors.white),
"${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId!}"
.toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true),
],
).expanded,
@ -86,13 +87,8 @@ class _HabibWalletState extends State<HabibWalletPage> {
LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white),
SizedBox(height: 4.h),
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2(
habibWalletVM.habibWalletAmount,
textColor: Colors.white,
iconColor: Colors.white,
iconSize: 16,
isExpanded: false,
letterSpacing: -1)
return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount,
textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1)
.toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h);
}),
],
@ -129,6 +125,23 @@ class _HabibWalletState extends State<HabibWalletPage> {
},
),
),
SizedBox(width: 8.w),
Flexible(
child: CustomButton(
height: 40.h,
icon: AppAssets.refundIcon,
iconSize: 24.w,
backgroundColor: AppColors.successColor,
textColor: Colors.white,
text: LocaleKeys.withdraw.tr(context: context),
borderWidth: 0.w,
fontWeight: FontWeight.w600,
borderColor: Colors.transparent,
padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
fontSize: 14.f,
onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())),
),
),
],
),
SizedBox(height: 16.h),
@ -148,9 +161,18 @@ class _HabibWalletState extends State<HabibWalletPage> {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: habibWalletVM.habibWalletBalanceList[index].projectDescription!.toText16(isBold: true, color: AppColors.textColor)),
Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value
textColor: AppColors.textColor, iconColor: AppColors.textColor, iconSize: 18.h, isExpanded: false, fontSize: 28.f, fontWeight: FontWeight.w600, letterSpacing: -1),
Expanded(
child: habibWalletVM.habibWalletBalanceList[index].projectDescription!
.toText16(isBold: true, color: AppColors.textColor)),
Utils.getPaymentAmountWithSymbol2(
habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value
textColor: AppColors.textColor,
iconColor: AppColors.textColor,
iconSize: 18.h,
isExpanded: false,
fontSize: 28.f,
fontWeight: FontWeight.w600,
letterSpacing: -1),
],
).paddingSymmetrical(0, 12.h);
},

@ -11,8 +11,8 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/wallet_payment_confirm_page.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
@ -44,7 +44,9 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
// Clear errors when entering recharge wallet page
WidgetsBinding.instance.addPostFrameCallback((_) {
habibWalletVM = context.read<HabibWalletViewModel>();
habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
habibWalletVM.setDepositorDetails(
appState.getAuthenticatedUser()!.patientId.toString(),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
appState.getAuthenticatedUser()!.mobileNumber!);
habibWalletVM.setSelectedRechargeType(0);
habibWalletVM.getProjectsList();
@ -97,262 +99,272 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
return Container(
height: 135.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
side: BorderSide(
color: viewModel.amountError != null ? AppColors.primaryRedBorderColor : AppColors.textColor,
width: 2.h,
),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//TODO: Check with hussain to show AED or SAR
LocaleKeys.amount.tr(context: context).toText14(color: AppColors.greyTextColor, isBold: true),
Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
return Container(
height: 135.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
side: BorderSide(
color: viewModel.amountError != null ? AppColors.primaryRedBorderColor : AppColors.textColor,
width: 2.h,
),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 150.h,
child: TextInputWidget(
controller: amountTextController,
labelText: "",
hintText: "",
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
fontSize: 25.f,
padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h),
focusNode: _amountFocusNode,
isWalletAmountInput: true,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
fontFamily: "Poppins",
onChange: (value) {
// Clear error when user starts typing
viewModel.clearAmountError();
},
// leadingIcon: AppAssets.student_card,
),
//TODO: Check with hussain to show AED or SAR
LocaleKeys.amount.tr(context: context).toText14(color: AppColors.greyTextColor, isBold: true),
Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(
width: 150.h,
child: TextInputWidget(
controller: amountTextController,
labelText: "",
hintText: "",
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
fontSize: 25.f,
padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h),
focusNode: _amountFocusNode,
isWalletAmountInput: true,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
fontFamily: "Poppins",
onChange: (value) {
// Clear error when user starts typing
viewModel.clearAmountError();
},
// leadingIcon: AppAssets.student_card,
),
),
const Spacer(),
LocaleKeys.sar.tr(context: context).toText20(color: AppColors.greyTextColor, isBold: true),
],
),
const Spacer(),
LocaleKeys.sar.tr(context: context).toText20(color: AppColors.greyTextColor, isBold: true),
],
),
],
),
),
);
},
),
// Show error message if exists
Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
if (viewModel.amountError != null) {
return Padding(
padding: EdgeInsets.only(left: 16.w, top: 8.h),
child: Text(
viewModel.amountError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
);
}
return SizedBox.shrink();
},
),
SizedBox(height: 24.h),
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.h),
border: Border.all(
color: habibWalletVM.hospitalError != null ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 2.h,
),
);
},
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// Show error message if exists
Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
if (viewModel.amountError != null) {
return Padding(
padding: EdgeInsets.only(left: 16.w, top: 8.h),
child: Text(
viewModel.amountError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
);
}
return SizedBox.shrink();
},
),
SizedBox(height: 24.h),
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.h),
border: Border.all(
color: habibWalletVM.hospitalError != null ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 2.h,
),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
Row(
children: [
(habibWalletVM.getSelectedRechargeTypeValue()).toText12(color: AppColors.greyTextColor, isBold: true),
"${LocaleKeys.medicalFile.tr(context: context)}: ${habibWalletVM.fileNumber}"
.toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true),
Utils.buildSvgWithAssets(
icon: AppAssets.my_account_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(habibWalletVM.getSelectedRechargeTypeValue()).toText12(color: AppColors.greyTextColor, isBold: true),
"${LocaleKeys.medicalFile.tr(context: context)}: ${habibWalletVM.fileNumber}"
.toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true),
],
),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
).onPress(() async {
habibWalletVM.setCurrentIndex(0);
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.medicalFile.tr(context: context),
titleWidget: Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return habibWalletVM.currentIndex != 0
? IconButton(
icon: Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 24.h, height: 24.h)),
padding: EdgeInsets.zero,
onPressed: () => habibWalletVM.setCurrentIndex(0),
highlightColor: Colors.transparent,
)
: LocaleKeys.medicalFile.tr(context: context).toText20(isBold: true);
}), child: Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return MultiPageBottomSheet();
}), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true);
}),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
).onPress(() async {
habibWalletVM.setCurrentIndex(0);
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.medicalFile.tr(context: context),
titleWidget: Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return habibWalletVM.currentIndex != 0
? IconButton(
icon: Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 24.h, height: 24.h)),
padding: EdgeInsets.zero,
onPressed: () => habibWalletVM.setCurrentIndex(0),
highlightColor: Colors.transparent,
)
: LocaleKeys.medicalFile.tr(context: context).toText20(isBold: true);
}), child: Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return MultiPageBottomSheet();
}), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true);
}),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.select_hospital_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
Row(
children: [
LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
SizedBox(
width: MediaQuery.of(context).size.width * 0.55,
child: (habibWalletVM.selectedHospital != null ? habibWalletVM.selectedHospital!.name : LocaleKeys.selectHospital.tr(context: context))!
.toText14(color: AppColors.textColor, isBold: true),
Utils.buildSvgWithAssets(
icon: AppAssets.select_hospital_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
SizedBox(
width: MediaQuery.of(context).size.width * 0.55,
child: (habibWalletVM.selectedHospital != null
? habibWalletVM.selectedHospital!.name
: LocaleKeys.selectHospital.tr(context: context))!
.toText14(color: AppColors.textColor, isBold: true),
),
],
),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
).onPress(() {
showCommonBottomSheetWithoutHeight(context,
title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: SelectHospitalBottomSheet(), callBackFunc: () {});
}),
// Show hospital error message if exists
if (habibWalletVM.hospitalError != null)
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
habibWalletVM.hospitalError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
).onPress(() {
showCommonBottomSheetWithoutHeight(context,
title: LocaleKeys.selectHospital.tr(context: context),
isDismissible: false,
child: SelectHospitalBottomSheet(),
callBackFunc: () {});
}),
// Show hospital error message if exists
if (habibWalletVM.hospitalError != null)
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
habibWalletVM.hospitalError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
),
),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
Row(
children: [
LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
"${appState.getAuthenticatedUser()!.emailAddress}".toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true),
Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 8.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
"${appState.getAuthenticatedUser()!.emailAddress}"
.toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true),
],
),
],
),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
TextInputWidget(
labelText: LocaleKeys.notes.tr(context: context),
hintText: "",
controller: notesTextController,
keyboardType: TextInputType.text,
isEnable: true,
prefix: null,
autoFocus: false,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
leadingIcon: AppAssets.notes_icon,
hasError: false,
),
SizedBox(height: 8.h),
],
),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
TextInputWidget(
labelText: LocaleKeys.notes.tr(context: context),
hintText: "",
controller: notesTextController,
keyboardType: TextInputType.text,
isEnable: true,
prefix: null,
autoFocus: false,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
leadingIcon: AppAssets.notes_icon,
hasError: false,
),
SizedBox(height: 8.h),
],
),
),
);
}),
],
),
);
}),
],
),
),
),
),
),
),
),
//TODO: Handle Family member selection & Other Account Selection
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: CustomButton(
text: LocaleKeys.next.tr(context: context),
onPressed: () {
_amountFocusNode.unfocus();
FocusScope.of(context).unfocus();
//TODO: Handle Family member selection & Other Account Selection
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: CustomButton(
text: LocaleKeys.next.tr(context: context),
onPressed: () {
_amountFocusNode.unfocus();
FocusScope.of(context).unfocus();
// Use ViewModel validation method
if (habibWalletVM.validateRechargeForm(amountTextController.text)) {
habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', '')));
habibWalletVM.setNotesText(notesTextController.text);
// habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
// appState.getAuthenticatedUser()!.mobileNumber!);
Navigator.of(context).push(
CustomPageRoute(
page: WalletPaymentConfirmPage(),
),
);
}
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 50.h,
icon: AppAssets.appointment_pay_icon,
iconColor: Colors.white,
iconSize: 18.h,
).paddingSymmetrical(24.h, 24.h),
// Use ViewModel validation method
if (habibWalletVM.validateRechargeForm(amountTextController.text)) {
habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', '')));
habibWalletVM.setNotesText(notesTextController.text);
// habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
// appState.getAuthenticatedUser()!.mobileNumber!);
Navigator.of(context).push(
CustomPageRoute(
page: WalletPaymentConfirmPage(),
),
);
}
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 50.h,
icon: AppAssets.appointment_pay_icon,
iconColor: Colors.white,
iconSize: 18.h,
).paddingSymmetrical(24.h, 24.h),
),
],
),
],
),
),
),
),
);
}

@ -19,7 +19,12 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class SelectHospitalBottomSheet extends StatefulWidget {
const SelectHospitalBottomSheet({super.key});
/// Optional callback invoked when the user picks a hospital.
/// When provided, the caller handles the selection instead of the default
/// [HabibWalletViewModel.setSelectedHospital] behaviour.
final void Function(HospitalsModel hospital)? onHospitalSelected;
const SelectHospitalBottomSheet({super.key, this.onHospitalSelected});
@override
State<SelectHospitalBottomSheet> createState() => _SelectHospitalBottomSheetState();
@ -71,6 +76,7 @@ class _SelectHospitalBottomSheetState extends State<SelectHospitalBottomSheet> {
_updateDisplayList();
}
}
habibWalletVM.addListener(listener);
habibWalletVM.getProjectsList();
}
@ -195,22 +201,26 @@ class _SelectHospitalBottomSheetState extends State<SelectHospitalBottomSheet> {
child: Center(child: Utils.getLoadingWidget()),
)
: SizedBox(
height: MediaQuery.sizeOf(context).height * .4,
child: ListView.separated(
itemBuilder: (_, index) {
return HospitalListItemAdvancePayment(
hospitalModel: displayList[index],
isLocationEnabled: sortByLocation,
).onPress(() {
habibWalletVM.setSelectedHospital(displayList[index]);
Navigator.of(context).pop();
});
},
separatorBuilder: (_, __) => SizedBox(
height: 16.h,
),
itemCount: displayList.length),
)
height: MediaQuery.sizeOf(context).height * .4,
child: ListView.separated(
itemBuilder: (_, index) {
return HospitalListItemAdvancePayment(
hospitalModel: displayList[index],
isLocationEnabled: sortByLocation,
).onPress(() {
if (widget.onHospitalSelected != null) {
widget.onHospitalSelected!(displayList[index]);
} else {
habibWalletVM.setSelectedHospital(displayList[index]);
}
Navigator.of(context).pop();
});
},
separatorBuilder: (_, __) => SizedBox(
height: 16.h,
),
itemCount: displayList.length),
)
],
);
}

@ -0,0 +1,445 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/image_picker.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class WithdrawRequestCreatePage extends StatefulWidget {
const WithdrawRequestCreatePage({super.key});
@override
State<WithdrawRequestCreatePage> createState() => _WithdrawRequestCreatePageState();
}
class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
late HabibWalletViewModel _habibWalletViewModel;
late AppState _appState;
final TextEditingController _bankDetailsController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final FocusNode _ibanFocusNode = FocusNode();
final FocusNode _descriptionFocusNode = FocusNode();
@override
void initState() {
super.initState();
_appState = getIt.get<AppState>();
_habibWalletViewModel = context.read<HabibWalletViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) {
_habibWalletViewModel.getProjectsList();
});
}
@override
void dispose() {
_habibWalletViewModel.resetWithdrawForm(silent: true);
_bankDetailsController.dispose();
_descriptionController.dispose();
_ibanFocusNode.dispose();
_descriptionFocusNode.dispose();
super.dispose();
}
bool get _isFormValid {
final vm = _habibWalletViewModel;
return vm.selectedHospital != null &&
!vm.isAdvancesLoading &&
_bankDetailsController.text.trim().isNotEmpty &&
_descriptionController.text.trim().isNotEmpty;
}
// Bottom bar
Widget _buildBottomBar() {
return Consumer<HabibWalletViewModel>(
builder: (context, vm, _) {
final isValid = vm.selectedHospital != null &&
!vm.isAdvancesLoading &&
_bankDetailsController.text.trim().isNotEmpty &&
_descriptionController.text.trim().isNotEmpty;
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CustomButton(
text: LocaleKeys.submitRequest.tr(context: context),
isDisabled: !isValid,
onPressed: () {
final isFormValid = vm.validateWithdrawForm(
iban: _bankDetailsController.text,
description: _descriptionController.text,
);
if (isFormValid) {
_handleWalletWithdrawalSubmit(vm);
}
},
).paddingSymmetrical(24.h, 24.h),
],
),
);
},
);
}
Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) {
final isArabic = _appState.isArabic();
return Container(
padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.select_hospital_icon,
width: 40.h,
height: 40.h,
applyThemeColor: false,
),
SizedBox(width: 8.h),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
(habibWalletViewModel.selectedHospital != null
? ((isArabic
? (habibWalletViewModel.selectedHospital!.nameN?.toString() ?? habibWalletViewModel.selectedHospital!.name)
: habibWalletViewModel.selectedHospital!.name) ??
LocaleKeys.selectHospital.tr(context: context))
: LocaleKeys.selectHospital.tr(context: context))
.toText14(color: AppColors.textColor, isBold: true),
]),
],
),
Utils.buildSvgWithAssets(
icon: AppAssets.arrow_down,
width: 25.h,
height: 25.h,
),
],
).onPress(() {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.selectHospital.tr(context: context),
isDismissible: false,
child: SelectHospitalBottomSheet(
onHospitalSelected: (hospital) {
habibWalletViewModel.setSelectedHospital(hospital);
habibWalletViewModel.fetchAdvancesForHospital();
},
),
callBackFunc: () {},
);
}),
// Only show divider + amount once hospital is selected
if (habibWalletViewModel.selectedHospital != null) ...[
Divider(color: AppColors.bottomNAVBorder, thickness: 1).paddingOnly(top: 16.h),
habibWalletViewModel.isAdvancesLoading
? Container(
height: 60.h,
// width: 160.w,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
),
).toShimmer2(isShow: true, radius: 12.r)
: Utils.getPaymentAmountWithSymbol(
habibWalletViewModel.totalAdvancesBalance.toStringAsFixed(2).toText44(isBold: true),
AppColors.blackColor,
18.w,
isSaudiCurrency: true,
isExpanded: false,
),
],
],
),
);
}
Widget _buildBankInputDetails() {
return Consumer<HabibWalletViewModel>(
builder: (context, viewModel, _) {
final hasError = viewModel.ibanError != null || viewModel.withdrawDescriptionError != null;
return Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
),
child: Column(
children: [
TextInputWidget(
labelText: LocaleKeys.addIbanDetails.tr(),
hintText: "SA0000000000000000000000",
controller: _bankDetailsController,
focusNode: _ibanFocusNode,
autoFocus: false,
keyboardType: TextInputType.text,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.bankIcon,
fontFamily: "Poppins",
hasError: false,
errorMessage: viewModel.ibanError,
onChange: (value) {
viewModel.clearIbanError();
},
onSubmitted: (value) {
_descriptionFocusNode.requestFocus();
},
),
Divider(color: AppColors.bottomNAVBorder, thickness: 1).withVerticalPadding(16.h),
TextInputWidget(
labelText: LocaleKeys.description.tr(),
hintText: LocaleKeys.enterDescription.tr(),
controller: _descriptionController,
focusNode: _descriptionFocusNode,
autoFocus: false,
keyboardType: TextInputType.text,
// leadingIconNeedsContainer: false,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.descriptionIcon,
fontFamily: "Poppins",
hasError: false,
errorMessage: viewModel.withdrawDescriptionError,
onChange: (value) {
viewModel.clearWithdrawDescriptionError();
},
),
],
),
);
},
);
}
Widget _buildImagePickerDropDown() {
return Container(
padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.selectImageIcon,
width: 40.h,
height: 40.h,
applyThemeColor: false,
),
SizedBox(width: 8.h),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
LocaleKeys.uploadImage.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
LocaleKeys.selectImage.tr(context: context).toText14(color: AppColors.textColor, isBold: true),
]),
],
),
Utils.buildSvgWithAssets(
icon: AppAssets.arrow_down,
width: 25.h,
height: 25.h,
),
],
).onPress(() {
ImageOptions.showImageOptionsNew(
context,
false,
title: LocaleKeys.selectOptions.tr(context: context),
(String image, File file) {},
);
}),
);
}
// Submit
void _handleWalletWithdrawalSubmit(HabibWalletViewModel vm) {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.notice.tr(context: context),
child: Utils.getWarningWidget(
loadingText: LocaleKeys.confirmSubmitRequest.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader();
final user = _appState.getAuthenticatedUser();
final hospital = vm.selectedHospital;
await vm.submitWalletWithdrawalRequest(
iban: _bankDetailsController.text.trim(),
remarks: _descriptionController.text.trim(),
patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(),
patientId: (user?.patientId ?? 0).toString(),
patientMobileNumber: user?.mobileNumber ?? '',
patientNationalId: user?.patientIdentificationNo?.toString() ?? '',
projectId: hospital?.mainProjectID ?? 0,
projectName: hospital?.name ?? '',
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
_showWithdrawalSuccessBottomSheet(response.refNo ?? '');
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
);
},
);
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
void _showWithdrawalSuccessBottomSheet(String refNo) {
showCommonBottomSheetWithoutHeight(
context,
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)),
if (refNo.isNotEmpty) ...[
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
LocaleKeys.hereIsYourRequestNumber.tr().toText16(
color: AppColors.textColorLight,
),
SizedBox(width: 6.w),
refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
],
).paddingSymmetrical(24.w, 0),
],
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
height: 56.h,
text: LocaleKeys.ok.tr(),
onPressed: () {
Navigator.pop(context); // pop success sheet
Navigator.pop(context); // pop refund page
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
),
),
);
},
textColor: AppColors.whiteColor,
),
),
],
),
],
),
),
isCloseButtonVisible: false,
isDismissible: false,
isFullScreen: false,
isAutoDismiss: false,
);
}
// Build
@override
Widget build(BuildContext context) {
return Consumer<HabibWalletViewModel>(
builder: (context, habibWalletViewModel, _) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.requestRefund.tr(context: context),
requests: () => Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 2, // 2 for wallet, 1 for invoice
),
),
),
child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProjectSelectionAndBalance(habibWalletViewModel),
if (habibWalletViewModel.selectedHospital != null) ...[
SizedBox(height: 16.h),
_buildBankInputDetails(),
// SizedBox(height: 16.h),
// _buildImagePickerDropDown(),
],
],
),
),
),
),
_buildBottomBar(),
],
),
);
},
);
}
}
// I/flutter (13750): uri: https://uat.hmgwebservices.com/Services/COCWS.svc/REST/SubmitRefund
// [log] body: {"ProjectID":15,"Refund_RefundType":"2","Refund_Project":"مستشفى الريان","Refund_ProjectId":15,"Refund_PatientId":"4770714","PatientMobileNumber":"0504278212","Refund_PatientName":"FAIZ UR REHMAN REHMAN","Refund_NationalId":"2530976584","Refund_DoctorId":0,"Refund_DoctorName":"","Refund_RefundDestination":"2","Refund_IBAN":"SA0380000000608010167519","Refund_Remarks":"teskchjvs","Refund_TotalRefundAmount":1714.75,"Refund_AdvancesList":[{"AdvanceNo":"2026000444","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1775682000000+0300)/"},{"AdvanceNo":"2026000392","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1774645200000+0300)/"},{"AdvanceNo":"2026000066","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1770152400000+0300)/"},{"AdvanceNo":"2026000047","PaidAmount":30.0,"BalanceAmount":30.0,"AdvanceDate":"/Date(1769547600000+0300)/"},{"AdvanceNo":"2025000751","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1765746000000+0300)/"},{"AdvanceNo":"2025000414","PaidAmount":115.0,"BalanceAmount":115.0,"AdvanceDate":"/Date(1747083600000+0300)/"},{"AdvanceNo":"2025000210","PaidAmount":120.75,"BalanceAmount":120.75,"AdvanceDate":"/Date(1741726800000+0300)/"}],"PatientType":1,"PatientTypeID":1,"TokenID":"@dm!n","PatientID":4770714,"PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNi0wNS0xOFQwNzo1MzoyMS4wMjQ5MDM2WiIsIlBhdGllbnRJRCI6IjQ3NzIxNjMiLCJQYXRpZW50TWVyZ2VkSURzIjoiNDc3MjE2MyIsIlBhdGllbnRPdXRTQSI6ZmFsc2UsIlNoYXJlZElkcyI6IiJ9LCJleHAiOjE3NzkxMzgwMDB9.VHd_kCya33GfK-DywTHwSU1PMewbQB7UhQD1zXS7X5w","VersionID":"21.2","Channel":"3","IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","LanguageID":"1","Latitude":"0.0","Longitude":"0.0","DeviceTypeID":"2","IsNewFlutterApp":true}
// [log] response: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":"Internal server error occurred.","ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":2,"NumberOfResultRecords":0,"PatientBlodType":null,"RequestId":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"ENSAT_GetPatientRequestListResponse":null,"ENSAT_Patient_External_Response":null,"ETQAN_CreateIncident_Response":null,"ETQAN_GetEmployeeOVRRequests_Response":null,"ETQAN_GetIncidentRequest_Response":null,"ETQAN_GetProjects_Response":null,"ITGRequest":null,"Itg_FormAttachmentsList":null,"ListCOCItems":null,"Message":"Refunds API returned status 500. Body: {\"message\":\"Internal server error occurred.\"}","Mohemm_ENSAT_GetItgToken_Response":null,"Mohemm_ITG_DepartmentSectionsList":null,"Mohemm_ITG_Pending_Task_ResponseItem":null,"Mohemm_ITG_ProjectDepartmentsList":null,"Mohemm_ITG_ResponseItem":null,"Mohemm_ITG_SectionTopicsList":null,"Mohemm_ITG_TicketDetailsList":null,"Mohemm_ITG_TicketTransactionsList":null,"Mohemm_ITG_TicketsByEmployeeList":null,"Mohemm_Itg_ProjectsList":null,"Mohemm_Itg_TicketTypesList":null,"ReferenceNumber":null,"Refund_InvoicesList":null,"Refund_PatientIdDisplay":null,"Refund_RefNo":null,"Refund_RefundTypeDisplay":null,"Refund_StatusItem":null,"Refund_TotalInvoices":0,"Refund_VerifyResult":null,"RequestType":null,"TotalCount":0,"statuseCode":500}

@ -20,6 +20,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart';
import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart';
@ -30,7 +31,7 @@ import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dar
import 'package:hmg_patient_app_new/presentation/home/service_info_page.dart';
import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart';
import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart';
import 'package:hmg_patient_app_new/presentation/servicesPriceList/services_price_list_page.dart';
import 'package:hmg_patient_app_new/presentation/service_price/services_price_list_page.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@ -42,12 +43,12 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/dependencies.dart' show getIt;
import '../../core/dependencies.dart';
class ServicesPage extends StatefulWidget {
bool showBackIcon;
final bool showBackIcon;
ServicesPage({super.key, this.showBackIcon = true});
const ServicesPage({super.key, this.showBackIcon = true});
@override
State<ServicesPage> createState() => _ServicesPageState();
@ -101,7 +102,7 @@ class _ServicesPageState extends State<ServicesPage> {
HmgServicesComponentModel(
5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async {
if (getIt.get<AppState>().isAuthenticated) {
getIt.get<NavigationService>().pushPageRoute(AppRoutes.comprehensiveCheckupPage);
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(CustomPageRoute(page: ComprehensiveCheckupPage()));
} else {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
@ -560,55 +561,55 @@ class _ServicesPageState extends State<ServicesPage> {
// isExpanded: false, letterSpacing: -1)
// .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h);
})
: LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2),
Spacer(),
getIt.get<AppState>().isAuthenticated
? CustomButton(
height: 40.h,
icon: AppAssets.recharge_icon,
iconSize: 24.w,
iconColor: AppColors.infoColor,
textColor: AppColors.infoColor,
text: LocaleKeys.recharge.tr(),
borderWidth: 0.w,
isBold: true,
borderColor: Colors.transparent,
backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w),
fontSize: 14.f,
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage()));
},
)
: SizedBox.shrink(),
],
).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage()));
} else {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
}
}),
),
),
SizedBox(width: 16.w),
Expanded(
child: Container(
height: 183.h,
width: 183.h,
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
: LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2),
Spacer(),
getIt.get<AppState>().isAuthenticated
? CustomButton(
height: 40.h,
icon: AppAssets.recharge_icon,
iconSize: 24.w,
iconColor: AppColors.infoColor,
textColor: AppColors.infoColor,
text: LocaleKeys.recharge.tr(),
borderWidth: 0.w,
isBold: true,
borderColor: Colors.transparent,
backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08),
padding: EdgeInsets.all(8.w),
fontSize: 14.f,
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage()));
},
)
: SizedBox.shrink(),
],
).onPress(() async {
if (getIt.get<AppState>().isAuthenticated) {
Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage()));
} else {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
}
}),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 8.w,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
),
SizedBox(width: 16.w),
Expanded(
child: Container(
height: 183.h,
width: 183.h,
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 8.w,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false),
LocaleKeys.familyTitle.tr().toText16(isBold: true, maxlines: 2).expanded,

@ -494,7 +494,9 @@ class _LandingPageState extends State<LandingPage> {
height: isTablet ? 290.h : 255.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 ? myAppointmentsVM.patientAppointmentsHistoryList.length : 3,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3
? myAppointmentsVM.patientAppointmentsHistoryList.length
: 3,
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
@ -868,8 +870,7 @@ class _LandingPageState extends State<LandingPage> {
children: [
LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h),
SizedBox(
// height: 200.h,
height: 170.h,
height: 175.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: 2,

File diff suppressed because one or more lines are too long

@ -41,9 +41,6 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor
import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/insurance/insurance_approvals_page.dart';
import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart';
import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart';
@ -647,11 +644,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
isLargeText: true,
iconSize: 36.w,
).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: InsuranceApprovalsPage(),
),
);
Navigator.of(context).push(CustomPageRoute(page: InsuranceApprovalsPage()));
}),
MedicalFileCard(
label: LocaleKeys.myInvoicesList.tr(context: context),
@ -661,12 +654,22 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
isLargeText: true,
iconSize: 36.w,
).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: MyInvoicesList(),
),
);
Navigator.of(context).push(CustomPageRoute(page: MyInvoicesList()));
}),
// MedicalFileCard(
// label: LocaleKeys.requestRefund.tr(context: context),
// textColor: AppColors.blackColor,
// backgroundColor: AppColors.whiteColor,
// svgIcon: AppAssets.ancillary_orders_list_icon,
// isLargeText: true,
// iconSize: 36.w,
// ).onPress(() {
// Navigator.of(context).push(
// CustomPageRoute(
// page: const RefundRequestCreatePage(initialTabIndex: 0),
// ),
// );
// }),
MedicalFileCard(
label: LocaleKeys.ancillaryOrdersListNew.tr(context: context),
textColor: AppColors.blackColor,

@ -137,215 +137,101 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
}
},
).paddingSymmetrical(24.h, 0.h),
myInvoicesVM.selectedTabIndex == 0 ? Column(
children: [
SizedBox(height: 16.h),
Row(
children: [
CustomButton(
text: LocaleKeys.allInvoices.tr(context: context),
onPressed: () {
myInvoicesViewModel.filterInvoices(InvoiceFilterType.all);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.hospitals.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showHospitalFilterBottomSheet(context);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.clinics.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showClinicFilterBottomSheet(context);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.doctors.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showDoctorFilterBottomSheet(context);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
ListView.builder(
itemCount: myInvoicesVM.isInvoicesListLoading
? 4
: myInvoicesVM.allInvoicesList.isEmpty
? 1
: myInvoicesVM.allInvoicesList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
return myInvoicesVM.isInvoicesListLoading
? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true)
: myInvoicesVM.allInvoicesList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: InvoiceListCard(
getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
onTap: () async {
if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context));
await myInvoicesViewModel.sendInvoiceEmail(
appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
isAutoDismiss: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myInvoicesViewModel.downloadInvoicePDF(
setupId: myInvoicesVM.allInvoicesList[index].setupId!,
invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
}
},
);
}
},
// onTap: () async {
// myInvoicesVM.setInvoiceDetailLoading();
// LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingInvoiceDetails.tr(context: context));
// await myInvoicesVM.getInvoiceDetails(
// appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
// invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!,
// projectID: myInvoicesVM.allInvoicesList[index].projectId!,
// onSuccess: (val) {
// LoaderBottomSheet.hideLoader();
// Navigator.of(context).push(
// CustomPageRoute(
// page: MyInvoicesDetailsPage(
// getInvoiceDetailsResponseModel: myInvoicesVM.invoiceDetailsResponseModel,
// getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
// ),
// ),
// );
// },
// onError: (err) {
// LoaderBottomSheet.hideLoader();
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getErrorWidget(loadingText: err),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// );
// });
// },
),
),
),
),
)
: Utils.getNoDataWidget(context);
}).paddingSymmetrical(24.w, 0.h),
],
)
: Column(
myInvoicesVM.selectedTabIndex == 0
? Column(
children: [
SizedBox(height: 16.h),
Row(
children: [
CustomButton(
text: LocaleKeys.allInvoices.tr(context: context),
onPressed: () {
myInvoicesViewModel.filterInvoices(InvoiceFilterType.all);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.hospitals.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showHospitalFilterBottomSheet(context);
},
backgroundColor:
myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.clinics.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showClinicFilterBottomSheet(context);
},
backgroundColor:
myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.doctors.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showDoctorFilterBottomSheet(context);
},
backgroundColor:
myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
ListView.builder(
itemCount: myInvoicesVM.isPharmacyInvoicesLoading
itemCount: myInvoicesVM.isInvoicesListLoading
? 4
: myInvoicesVM.allPharmacyInvoicesList.isEmpty
: myInvoicesVM.allInvoicesList.isEmpty
? 1
: myInvoicesVM.allPharmacyInvoicesList.length,
: myInvoicesVM.allInvoicesList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
return myInvoicesVM.isPharmacyInvoicesLoading
return myInvoicesVM.isInvoicesListLoading
? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true)
: myInvoicesVM.allPharmacyInvoicesList.isNotEmpty
: myInvoicesVM.allInvoicesList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
@ -355,29 +241,41 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: PharmacyInvoiceCard(
pharmacyInvoice: myInvoicesVM.allPharmacyInvoicesList[index],
child: InvoiceListCard(
getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
onTap: () async {
// if (Utils.isVidaPlusProject(myInvoicesVM.allPharmacyInvoicesList[index].projectID ?? 0)) {
// // For Vida+ projects, send email (if API is available)
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getSuccessWidget(
// loadingText: LocaleKeys.comingSoon.tr(context: context),
// ),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// isAutoDismiss: true,
// );
// } else {
// Download pharmacy invoice PDF
if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) {
LoaderBottomSheet.showLoader(
loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context));
await myInvoicesViewModel.sendInvoiceEmail(
appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context,
child: Utils.getSuccessWidget(
loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
isAutoDismiss: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myInvoicesViewModel.downloadPharmacyInvoicePDF(
setupId: myInvoicesVM.allPharmacyInvoicesList[index].setupId ?? "",
invoiceNo: myInvoicesVM.allPharmacyInvoicesList[index].invoiceNo ?? 0,
projectID: myInvoicesVM.allPharmacyInvoicesList[index].pharmacyProjectID ?? 0,
locationID: myInvoicesVM.allPharmacyInvoicesList[index].locationID ?? 0,
myInvoicesViewModel.downloadInvoicePDF(
setupId: myInvoicesVM.allInvoicesList[index].setupId!,
invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
@ -390,14 +288,15 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
},
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf");
if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(
myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"),
child: Utils.getErrorWidget(loadingText: LocaleKeys.cannotOpenThisFile.tr()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
@ -406,6 +305,94 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
}
},
);
}
},
),
),
),
),
)
: Utils.getNoDataWidget(context);
}).paddingSymmetrical(24.w, 0.h),
],
)
: Column(
children: [
SizedBox(height: 16.h),
ListView.builder(
itemCount: myInvoicesVM.isPharmacyInvoicesLoading
? 4
: myInvoicesVM.allPharmacyInvoicesList.isEmpty
? 1
: myInvoicesVM.allPharmacyInvoicesList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
return myInvoicesVM.isPharmacyInvoicesLoading
? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true)
: myInvoicesVM.allPharmacyInvoicesList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: PharmacyInvoiceCard(
pharmacyInvoice: myInvoicesVM.allPharmacyInvoicesList[index],
onTap: () async {
// if (Utils.isVidaPlusProject(myInvoicesVM.allPharmacyInvoicesList[index].projectID ?? 0)) {
// // For Vida+ projects, send email (if API is available)
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getSuccessWidget(
// loadingText: LocaleKeys.comingSoon.tr(context: context),
// ),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// isAutoDismiss: true,
// );
// } else {
// Download pharmacy invoice PDF
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myInvoicesViewModel.downloadPharmacyInvoicePDF(
setupId: myInvoicesVM.allPharmacyInvoicesList[index].setupId ?? "",
invoiceNo: myInvoicesVM.allPharmacyInvoicesList[index].invoiceNo ?? 0,
projectID: myInvoicesVM.allPharmacyInvoicesList[index].pharmacyProjectID ?? 0,
locationID: myInvoicesVM.allPharmacyInvoicesList[index].locationID ?? 0,
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(
myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
}
},
);
// }
},
),

@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -9,12 +11,16 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'dart:ui' as ui;
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:provider/provider.dart';
class InvoiceListCard extends StatelessWidget {
final GetInvoicesListResponseModel getInvoicesListResponseModel;
@ -22,15 +28,51 @@ class InvoiceListCard extends StatelessWidget {
InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap});
void _handleRefundInvoiceTap(BuildContext context) {
final invoiceNo = getInvoicesListResponseModel.invoiceNo;
final projectId = getInvoicesListResponseModel.projectId;
if (invoiceNo == null || projectId == null) {
Navigator.of(context).push(
CustomPageRoute(
page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel),
),
);
return;
}
final viewModel = context.read<RefundRequestViewModel>();
LoaderBottomSheet.showLoader();
viewModel.checkIsRefundableInvoice(
invoiceNo: invoiceNo,
projectId: projectId,
onSuccess: (result) {
LoaderBottomSheet.hideLoader();
if (result.isRefundable == true) {
Navigator.of(context).push(
CustomPageRoute(
page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel),
),
);
} else {
final message = result.message?.isNotEmpty == true ? result.message! : LocaleKeys.thisInvoiceIsNotEligibleForRefund.tr(context: context);
getIt<DialogService>().showErrorBottomSheet(message: message);
}
},
onError: (error) {
LoaderBottomSheet.hideLoader();
getIt<DialogService>().showErrorBottomSheet(message: error);
},
);
}
@override
Widget build(BuildContext context) {
AppState appState = getIt<AppState>();
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
@ -49,7 +91,7 @@ class InvoiceListCard extends StatelessWidget {
textColor: AppColors.textColor,
),
AppCustomChipWidget(
labelText: appState.isArabic() ? "العيادات الخارجيه" :"Out Patient",
labelText: appState.isArabic() ? "العيادات الخارجيه" : "Out Patient",
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1),
textColor: AppColors.warningColorYellow,
),
@ -98,9 +140,13 @@ class InvoiceListCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(getIt<AppState>().isArabic()
? (getInvoicesListResponseModel.doctorNameN ?? getInvoicesListResponseModel.doctorName ?? LocaleKeys.doctor.tr(context: context))
: (getInvoicesListResponseModel.doctorName ?? getInvoicesListResponseModel.doctorNameN ?? LocaleKeys.doctor.tr(context: context))
).toText16(isBold: true),
? (getInvoicesListResponseModel.doctorNameN ??
getInvoicesListResponseModel.doctorName ??
LocaleKeys.doctor.tr(context: context))
: (getInvoicesListResponseModel.doctorName ??
getInvoicesListResponseModel.doctorNameN ??
LocaleKeys.doctor.tr(context: context)))
.toText16(isBold: true),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
@ -128,7 +174,8 @@ class InvoiceListCard extends StatelessWidget {
child: AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w),
icon: AppAssets.doctor_calendar_icon,
labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false),
labelText:
DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false),
isEnglishOnly: true,
),
),
@ -139,25 +186,57 @@ class InvoiceListCard extends StatelessWidget {
),
],
),
SizedBox(height: 16.h),
CustomButton(
// text: LocaleKeys.viewInvoiceDetails.tr(context: context),
text: Utils.isVidaPlusProject(getInvoicesListResponseModel.projectId ?? 0) ? LocaleKeys.sendEmail.tr(context: context) : LocaleKeys.downloadInvoice.tr(context: context),
onPressed: () {
if (onTap != null) {
onTap!();
}
},
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01),
textColor: AppColors.primaryRedColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconSize: 14.h,
),
if (onTap != null) ...[
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
// text: LocaleKeys.viewInvoiceDetails.tr(context: context),
text: Utils.isVidaPlusProject(getInvoicesListResponseModel.projectId ?? 0)
? LocaleKeys.sendEmail.tr(context: context)
: LocaleKeys.downloadInvoice.tr(context: context),
onPressed: () {
if (onTap != null) {
onTap!();
}
},
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01),
textColor: AppColors.primaryRedColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconSize: 14.h,
),
),
if (getInvoicesListResponseModel.isAllowRefund == true) ...[
SizedBox(width: 8.w),
Expanded(
child: CustomButton(
text: LocaleKeys.requestRefund.tr(context: context),
onPressed: () {
_handleRefundInvoiceTap(context);
},
icon: AppAssets.refundIcon,
iconColor: AppColors.caloriesCalculatorColor,
backgroundColor: AppColors.blueColor.withValues(alpha: 0.14),
borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01),
textColor: AppColors.caloriesCalculatorColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconSize: 14.h,
),
),
]
],
),
],
],
),
),

@ -0,0 +1,264 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/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/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class RefundHistoryScreen extends StatefulWidget {
final int refundType;
const RefundHistoryScreen({super.key, required this.refundType});
@override
State<RefundHistoryScreen> createState() => _RefundHistoryScreenState();
}
class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
@override
void initState() {
super.initState();
final vm = context.read<RefundRequestViewModel>();
scheduleMicrotask(() async {
await vm.getRefundInvoices(refundType: widget.refundType);
});
}
// Helpers
String _formatDate(String? rawDate) {
if (rawDate == null || rawDate.isEmpty) return '';
try {
final date = DateUtil.convertStringToDate(rawDate);
return DateFormat('MMM dd, yyyy').format(date);
} catch (_) {
return rawDate;
}
}
// Shimmer
Widget _buildLoadingShimmer() {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(top: 24.h),
itemCount: 4,
separatorBuilder: (_, __) => SizedBox(height: 16.h),
itemBuilder: (_, __) => _buildInvoiceCard(null, isLoading: true),
);
}
// Card
Color getStatusColor(int status) {
Color statusColor = AppColors.textColor;
if (status == 3) {
statusColor = AppColors.statusRejectedColor;
} else if (status == 2) {
statusColor = AppColors.statusCompletedColor;
} else if (status == 1) {
statusColor = AppColors.statusPendingColor;
}
return statusColor;
}
String getRefundDestinationIcon(String refundDestination) {
String icon = "";
if (refundDestination.toLowerCase().contains("wallet")) {
icon = AppAssets.habibLogoOnly;
} else if (refundDestination.toLowerCase().contains("bank")) {
icon = AppAssets.bankIconOnly;
}
return icon;
}
Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) {
final referenceNo = isLoading ? 'INV-000000' : (invoice?.referenceNumber ?? '-');
final String requestStatusName = isLoading ? 'Processed' : (invoice?.requestStatusName ?? '');
final int requestStatusId = isLoading ? 1 : (invoice?.requestStatusId ?? 1);
final refundDestinationId = (invoice?.refundDestination ?? '');
final refundDestination = isLoading ? 'Wallet' : (invoice?.refundDestination ?? '');
final isInvoiceRefund = invoice?.refundTypeId == 1;
// Resolve total refund amount from whichever payload is present
final totalAmount = isLoading
? 0.0
: isInvoiceRefund
? (invoice?.invoiceRequestPayload?.totalRefundAmount)
: (invoice?.walletRequestPayload?.totalRefundAmount);
final totalAmountStr = totalAmount?.toStringAsFixed(2);
// Resolve total refund amount from whichever payload is present
final projectName = isInvoiceRefund ? (invoice?.invoiceRequestPayload?.project ?? "") : "";
final String totalProcedures = (invoice?.invoiceRequestPayload?.proceduresList?.length.toString() ?? '');
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Invoice No + Last Updated row
AppCustomChipWidget(
icon: "",
labelText: requestStatusName,
textColor: getStatusColor(requestStatusId),
backgroundColor: getStatusColor(requestStatusId).withValues(alpha: 0.2),
).toShimmer2(isShow: isLoading),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"${LocaleKeys.referenceNo.tr(context: context)} $referenceNo"
.toText16(
color: AppColors.textColor,
isBold: true,
)
.toShimmer2(isShow: isLoading),
],
),
SizedBox(height: 8.h),
Wrap(
spacing: 6.w,
runSpacing: 6.h,
children: [
if (!isLoading && refundDestination.isNotEmpty)
AppCustomChipWidget(icon: getRefundDestinationIcon(refundDestination), iconHasColor: false, labelText: refundDestination),
if (totalProcedures.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$totalProcedures ${LocaleKeys.procedures.tr()}"),
if (!isLoading && projectName.isNotEmpty) AppCustomChipWidget(icon: "", labelText: projectName),
],
),
SizedBox(height: 16.h),
// Total refund amount
if (totalAmountStr != null)
Utils.getPaymentAmountWithSymbol(
isExpanded: false,
(isLoading ? '0.00' : totalAmountStr).toText20(isBold: true, color: AppColors.textColor),
AppColors.textColor,
18,
isSaudiCurrency: true,
).toShimmer2(isShow: isLoading),
SizedBox(height: 16.h),
CustomButton(
text: LocaleKeys.viewDetails.tr(context: context),
onPressed: () => Navigator.of(context).push(CustomPageRoute(page: RefundRequestDetailScreen(invoice: invoice!))),
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01),
textColor: AppColors.primaryRedColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconColor: AppColors.whiteColor,
iconSize: 18.h,
).toShimmer2(isShow: isLoading),
],
),
),
);
}
// Empty
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40.h),
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.r,
hasShadow: false,
),
child: Utils.getNoDataWidget(
context,
noDataText: LocaleKeys.noRefundHistoryFound.tr(context: context),
isSmallWidget: true,
width: 62.w,
height: 62.h,
),
),
),
);
}
// Build
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.refundHistory.tr(context: context),
isLeading: true,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Consumer<RefundRequestViewModel>(
builder: (context, viewModel, _) {
if (viewModel.isRefundInvoicesLoading) {
return _buildLoadingShimmer();
}
if (viewModel.refundInvoicesList.isEmpty) {
return _buildEmptyState();
}
return ListView.separated(
padding: EdgeInsets.only(top: 24.h),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.refundInvoicesList.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final invoice = viewModel.refundInvoicesList[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: _buildInvoiceCard(invoice),
),
),
);
},
);
},
),
],
).paddingSymmetrical(24.w, 0),
),
);
}
}

@ -0,0 +1,720 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/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/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.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';
class RefundRequestCreatePage extends StatefulWidget {
final GetInvoicesListResponseModel invoice;
/// 0 = Invoices (default), 1 = HMG Wallet
final int initialTabIndex;
const RefundRequestCreatePage({
super.key,
required this.invoice,
this.initialTabIndex = 0,
});
@override
State<RefundRequestCreatePage> createState() => _RefundRequestCreatePageState();
}
class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
late RefundRequestViewModel _refundRequestViewModel;
late HabibWalletViewModel _habibWalletViewModel;
late AppState _appState;
final TextEditingController _ibanController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final FocusNode _ibanFocusNode = FocusNode();
final FocusNode _descriptionFocusNode = FocusNode();
@override
void initState() {
super.initState();
_appState = getIt.get<AppState>();
WidgetsBinding.instance.addPostFrameCallback((_) {
_refundRequestViewModel = context.read<RefundRequestViewModel>();
_ibanController.addListener(() => _refundRequestViewModel.triggerRebuild());
_descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild());
_loadInvoiceDetails();
});
}
@override
void dispose() {
_ibanController.dispose();
_descriptionController.dispose();
_ibanFocusNode.dispose();
_descriptionFocusNode.dispose();
_refundRequestViewModel.resetState();
super.dispose();
}
void _loadInvoiceDetails() {
final invoiceNo = widget.invoice.invoiceNo;
final projectId = widget.invoice.projectId;
if (invoiceNo == null || projectId == null) return;
_refundRequestViewModel.getRefundableInvoicesDetails(
invoiceNo: invoiceNo,
projectId: projectId,
onSuccess: (details) {
final procedures = details.refundableInvoiceDetailsItems
.map((item) => RefundableProcedure(
procedureId: item.lineItemNo ?? 0,
procedureName: item.procedureName ?? '',
patientPaidAmount: item.patientPaidAmount ?? 0.0,
apiLineItemNo: item.lineItemNo,
apiProcedureId: item.procedureId,
))
.toList();
_refundRequestViewModel.setProcedures(procedures);
},
);
}
// Stepper
Widget _buildStepper(RefundRequestViewModel vm) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: LayoutBuilder(
builder: (context, constraints) {
// StepperWidget appends SizedBox(width: 8.h) after itself
final itemWidth = (constraints.maxWidth - 2 * 8.h) / 2;
return Row(
children: List.generate(2, (index) {
final isActive = index <= vm.currentStep;
final isThumb = index == vm.currentStep;
return StepperWidget(
itemWidth,
isActive ? AppColors.primaryRedColor : AppColors.greyColor,
isThumb,
6.h,
);
}),
);
},
),
);
}
// Step 1 exactly mirrors RechargeWalletPage hospital row
Widget _buildStep1Content(RefundRequestViewModel vm) {
return Consumer<RefundRequestViewModel>(
builder: (context, refundVM, _) {
final isArabic = _appState.isArabic();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.selectProcedures.tr(context: context).toText20(isBold: true),
SizedBox(height: 24.h),
if (vm.isProceduresLoading)
ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => SizedBox(height: 16.h),
itemCount: 4,
itemBuilder: (_, index) => Container(
height: 40.h,
margin: EdgeInsets.only(bottom: 24.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 16.h,
),
).toShimmer2(isShow: true, radius: 16.r),
)
else if (vm.procedures.isEmpty)
Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 24.h),
child: LocaleKeys.noProceduresAvailableForRefund.tr().toText14(color: AppColors.greyTextColor),
),
)
else
AnimationLimiter(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: false),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Select All row
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _refundRequestViewModel.toggleSelectAll(),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Row(
children: [
SizedBox(
width: 24.w,
height: 24.w,
child: Transform.scale(
scale: 24.w / 22,
child: Checkbox(
value: vm.isAllSelected,
onChanged: (_) => _refundRequestViewModel.toggleSelectAll(),
activeColor: AppColors.primaryRedColor,
side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)),
),
),
),
SizedBox(width: 8.w),
LocaleKeys.selectAll.tr(context: context).toText16(isBold: true),
],
),
),
),
),
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
...List.generate(
vm.procedures.length,
(index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length),
),
],
),
),
),
],
);
},
);
}
// Step 2 payment method selection
Widget _buildStep2Content(RefundRequestViewModel vm) {
final paymentOptions = [
RefundTabType.hmgWallet,
RefundTabType.invoices,
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.selectRefundMethod.tr(context: context).toText20(isBold: true),
SizedBox(height: 24.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: paymentOptions.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final type = paymentOptions[index];
return Column(
children: [
RadioListTileWidget<RefundTabType>(
value: type,
groupValue: vm.selectedPaymentMethod,
title: type.toDisplayString(),
onChanged: (value) {
if (value != null) vm.setPaymentMethod(value);
},
),
if (index != paymentOptions.length - 1)
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
],
);
},
),
),
if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[
SizedBox(height: 16.h),
_buildBankInputDetails(vm),
],
],
);
}
// Bank Transfer Input
Widget _buildBankInputDetails(RefundRequestViewModel vm) {
final hasError = vm.ibanError != null || vm.descriptionError != null;
return Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
),
child: Column(
children: [
TextInputWidget(
labelText: LocaleKeys.addIbanDetails.tr(),
hintText: "SA0000000000000000000000",
controller: _ibanController,
focusNode: _ibanFocusNode,
autoFocus: false,
keyboardType: TextInputType.text,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.bankIcon,
fontFamily: "Poppins",
hasError: false,
errorMessage: vm.ibanError,
onChange: (value) {
vm.clearIbanError();
},
onSubmitted: (value) {
_descriptionFocusNode.requestFocus();
},
),
Divider(color: AppColors.bottomNAVBorder, thickness: 1).withVerticalPadding(16.h),
TextInputWidget(
labelText: LocaleKeys.description.tr(),
hintText: LocaleKeys.enterDescription.tr(),
controller: _descriptionController,
focusNode: _descriptionFocusNode,
autoFocus: false,
keyboardType: TextInputType.text,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.descriptionIcon,
fontFamily: "Poppins",
hasError: false,
errorMessage: vm.descriptionError,
onChange: (value) {
vm.clearDescriptionError();
},
),
],
),
);
}
// Step 2 procedure checkboxes
Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) {
final isArabic = _appState.isArabic();
final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName;
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 400),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 24.w,
height: 24.w,
child: Transform.scale(
scale: 24.w / 22,
child: Checkbox(
value: procedure.isSelected,
onChanged: (_) => _refundRequestViewModel.toggleProcedureSelection(procedure),
activeColor: AppColors.primaryRedColor,
side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(child: name.toText16(isBold: true)),
SizedBox(width: 8.w),
Utils.getPaymentAmountWithSymbol(
procedure.patientPaidAmount.toStringAsFixed(2).toText20(
isBold: true,
isEnglishOnly: true,
color: AppColors.textColor,
),
AppColors.textColor,
10.f,
isSaudiCurrency: true,
),
],
),
if (procedure.isSelected) ...[
InkWell(
onTap: () => showRefundReasonBottomSheet(
context,
selectedReason: procedure.refundReason,
onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason),
),
child: Padding(
padding: EdgeInsets.only(top: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
(procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context))
.toText14(color: AppColors.textColor, isBold: true),
],
),
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
),
),
],
if (index != length - 1) ...[
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h),
]
],
),
),
),
),
),
),
),
);
}
// Bottom bar
Widget _buildBottomBar(RefundRequestViewModel refundRequestViewModel) {
final isStep2 = refundRequestViewModel.currentStep == 1;
final isWallet = refundRequestViewModel.selectedTab == RefundTabType.hmgWallet;
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!isWallet && refundRequestViewModel.selectedProcedures.isNotEmpty) ...[
Padding(
padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.totalAmount.tr().toText18(color: AppColors.textColor, isBold: true),
Utils.getPaymentAmountWithSymbol(
refundRequestViewModel.totalRefundableAmount.toStringAsFixed(2).toText24(
isBold: true,
isEnglishOnly: true,
color: AppColors.textColor,
),
AppColors.textColorLight,
14,
isSaudiCurrency: true,
),
],
),
),
],
CustomButton(
icon: isStep2 ? AppAssets.refundIcon : null,
text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context),
onPressed: () {
if (!isStep2) {
if (refundRequestViewModel.selectedProcedures.isNotEmpty) refundRequestViewModel.nextStep();
} else {
final isBankTransfer = refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices;
if (isBankTransfer) {
final isValid = refundRequestViewModel.validateBankTransferForm(
iban: _ibanController.text,
description: _descriptionController.text,
);
if (!isValid) return;
}
_handleInvoiceRefundRequestSubmit(refundRequestViewModel);
}
},
isDisabled: isStep2
? (refundRequestViewModel.selectedPaymentMethod == null ||
(refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices &&
(_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)))
: (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons),
).paddingSymmetrical(24.h, 24.h),
],
),
);
}
// HMG Wallet placeholder
Widget buildInvoiceCard() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: InvoiceListCard(onTap: null, getInvoicesListResponseModel: widget.invoice),
);
}
// Submit
void _handleInvoiceRefundRequestSubmit(RefundRequestViewModel vm) {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.notice.tr(context: context),
child: Utils.getWarningWidget(
loadingText: LocaleKeys.confirmSubmitRequest.tr(context: context),
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader();
final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices;
final invoice = widget.invoice;
final userPayload = _appState.getAuthenticatedUser();
final patientId = userPayload!.patientId;
final patientName = "${userPayload.firstName} ${userPayload.lastName}";
final patientMobileNumber = "${userPayload.mobileNumber}";
final patientNationalId = userPayload.patientIdentificationNo.toString();
final request = SubmitRefundRequestModel(
projectId: invoice.projectId ?? 0,
refundType: '1',
// 1 = invoice refund
refundProject: invoice.projectName ?? '',
refundProjectId: invoice.projectId ?? 0,
refundPatientId: patientId.toString(),
refundPatientName: patientName,
refundNationalId: patientNationalId,
patientMobileNumber: patientMobileNumber,
refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(),
refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(),
refundDoctorId: invoice.doctorId ?? 0,
refundDoctorName: invoice.doctorName ?? '',
refundClinicId: invoice.clinicId ?? 0,
refundClinicName: invoice.clinicName ?? '',
refundDestination: isBankTransfer ? '2' : '1',
refundIban: isBankTransfer ? _ibanController.text.trim() : '',
refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '',
refundTotalAmount: vm.totalRefundableAmount,
refundProceduresList: vm.selectedProcedures
.map((p) => SubmitRefundProcedureItem(
lineItemNo: p.apiLineItemNo ?? 0,
procedureId: p.apiProcedureId ?? '',
procedureName: p.procedureName,
patientPaidAmount: p.patientPaidAmount,
refundReason: p.refundReason ?? '',
))
.toList(),
);
await vm.submitInvoiceRefundRequest(
request: request,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
_showSuccessBottomSheet(response.refNo ?? '');
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
);
},
);
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
void _showSuccessBottomSheet(String refNo) {
showCommonBottomSheetWithoutHeight(
context,
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)),
if (refNo.isNotEmpty) ...[
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
LocaleKeys.hereIsYourRequestNumber.tr().toText16(
color: AppColors.textColorLight,
),
SizedBox(width: 6.w),
refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
],
).paddingSymmetrical(24.w, 0),
],
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
height: 56.h,
text: LocaleKeys.ok.tr(),
onPressed: () {
Navigator.pop(context); // pop success sheet
Navigator.pop(context); // pop refund page
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
),
),
);
},
textColor: AppColors.whiteColor,
),
),
],
),
],
),
),
isCloseButtonVisible: false,
isDismissible: false,
isFullScreen: false,
isAutoDismiss: false,
);
}
// Build
@override
Widget build(BuildContext context) {
_habibWalletViewModel = context.read<HabibWalletViewModel>();
return Consumer<RefundRequestViewModel>(
builder: (context, vm, _) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.requestRefund.tr(context: context),
leadingCallback: () {
final refundRequestViewModel = context.read<RefundRequestViewModel>();
if (refundRequestViewModel.currentStep > 0) {
refundRequestViewModel.previousStep();
} else {
context.pop();
}
},
requests: () => Navigator.of(context).push(CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 1 for invoice, 2 for wallet
))),
child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 32.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Stepper (invoices tab only)
if (vm.selectedTab == RefundTabType.invoices) ...[
buildInvoiceCard(),
_buildStepper(vm),
SizedBox(height: 16.h),
],
// Content
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
),
child: vm.currentStep == 0
? KeyedSubtree(
key: const ValueKey('step1'),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _buildStep1Content(vm),
),
)
: KeyedSubtree(
key: const ValueKey('step2'),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _buildStep2Content(vm),
),
),
),
],
),
),
),
),
_buildBottomBar(vm),
],
),
);
},
);
}
}

@ -0,0 +1,283 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
class RefundRequestDetailScreen extends StatelessWidget {
final RefundInvoiceModel invoice;
const RefundRequestDetailScreen({super.key, required this.invoice});
String _formatDate(String? rawDate) {
if (rawDate == null || rawDate.isEmpty) return '';
try {
final date = DateUtil.convertStringToDate(rawDate);
return DateFormat('MMM dd, yyyy').format(date);
} catch (_) {
return rawDate;
}
}
String rawDateForAdvance(String? rawDate) {
if (rawDate == null || rawDate.isEmpty) return '';
try {
final date = DateTime.parse(rawDate);
return DateFormat('MMM dd, yyyy').format(date);
} catch (_) {
return rawDate;
}
}
Color getStatusColor(int status) {
Color statusColor = AppColors.textColor;
if (status == 3) {
statusColor = AppColors.statusRejectedColor;
} else if (status == 2) {
statusColor = AppColors.statusCompletedColor;
} else if (status == 1) {
statusColor = AppColors.statusPendingColor;
}
return statusColor;
}
String getRefundDestinationIcon(String refundDestination) {
String icon = "";
if (refundDestination.toLowerCase().contains("wallet")) {
icon = AppAssets.habibLogoOnly;
} else if (refundDestination.toLowerCase().contains("bank")) {
icon = AppAssets.bankIconOnly;
}
return icon;
}
Widget _buildInvoiceCard(BuildContext context, RefundInvoiceModel? invoice, {bool isLoading = false}) {
final isInvoiceType = (invoice?.refundTypeId ?? 0) == 1;
final isWalletType = (invoice?.refundTypeId ?? 0) == 2;
final referenceNo = (invoice?.referenceNumber ?? '');
final lastUpdated = _formatDate(invoice?.lastUpdated);
final invoicePayload = invoice?.invoiceRequestPayload;
final walletPayload = invoice?.walletRequestPayload;
final totalProcedures = (invoicePayload?.proceduresList?.length.toString() ?? '');
final statusName = (invoice?.requestStatusName ?? '');
final statusId = (invoice?.requestStatusId ?? 1);
final refundDestination = (invoice?.refundDestination ?? '');
final refundType = invoice?.refundType ?? '';
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppCustomChipWidget(
icon: "",
labelText: statusName,
textColor: getStatusColor(statusId),
backgroundColor: getStatusColor(statusId).withValues(alpha: 0.2),
),
" $lastUpdated"
.toText12(
color: AppColors.textColorLight,
isBold: false,
)
.toShimmer2(isShow: isLoading),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"${LocaleKeys.referenceNo.tr(context: context)} $referenceNo"
.toText16(
color: AppColors.textColor,
isBold: true,
)
.toShimmer2(isShow: isLoading),
// lastUpdated.toText12(color: AppColors.textColorLight).toShimmer2(isShow: isLoading),
],
),
SizedBox(height: 8.h),
Wrap(
spacing: 6.w,
runSpacing: 6.h,
children: [
if (!isLoading && refundDestination.isNotEmpty)
AppCustomChipWidget(icon: getRefundDestinationIcon(refundDestination), iconHasColor: false, labelText: refundDestination),
if (refundType.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$refundType ${LocaleKeys.refund.tr()}"),
if (isInvoiceType && invoicePayload != null) ...[
if (totalProcedures.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$totalProcedures ${LocaleKeys.procedures.tr()}"),
if (invoicePayload.project != null && invoicePayload.project!.isNotEmpty)
AppCustomChipWidget(icon: '', labelText: invoicePayload.project ?? ''),
if (invoicePayload.clinicName != null && invoicePayload.clinicName!.isNotEmpty)
AppCustomChipWidget(icon: '', labelText: invoicePayload.clinicName ?? ''),
if (invoicePayload.doctorName != null && invoicePayload.doctorName!.isNotEmpty)
AppCustomChipWidget(icon: '', labelText: invoicePayload.doctorName ?? ''),
// AppCustomChipWidget(icon: '', labelText: invoicePayload?.refundTypeName ?? ''),
] else if (isWalletType && walletPayload != null) ...[
AppCustomChipWidget(icon: '', labelText: walletPayload.project ?? ''),
// AppCustomChipWidget(icon: '', labelText: invoicePayload?.refundTypeName ?? ''),
],
],
),
SizedBox(height: 16.h),
if (isInvoiceType) ...[
Utils.getPaymentAmountWithSymbol(
isExpanded: false,
('${invoicePayload?.totalRefundAmount ?? 0}').toText20(isBold: true, color: AppColors.textColor),
AppColors.textColor,
14,
isSaudiCurrency: true,
)
] else ...[
Utils.getPaymentAmountWithSymbol(
isExpanded: false,
('${walletPayload?.totalRefundAmount ?? 0}').toText20(isBold: true, color: AppColors.textColor),
AppColors.textColor,
14,
isSaudiCurrency: true,
)
]
],
),
),
);
}
Widget buildListDetails(BuildContext context, RefundInvoiceModel? invoice, {bool isLoading = false}) {
final isInvoiceType = (invoice?.refundTypeId ?? 0) == 1;
final isWalletType = (invoice?.refundTypeId ?? 0) == 2;
final invoicePayload = invoice?.invoiceRequestPayload;
final walletPayload = invoice?.walletRequestPayload;
bool shouldShowPadding = (isWalletType && walletPayload != null) || (isInvoiceType && invoicePayload != null);
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(shouldShowPadding ? 16.w : 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Summary info
if (isInvoiceType && invoicePayload != null) ...[
// Procedures header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true),
LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true),
],
),
SizedBox(height: 16.h),
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1),
SizedBox(height: 16.h),
if (invoicePayload.proceduresList != null && invoicePayload.proceduresList!.isNotEmpty)
...invoicePayload.proceduresList!.map((p) => _buildProcedureRow(context, p)),
],
if (isWalletType && walletPayload != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: LocaleKeys.advanceNumber.tr(context: context).toText12(color: AppColors.textColor, isBold: true)),
SizedBox(
width: 80.w,
child:
LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true, textAlignment: TextAlign.end),
),
],
),
SizedBox(height: 16.h),
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1),
SizedBox(height: 16.h),
if (walletPayload.advancesList != null && walletPayload.advancesList!.isNotEmpty)
...walletPayload.advancesList!.map((a) => _buildAdvanceRow(context, a)),
],
],
),
),
);
}
Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 3,
child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor),
),
SizedBox(width: 8.w),
(procedure.patientPaidAmount?.toString() ?? '-').toText12(color: AppColors.textColor, isBold: true),
],
),
);
}
Widget _buildAdvanceRow(BuildContext context, WalletAdvanceItem advance) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(advance.advanceNo ?? '').toText12(color: AppColors.textColor),
if (advance.advanceDate != null && advance.advanceDate!.isNotEmpty)
rawDateForAdvance(advance.advanceDate!).toText10(color: AppColors.greyTextColor),
],
),
),
SizedBox(
width: 80.w,
child: (advance.balanceAmount?.toStringAsFixed(2) ?? '').toText12(color: AppColors.textColor, isBold: true, textAlignment: TextAlign.end),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.refundDetails.tr(context: context),
isLeading: true,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInvoiceCard(context, invoice),
SizedBox(height: 16.h),
buildListDetails(context, invoice),
],
).paddingSymmetrical(24.w, 0),
),
);
}
}

@ -0,0 +1,111 @@
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/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart';
void showRefundReasonBottomSheet(
BuildContext context, {
required String? selectedReason,
required ValueChanged<String> onDone,
}) {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.selectRefundReason.tr(context: context),
isDismissible: true,
child: _RefundReasonSheetContent(
selectedReason: selectedReason,
onDone: onDone,
),
callBackFunc: () {},
);
}
class _RefundReasonSheetContent extends StatefulWidget {
final String? selectedReason;
final ValueChanged<String> onDone;
const _RefundReasonSheetContent({
required this.selectedReason,
required this.onDone,
});
@override
State<_RefundReasonSheetContent> createState() => _RefundReasonSheetContentState();
}
class _RefundReasonSheetContentState extends State<_RefundReasonSheetContent> {
String? _selected;
static List<String> _reasons(BuildContext context) => [
LocaleKeys.refundReason1.tr(context: context),
LocaleKeys.refundReason2.tr(context: context),
LocaleKeys.refundReason3.tr(context: context),
LocaleKeys.refundReason4.tr(context: context),
LocaleKeys.refundReason5.tr(context: context),
LocaleKeys.refundReason6.tr(context: context),
LocaleKeys.refundReason7.tr(context: context),
];
@override
void initState() {
super.initState();
_selected = widget.selectedReason;
}
@override
Widget build(BuildContext context) {
final reasons = _reasons(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: reasons.length,
separatorBuilder: (_, __) => Divider(
color: AppColors.bottomNAVBorder,
height: 1,
thickness: 1,
).paddingSymmetrical(16.w, 0.w),
itemBuilder: (context, index) {
final reason = reasons[index];
return RadioListTileWidget<String>(
value: reason,
groupValue: _selected,
title: reason,
onChanged: (value) {
if (value != null) setState(() => _selected = value);
},
);
},
),
),
SizedBox(height: 16.h),
CustomButton(
text: LocaleKeys.done.tr(context: context),
isDisabled: _selected == null,
onPressed: () {
if (_selected != null) {
widget.onDone(_selected!);
Navigator.of(context).pop();
}
},
).paddingSymmetrical(0, 0),
SizedBox(height: 8.h),
],
);
}
}

@ -77,8 +77,10 @@ class _TriagePageState extends State<TriagePage> {
highestProbability = (sortedConditions.first.probability ?? 0.0) * 100;
}
// Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered
if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) {
// Case 2: Should stop flag is true OR
// Case 3: Probability >= 70% OR
// Case 4: 30 or more questions answered
if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) {
// Navigate to results/possible conditions screen
context.navigateWithName(AppRoutes.possibleConditionsPage);
return;

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart';
import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart';
@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehen
import 'package:hmg_patient_app_new/presentation/covid19test/covid19_landing_page.dart';
import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart';
import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculators_page.dart';
import 'package:hmg_patient_app_new/presentation/health_trackers/add_health_tracker_entry_page.dart';
import 'package:hmg_patient_app_new/presentation/health_trackers/health_tracker_detail_page.dart';
@ -19,7 +21,11 @@ import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart';
import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart';
import 'package:hmg_patient_app_new/presentation/servicesPriceList/services_price_list_page.dart';
import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/presentation/service_price/services_price_list_page.dart';
import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_home_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart';
@ -41,7 +47,6 @@ import '../core/dependencies.dart';
import '../features/monthly_reports/monthly_reports_repo.dart';
import '../features/monthly_reports/monthly_reports_view_model.dart';
import '../features/qr_parking/qr_parking_view_model.dart';
import '../presentation/parking/paking_page.dart';
import '../services/error_handler_service.dart';
class AppRoutes {
@ -101,6 +106,12 @@ class AppRoutes {
// Weather
static const String familyFilesPage = '/familyFilesPage';
// Refund Request
static const String refundRequestCreatePage = '/refundRequestCreatePage';
static const String createWithdrawRequest = '/createWithdrawRequest';
static const String refundHistoryScreen = '/refundHistoryScreen';
static const String refundRequestDetailScreen = '/refundRequestDetailScreen';
static Map<String, WidgetBuilder> get routes => {
initialRoute: (context) => SplashPage(),
loginScreen: (context) => LoginScreen(),
@ -160,6 +171,17 @@ class AppRoutes {
qrParking: (context) => ChangeNotifierProvider<QrParkingViewModel>(
create: (_) => getIt<QrParkingViewModel>(),
child: const ParkingPage(),
)
),
refundRequestCreatePage: (context) {
final args = ModalRoute.of(context)?.settings.arguments as GetInvoicesListResponseModel;
return RefundRequestCreatePage(invoice: args);
},
createWithdrawRequest: (context) {
return WithdrawRequestCreatePage();
},
refundRequestDetailScreen: (context) {
final args = ModalRoute.of(context)?.settings.arguments as RefundInvoiceModel;
return RefundRequestDetailScreen(invoice: args);
},
};
}

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
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/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
@ -28,7 +27,6 @@ import 'package:lottie/lottie.dart';
import 'core/cache_consts.dart';
import 'core/utils/push_notification_handler.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@ -48,7 +46,6 @@ class _SplashScreenState extends State<SplashPage> {
PushNotificationHandler().init(context); // Asyncronously
},
);
// TODO: This next line should be tested. This stops the app at this step, sometimes
await authVm.getServicePrivilege();
Timer(Duration(seconds: 2, milliseconds: 500), () async {
bool isAppOpenedFromCall = getIt.get<CacheService>().getBool(key: CacheConst.isAppOpenedFromCall) ?? false;
@ -80,7 +77,6 @@ class _SplashScreenState extends State<SplashPage> {
}
navigateToTeleConsult() async {
String roomID = await Utils.getStringFromPrefs(CacheConst.zoomRoomID);
String callTypeID = await Utils.getStringFromPrefs(CacheConst.callTypeID);
@ -192,9 +188,7 @@ class _SplashScreenState extends State<SplashPage> {
String sessionIdFromCache = await Utils.getStringFromPrefs(CacheConst.zoomRoomID);
// Use event data if available, otherwise use cache
String sessionId = (sessionIdFromEvent != null && sessionIdFromEvent.isNotEmpty)
? sessionIdFromEvent
: sessionIdFromCache;
String sessionId = (sessionIdFromEvent != null && sessionIdFromEvent.isNotEmpty) ? sessionIdFromEvent : sessionIdFromCache;
print('🔍 Session ID from event: "$sessionIdFromEvent"');
print('🔍 Session ID from cache: "$sessionIdFromCache"');
@ -259,11 +253,7 @@ class _SplashScreenState extends State<SplashPage> {
final apiClient = getIt.get<ApiClient>();
Map<String, dynamic> requestBody = {
"Open_SessionID": sessionId,
"SessionStatus": sessionStatus,
"SessionEndedBy": sessionEndedBy
};
Map<String, dynamic> requestBody = {"Open_SessionID": sessionId, "SessionStatus": sessionStatus, "SessionEndedBy": sessionEndedBy};
await apiClient.post(
CHANGE_PATIENT_ER_SESSION,

@ -81,13 +81,18 @@ class CustomButton extends StatelessWidget {
children: [
if (icon != null)
Padding(
padding: text.isNotEmpty ? EdgeInsets.only(right: 4.w, left: 4.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isButtonDisabled, width: iconS, height: iconS, applyThemeColor: applyThemeColor),
padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(
icon: icon!,
iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor,
isDisabled: isButtonDisabled,
width: iconS,
height: iconS,
applyThemeColor: applyThemeColor),
),
if (text.isNotEmpty && icon != null)
Flexible(
child:
Text(
child: Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,

@ -8,29 +8,28 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:smooth_corner/smooth_corner.dart';
class AppCustomChipWidget extends StatelessWidget {
const AppCustomChipWidget({
super.key,
this.labelText,
this.textColor,
this.backgroundColor,
this.iconSize,
this.icon = "",
this.iconColor,
this.richText,
this.iconHasColor = true,
this.shape,
this.deleteIcon,
this.deleteIconSize,
this.deleteIconColor,
this.deleteIconHasColor = false,
this.padding = EdgeInsets.zero,
this.isIconPNG = false,
this.onChipTap,
this.labelPadding,
this.onDeleteTap,
this.applyThemeColor = true,
this.isEnglishOnly = false
});
const AppCustomChipWidget(
{super.key,
this.labelText,
this.textColor,
this.backgroundColor,
this.iconSize,
this.icon = "",
this.iconColor,
this.richText,
this.iconHasColor = true,
this.shape,
this.deleteIcon,
this.deleteIconSize,
this.deleteIconColor,
this.deleteIconHasColor = false,
this.padding = EdgeInsets.zero,
this.isIconPNG = false,
this.onChipTap,
this.labelPadding,
this.onDeleteTap,
this.applyThemeColor = true,
this.isEnglishOnly = false});
final String? labelText;
final Widget? richText;
@ -58,7 +57,7 @@ class AppCustomChipWidget extends StatelessWidget {
final iconS = iconSize ?? 12.w;
final resolvedTextColor = textColor ?? AppColors.textColor;
final resolvedBackgroundColor = backgroundColor ?? AppColors.greyColor;
final resolvedIconColor = iconColor ?? AppColors.textColor;
final resolvedIconColor = (iconColor ?? AppColors.textColor);
final resolvedDeleteIconColor = deleteIconColor ?? AppColors.textColor;
return GestureDetector(
onTap: onChipTap,
@ -89,7 +88,8 @@ class AppCustomChipWidget extends StatelessWidget {
applyThemeColor: applyThemeColor,
)
: SizedBox.shrink(),
label: richText ?? (labelText?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isEnglishOnly: isEnglishOnly),
label:
richText ?? (labelText ?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isEnglishOnly: isEnglishOnly),
padding: padding,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
labelPadding: labelPadding ?? EdgeInsetsDirectional.only(end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w),
@ -119,7 +119,9 @@ class AppCustomChipWidget extends StatelessWidget {
)
: Chip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
label: richText ?? (labelText?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isCenter: true, isEnglishOnly: isEnglishOnly),
label: richText ??
(labelText ?? "")
.toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isCenter: true, isEnglishOnly: isEnglishOnly),
padding: EdgeInsets.zero,
backgroundColor: resolvedBackgroundColor,
shape: shape ??
@ -133,7 +135,11 @@ class AppCustomChipWidget extends StatelessWidget {
? InkWell(
onTap: onDeleteTap,
child: Utils.buildSvgWithAssets(
icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, applyThemeColor: applyThemeColor))
icon: deleteIcon!,
width: iconS,
height: iconS,
iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null,
applyThemeColor: applyThemeColor))
: null,
onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null,
),

@ -2,12 +2,12 @@ import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/attachment_options.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_picker_android/image_picker_android.dart';
@ -21,12 +21,13 @@ class ImageOptions {
BuildContext context,
bool showFilesOption,
Function(String, File) image, {
String? title,
Future<bool> Function()? checkCameraPermission,
Future<bool> Function()? checkGalleryPermission,
}) {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.profilePicture.tr(context: context),
title: title ?? LocaleKeys.profilePicture.tr(context: context),
child: AttachmentOptions(
showFilesOption: showFilesOption,
onCameraTap: () async {
@ -78,11 +79,11 @@ class ImageOptions {
}
},
onFilesTap: () async {
FilePickerResult? result = await FilePicker.pickFiles(
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: [
'jpg',
'jpeg ',
'jpeg',
'pdf',
'txt',
'docx',
@ -223,7 +224,8 @@ void galleryImageAndroid(Function(String, File) image) async {
}
try {
final XFile? pickedFile = await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20));
final XFile? pickedFile =
await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20));
if (pickedFile == null) return;
@ -269,7 +271,9 @@ class _BottomSheet extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12.0),
decoration: BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0))),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0))),
child: SafeArea(
top: false,
child: Column(

@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -16,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calen
import 'package:hmg_patient_app_new/widgets/date_range_selector/models/calendar_design_type.dart';
import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart';
import 'package:hmg_patient_app_new/widgets/time_picker_widget.dart';
import 'dart:ui' as ui;
class TextInputWidget extends StatelessWidget {
final String labelText;
@ -54,51 +55,53 @@ class TextInputWidget extends StatelessWidget {
final Color? hintColor;
final bool? isHideSwitcher;
final bool? isArrowTrailing;
final bool leadingIconNeedsContainer;
final String? fontFamily;
// final List<Country> countryList;
// final Function(Country)? onCountryChange;
TextInputWidget(
{super.key,
required this.labelText,
required this.hintText,
this.controller,
this.onChange,
this.onCalendarTypeChanged,
this.prefix,
this.isEnable = true,
this.isBorderAllowed = true,
this.isAllowRadius = true,
this.isReadOnly = false,
this.keyboardType = TextInputType.number,
this.focusNode,
this.autoFocus = false,
this.padding,
this.isAllowLeadingIcon = false,
this.leadingIcon,
this.isCountryDropDown = false,
this.hasError = false,
this.errorMessage,
this.onCountryChange,
this.selectionType,
this.fontSize,
this.isWalletAmountInput = false,
this.suffix,
this.labelColor,
this.onSubmitted,
this.hintColor,
// multiline defaults
this.isMultiline = false,
this.minLines = 3,
this.maxLines = 6,
this.isHideSwitcher,
this.isArrowTrailing,
this.fontFamily,
// this.countryList = const [],
// this.onCountryChange,
this.btnTitle});
TextInputWidget({
super.key,
required this.labelText,
required this.hintText,
this.controller,
this.onChange,
this.onCalendarTypeChanged,
this.prefix,
this.isEnable = true,
this.isBorderAllowed = true,
this.isAllowRadius = true,
this.isReadOnly = false,
this.keyboardType = TextInputType.number,
this.focusNode,
this.autoFocus = false,
this.padding,
this.isAllowLeadingIcon = false,
this.leadingIcon,
this.isCountryDropDown = false,
this.hasError = false,
this.errorMessage,
this.onCountryChange,
this.selectionType,
this.fontSize,
this.isWalletAmountInput = false,
this.suffix,
this.labelColor,
this.onSubmitted,
this.hintColor,
// multiline defaults
this.isMultiline = false,
this.minLines = 3,
this.maxLines = 6,
this.isHideSwitcher,
this.isArrowTrailing,
this.fontFamily,
this.leadingIconNeedsContainer = true,
// this.countryList = const [],
// this.onCountryChange,
this.btnTitle,
});
final FocusNode _focusNode = FocusNode();
@ -220,16 +223,19 @@ class TextInputWidget extends StatelessWidget {
}
Widget _buildLeadingIcon(BuildContext context) {
return Container(
height: 40.h,
width: 40.h,
// margin: EdgeInsets.symmetric(horizontal: 10.w),
padding: EdgeInsets.all(8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
borderRadius: 12.r,
color: AppColors.greyColor,
),
child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false));
if (leadingIconNeedsContainer) {
return Container(
height: 40.h,
width: 40.h,
// margin: EdgeInsets.symmetric(horizontal: 10.w),
padding: EdgeInsets.all(8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
borderRadius: 12.r,
color: AppColors.greyColor,
),
child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false));
}
return Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false, height: 40.h, width: 40.h);
}
Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) {
@ -403,7 +409,9 @@ class TextInputWidget extends StatelessWidget {
hintLocales: const [Locale('en', 'US')],
enabled: isEnable,
scrollPadding: EdgeInsets.zero,
keyboardType: isMultiline ? TextInputType.multiline : (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType),
keyboardType: isMultiline
? TextInputType.multiline
: (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType),
controller: controller,
readOnly: isReadOnly,
textAlignVertical: TextAlignVertical.top,

@ -1042,13 +1042,13 @@ packages:
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
dependency: "direct main"
description:
name: image_picker_android
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev"
source: hosted
version: "0.8.13+10"
version: "0.8.13+17"
image_picker_for_web:
dependency: transitive
description:
@ -1082,7 +1082,7 @@ packages:
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
dependency: "direct main"
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"

Loading…
Cancel
Save