Refund Module Completed

pull/322/head
faizatflutter 4 weeks 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">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3335 9C9.30849 9 7.66686 10.6416 7.66686 12.6667C7.66686 14.6917 9.30849 16.3333 11.3335 16.3333C13.3586 16.3333 15.0002 14.6917 15.0002 12.6667C15.0002 10.6416 13.3586 9 11.3335 9ZM9.66686 12.6667C9.66686 11.7462 10.4131 11 11.3335 11C12.254 11 13.0002 11.7462 13.0002 12.6667C13.0002 13.5871 12.254 14.3333 11.3335 14.3333C10.4131 14.3333 9.66686 13.5871 9.66686 12.6667Z" fill="#2E3039"/>
<path d="M7.57253 22.424C8.17053 21.1466 9.65415 20.3583 11.3085 20.3339C12.9508 20.3097 14.4477 21.0424 15.0945 22.424C15.3287 22.9242 15.924 23.1398 16.4242 22.9057C16.9244 22.6715 17.14 22.0762 16.9059 21.576C15.8356 19.2897 13.4714 18.3018 11.279 18.3341C9.09878 18.3663 6.77427 19.412 5.76119 21.576C5.52703 22.0762 5.74269 22.6715 6.24288 22.9057C6.74307 23.1398 7.33838 22.9242 7.57253 22.424Z" fill="#2E3039"/>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

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

@ -1850,7 +1850,7 @@
"submitRequest": "Submit Request",
"ibanIsRequired": "IBAN 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",
"noRefundHistoryFound": "You don't have any refund history yet.",
"invoiceNo": "Invoice No.",
@ -1873,12 +1873,20 @@
"refundReason6": "Clinical procedure cancelled or changed by requesting doctor",
"refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor",
"selectProcedures": "Select Procedures",
"submitEReferral": "Submit",
"redeem": "Redeem",
"points": "Points",
"transactions": "Transactions",
"pharmacy": "Pharmacy",
"visitsOrders": "Visits/Orders"
"visitsOrders": "Visits/Orders",
"cannotOpenThisFile": "Cannot open this file",
"thisInvoiceIsNotEligibleForRefund": "This invoice is not eligible for refund",
"refundDetails": "Refund Details",
"requestNo": "Request No.",
"referenceNo": "Reference No.",
"refund": "Refund",
"advanceNumber": "Advance Number",
"amountOnly": "Amount"
}

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

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

@ -390,6 +390,8 @@ class AppAssets {
static const String refundIcon = '$svgBasePath/refund_icon.svg';
static const String bankIcon = '$svgBasePath/bank_icon.svg';
static const String bankIconOnly = '$svgBasePath/bank_icon_only.svg';
static const String habibLogoOnly = '$svgBasePath/habib logo_only.svg';
static const String descriptionIcon = '$svgBasePath/description_icon.svg';
static const String selectImageIcon = '$svgBasePath/select_image_icon.svg';

@ -339,17 +339,15 @@ class AppDependencies {
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(
appState: getIt(),
refundRequestRepo: getIt(),
errorHandlerService: getIt(),
));
getIt.registerLazySingleton<ServicesPriceListViewModel>(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(
() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(
() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt()));
}

@ -181,7 +181,7 @@ class ValidationUtils {
}
}
if (phoneNumber == null || phoneNumber.isEmpty) {
if (phoneNumber == null || phoneNumber.isEmpty) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidPhoneNumber.tr(), onOkPressed: onOkPress);
return false;
}
@ -189,13 +189,41 @@ class ValidationUtils {
return isCorrectID;
}
static bool isNullOrEmpty(String? value) {
static bool isNullOrEmpty(String? value) {
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 validateKsaIban(String iban) {
final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false);
return ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''));
static bool validateIban(String iban) {
final normalized = iban.replaceAll(RegExp(r'\s+'), '').toUpperCase();
// General IBAN structure: country code + check digits + BBAN
if (!RegExp(r'^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$').hasMatch(normalized)) {
return false;
}
// IBAN total length must be between 15 and 34 characters
if (normalized.length < 15 || normalized.length > 34) {
return false;
}
// Move first 4 chars to the end
final rearranged = normalized.substring(4) + normalized.substring(0, 4);
// Convert letters to numbers: A=10, B=11, ..., Z=35
final numeric = rearranged.split('').map((char) {
final code = char.codeUnitAt(0);
if (code >= 65 && code <= 90) {
return (code - 55).toString();
}
return char;
}).join();
// MOD-97 check without parsing huge integer at once
int remainder = 0;
for (final ch in numeric.split('')) {
remainder = (remainder * 10 + int.parse(ch)) % 97;
}
return remainder == 1;
}
}

@ -1,16 +1,16 @@
import 'package:easy_localization/easy_localization.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/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/lakum_inquiry_information_response_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/patient_advance_balance_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/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/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -96,7 +96,7 @@ class HabibWalletViewModel extends ChangeNotifier {
allTransactions.sort((a, b) {
// Null dates go to the end
if (a.transactionDate == null && b.transactionDate == null) return 0;
if (a.transactionDate == null) return 1; // a goes after b
if (a.transactionDate == null) return 1; // a goes after b
if (b.transactionDate == null) return -1; // a goes before b
try {
@ -161,7 +161,7 @@ class HabibWalletViewModel extends ChangeNotifier {
_ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false;
} else {
if (!ValidationUtils.validateKsaIban(iban.trim())) {
if (!ValidationUtils.validateIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr();
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 remarks,
required String patientName,
@ -255,13 +266,15 @@ class HabibWalletViewModel extends ChangeNotifier {
advanceNo: (a.advanceNo ?? 0).toString(),
paidAmount: a.paidAmount ?? 0.0,
balanceAmount: a.balanceAmount ?? 0.0,
advanceDate: a.advanceDate ?? '',
advanceDate: a.advanceDate ?? "",
// advanceDate: _formatAdvanceDate(a.advanceDate),
))
.toList();
final request = SubmitRefundRequestModel(
projectId: projectId,
refundType: '2', // wallet withdrawal
refundType: '2',
// wallet withdrawal
refundProject: projectName,
refundProjectId: projectId,
refundPatientId: patientId,
@ -274,7 +287,7 @@ class HabibWalletViewModel extends ChangeNotifier {
refundDoctorName: '',
refundClinicId: 0,
refundClinicName: '',
refundDestination: '2', // bank transfer
refundDestination: '2', // only bank transfer
refundIban: iban,
refundRemarks: remarks,
refundTotalAmount: totalAdvancesBalance,
@ -456,7 +469,7 @@ class HabibWalletViewModel extends ChangeNotifier {
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
if(onError != null) {
if (onError != null) {
onError(failure.message);
}
},
@ -522,7 +535,8 @@ class HabibWalletViewModel extends ChangeNotifier {
lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners();
final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
final result = await habibWalletRepo.getLakumAccountInformation(
identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),

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

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

@ -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/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -20,9 +21,18 @@ abstract class RefundRequestRepo {
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<IsRefundableInvoiceObject>>> checkIsRefundableInvoice({
required int invoiceNo,
required int projectId,
});
}
class RefundRequestRepoImp implements RefundRequestRepo {
@ -215,10 +225,12 @@ class RefundRequestRepoImp implements RefundRequestRepo {
@override
Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({
required int refundType,
required String patientId,
String invoiceNumberQuery = "",
}) async {
final Map<String, dynamic> body = {
"Refund_RefundType": refundType,
"Refund_PatientId": patientId,
"Refund_InvoiceNumberQuery": invoiceNumberQuery,
};
@ -235,7 +247,7 @@ class RefundRequestRepoImp implements RefundRequestRepo {
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final listJson = response['RefundInvoicesList'];
final listJson = response['Refund_InvoicesList'];
final List<RefundInvoiceModel> invoices =
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()));
}
}
@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/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart';
@ -28,7 +29,7 @@ class RefundableProcedure {
final int procedureId;
final String procedureName;
final String? procedureNameN;
final double patientShare;
final double patientPaidAmount;
bool isSelected;
String? refundReason;
@ -40,7 +41,7 @@ class RefundableProcedure {
required this.procedureId,
required this.procedureName,
this.procedureNameN,
required this.patientShare,
required this.patientPaidAmount,
this.isSelected = false,
this.refundReason,
this.apiLineItemNo,
@ -240,7 +241,7 @@ class RefundRequestViewModel extends ChangeNotifier {
double get totalRefundableAmount {
double total = 0.0;
for (final p in selectedProcedures) {
total += p.patientShare;
total += p.patientPaidAmount;
}
return total;
}
@ -276,7 +277,7 @@ class RefundRequestViewModel extends ChangeNotifier {
_ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false;
} else {
if (!ValidationUtils.validateKsaIban(iban.trim())) {
if (!ValidationUtils.validateIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false;
}
@ -457,7 +458,7 @@ class RefundRequestViewModel extends ChangeNotifier {
bool isRefundInvoicesLoading = false;
Future<void> getRefundInvoices({
String? patientId,
required int refundType,
String invoiceNumberQuery = "",
Function(List<RefundInvoiceModel>)? onSuccess,
Function(String)? onError,
@ -466,10 +467,10 @@ class RefundRequestViewModel extends ChangeNotifier {
refundInvoicesList.clear();
notifyListeners();
// final resolvedPatientId = patientId ?? appState.getAuthenticatedUser()?.patientId?.toString() ?? "";
final resolvedPatientId = 4768571;
final resolvedPatientId = appState.getAuthenticatedUser()?.patientId?.toString() ?? "";
final result = await refundRequestRepo.getRefundInvoices(
patientId: resolvedPatientId.toString(),
refundType: refundType,
patientId: resolvedPatientId,
invoiceNumberQuery: invoiceNumberQuery,
);
@ -502,7 +503,7 @@ class RefundRequestViewModel extends ChangeNotifier {
// Submit Refund
bool isSubmitting = false;
Future<void> submitRefund({
Future<void> submitInvoiceRefundRequest({
required SubmitRefundRequestModel request,
Function(SubmitRefundResponseModel)? onSuccess,
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 pharmacy = 'pharmacy';
static const visitsOrders = 'visitsOrders';
static const cannotOpenThisFile = 'cannotOpenThisFile';
static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund';
static const refundDetails = 'refundDetails';
static const requestNo = 'requestNo';
static const referenceNo = 'referenceNo';
static const refund = 'refund';
static const advanceNumber = 'advanceNumber';
static const amountOnly = 'amountOnly';
}

@ -188,254 +188,270 @@ class _RegisterNew extends State<RegisterNewStep2> {
),
padding: EdgeInsets.only(left: 16.h, right: 16.h),
child: Column(
children: [
TextInputWidget(
labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr(context: context) : LocaleKeys.name.tr(context: context),
hintText: authVM!.isUserFromUAE() ? LocaleKeys.enterNameHere.tr(context: context) : (name),
controller: authVM!.isUserFromUAE() ? authVM!.nameController : null,
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
keyboardType: TextInputType.text,
// textInputAction: TextInputAction.done,
onSubmitted: (value) {
FocusScope.of(context).unfocus();
},
onChange: (value) {
// Clear error when user starts typing
authVM!.clearNameError();
},
isAllowLeadingIcon: true,
isReadOnly: authVM!.isUserFromUAE() ? false : true,
leadingIcon: AppAssets.user_circle,
labelColor: AppColors.textColor,
).paddingSymmetrical(0.h, 8.h),
// Show error message if exists
if (authVM!.isUserFromUAE() && authVM!.nameError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.nameError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
children: [
TextInputWidget(
labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr(context: context) : LocaleKeys.name.tr(context: context),
hintText: authVM!.isUserFromUAE() ? LocaleKeys.enterNameHere.tr(context: context) : (name),
controller: authVM!.isUserFromUAE() ? authVM!.nameController : null,
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
keyboardType: TextInputType.text,
// textInputAction: TextInputAction.done,
onSubmitted: (value) {
FocusScope.of(context).unfocus();
},
onChange: (value) {
// Clear error when user starts typing
authVM!.clearNameError();
},
isAllowLeadingIcon: true,
isReadOnly: authVM!.isUserFromUAE() ? false : true,
leadingIcon: AppAssets.user_circle,
labelColor: AppColors.textColor,
).paddingSymmetrical(0.h, 8.h),
// Show error message if exists
if (authVM!.isUserFromUAE() && authVM!.nameError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.nameError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
),
),
Divider(height: 1.h, color: AppColors.greyColor),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context),
hintText: authVM!.isUserFromUAE() ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : (appState.getNHICUserData.idNumber ?? ""),
controller: null,
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.student_card)
.paddingSymmetrical(0.h, 8.h),
Divider(height: 1, color: AppColors.greyColor),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, GenderTypeEnum?>(
selector: (_, authViewModel) => authViewModel.genderType,
shouldRebuild: (previous, next) => previous != next,
builder: (context, genderType, child) {
final authVM = context.read<AuthenticationViewModel>();
return DropdownWidget(
labelText: LocaleKeys.gender.tr(context: context),
hintText: LocaleKeys.malE.tr(context: context),
isEnable: true,
dropdownItems: GenderTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(),
selectedValue: genderType != null ? (appState.isArabic() ? genderType.typeAr : genderType.type) : "",
onChange: authVM.onGenderChange,
isBorderAllowed: false,
hasSelectionCustomIcon: true,
isAllowRadius: false,
labelColor: AppColors.textColor,
padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0),
selectionCustomIcon: AppAssets.arrow_down,
leadingIcon: AppAssets.user_full,
).withVerticalPadding(8);
})
: TextInputWidget(
labelText: LocaleKeys.gender.tr(context: context),
hintText: (appState.getNHICUserData.gender ?? ""),
Divider(height: 1.h, color: AppColors.greyColor),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context),
hintText: authVM!.isUserFromUAE()
? appState.getUserRegistrationPayload.patientIdentificationId.toString()
: (appState.getNHICUserData.idNumber ?? ""),
controller: null,
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: authVM!.isUserFromUAE() ? false : true,
leadingIcon: AppAssets.user_full,
labelColor: AppColors.textColor,
onChange: (value) {})
.paddingSymmetrical(0.h, 8.h),
// Show gender error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.genderError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.genderError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(height: 1, color: AppColors.greyColor),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, MaritalStatusTypeEnum?>(
selector: (_, authViewModel) => authViewModel.maritalStatus,
shouldRebuild: (previous, next) => previous != next,
builder: (context, maritalStatus, child) {
final authVM = context.read<AuthenticationViewModel>(); // For onChange
return DropdownWidget(
labelText: LocaleKeys.maritalStatus.tr(context: context),
hintText: LocaleKeys.married.tr(context: context),
isEnable: true,
dropdownItems: MaritalStatusTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(),
selectedValue: maritalStatus != null ? (appState.isArabic() ? maritalStatus.typeAr : maritalStatus.type) : "",
onChange: authVM.onMaritalStatusChange,
isBorderAllowed: false,
hasSelectionCustomIcon: true,
isAllowRadius: false,
labelColor: AppColors.textColor,
padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0),
selectionCustomIcon: AppAssets.arrow_down,
leadingIcon: AppAssets.smart_phone,
).withVerticalPadding(8);
},
)
: TextInputWidget(
labelText: LocaleKeys.maritalStatus.tr(context: context),
hintText: appState.isArabic()
? (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.typeAr ?? '')
: (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.type ?? ''),
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.smart_phone,
onChange: (value) {})
leadingIcon: AppAssets.student_card)
.paddingSymmetrical(0.h, 8.h),
// Show marital status error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.maritalStatusError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.maritalStatusError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
Divider(height: 1, color: AppColors.greyColor),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, GenderTypeEnum?>(
selector: (_, authViewModel) => authViewModel.genderType,
shouldRebuild: (previous, next) => previous != next,
builder: (context, genderType, child) {
final authVM = context.read<AuthenticationViewModel>();
return DropdownWidget(
labelText: LocaleKeys.gender.tr(context: context),
hintText: LocaleKeys.malE.tr(context: context),
isEnable: true,
dropdownItems: GenderTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(),
selectedValue: genderType != null ? (appState.isArabic() ? genderType.typeAr : genderType.type) : "",
onChange: authVM.onGenderChange,
isBorderAllowed: false,
hasSelectionCustomIcon: true,
isAllowRadius: false,
labelColor: AppColors.textColor,
padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0),
selectionCustomIcon: AppAssets.arrow_down,
leadingIcon: AppAssets.user_full,
).withVerticalPadding(8);
})
: TextInputWidget(
labelText: LocaleKeys.gender.tr(context: context),
hintText: (appState.getNHICUserData.gender ?? ""),
controller: null,
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: authVM!.isUserFromUAE() ? false : true,
leadingIcon: AppAssets.user_full,
labelColor: AppColors.textColor,
onChange: (value) {})
.paddingSymmetrical(0.h, 8.h),
// Show gender error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.genderError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.genderError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(height: 1, color: AppColors.greyColor),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, MaritalStatusTypeEnum?>(
selector: (_, authViewModel) => authViewModel.maritalStatus,
shouldRebuild: (previous, next) => previous != next,
builder: (context, maritalStatus, child) {
final authVM = context.read<AuthenticationViewModel>(); // For onChange
return DropdownWidget(
labelText: LocaleKeys.maritalStatus.tr(context: context),
hintText: LocaleKeys.married.tr(context: context),
isEnable: true,
dropdownItems: MaritalStatusTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(),
selectedValue: maritalStatus != null ? (appState.isArabic() ? maritalStatus.typeAr : maritalStatus.type) : "",
onChange: authVM.onMaritalStatusChange,
isBorderAllowed: false,
hasSelectionCustomIcon: true,
isAllowRadius: false,
labelColor: AppColors.textColor,
padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0),
selectionCustomIcon: AppAssets.arrow_down,
leadingIcon: AppAssets.smart_phone,
).withVerticalPadding(8);
},
)
: TextInputWidget(
labelText: LocaleKeys.maritalStatus.tr(context: context),
hintText: appState.isArabic()
? (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.typeAr ?? '')
: (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.type ?? ''),
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.smart_phone,
onChange: (value) {})
.paddingSymmetrical(0.h, 8.h),
// Show marital status error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.maritalStatusError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.maritalStatusError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(height: 1.h, color: AppColors.greyColor),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel,
({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>(
selector: (context, authViewModel) {
final appState = getIt.get<AppState>();
return (
countriesList: authViewModel.countriesList,
selectedCountry: authViewModel.pickedCountryByUAEUser,
isArabic: appState.isArabic(),
);
},
shouldRebuild: (previous, next) =>
previous.countriesList != next.countriesList ||
previous.selectedCountry != next.selectedCountry ||
previous.isArabic != next.isArabic,
builder: (context, data, child) {
final authVM = context.read<AuthenticationViewModel>();
return DropdownWidget(
labelText: LocaleKeys.country.tr(context: context),
hintText: LocaleKeys.uae.tr(context: context),
isEnable: true,
dropdownItems: (data.countriesList ?? []).map((e) => data.isArabic ? e.nameN ?? "" : e.name ?? "").toList(),
selectedValue: data.selectedCountry != null
? data.isArabic
? data.selectedCountry!.nameN ?? ""
: data.selectedCountry!.name ?? ""
: "",
onChange: authVM.onUAEUserCountrySelection,
isBorderAllowed: false,
hasSelectionCustomIcon: true,
labelColor: AppColors.textColor,
isAllowRadius: false,
padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0),
selectionCustomIcon: AppAssets.arrow_down,
leadingIcon: AppAssets.globe,
).withVerticalPadding(8.h);
},
)
: TextInputWidget(
labelText: LocaleKeys.nationality.tr(context: context),
hintText: appState.isArabic()
? (authVM!.countriesList!
.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""),
orElse: () => NationalityCountries())
.nameN ??
"")
: (authVM!.countriesList!
.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""),
orElse: () => NationalityCountries())
.name ??
""),
isEnable: true,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.globe,
onChange: (value) {})
.paddingSymmetrical(0.h, 8.h),
// Show country error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.countryError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.countryError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(
height: 1,
color: AppColors.greyColor,
),
),
Divider(height: 1.h, color: AppColors.greyColor),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, ({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>(
selector: (context, authViewModel) {
final appState = getIt.get<AppState>();
return (
countriesList: authViewModel.countriesList,
selectedCountry: authViewModel.pickedCountryByUAEUser,
isArabic: appState.isArabic(),
);
},
shouldRebuild: (previous, next) => previous.countriesList != next.countriesList || previous.selectedCountry != next.selectedCountry || previous.isArabic != next.isArabic,
builder: (context, data, child) {
final authVM = context.read<AuthenticationViewModel>();
return DropdownWidget(
labelText: LocaleKeys.country.tr(context: context),
hintText: LocaleKeys.uae.tr(context: context),
isEnable: true,
dropdownItems: (data.countriesList ?? []).map((e) => data.isArabic ? e.nameN ?? "" : e.name ?? "").toList(),
selectedValue: data.selectedCountry != null
? data.isArabic
? data.selectedCountry!.nameN ?? ""
: data.selectedCountry!.name ?? ""
: "",
onChange: authVM.onUAEUserCountrySelection,
isBorderAllowed: false,
hasSelectionCustomIcon: true,
labelColor: AppColors.textColor,
isAllowRadius: false,
padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0),
selectionCustomIcon: AppAssets.arrow_down,
leadingIcon: AppAssets.globe,
).withVerticalPadding(8.h);
},
)
: TextInputWidget(
labelText: LocaleKeys.nationality.tr(context: context),
hintText: appState.isArabic()
? (authVM!.countriesList!.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), orElse: () => NationalityCountries()).nameN ?? "")
: (authVM!.countriesList!.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), orElse: () => NationalityCountries()).name ?? ""),
isEnable: true,
TextInputWidget(
labelText: LocaleKeys.mobileNumber.tr(context: context),
hintText: appState.getUserRegistrationPayload.patientMobileNumber.toString(),
controller: null,
isEnable: false,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.globe,
onChange: (value) {})
isReadOnly: true,
leadingIcon: AppAssets.call)
.paddingSymmetrical(0.h, 8.h),
// Show country error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.countryError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.countryError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
Divider(
height: 1.h,
color: AppColors.greyColor,
),
),
Divider(
height: 1,
color: AppColors.greyColor,
),
TextInputWidget(
labelText: LocaleKeys.mobileNumber.tr(context: context),
hintText: appState.getUserRegistrationPayload.patientMobileNumber.toString(),
controller: null,
isEnable: false,
prefix: null,
isAllowRadius: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
labelColor: AppColors.textColor,
isReadOnly: true,
leadingIcon: AppAssets.call)
.paddingSymmetrical(0.h, 8.h),
Divider(
height: 1.h,
color: AppColors.greyColor,
TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context),
hintText: authVM!.isUserFromUAE()
? (appState.getUserRegistrationPayload.dob ?? '')
: (appState.getNHICUserData.dateOfBirth ?? ""),
controller: authVM!.isUserFromUAE() ? authVM!.dobController : null,
isEnable: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.birthday_cake,
selectionType: null,
).paddingSymmetrical(0.h, 8.h),
],
),
TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context),
hintText: authVM!.isUserFromUAE() ? (appState.getUserRegistrationPayload.dob ?? '') : (appState.getNHICUserData.dateOfBirth ?? ""),
controller: authVM!.isUserFromUAE() ? authVM!.dobController : null,
isEnable: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
isReadOnly: true,
labelColor: AppColors.textColor,
leadingIcon: AppAssets.birthday_cake,
selectionType: null,
).paddingSymmetrical(0.h, 8.h),
],
),
);
);
},
),
SizedBox(height: 50.h),

@ -185,9 +185,17 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Widget _buildBankInputDetails() {
return Consumer<HabibWalletViewModel>(
builder: (context, viewModel, _) {
final hasError = viewModel.ibanError != null || viewModel.withdrawDescriptionError != null;
return Container(
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(
children: [
TextInputWidget(
@ -195,7 +203,6 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
hintText: "SA0000000000000000000000",
controller: _bankDetailsController,
focusNode: _ibanFocusNode,
leadingIconNeedsContainer: false,
autoFocus: false,
keyboardType: TextInputType.text,
isEnable: true,
@ -207,7 +214,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
isCountryDropDown: false,
leadingIcon: AppAssets.bankIcon,
fontFamily: "Poppins",
hasError: viewModel.ibanError != null,
hasError: false,
errorMessage: viewModel.ibanError,
onChange: (value) {
viewModel.clearIbanError();
@ -224,7 +231,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
focusNode: _descriptionFocusNode,
autoFocus: false,
keyboardType: TextInputType.text,
leadingIconNeedsContainer: false,
// leadingIconNeedsContainer: false,
isEnable: true,
isReadOnly: false,
prefix: null,
@ -234,7 +241,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
isCountryDropDown: false,
leadingIcon: AppAssets.descriptionIcon,
fontFamily: "Poppins",
hasError: viewModel.withdrawDescriptionError != null,
hasError: false,
errorMessage: viewModel.withdrawDescriptionError,
onChange: (value) {
viewModel.clearWithdrawDescriptionError();
@ -301,10 +308,9 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader();
final user = _appState.getAuthenticatedUser();
final hospital = vm.selectedHospital;
await vm.submitWalletWithdrawal(
await vm.submitWalletWithdrawalRequest(
iban: _bankDetailsController.text.trim(),
remarks: _descriptionController.text.trim(),
patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(),
@ -342,16 +348,36 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
child: Column(
children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)),
if (refNo.isNotEmpty) ...[
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
LocaleKeys.hereIsYourRequestNumber.tr().toText16(
color: AppColors.textColorLight,
),
SizedBox(width: 6.w),
refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
],
).paddingSymmetrical(24.w, 0),
],
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
height: 56.h,
text: LocaleKeys.ok.tr(context: context),
text: LocaleKeys.ok.tr(),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
Navigator.pop(context); // pop success sheet
Navigator.pop(context); // pop refund page
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
),
),
);
},
textColor: AppColors.whiteColor,
),
@ -364,7 +390,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
isCloseButtonVisible: false,
isDismissible: false,
isFullScreen: false,
callBackFunc: () {},
isAutoDismiss: false,
);
}
@ -381,7 +407,13 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Expanded(
child: CollapsingListView(
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(
padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w),
child: Column(
@ -391,8 +423,8 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
if (habibWalletViewModel.selectedHospital != null) ...[
SizedBox(height: 16.h),
_buildBankInputDetails(),
SizedBox(height: 16.h),
_buildImagePickerDropDown(),
// SizedBox(height: 16.h),
// _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,215 +137,101 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
}
},
).paddingSymmetrical(24.h, 0.h),
myInvoicesVM.selectedTabIndex == 0 ? Column(
children: [
SizedBox(height: 16.h),
Row(
children: [
CustomButton(
text: LocaleKeys.allInvoices.tr(context: context),
onPressed: () {
myInvoicesViewModel.filterInvoices(InvoiceFilterType.all);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.hospitals.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showHospitalFilterBottomSheet(context);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.clinics.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showClinicFilterBottomSheet(context);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.doctors.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showDoctorFilterBottomSheet(context);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
ListView.builder(
itemCount: myInvoicesVM.isInvoicesListLoading
? 4
: myInvoicesVM.allInvoicesList.isEmpty
? 1
: myInvoicesVM.allInvoicesList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
return myInvoicesVM.isInvoicesListLoading
? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true)
: myInvoicesVM.allInvoicesList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: InvoiceListCard(
getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
onTap: () async {
if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context));
await myInvoicesViewModel.sendInvoiceEmail(
appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
isAutoDismiss: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myInvoicesViewModel.downloadInvoicePDF(
setupId: myInvoicesVM.allInvoicesList[index].setupId!,
invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
}
},
);
}
},
// onTap: () async {
// myInvoicesVM.setInvoiceDetailLoading();
// LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingInvoiceDetails.tr(context: context));
// await myInvoicesVM.getInvoiceDetails(
// appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
// invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!,
// projectID: myInvoicesVM.allInvoicesList[index].projectId!,
// onSuccess: (val) {
// LoaderBottomSheet.hideLoader();
// Navigator.of(context).push(
// CustomPageRoute(
// page: MyInvoicesDetailsPage(
// getInvoiceDetailsResponseModel: myInvoicesVM.invoiceDetailsResponseModel,
// getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
// ),
// ),
// );
// },
// onError: (err) {
// LoaderBottomSheet.hideLoader();
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getErrorWidget(loadingText: err),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// );
// });
// },
),
),
),
),
)
: Utils.getNoDataWidget(context);
}).paddingSymmetrical(24.w, 0.h),
],
)
: Column(
myInvoicesVM.selectedTabIndex == 0
? Column(
children: [
SizedBox(height: 16.h),
Row(
children: [
CustomButton(
text: LocaleKeys.allInvoices.tr(context: context),
onPressed: () {
myInvoicesViewModel.filterInvoices(InvoiceFilterType.all);
},
backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.hospitals.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showHospitalFilterBottomSheet(context);
},
backgroundColor:
myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.clinics.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showClinicFilterBottomSheet(context);
},
backgroundColor:
myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.doctors.tr(context: context),
onPressed: () {
if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return;
_showDoctorFilterBottomSheet(context);
},
backgroundColor:
myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor
? AppColors.primaryRedColor
: AppColors.textColor.withValues(alpha: 0.2),
textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w600,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
ListView.builder(
itemCount: myInvoicesVM.isPharmacyInvoicesLoading
itemCount: myInvoicesVM.isInvoicesListLoading
? 4
: myInvoicesVM.allPharmacyInvoicesList.isEmpty
: myInvoicesVM.allInvoicesList.isEmpty
? 1
: myInvoicesVM.allPharmacyInvoicesList.length,
: myInvoicesVM.allInvoicesList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
return myInvoicesVM.isPharmacyInvoicesLoading
return myInvoicesVM.isInvoicesListLoading
? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true)
: myInvoicesVM.allPharmacyInvoicesList.isNotEmpty
: myInvoicesVM.allInvoicesList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
@ -355,29 +241,41 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: PharmacyInvoiceCard(
pharmacyInvoice: myInvoicesVM.allPharmacyInvoicesList[index],
child: InvoiceListCard(
getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index],
onTap: () async {
// if (Utils.isVidaPlusProject(myInvoicesVM.allPharmacyInvoicesList[index].projectID ?? 0)) {
// // For Vida+ projects, send email (if API is available)
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getSuccessWidget(
// loadingText: LocaleKeys.comingSoon.tr(context: context),
// ),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// isAutoDismiss: true,
// );
// } else {
// Download pharmacy invoice PDF
if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) {
LoaderBottomSheet.showLoader(
loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context));
await myInvoicesViewModel.sendInvoiceEmail(
appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(context,
child: Utils.getSuccessWidget(
loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
isAutoDismiss: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myInvoicesViewModel.downloadPharmacyInvoicePDF(
setupId: myInvoicesVM.allPharmacyInvoicesList[index].setupId ?? "",
invoiceNo: myInvoicesVM.allPharmacyInvoicesList[index].invoiceNo ?? 0,
projectID: myInvoicesVM.allPharmacyInvoicesList[index].pharmacyProjectID ?? 0,
locationID: myInvoicesVM.allPharmacyInvoicesList[index].locationID ?? 0,
myInvoicesViewModel.downloadInvoicePDF(
setupId: myInvoicesVM.allInvoicesList[index].setupId!,
invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!,
projectID: myInvoicesVM.allInvoicesList[index].projectId!,
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
@ -390,14 +288,15 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
},
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf");
if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(
myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"),
child: Utils.getErrorWidget(loadingText: LocaleKeys.cannotOpenThisFile.tr()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
@ -406,6 +305,94 @@ class _MyInvoicesListState extends State<MyInvoicesList> {
}
},
);
}
},
),
),
),
),
)
: Utils.getNoDataWidget(context);
}).paddingSymmetrical(24.w, 0.h),
],
)
: Column(
children: [
SizedBox(height: 16.h),
ListView.builder(
itemCount: myInvoicesVM.isPharmacyInvoicesLoading
? 4
: myInvoicesVM.allPharmacyInvoicesList.isEmpty
? 1
: myInvoicesVM.allPharmacyInvoicesList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
return myInvoicesVM.isPharmacyInvoicesLoading
? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true)
: myInvoicesVM.allPharmacyInvoicesList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: PharmacyInvoiceCard(
pharmacyInvoice: myInvoicesVM.allPharmacyInvoicesList[index],
onTap: () async {
// if (Utils.isVidaPlusProject(myInvoicesVM.allPharmacyInvoicesList[index].projectID ?? 0)) {
// // For Vida+ projects, send email (if API is available)
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getSuccessWidget(
// loadingText: LocaleKeys.comingSoon.tr(context: context),
// ),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// isAutoDismiss: true,
// );
// } else {
// Download pharmacy invoice PDF
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myInvoicesViewModel.downloadPharmacyInvoicePDF(
setupId: myInvoicesVM.allPharmacyInvoicesList[index].setupId ?? "",
invoiceNo: myInvoicesVM.allPharmacyInvoicesList[index].invoiceNo ?? 0,
projectID: myInvoicesVM.allPharmacyInvoicesList[index].pharmacyProjectID ?? 0,
locationID: myInvoicesVM.allPharmacyInvoicesList[index].locationID ?? 0,
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) {
String path = await Utils.createFileFromString(
myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Cannot open file"),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
}
},
);
// }
},
),

@ -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/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class InvoiceListCard extends StatelessWidget {
final GetInvoicesListResponseModel getInvoicesListResponseModel;
@ -24,6 +28,46 @@ class InvoiceListCard extends StatelessWidget {
InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap});
void _handleRefundInvoiceTap(BuildContext context) {
final invoiceNo = getInvoicesListResponseModel.invoiceNo;
final projectId = getInvoicesListResponseModel.projectId;
if (invoiceNo == null || projectId == null) {
Navigator.of(context).push(
CustomPageRoute(
page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel),
),
);
return;
}
final viewModel = context.read<RefundRequestViewModel>();
LoaderBottomSheet.showLoader();
viewModel.checkIsRefundableInvoice(
invoiceNo: invoiceNo,
projectId: projectId,
onSuccess: (result) {
LoaderBottomSheet.hideLoader();
if (result.isRefundable == true) {
Navigator.of(context).push(
CustomPageRoute(
page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel),
),
);
} else {
final message = result.message?.isNotEmpty == true ? result.message! : LocaleKeys.thisInvoiceIsNotEligibleForRefund.tr(context: context);
getIt<DialogService>().showErrorBottomSheet(message: message);
}
},
onError: (error) {
LoaderBottomSheet.hideLoader();
getIt<DialogService>().showErrorBottomSheet(message: error);
},
);
}
@override
Widget build(BuildContext context) {
AppState appState = getIt<AppState>();
@ -174,11 +218,7 @@ class InvoiceListCard extends StatelessWidget {
child: CustomButton(
text: LocaleKeys.requestRefund.tr(context: context),
onPressed: () {
Navigator.of(context).push(
CustomPageRoute(
page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel),
),
);
_handleRefundInvoiceTap(context);
},
icon: AppAssets.refundIcon,
iconColor: AppColors.caloriesCalculatorColor,

@ -3,8 +3,8 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.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';
@ -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/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class RefundHistoryScreen extends StatefulWidget {
const RefundHistoryScreen({super.key});
final int refundType;
const RefundHistoryScreen({super.key, required this.refundType});
@override
State<RefundHistoryScreen> createState() => _RefundHistoryScreenState();
}
class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
late AppState _appState;
@override
void initState() {
super.initState();
_appState = getIt<AppState>();
final vm = context.read<RefundRequestViewModel>();
scheduleMicrotask(() async {
await vm.getRefundInvoices();
await vm.getRefundInvoices(refundType: widget.refundType);
});
}
// Helpers <EFBFBD><EFBFBD><EFBFBD>
// Helpers
String _formatDate(String? rawDate) {
if (rawDate == null || rawDate.isEmpty) return '';
try {
// Handle WCF /Date(...)/ format
final match = RegExp(r'/Date\((-?\d+)').firstMatch(rawDate);
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));
final date = DateUtil.convertStringToDate(rawDate);
return DateFormat('MMM dd, yyyy').format(date);
} catch (_) {
return rawDate;
}
@ -69,17 +66,49 @@ class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
// Card
Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) {
final isArabic = _appState.isArabic();
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;
}
final clinicName = isLoading ? 'Clinic Name' : (isArabic ? (invoice?.clinicNameN ?? invoice?.clinicName) : invoice?.clinicName) ?? '-';
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;
final projectName = isLoading ? 'Hospital Name' : (isArabic ? (invoice?.projectNameN ?? invoice?.projectName) : invoice?.projectName) ?? '-';
// 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);
final invoiceNo = isLoading ? 'INV-000000' : (invoice?.invoiceNumber ?? '-');
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) ?? '-');
// Resolve total refund amount from whichever payload is present
final projectName = isInvoiceRefund ? (invoice?.invoiceRequestPayload?.project ?? "") : "";
final String totalProcedures = (invoice?.invoiceRequestPayload?.proceduresList?.length.toString() ?? '');
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
@ -94,89 +123,67 @@ class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (!isLoading)
LocaleKeys.invoiceNo.tr(context: context).toText12(
color: AppColors.textColorLight,
isBold: true,
),
if (!isLoading) SizedBox(width: 4.w),
invoiceNo.toText14(isBold: true, isEnglishOnly: true).toShimmer2(isShow: isLoading),
],
),
invoiceDate.toText12(color: AppColors.textColorLight, isBold: true).toShimmer2(isShow: isLoading),
"${LocaleKeys.referenceNo.tr(context: context)} $referenceNo"
.toText16(
color: AppColors.textColor,
isBold: true,
)
.toShimmer2(isShow: isLoading),
],
),
SizedBox(height: 12.h),
// Chips clinic & hospital
SizedBox(height: 8.h),
Wrap(
spacing: 6.w,
runSpacing: 6.h,
children: [
AppCustomChipWidget(
icon: "",
labelText: clinicName,
).toShimmer2(isShow: isLoading),
AppCustomChipWidget(
icon: "",
labelText: projectName,
).toShimmer2(isShow: isLoading),
if (!isLoading && refundDestination.isNotEmpty)
AppCustomChipWidget(icon: getRefundDestinationIcon(refundDestination), iconHasColor: false, labelText: refundDestination),
if (totalProcedures.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$totalProcedures ${LocaleKeys.procedures.tr()}"),
if (!isLoading && projectName.isNotEmpty) AppCustomChipWidget(icon: "", labelText: projectName),
],
),
SizedBox(height: 12.h),
SizedBox(height: 16.h),
// Amounts row
Row(
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(
totalAmount.toText16(isBold: true, isEnglishOnly: true),
AppColors.blackColor,
11,
isSaudiCurrency: true,
isExpanded: false,
).toShimmer2(isShow: isLoading),
],
),
),
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,
isExpanded: false,
).toShimmer2(isShow: isLoading),
],
),
),
],
),
// Total refund amount
if (totalAmountStr != null)
Utils.getPaymentAmountWithSymbol(
isExpanded: false,
(isLoading ? '0.00' : totalAmountStr).toText20(isBold: true, color: AppColors.textColor),
AppColors.textColor,
18,
isSaudiCurrency: true,
).toShimmer2(isShow: isLoading),
SizedBox(height: 16.h),
CustomButton(
text: LocaleKeys.viewDetails.tr(context: context),
onPressed: () => Navigator.of(context).push(CustomPageRoute(page: RefundRequestDetailScreen(invoice: invoice!))),
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1),
borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01),
textColor: AppColors.primaryRedColor,
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
iconColor: AppColors.whiteColor,
iconSize: 18.h,
).toShimmer2(isShow: isLoading),
],
),
),
@ -227,7 +234,6 @@ class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
if (viewModel.refundInvoicesList.isEmpty) {
return _buildEmptyState();
}
return ListView.separated(
padding: EdgeInsets.only(top: 24.h),
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/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
@ -88,7 +89,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
.map((item) => RefundableProcedure(
procedureId: item.lineItemNo ?? 0,
procedureName: item.procedureName ?? '',
patientShare: item.patientPaidAmount ?? 0.0,
patientPaidAmount: item.patientPaidAmount ?? 0.0,
apiLineItemNo: item.lineItemNo,
apiProcedureId: item.procedureId,
))
@ -266,11 +267,16 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// Bank Transfer Input
Widget _buildBankInputDetails(RefundRequestViewModel vm) {
final hasError = vm.ibanError != null || vm.descriptionError != null;
return Container(
padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
),
child: Column(
children: [
@ -279,7 +285,6 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
hintText: "SA0000000000000000000000",
controller: _ibanController,
focusNode: _ibanFocusNode,
leadingIconNeedsContainer: false,
autoFocus: false,
keyboardType: TextInputType.text,
isEnable: true,
@ -291,7 +296,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
isCountryDropDown: false,
leadingIcon: AppAssets.bankIcon,
fontFamily: "Poppins",
hasError: vm.ibanError != null,
hasError: false,
errorMessage: vm.ibanError,
onChange: (value) {
vm.clearIbanError();
@ -308,7 +313,6 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
focusNode: _descriptionFocusNode,
autoFocus: false,
keyboardType: TextInputType.text,
leadingIconNeedsContainer: false,
isEnable: true,
isReadOnly: false,
prefix: null,
@ -318,7 +322,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
isCountryDropDown: false,
leadingIcon: AppAssets.descriptionIcon,
fontFamily: "Poppins",
hasError: vm.descriptionError != null,
hasError: false,
errorMessage: vm.descriptionError,
onChange: (value) {
vm.clearDescriptionError();
@ -378,7 +382,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
Expanded(child: name.toText16(isBold: true)),
SizedBox(width: 8.w),
Utils.getPaymentAmountWithSymbol(
procedure.patientShare.toStringAsFixed(2).toText20(
procedure.patientPaidAmount.toStringAsFixed(2).toText20(
isBold: true,
isEnglishOnly: true,
color: AppColors.textColor,
@ -397,26 +401,22 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason),
),
child: Padding(
padding: EdgeInsets.only(top: 12.h),
padding: EdgeInsets.only(top: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
(procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)).toText14(
color: procedure.refundReason != null ? AppColors.textColor : AppColors.greyTextColor, isBold: true),
(procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context))
.toText14(color: AppColors.textColor, isBold: true),
],
),
),
Utils.buildSvgWithAssets(
icon: AppAssets.arrow_down,
width: 25.h,
height: 25.h,
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
),
@ -438,9 +438,9 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// Bottom bar
Widget _buildBottomBar(RefundRequestViewModel vm) {
final isStep2 = vm.currentStep == 1;
final isWallet = vm.selectedTab == RefundTabType.hmgWallet;
Widget _buildBottomBar(RefundRequestViewModel refundRequestViewModel) {
final isStep2 = refundRequestViewModel.currentStep == 1;
final isWallet = refundRequestViewModel.selectedTab == RefundTabType.hmgWallet;
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
@ -451,15 +451,15 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!isWallet && vm.selectedProcedures.isNotEmpty) ...[
if (!isWallet && refundRequestViewModel.selectedProcedures.isNotEmpty) ...[
Padding(
padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.totalAmount.toText18(color: AppColors.textColor, isBold: true),
LocaleKeys.totalAmount.tr().toText18(color: AppColors.textColor, isBold: true),
Utils.getPaymentAmountWithSymbol(
vm.totalRefundableAmount.toStringAsFixed(2).toText24(
refundRequestViewModel.totalRefundableAmount.toStringAsFixed(2).toText24(
isBold: true,
isEnglishOnly: true,
color: AppColors.textColor,
@ -477,24 +477,24 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context),
onPressed: () {
if (!isStep2) {
if (vm.selectedProcedures.isNotEmpty) vm.nextStep();
if (refundRequestViewModel.selectedProcedures.isNotEmpty) refundRequestViewModel.nextStep();
} else {
final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices;
final isBankTransfer = refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices;
if (isBankTransfer) {
final isValid = vm.validateBankTransferForm(
final isValid = refundRequestViewModel.validateBankTransferForm(
iban: _ibanController.text,
description: _descriptionController.text,
);
if (!isValid) return;
}
_handleInvoiceRefundRequestSubmit(vm);
_handleInvoiceRefundRequestSubmit(refundRequestViewModel);
}
},
isDisabled: isStep2
? (vm.selectedPaymentMethod == null ||
(vm.selectedPaymentMethod == RefundTabType.invoices &&
? (refundRequestViewModel.selectedPaymentMethod == null ||
(refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices &&
(_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)))
: (vm.selectedProcedures.isEmpty || !vm.allSelectedProceduresHaveReasons),
: (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons),
).paddingSymmetrical(24.h, 24.h),
],
),
@ -537,14 +537,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// 1 = invoice refund
refundProject: invoice.projectName ?? '',
refundProjectId: invoice.projectId ?? 0,
// TODO: It should be removed
refundPatientId: 4768571.toString(),
refundPatientId: patientId.toString(),
refundPatientName: patientName,
refundNationalId: patientNationalId,
patientMobileNumber: patientMobileNumber,
refundInvoiceNo: 260000075.toString(),
// refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), TODO: It should be removed
refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(),
refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(),
refundDoctorId: invoice.doctorId ?? 0,
refundDoctorName: invoice.doctorName ?? '',
@ -559,13 +556,13 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
lineItemNo: p.apiLineItemNo ?? 0,
procedureId: p.apiProcedureId ?? '',
procedureName: p.procedureName,
patientPaidAmount: p.patientShare,
patientPaidAmount: p.patientPaidAmount,
refundReason: p.refundReason ?? '',
))
.toList(),
);
await vm.submitRefund(
await vm.submitInvoiceRefundRequest(
request: request,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
@ -597,17 +594,17 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)),
if (refNo.isNotEmpty) ...[
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
LocaleKeys.hereIsYourRequestNumber.tr(context: context).toText14(
LocaleKeys.hereIsYourRequestNumber.tr().toText16(
color: AppColors.textColorLight,
weight: FontWeight.w600,
),
SizedBox(width: 4.w),
refNo.toText16(isBold: true),
SizedBox(width: 6.w),
refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true),
],
),
).paddingSymmetrical(24.w, 0),
],
SizedBox(height: 24.h),
Row(
@ -619,6 +616,13 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
onPressed: () {
Navigator.pop(context); // pop success sheet
Navigator.pop(context); // pop refund page
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
),
),
);
},
textColor: AppColors.whiteColor,
),
@ -649,7 +653,18 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
Expanded(
child: CollapsingListView(
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(
padding: EdgeInsets.only(bottom: 32.h),
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;
}
// Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered
if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) {
// Case 2: Should stop flag is true OR
// Case 3: Probability >= 70% OR
// Case 4: 30 or more questions answered
if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) {
// Navigate to results/possible conditions screen
context.navigateWithName(AppRoutes.possibleConditionsPage);
return;

@ -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/parking/paking_page.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/presentation/service_price/services_price_list_page.dart';
import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_home_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart';
@ -108,6 +110,7 @@ class AppRoutes {
static const String refundRequestCreatePage = '/refundRequestCreatePage';
static const String createWithdrawRequest = '/createWithdrawRequest';
static const String refundHistoryScreen = '/refundHistoryScreen';
static const String refundRequestDetailScreen = '/refundRequestDetailScreen';
static Map<String, WidgetBuilder> get routes => {
initialRoute: (context) => SplashPage(),
@ -176,5 +179,9 @@ class AppRoutes {
createWithdrawRequest: (context) {
return WithdrawRequestCreatePage();
},
refundRequestDetailScreen: (context) {
final args = ModalRoute.of(context)?.settings.arguments as RefundInvoiceModel;
return RefundRequestDetailScreen(invoice: args);
},
};
}

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

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

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

Loading…
Cancel
Save