Refund Module Completed

pull/322/head
faizatflutter 2 months ago
parent 64da8f1804
commit 897bd30879

@ -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"> <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 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"/> <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,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

@ -1858,7 +1858,7 @@
"submitRequest": "تقديم الطلب", "submitRequest": "تقديم الطلب",
"ibanIsRequired": "حقل رقم الآيبان مطلوب", "ibanIsRequired": "حقل رقم الآيبان مطلوب",
"descriptionIsRequired": "الوصف مطلوب", "descriptionIsRequired": "الوصف مطلوب",
"invalidIbanFormat": "صيغة الآيبان غير صحيحة. يجب أن تبدأ بـ SA يليها 20 رقمًا (مثال: SA0380000000608010167519)", "invalidIbanFormat": "رقم الآيبان غير صالح. يرجى إدخال رقم آيبان صالح (مثال: SA0380000000608010167519)",
"refundHistory": "سجل الاسترداد", "refundHistory": "سجل الاسترداد",
"noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.", "noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.",
"invoiceNo": "رقم الفاتورة", "invoiceNo": "رقم الفاتورة",
@ -1886,5 +1886,11 @@
"points": "نقاط", "points": "نقاط",
"transactions": "المعاملات", "transactions": "المعاملات",
"pharmacy": "الصيدلية", "pharmacy": "الصيدلية",
"visitsOrders": "الزيارات/الطلبات" "visitsOrders": "الزيارات/الطلبات",
"cannotOpenThisFile": "لا يمكن فتح هذا الملف",
"refundDetails": "تفاصيل الاسترداد",
"requestNo": "رقم الطلب.",
"referenceNo": "رقم المرجع",
"advanceNumber": "رقم الدفعة المقدمة",
"amountOnly": "المبلغ"
} }

@ -1850,7 +1850,7 @@
"submitRequest": "Submit Request", "submitRequest": "Submit Request",
"ibanIsRequired": "IBAN is required", "ibanIsRequired": "IBAN is required",
"descriptionIsRequired": "Description is required", "descriptionIsRequired": "Description is required",
"invalidIbanFormat": "Invalid IBAN. Must start with SA followed by 20 digits (e.g. SA0380000000608010167519)", "invalidIbanFormat": "Invalid IBAN. Please enter a valid IBAN (e.g. SA0380000000608010167519)",
"refundHistory": "Refund History", "refundHistory": "Refund History",
"noRefundHistoryFound": "You don't have any refund history yet.", "noRefundHistoryFound": "You don't have any refund history yet.",
"invoiceNo": "Invoice No.", "invoiceNo": "Invoice No.",
@ -1873,12 +1873,20 @@
"refundReason6": "Clinical procedure cancelled or changed by requesting doctor", "refundReason6": "Clinical procedure cancelled or changed by requesting doctor",
"refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor", "refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor",
"selectProcedures": "Select Procedures", "selectProcedures": "Select Procedures",
"submitEReferral": "Submit",
"redeem": "Redeem", "redeem": "Redeem",
"points": "Points", "points": "Points",
"transactions": "Transactions", "transactions": "Transactions",
"pharmacy": "Pharmacy", "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"
} }

@ -208,9 +208,9 @@ class ApiClientImp implements ApiClient {
body['TokenID'] = "@dm!n"; body['TokenID'] = "@dm!n";
} }
// TODO: REMOVE THIS MUST
// body['TokenID'] = "@dm!n"; // body['TokenID'] = "@dm!n";
// body['PatientID'] = 4768571; // body['PatientID'] = 4770714;
// body['PatientID'] = 89754444;
// body['PatientTypeID'] = 1; // body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0; // body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q"; // body['SessionID'] = "45786230487560q";

@ -276,6 +276,7 @@ class ApiConsts {
static String getRefundStatus = "Services/COCWS.svc/REST/GetRefundStatus"; static String getRefundStatus = "Services/COCWS.svc/REST/GetRefundStatus";
static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices"; static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices";
static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund"; 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 getAiAnalysisURL = "https://api.labpredict.com";
static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology";

@ -390,6 +390,8 @@ class AppAssets {
static const String refundIcon = '$svgBasePath/refund_icon.svg'; static const String refundIcon = '$svgBasePath/refund_icon.svg';
static const String bankIcon = '$svgBasePath/bank_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 descriptionIcon = '$svgBasePath/description_icon.svg';
static const String selectImageIcon = '$svgBasePath/select_image_icon.svg'; static const String selectImageIcon = '$svgBasePath/select_image_icon.svg';

@ -339,17 +339,15 @@ class AppDependencies {
getIt.registerLazySingleton<AskDoctorViewModel>(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton<AskDoctorViewModel>(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(
() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(
() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton<RefundRequestViewModel>(() => RefundRequestViewModel( getIt.registerLazySingleton<RefundRequestViewModel>(() => RefundRequestViewModel(
appState: getIt(), appState: getIt(),
refundRequestRepo: getIt(), refundRequestRepo: getIt(),
errorHandlerService: getIt(), errorHandlerService: getIt(),
)); ));
getIt.registerLazySingleton<ServicesPriceListViewModel>(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton<ServicesPriceListViewModel>(
getIt.registerLazySingleton<DateRangCalenderModel>(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(
() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt())); getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt()));
} }

@ -193,9 +193,37 @@ class ValidationUtils {
return value == null || value.trim().isEmpty; return value == null || value.trim().isEmpty;
} }
/// Validates a KSA IBAN: must start with SA + 2 check digits + 20 alphanumeric chars (24 chars total). static bool validateIban(String iban) {
static bool validateKsaIban(String iban) { final normalized = iban.replaceAll(RegExp(r'\s+'), '').toUpperCase();
final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false);
return ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', '')); // 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;
} }
} }

@ -1,16 +1,16 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/core/app_state.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/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.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/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/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/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/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/req_models/submit_refund_request_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/models/resp_models/refund_request_list_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/features/refund_request/refund_request_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -161,7 +161,7 @@ class HabibWalletViewModel extends ChangeNotifier {
_ibanError = LocaleKeys.ibanIsRequired.tr(); _ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false; isValid = false;
} else { } else {
if (!ValidationUtils.validateKsaIban(iban.trim())) { if (!ValidationUtils.validateIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr(); _ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false; isValid = false;
} }
@ -235,7 +235,18 @@ class HabibWalletViewModel extends ChangeNotifier {
); );
} }
Future<void> submitWalletWithdrawal({ /// 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 iban,
required String remarks, required String remarks,
required String patientName, required String patientName,
@ -255,13 +266,15 @@ class HabibWalletViewModel extends ChangeNotifier {
advanceNo: (a.advanceNo ?? 0).toString(), advanceNo: (a.advanceNo ?? 0).toString(),
paidAmount: a.paidAmount ?? 0.0, paidAmount: a.paidAmount ?? 0.0,
balanceAmount: a.balanceAmount ?? 0.0, balanceAmount: a.balanceAmount ?? 0.0,
advanceDate: a.advanceDate ?? '', advanceDate: a.advanceDate ?? "",
// advanceDate: _formatAdvanceDate(a.advanceDate),
)) ))
.toList(); .toList();
final request = SubmitRefundRequestModel( final request = SubmitRefundRequestModel(
projectId: projectId, projectId: projectId,
refundType: '2', // wallet withdrawal refundType: '2',
// wallet withdrawal
refundProject: projectName, refundProject: projectName,
refundProjectId: projectId, refundProjectId: projectId,
refundPatientId: patientId, refundPatientId: patientId,
@ -274,7 +287,7 @@ class HabibWalletViewModel extends ChangeNotifier {
refundDoctorName: '', refundDoctorName: '',
refundClinicId: 0, refundClinicId: 0,
refundClinicName: '', refundClinicName: '',
refundDestination: '2', // bank transfer refundDestination: '2', // only bank transfer
refundIban: iban, refundIban: iban,
refundRemarks: remarks, refundRemarks: remarks,
refundTotalAmount: totalAdvancesBalance, refundTotalAmount: totalAdvancesBalance,
@ -522,7 +535,8 @@ class HabibWalletViewModel extends ChangeNotifier {
lakumAccountInfo = LakumInquiryInformationResponseModel(); lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners(); 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( result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure), (failure) async => await errorHandlerService.handleError(failure: failure),

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

@ -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?,
);
}
}

@ -13,10 +13,10 @@ class SubmitRefundResponseModel {
factory SubmitRefundResponseModel.fromJson(Map<String, dynamic> json) { factory SubmitRefundResponseModel.fromJson(Map<String, dynamic> json) {
return SubmitRefundResponseModel( return SubmitRefundResponseModel(
message: json['message'] as String?, message: (json['Message'] ?? json['message']) as String?,
refundType: json['refundType'] as String?, refundType: (json['Refund_RefundTypeDisplay'] ?? json['refundType'])?.toString(),
patientId: json['patientId']?.toString(), patientId: (json['Refund_PatientIdDisplay'] ?? json['patientId'])?.toString(),
refNo: json['refNo']?.toString(), refNo: (json['Refund_RefNo'] ?? json['ReferenceNumber'] ?? json['refNo'])?.toString(),
); );
} }
} }
@ -53,40 +53,260 @@ class RefundStatusModel {
} }
} }
class RefundInvoiceModel { class WalletAdvanceItem {
final String? invoiceNumber; final String? advanceDate;
final String? invoiceDate; final String? advanceNo;
final double? totalAmount; final double? balanceAmount;
final double? patientShare; 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 String? clinicName;
final String? clinicNameN; 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 int? projectId;
final String? projectName; final String? refundAmountType;
final String? projectNameN; final String? refundAmountTypeName;
final String? refundDestination;
final String? refundDestinationName;
final double? refundTaxAmount;
final String? refundType;
final String? refundTypeName;
final String? remarks;
final double? totalRefundAmount;
RefundInvoiceModel({ InvoiceRequestPayload({
this.invoiceNumber, this.advancesList,
this.invoiceDate, this.appointmentNo,
this.totalAmount, this.clinicId,
this.patientShare,
this.clinicName, this.clinicName,
this.clinicNameN, 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.projectId,
this.projectName, this.refundAmountType,
this.projectNameN, 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) { factory RefundInvoiceModel.fromJson(Map<String, dynamic> json) {
return RefundInvoiceModel( return RefundInvoiceModel(
invoiceNumber: json['InvoiceNumber'] as String?, invoiceNumber: json['InvoiceNumber'] as String?,
invoiceDate: json['InvoiceDate'] as String?, lastUpdated: json['LastUpdated'] as String?,
totalAmount: (json['TotalAmount'] as num?)?.toDouble(), patientId: json['PatientID'] as String?,
patientShare: (json['PatientShare'] as num?)?.toDouble(), pendingVerification: json['PendingVerification'] as bool?,
clinicName: json['ClinicName'] as String?, referenceNumber: json['ReferenceNumber'] as String?,
clinicNameN: json['ClinicNameN'] as String?, refundDestination: json['RefundDestination'] as String?,
projectId: json['ProjectID'] as int?, refundDestinationId: json['RefundDestinationId'] as int?,
projectName: json['ProjectName'] as String?, refundType: json['RefundType'] as String?,
projectNameN: json['ProjectNameN'] 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,
); );
} }
} }

@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.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/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/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/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -20,9 +21,18 @@ abstract class RefundRequestRepo {
Future<Either<Failure, GenericApiModel<RefundStatusModel>>> getRefundStatus({required String referenceNumber}); Future<Either<Failure, GenericApiModel<RefundStatusModel>>> getRefundStatus({required String referenceNumber});
Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({required String patientId, String invoiceNumberQuery}); 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<SubmitRefundResponseModel>>> submitRefund({required SubmitRefundRequestModel request});
Future<Either<Failure, GenericApiModel<IsRefundableInvoiceObject>>> checkIsRefundableInvoice({
required int invoiceNo,
required int projectId,
});
} }
class RefundRequestRepoImp implements RefundRequestRepo { class RefundRequestRepoImp implements RefundRequestRepo {
@ -215,10 +225,12 @@ class RefundRequestRepoImp implements RefundRequestRepo {
@override @override
Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({ Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({
required int refundType,
required String patientId, required String patientId,
String invoiceNumberQuery = "", String invoiceNumberQuery = "",
}) async { }) async {
final Map<String, dynamic> body = { final Map<String, dynamic> body = {
"Refund_RefundType": refundType,
"Refund_PatientId": patientId, "Refund_PatientId": patientId,
"Refund_InvoiceNumberQuery": invoiceNumberQuery, "Refund_InvoiceNumberQuery": invoiceNumberQuery,
}; };
@ -235,7 +247,7 @@ class RefundRequestRepoImp implements RefundRequestRepo {
}, },
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
final listJson = response['RefundInvoicesList']; final listJson = response['Refund_InvoicesList'];
final List<RefundInvoiceModel> invoices = final List<RefundInvoiceModel> invoices =
listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map<String, dynamic>)).toList() : []; listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map<String, dynamic>)).toList() : [];
@ -297,4 +309,52 @@ class RefundRequestRepoImp implements RefundRequestRepo {
return Left(UnknownFailure(e.toString())); 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()));
}
}
} }

@ -4,6 +4,7 @@ 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/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/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/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/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/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/features/refund_request/refund_request_repo.dart';
@ -28,7 +29,7 @@ class RefundableProcedure {
final int procedureId; final int procedureId;
final String procedureName; final String procedureName;
final String? procedureNameN; final String? procedureNameN;
final double patientShare; final double patientPaidAmount;
bool isSelected; bool isSelected;
String? refundReason; String? refundReason;
@ -40,7 +41,7 @@ class RefundableProcedure {
required this.procedureId, required this.procedureId,
required this.procedureName, required this.procedureName,
this.procedureNameN, this.procedureNameN,
required this.patientShare, required this.patientPaidAmount,
this.isSelected = false, this.isSelected = false,
this.refundReason, this.refundReason,
this.apiLineItemNo, this.apiLineItemNo,
@ -240,7 +241,7 @@ class RefundRequestViewModel extends ChangeNotifier {
double get totalRefundableAmount { double get totalRefundableAmount {
double total = 0.0; double total = 0.0;
for (final p in selectedProcedures) { for (final p in selectedProcedures) {
total += p.patientShare; total += p.patientPaidAmount;
} }
return total; return total;
} }
@ -276,7 +277,7 @@ class RefundRequestViewModel extends ChangeNotifier {
_ibanError = LocaleKeys.ibanIsRequired.tr(); _ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false; isValid = false;
} else { } else {
if (!ValidationUtils.validateKsaIban(iban.trim())) { if (!ValidationUtils.validateIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr(); _ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false; isValid = false;
} }
@ -457,7 +458,7 @@ class RefundRequestViewModel extends ChangeNotifier {
bool isRefundInvoicesLoading = false; bool isRefundInvoicesLoading = false;
Future<void> getRefundInvoices({ Future<void> getRefundInvoices({
String? patientId, required int refundType,
String invoiceNumberQuery = "", String invoiceNumberQuery = "",
Function(List<RefundInvoiceModel>)? onSuccess, Function(List<RefundInvoiceModel>)? onSuccess,
Function(String)? onError, Function(String)? onError,
@ -466,10 +467,10 @@ class RefundRequestViewModel extends ChangeNotifier {
refundInvoicesList.clear(); refundInvoicesList.clear();
notifyListeners(); notifyListeners();
// final resolvedPatientId = patientId ?? appState.getAuthenticatedUser()?.patientId?.toString() ?? ""; final resolvedPatientId = appState.getAuthenticatedUser()?.patientId?.toString() ?? "";
final resolvedPatientId = 4768571;
final result = await refundRequestRepo.getRefundInvoices( final result = await refundRequestRepo.getRefundInvoices(
patientId: resolvedPatientId.toString(), refundType: refundType,
patientId: resolvedPatientId,
invoiceNumberQuery: invoiceNumberQuery, invoiceNumberQuery: invoiceNumberQuery,
); );
@ -502,7 +503,7 @@ class RefundRequestViewModel extends ChangeNotifier {
// Submit Refund // Submit Refund
bool isSubmitting = false; bool isSubmitting = false;
Future<void> submitRefund({ Future<void> submitInvoiceRefundRequest({
required SubmitRefundRequestModel request, required SubmitRefundRequestModel request,
Function(SubmitRefundResponseModel)? onSuccess, Function(SubmitRefundResponseModel)? onSuccess,
Function(String)? onError, Function(String)? onError,
@ -535,4 +536,34 @@ class RefundRequestViewModel extends ChangeNotifier {
}, },
); );
} }
// 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");
}
}
},
);
}
} }

@ -1871,5 +1871,12 @@ abstract class LocaleKeys {
static const transactions = 'transactions'; static const transactions = 'transactions';
static const pharmacy = 'pharmacy'; static const pharmacy = 'pharmacy';
static const visitsOrders = 'visitsOrders'; 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';
} }

@ -226,7 +226,9 @@ class _RegisterNew extends State<RegisterNewStep2> {
Divider(height: 1.h, color: AppColors.greyColor), Divider(height: 1.h, color: AppColors.greyColor),
TextInputWidget( TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context), labelText: LocaleKeys.nationalIdNumber.tr(context: context),
hintText: authVM!.isUserFromUAE() ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : (appState.getNHICUserData.idNumber ?? ""), hintText: authVM!.isUserFromUAE()
? appState.getUserRegistrationPayload.patientIdentificationId.toString()
: (appState.getNHICUserData.idNumber ?? ""),
controller: null, controller: null,
isEnable: true, isEnable: true,
prefix: null, prefix: null,
@ -339,7 +341,8 @@ class _RegisterNew extends State<RegisterNewStep2> {
), ),
Divider(height: 1.h, color: AppColors.greyColor), Divider(height: 1.h, color: AppColors.greyColor),
authVM!.isUserFromUAE() authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, ({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( ? Selector<AuthenticationViewModel,
({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>(
selector: (context, authViewModel) { selector: (context, authViewModel) {
final appState = getIt.get<AppState>(); final appState = getIt.get<AppState>();
return ( return (
@ -348,7 +351,10 @@ class _RegisterNew extends State<RegisterNewStep2> {
isArabic: appState.isArabic(), isArabic: appState.isArabic(),
); );
}, },
shouldRebuild: (previous, next) => previous.countriesList != next.countriesList || previous.selectedCountry != next.selectedCountry || previous.isArabic != next.isArabic, shouldRebuild: (previous, next) =>
previous.countriesList != next.countriesList ||
previous.selectedCountry != next.selectedCountry ||
previous.isArabic != next.isArabic,
builder: (context, data, child) { builder: (context, data, child) {
final authVM = context.read<AuthenticationViewModel>(); final authVM = context.read<AuthenticationViewModel>();
return DropdownWidget( return DropdownWidget(
@ -375,8 +381,16 @@ class _RegisterNew extends State<RegisterNewStep2> {
: TextInputWidget( : TextInputWidget(
labelText: LocaleKeys.nationality.tr(context: context), labelText: LocaleKeys.nationality.tr(context: context),
hintText: appState.isArabic() hintText: appState.isArabic()
? (authVM!.countriesList!.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), orElse: () => NationalityCountries()).nameN ?? "") ? (authVM!.countriesList!
: (authVM!.countriesList!.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), orElse: () => NationalityCountries()).name ?? ""), .firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""),
orElse: () => NationalityCountries())
.nameN ??
"")
: (authVM!.countriesList!
.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""),
orElse: () => NationalityCountries())
.name ??
""),
isEnable: true, isEnable: true,
prefix: null, prefix: null,
isAllowRadius: false, isAllowRadius: false,
@ -422,7 +436,9 @@ class _RegisterNew extends State<RegisterNewStep2> {
), ),
TextInputWidget( TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context), labelText: LocaleKeys.dob.tr(context: context),
hintText: authVM!.isUserFromUAE() ? (appState.getUserRegistrationPayload.dob ?? '') : (appState.getNHICUserData.dateOfBirth ?? ""), hintText: authVM!.isUserFromUAE()
? (appState.getUserRegistrationPayload.dob ?? '')
: (appState.getNHICUserData.dateOfBirth ?? ""),
controller: authVM!.isUserFromUAE() ? authVM!.dobController : null, controller: authVM!.isUserFromUAE() ? authVM!.dobController : null,
isEnable: false, isEnable: false,
prefix: null, prefix: null,

@ -185,9 +185,17 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Widget _buildBankInputDetails() { Widget _buildBankInputDetails() {
return Consumer<HabibWalletViewModel>( return Consumer<HabibWalletViewModel>(
builder: (context, viewModel, _) { builder: (context, viewModel, _) {
final hasError = viewModel.ibanError != null || viewModel.withdrawDescriptionError != null;
return Container( return Container(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
),
child: Column( child: Column(
children: [ children: [
TextInputWidget( TextInputWidget(
@ -195,7 +203,6 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
hintText: "SA0000000000000000000000", hintText: "SA0000000000000000000000",
controller: _bankDetailsController, controller: _bankDetailsController,
focusNode: _ibanFocusNode, focusNode: _ibanFocusNode,
leadingIconNeedsContainer: false,
autoFocus: false, autoFocus: false,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
isEnable: true, isEnable: true,
@ -207,7 +214,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
isCountryDropDown: false, isCountryDropDown: false,
leadingIcon: AppAssets.bankIcon, leadingIcon: AppAssets.bankIcon,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: viewModel.ibanError != null, hasError: false,
errorMessage: viewModel.ibanError, errorMessage: viewModel.ibanError,
onChange: (value) { onChange: (value) {
viewModel.clearIbanError(); viewModel.clearIbanError();
@ -224,7 +231,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
focusNode: _descriptionFocusNode, focusNode: _descriptionFocusNode,
autoFocus: false, autoFocus: false,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
leadingIconNeedsContainer: false, // leadingIconNeedsContainer: false,
isEnable: true, isEnable: true,
isReadOnly: false, isReadOnly: false,
prefix: null, prefix: null,
@ -234,7 +241,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
isCountryDropDown: false, isCountryDropDown: false,
leadingIcon: AppAssets.descriptionIcon, leadingIcon: AppAssets.descriptionIcon,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: viewModel.withdrawDescriptionError != null, hasError: false,
errorMessage: viewModel.withdrawDescriptionError, errorMessage: viewModel.withdrawDescriptionError,
onChange: (value) { onChange: (value) {
viewModel.clearWithdrawDescriptionError(); viewModel.clearWithdrawDescriptionError();
@ -301,10 +308,9 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
onConfirmTap: () async { onConfirmTap: () async {
Navigator.pop(context); Navigator.pop(context);
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
final user = _appState.getAuthenticatedUser(); final user = _appState.getAuthenticatedUser();
final hospital = vm.selectedHospital; final hospital = vm.selectedHospital;
await vm.submitWalletWithdrawal( await vm.submitWalletWithdrawalRequest(
iban: _bankDetailsController.text.trim(), iban: _bankDetailsController.text.trim(),
remarks: _descriptionController.text.trim(), remarks: _descriptionController.text.trim(),
patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(), patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(),
@ -342,16 +348,36 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
child: Column( child: Column(
children: [ children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), 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), SizedBox(height: 24.h),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: CustomButton( child: CustomButton(
height: 56.h, height: 56.h,
text: LocaleKeys.ok.tr(context: context), text: LocaleKeys.ok.tr(),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context); // pop success sheet
Navigator.pop(context); Navigator.pop(context); // pop refund page
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
),
),
);
}, },
textColor: AppColors.whiteColor, textColor: AppColors.whiteColor,
), ),
@ -364,7 +390,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
isCloseButtonVisible: false, isCloseButtonVisible: false,
isDismissible: false, isDismissible: false,
isFullScreen: false, isFullScreen: false,
callBackFunc: () {}, isAutoDismiss: false,
); );
} }
@ -381,7 +407,13 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Expanded( Expanded(
child: CollapsingListView( child: CollapsingListView(
title: LocaleKeys.requestRefund.tr(context: context), title: LocaleKeys.requestRefund.tr(context: context),
requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), requests: () => Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 2, // 2 for wallet, 1 for invoice
),
),
),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w), padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w),
child: Column( child: Column(
@ -391,8 +423,8 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
if (habibWalletViewModel.selectedHospital != null) ...[ if (habibWalletViewModel.selectedHospital != null) ...[
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildBankInputDetails(), _buildBankInputDetails(),
SizedBox(height: 16.h), // SizedBox(height: 16.h),
_buildImagePickerDropDown(), // _buildImagePickerDropDown(),
], ],
], ],
), ),
@ -407,3 +439,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
); );
} }
} }
// 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}

@ -137,7 +137,8 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
} }
}, },
).paddingSymmetrical(24.h, 0.h), ).paddingSymmetrical(24.h, 0.h),
myInvoicesVM.selectedTabIndex == 0 ? Column( myInvoicesVM.selectedTabIndex == 0
? Column(
children: [ children: [
SizedBox(height: 16.h), SizedBox(height: 16.h),
Row( Row(
@ -148,7 +149,9 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
myInvoicesViewModel.filterInvoices(InvoiceFilterType.all); myInvoicesViewModel.filterInvoices(InvoiceFilterType.all);
}, },
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor, textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -163,8 +166,11 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showHospitalFilterBottomSheet(context); _showHospitalFilterBottomSheet(context);
}, },
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor:
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), 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, textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -179,8 +185,11 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showClinicFilterBottomSheet(context); _showClinicFilterBottomSheet(context);
}, },
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor:
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), 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, textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -195,8 +204,11 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showDoctorFilterBottomSheet(context); _showDoctorFilterBottomSheet(context);
}, },
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor:
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), 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, textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -233,14 +245,16 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
onTap: () async { onTap: () async {
if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) { if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); LoaderBottomSheet.showLoader(
loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context));
await myInvoicesViewModel.sendInvoiceEmail( await myInvoicesViewModel.sendInvoiceEmail(
appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!, projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onSuccess: (val) { onSuccess: (val) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context, showCommonBottomSheetWithoutHeight(context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), child: Utils.getSuccessWidget(
loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)),
callBackFunc: () {}, callBackFunc: () {},
isFullScreen: false, isFullScreen: false,
isCloseButtonVisible: true, isCloseButtonVisible: true,
@ -275,13 +289,14 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
onSuccess: (value) async { onSuccess: (value) async {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) { if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); String path = await Utils.createFileFromString(
myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf");
try { try {
OpenFilex.open(path); OpenFilex.open(path);
} catch (ex) { } catch (ex) {
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"), child: Utils.getErrorWidget(loadingText: LocaleKeys.cannotOpenThisFile.tr()),
callBackFunc: () {}, callBackFunc: () {},
isFullScreen: false, isFullScreen: false,
isCloseButtonVisible: true, isCloseButtonVisible: true,
@ -292,35 +307,6 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
); );
} }
}, },
// 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,
// );
// });
// },
), ),
), ),
), ),
@ -391,7 +377,8 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
onSuccess: (value) async { onSuccess: (value) async {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) { if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); String path = await Utils.createFileFromString(
myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf");
try { try {
OpenFilex.open(path); OpenFilex.open(path);
} catch (ex) { } catch (ex) {

@ -11,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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_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/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/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/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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.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/chip/app_custom_chip_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:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class InvoiceListCard extends StatelessWidget { class InvoiceListCard extends StatelessWidget {
final GetInvoicesListResponseModel getInvoicesListResponseModel; final GetInvoicesListResponseModel getInvoicesListResponseModel;
@ -24,6 +28,46 @@ class InvoiceListCard extends StatelessWidget {
InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppState appState = getIt<AppState>(); AppState appState = getIt<AppState>();
@ -174,11 +218,7 @@ class InvoiceListCard extends StatelessWidget {
child: CustomButton( child: CustomButton(
text: LocaleKeys.requestRefund.tr(context: context), text: LocaleKeys.requestRefund.tr(context: context),
onPressed: () { onPressed: () {
Navigator.of(context).push( _handleRefundInvoiceTap(context);
CustomPageRoute(
page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel),
),
);
}, },
icon: AppAssets.refundIcon, icon: AppAssets.refundIcon,
iconColor: AppColors.caloriesCalculatorColor, iconColor: AppColors.caloriesCalculatorColor,

@ -3,8 +3,8 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
@ -12,43 +12,40 @@ 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/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/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/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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class RefundHistoryScreen extends StatefulWidget { class RefundHistoryScreen extends StatefulWidget {
const RefundHistoryScreen({super.key}); final int refundType;
const RefundHistoryScreen({super.key, required this.refundType});
@override @override
State<RefundHistoryScreen> createState() => _RefundHistoryScreenState(); State<RefundHistoryScreen> createState() => _RefundHistoryScreenState();
} }
class _RefundHistoryScreenState extends State<RefundHistoryScreen> { class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
late AppState _appState;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_appState = getIt<AppState>();
final vm = context.read<RefundRequestViewModel>(); final vm = context.read<RefundRequestViewModel>();
scheduleMicrotask(() async { scheduleMicrotask(() async {
await vm.getRefundInvoices(); await vm.getRefundInvoices(refundType: widget.refundType);
}); });
} }
// Helpers <EFBFBD><EFBFBD><EFBFBD> // Helpers
String _formatDate(String? rawDate) { String _formatDate(String? rawDate) {
if (rawDate == null || rawDate.isEmpty) return ''; if (rawDate == null || rawDate.isEmpty) return '';
try { try {
// Handle WCF /Date(...)/ format final date = DateUtil.convertStringToDate(rawDate);
final match = RegExp(r'/Date\((-?\d+)').firstMatch(rawDate); return DateFormat('MMM dd, yyyy').format(date);
if (match != null) {
final ms = int.parse(match.group(1)!);
return DateFormat('MMM dd, yyyy').format(DateTime.fromMillisecondsSinceEpoch(ms));
}
return DateFormat('MMM dd, yyyy').format(DateTime.parse(rawDate));
} catch (_) { } catch (_) {
return rawDate; return rawDate;
} }
@ -69,17 +66,49 @@ class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
// Card // Card
Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) { Color getStatusColor(int status) {
final isArabic = _appState.isArabic(); 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;
}
final clinicName = isLoading ? 'Clinic Name' : (isArabic ? (invoice?.clinicNameN ?? invoice?.clinicName) : invoice?.clinicName) ?? '-'; String getRefundDestinationIcon(String refundDestination) {
String icon = "";
final projectName = isLoading ? 'Hospital Name' : (isArabic ? (invoice?.projectNameN ?? invoice?.projectName) : invoice?.projectName) ?? '-'; if (refundDestination.toLowerCase().contains("wallet")) {
icon = AppAssets.habibLogoOnly;
} else if (refundDestination.toLowerCase().contains("bank")) {
icon = AppAssets.bankIconOnly;
}
final invoiceNo = isLoading ? 'INV-000000' : (invoice?.invoiceNumber ?? '-'); return icon;
final invoiceDate = isLoading ? 'Jan 01, 2024' : _formatDate(invoice?.invoiceDate); }
final totalAmount = isLoading ? '0.00' : (invoice?.totalAmount?.toStringAsFixed(2) ?? '-');
final patientShare = isLoading ? '0.00' : (invoice?.patientShare?.toStringAsFixed(2) ?? '-'); 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( return AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@ -94,89 +123,67 @@ class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Invoice No + Date row // 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( "${LocaleKeys.referenceNo.tr(context: context)} $referenceNo"
children: [ .toText16(
if (!isLoading) color: AppColors.textColor,
LocaleKeys.invoiceNo.tr(context: context).toText12(
color: AppColors.textColorLight,
isBold: true, isBold: true,
), )
if (!isLoading) SizedBox(width: 4.w), .toShimmer2(isShow: isLoading),
invoiceNo.toText14(isBold: true, isEnglishOnly: true).toShimmer2(isShow: isLoading),
],
),
invoiceDate.toText12(color: AppColors.textColorLight, isBold: true).toShimmer2(isShow: isLoading),
], ],
), ),
SizedBox(height: 12.h), SizedBox(height: 8.h),
// Chips clinic & hospital
Wrap( Wrap(
spacing: 6.w, spacing: 6.w,
runSpacing: 6.h, runSpacing: 6.h,
children: [ children: [
AppCustomChipWidget( if (!isLoading && refundDestination.isNotEmpty)
icon: "", AppCustomChipWidget(icon: getRefundDestinationIcon(refundDestination), iconHasColor: false, labelText: refundDestination),
labelText: clinicName, if (totalProcedures.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$totalProcedures ${LocaleKeys.procedures.tr()}"),
).toShimmer2(isShow: isLoading), if (!isLoading && projectName.isNotEmpty) AppCustomChipWidget(icon: "", labelText: projectName),
AppCustomChipWidget(
icon: "",
labelText: projectName,
).toShimmer2(isShow: isLoading),
], ],
), ),
SizedBox(height: 12.h), SizedBox(height: 16.h),
// Amounts row // Total refund amount
Row( if (totalAmountStr != null)
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.totalAmount
.tr(context: context)
.toText12(color: AppColors.textColorLight, isBold: true)
.toShimmer2(isShow: isLoading),
SizedBox(height: 4.h),
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(
totalAmount.toText16(isBold: true, isEnglishOnly: true),
AppColors.blackColor,
11,
isSaudiCurrency: true,
isExpanded: false, isExpanded: false,
).toShimmer2(isShow: isLoading), (isLoading ? '0.00' : totalAmountStr).toText20(isBold: true, color: AppColors.textColor),
], AppColors.textColor,
), 18,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.patientShare
.tr(context: context)
.toText12(color: AppColors.textColorLight, isBold: true)
.toShimmer2(isShow: isLoading),
SizedBox(height: 4.h),
Utils.getPaymentAmountWithSymbol(
patientShare.toText16(isBold: true, isEnglishOnly: true),
AppColors.primaryRedColor,
11,
isSaudiCurrency: true, isSaudiCurrency: true,
isExpanded: false,
).toShimmer2(isShow: isLoading), ).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),
], ],
), ),
), ),
@ -227,7 +234,6 @@ class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
if (viewModel.refundInvoicesList.isEmpty) { if (viewModel.refundInvoicesList.isEmpty) {
return _buildEmptyState(); return _buildEmptyState();
} }
return ListView.separated( return ListView.separated(
padding: EdgeInsets.only(top: 24.h), padding: EdgeInsets.only(top: 24.h),
shrinkWrap: true, shrinkWrap: true,

@ -6,6 +6,7 @@ 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/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_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/habib_wallet/habib_wallet_view_model.dart';
@ -88,7 +89,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
.map((item) => RefundableProcedure( .map((item) => RefundableProcedure(
procedureId: item.lineItemNo ?? 0, procedureId: item.lineItemNo ?? 0,
procedureName: item.procedureName ?? '', procedureName: item.procedureName ?? '',
patientShare: item.patientPaidAmount ?? 0.0, patientPaidAmount: item.patientPaidAmount ?? 0.0,
apiLineItemNo: item.lineItemNo, apiLineItemNo: item.lineItemNo,
apiProcedureId: item.procedureId, apiProcedureId: item.procedureId,
)) ))
@ -266,11 +267,16 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// Bank Transfer Input // Bank Transfer Input
Widget _buildBankInputDetails(RefundRequestViewModel vm) { Widget _buildBankInputDetails(RefundRequestViewModel vm) {
final hasError = vm.ibanError != null || vm.descriptionError != null;
return Container( return Container(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: BoxDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 24.r, borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
), ),
child: Column( child: Column(
children: [ children: [
@ -279,7 +285,6 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
hintText: "SA0000000000000000000000", hintText: "SA0000000000000000000000",
controller: _ibanController, controller: _ibanController,
focusNode: _ibanFocusNode, focusNode: _ibanFocusNode,
leadingIconNeedsContainer: false,
autoFocus: false, autoFocus: false,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
isEnable: true, isEnable: true,
@ -291,7 +296,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
isCountryDropDown: false, isCountryDropDown: false,
leadingIcon: AppAssets.bankIcon, leadingIcon: AppAssets.bankIcon,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: vm.ibanError != null, hasError: false,
errorMessage: vm.ibanError, errorMessage: vm.ibanError,
onChange: (value) { onChange: (value) {
vm.clearIbanError(); vm.clearIbanError();
@ -308,7 +313,6 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
focusNode: _descriptionFocusNode, focusNode: _descriptionFocusNode,
autoFocus: false, autoFocus: false,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
leadingIconNeedsContainer: false,
isEnable: true, isEnable: true,
isReadOnly: false, isReadOnly: false,
prefix: null, prefix: null,
@ -318,7 +322,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
isCountryDropDown: false, isCountryDropDown: false,
leadingIcon: AppAssets.descriptionIcon, leadingIcon: AppAssets.descriptionIcon,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: vm.descriptionError != null, hasError: false,
errorMessage: vm.descriptionError, errorMessage: vm.descriptionError,
onChange: (value) { onChange: (value) {
vm.clearDescriptionError(); vm.clearDescriptionError();
@ -378,7 +382,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
Expanded(child: name.toText16(isBold: true)), Expanded(child: name.toText16(isBold: true)),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(
procedure.patientShare.toStringAsFixed(2).toText20( procedure.patientPaidAmount.toStringAsFixed(2).toText20(
isBold: true, isBold: true,
isEnglishOnly: true, isEnglishOnly: true,
color: AppColors.textColor, color: AppColors.textColor,
@ -397,26 +401,22 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 12.h), padding: EdgeInsets.only(top: 16.h),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
(procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)).toText14( (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context))
color: procedure.refundReason != null ? AppColors.textColor : AppColors.greyTextColor, isBold: true), .toText14(color: AppColors.textColor, isBold: true),
], ],
), ),
), ),
Utils.buildSvgWithAssets( Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
icon: AppAssets.arrow_down,
width: 25.h,
height: 25.h,
),
], ],
), ),
), ),
@ -438,9 +438,9 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// Bottom bar // Bottom bar
Widget _buildBottomBar(RefundRequestViewModel vm) { Widget _buildBottomBar(RefundRequestViewModel refundRequestViewModel) {
final isStep2 = vm.currentStep == 1; final isStep2 = refundRequestViewModel.currentStep == 1;
final isWallet = vm.selectedTab == RefundTabType.hmgWallet; final isWallet = refundRequestViewModel.selectedTab == RefundTabType.hmgWallet;
return Container( return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
@ -451,15 +451,15 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (!isWallet && vm.selectedProcedures.isNotEmpty) ...[ if (!isWallet && refundRequestViewModel.selectedProcedures.isNotEmpty) ...[
Padding( Padding(
padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.totalAmount.toText18(color: AppColors.textColor, isBold: true), LocaleKeys.totalAmount.tr().toText18(color: AppColors.textColor, isBold: true),
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(
vm.totalRefundableAmount.toStringAsFixed(2).toText24( refundRequestViewModel.totalRefundableAmount.toStringAsFixed(2).toText24(
isBold: true, isBold: true,
isEnglishOnly: true, isEnglishOnly: true,
color: AppColors.textColor, color: AppColors.textColor,
@ -477,24 +477,24 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context),
onPressed: () { onPressed: () {
if (!isStep2) { if (!isStep2) {
if (vm.selectedProcedures.isNotEmpty) vm.nextStep(); if (refundRequestViewModel.selectedProcedures.isNotEmpty) refundRequestViewModel.nextStep();
} else { } else {
final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; final isBankTransfer = refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices;
if (isBankTransfer) { if (isBankTransfer) {
final isValid = vm.validateBankTransferForm( final isValid = refundRequestViewModel.validateBankTransferForm(
iban: _ibanController.text, iban: _ibanController.text,
description: _descriptionController.text, description: _descriptionController.text,
); );
if (!isValid) return; if (!isValid) return;
} }
_handleInvoiceRefundRequestSubmit(vm); _handleInvoiceRefundRequestSubmit(refundRequestViewModel);
} }
}, },
isDisabled: isStep2 isDisabled: isStep2
? (vm.selectedPaymentMethod == null || ? (refundRequestViewModel.selectedPaymentMethod == null ||
(vm.selectedPaymentMethod == RefundTabType.invoices && (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices &&
(_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)))
: (vm.selectedProcedures.isEmpty || !vm.allSelectedProceduresHaveReasons), : (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons),
).paddingSymmetrical(24.h, 24.h), ).paddingSymmetrical(24.h, 24.h),
], ],
), ),
@ -537,14 +537,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// 1 = invoice refund // 1 = invoice refund
refundProject: invoice.projectName ?? '', refundProject: invoice.projectName ?? '',
refundProjectId: invoice.projectId ?? 0, refundProjectId: invoice.projectId ?? 0,
refundPatientId: patientId.toString(),
// TODO: It should be removed
refundPatientId: 4768571.toString(),
refundPatientName: patientName, refundPatientName: patientName,
refundNationalId: patientNationalId, refundNationalId: patientNationalId,
patientMobileNumber: patientMobileNumber, patientMobileNumber: patientMobileNumber,
refundInvoiceNo: 260000075.toString(), refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(),
// refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), TODO: It should be removed
refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(), refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(),
refundDoctorId: invoice.doctorId ?? 0, refundDoctorId: invoice.doctorId ?? 0,
refundDoctorName: invoice.doctorName ?? '', refundDoctorName: invoice.doctorName ?? '',
@ -559,13 +556,13 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
lineItemNo: p.apiLineItemNo ?? 0, lineItemNo: p.apiLineItemNo ?? 0,
procedureId: p.apiProcedureId ?? '', procedureId: p.apiProcedureId ?? '',
procedureName: p.procedureName, procedureName: p.procedureName,
patientPaidAmount: p.patientShare, patientPaidAmount: p.patientPaidAmount,
refundReason: p.refundReason ?? '', refundReason: p.refundReason ?? '',
)) ))
.toList(), .toList(),
); );
await vm.submitRefund( await vm.submitInvoiceRefundRequest(
request: request, request: request,
onSuccess: (response) { onSuccess: (response) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
@ -597,17 +594,17 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
children: [ children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)),
if (refNo.isNotEmpty) ...[ if (refNo.isNotEmpty) ...[
SizedBox(height: 8.h),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
LocaleKeys.hereIsYourRequestNumber.tr(context: context).toText14( LocaleKeys.hereIsYourRequestNumber.tr().toText16(
color: AppColors.textColorLight, color: AppColors.textColorLight,
weight: FontWeight.w600,
), ),
SizedBox(width: 4.w), SizedBox(width: 6.w),
refNo.toText16(isBold: true), refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
], ],
), ).paddingSymmetrical(24.w, 0),
], ],
SizedBox(height: 24.h), SizedBox(height: 24.h),
Row( Row(
@ -619,6 +616,13 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
onPressed: () { onPressed: () {
Navigator.pop(context); // pop success sheet Navigator.pop(context); // pop success sheet
Navigator.pop(context); // pop refund page Navigator.pop(context); // pop refund page
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
),
),
);
}, },
textColor: AppColors.whiteColor, textColor: AppColors.whiteColor,
), ),
@ -649,7 +653,18 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
Expanded( Expanded(
child: CollapsingListView( child: CollapsingListView(
title: LocaleKeys.requestRefund.tr(context: context), title: LocaleKeys.requestRefund.tr(context: context),
requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), 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( child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 32.h), padding: EdgeInsets.only(bottom: 32.h),
child: Column( child: Column(

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

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

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

@ -57,7 +57,7 @@ class AppCustomChipWidget extends StatelessWidget {
final iconS = iconSize ?? 12.w; final iconS = iconSize ?? 12.w;
final resolvedTextColor = textColor ?? AppColors.textColor; final resolvedTextColor = textColor ?? AppColors.textColor;
final resolvedBackgroundColor = backgroundColor ?? AppColors.greyColor; final resolvedBackgroundColor = backgroundColor ?? AppColors.greyColor;
final resolvedIconColor = iconColor ?? AppColors.textColor; final resolvedIconColor = (iconColor ?? AppColors.textColor);
final resolvedDeleteIconColor = deleteIconColor ?? AppColors.textColor; final resolvedDeleteIconColor = deleteIconColor ?? AppColors.textColor;
return GestureDetector( return GestureDetector(
onTap: onChipTap, onTap: onChipTap,

@ -223,7 +223,7 @@ class TextInputWidget extends StatelessWidget {
} }
Widget _buildLeadingIcon(BuildContext context) { Widget _buildLeadingIcon(BuildContext context) {
if (leadingIconNeedsContainer == true) { if (leadingIconNeedsContainer) {
return Container( return Container(
height: 40.h, height: 40.h,
width: 40.h, width: 40.h,

Loading…
Cancel
Save