diff --git a/assets/images/svg/bank_icon_only.svg b/assets/images/svg/bank_icon_only.svg new file mode 100644 index 00000000..169731d6 --- /dev/null +++ b/assets/images/svg/bank_icon_only.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/card_user.svg b/assets/images/svg/card_user.svg index ee8bb0e2..bf42ee14 100644 --- a/assets/images/svg/card_user.svg +++ b/assets/images/svg/card_user.svg @@ -1,3 +1,4 @@ + diff --git a/assets/images/svg/habib logo_only.svg b/assets/images/svg/habib logo_only.svg new file mode 100644 index 00000000..c034b9dd --- /dev/null +++ b/assets/images/svg/habib logo_only.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 0904362e..12219c0c 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -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": "المبلغ" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 07e03830..8e1872b9 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -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" + } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 7b104ab3..d7717b54 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -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"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 1cd20a55..6a27f641 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -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"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index f4c18ff7..c4a79aed 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -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'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 81c1fedb..279b61c8 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -339,17 +339,15 @@ class AppDependencies { getIt.registerLazySingleton(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); getIt.registerLazySingleton(() => RefundRequestViewModel( appState: getIt(), refundRequestRepo: getIt(), errorHandlerService: getIt(), )); - getIt.registerLazySingleton(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); + getIt.registerLazySingleton( + () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt())); } diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index b5ba4bae..ca644e0c 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -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; } } diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index c9ff8649..e1e14ecf 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -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 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 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().getAuthenticatedUser()!.patientIdentificationNo ?? ""); + final result = await habibWalletRepo.getLakumAccountInformation( + identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? ""); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index c922c752..0e0273e6 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -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") { diff --git a/lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart b/lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart new file mode 100644 index 00000000..8e62b943 --- /dev/null +++ b/lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart @@ -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 json) { + return IsRefundableInvoiceObject( + errorCode: json['ErrorCode'] as String?, + isRefundable: json['IsRefundable'] as bool?, + message: json['Message'] as String?, + statusCode: json['StatusCode'] as int?, + ); + } +} + diff --git a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart index d8b6ffc9..b17dd5aa 100644 --- a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart +++ b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart @@ -13,10 +13,10 @@ class SubmitRefundResponseModel { factory SubmitRefundResponseModel.fromJson(Map 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 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? 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 json) { + return WalletRequestPayload( + advancesList: (json['AdvancesList'] as List?)?.map((e) => WalletAdvanceItem.fromJson(e as Map)).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 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? 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? 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 json) { + return InvoiceRequestPayload( + advancesList: (json['AdvancesList'] as List?)?.map((e) => WalletAdvanceItem.fromJson(e as Map)).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?)?.map((e) => InvoiceRefundProcedure.fromJson(e as Map)).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 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) : null, + invoiceRequestPayload: + json['InvoiceRequestPayload'] != null ? InvoiceRequestPayload.fromJson(json['InvoiceRequestPayload'] as Map) : null, ); } } diff --git a/lib/features/refund_request/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart index 03797e54..eaf7d9e0 100644 --- a/lib/features/refund_request/refund_request_repo.dart +++ b/lib/features/refund_request/refund_request_repo.dart @@ -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>> getRefundStatus({required String referenceNumber}); - Future>>> getRefundInvoices({required String patientId, String invoiceNumberQuery}); + Future>>> getRefundInvoices({ + required int refundType, + required String patientId, + String invoiceNumberQuery, + }); Future>> submitRefund({required SubmitRefundRequestModel request}); + + Future>> checkIsRefundableInvoice({ + required int invoiceNo, + required int projectId, + }); } class RefundRequestRepoImp implements RefundRequestRepo { @@ -215,10 +225,12 @@ class RefundRequestRepoImp implements RefundRequestRepo { @override Future>>> getRefundInvoices({ + required int refundType, required String patientId, String invoiceNumberQuery = "", }) async { final Map 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 invoices = listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map)).toList() : []; @@ -297,4 +309,52 @@ class RefundRequestRepoImp implements RefundRequestRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> checkIsRefundableInvoice({ + required int invoiceNo, + required int projectId, + }) async { + final Map body = { + "InvoiceNo": invoiceNo.toString(), + "ProjectID": projectId, + }; + + try { + GenericApiModel? 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) : IsRefundableInvoiceObject(); + + apiResponse = GenericApiModel( + 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())); + } + } } diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index 51212741..6c9b58dd 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -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 getRefundInvoices({ - String? patientId, + required int refundType, String invoiceNumberQuery = "", Function(List)? 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 submitRefund({ + Future submitInvoiceRefundRequest({ required SubmitRefundRequestModel request, Function(SubmitRefundResponseModel)? onSuccess, Function(String)? onError, @@ -535,4 +536,34 @@ class RefundRequestViewModel extends ChangeNotifier { }, ); } + + // ── Check Is Refundable Invoice ─────────────────────────────────────────── + + Future 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"); + } + } + }, + ); + } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index ec7bfb1d..7700984d 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -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'; } diff --git a/lib/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index c17d9ba7..639ebef1 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -188,254 +188,270 @@ class _RegisterNew extends State { ), 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( - selector: (_, authViewModel) => authViewModel.genderType, - shouldRebuild: (previous, next) => previous != next, - builder: (context, genderType, child) { - final authVM = context.read(); - 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( - selector: (_, authViewModel) => authViewModel.maritalStatus, - shouldRebuild: (previous, next) => previous != next, - builder: (context, maritalStatus, child) { - final authVM = context.read(); // 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( + selector: (_, authViewModel) => authViewModel.genderType, + shouldRebuild: (previous, next) => previous != next, + builder: (context, genderType, child) { + final authVM = context.read(); + 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( + selector: (_, authViewModel) => authViewModel.maritalStatus, + shouldRebuild: (previous, next) => previous != next, + builder: (context, maritalStatus, child) { + final authVM = context.read(); // 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? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( + selector: (context, authViewModel) { + final appState = getIt.get(); + 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(); + 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? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( - selector: (context, authViewModel) { - final appState = getIt.get(); - 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(); - 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), diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index b069b441..cc0c9731 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -185,9 +185,17 @@ class _WithdrawRequestCreatePageState extends State { Widget _buildBankInputDetails() { return Consumer( 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 { hintText: "SA0000000000000000000000", controller: _bankDetailsController, focusNode: _ibanFocusNode, - leadingIconNeedsContainer: false, autoFocus: false, keyboardType: TextInputType.text, isEnable: true, @@ -207,7 +214,7 @@ class _WithdrawRequestCreatePageState extends State { 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 { 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 { 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 { 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 { 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 { isCloseButtonVisible: false, isDismissible: false, isFullScreen: false, - callBackFunc: () {}, + isAutoDismiss: false, ); } @@ -381,7 +407,13 @@ class _WithdrawRequestCreatePageState extends State { 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 { 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 { ); } } + +// 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} diff --git a/lib/presentation/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart index 26f0d1c9..67ee05a6 100644 --- a/lib/presentation/my_invoices/my_invoices_list.dart +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -137,215 +137,101 @@ class _MyInvoicesListState extends State { } }, ).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 { 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 { }, 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 { } }, ); + } + }, + ), + ), + ), + ), + ) + : 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, + ); + } + } + }, + ); // } }, ), diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 2cafdc57..dc5414cd 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -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(); + + 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().showErrorBottomSheet(message: message); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + getIt().showErrorBottomSheet(message: error); + }, + ); + } + @override Widget build(BuildContext context) { AppState appState = getIt(); @@ -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, diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index 04f369d0..630c5dc0 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -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 createState() => _RefundHistoryScreenState(); } class _RefundHistoryScreenState extends State { - late AppState _appState; - @override void initState() { super.initState(); - _appState = getIt(); final vm = context.read(); scheduleMicrotask(() async { - await vm.getRefundInvoices(); + await vm.getRefundInvoices(refundType: widget.refundType); }); } - // ── Helpers ─────────────────────────────────���───────────────────────────── + // ── 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 { // ── 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 { 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 { if (viewModel.refundInvoicesList.isEmpty) { return _buildEmptyState(); } - return ListView.separated( padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 284d4581..fd64bb67 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -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 { .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 { // ── 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 { hintText: "SA0000000000000000000000", controller: _ibanController, focusNode: _ibanFocusNode, - leadingIconNeedsContainer: false, autoFocus: false, keyboardType: TextInputType.text, isEnable: true, @@ -291,7 +296,7 @@ class _RefundRequestCreatePageState extends State { 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 { focusNode: _descriptionFocusNode, autoFocus: false, keyboardType: TextInputType.text, - leadingIconNeedsContainer: false, isEnable: true, isReadOnly: false, prefix: null, @@ -318,7 +322,7 @@ class _RefundRequestCreatePageState extends State { 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 { 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 { 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 { // ── 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 { 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 { 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 { // 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 { 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 { 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 { 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 { Expanded( child: CollapsingListView( title: LocaleKeys.requestRefund.tr(context: context), - requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), + leadingCallback: () { + final refundRequestViewModel = context.read(); + 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( diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart new file mode 100644 index 00000000..a28aa4dd --- /dev/null +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -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), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 79c47d09..fc99d4e6 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -77,8 +77,10 @@ class _TriagePageState extends State { 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; diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index a82879f6..f292755b 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -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 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); + }, }; } diff --git a/lib/splashPage.dart b/lib/splashPage.dart index cf1ca7f4..22b9f979 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -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 { 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().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; @@ -80,7 +77,6 @@ class _SplashScreenState extends State { } navigateToTeleConsult() async { - String roomID = await Utils.getStringFromPrefs(CacheConst.zoomRoomID); String callTypeID = await Utils.getStringFromPrefs(CacheConst.callTypeID); @@ -192,9 +188,7 @@ class _SplashScreenState extends State { 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 { final apiClient = getIt.get(); - Map requestBody = { - "Open_SessionID": sessionId, - "SessionStatus": sessionStatus, - "SessionEndedBy": sessionEndedBy - }; + Map requestBody = {"Open_SessionID": sessionId, "SessionStatus": sessionStatus, "SessionEndedBy": sessionEndedBy}; await apiClient.post( CHANGE_PATIENT_ER_SESSION, diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index d5d96229..734ca2cc 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -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, diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 7502404c..16a955c8 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -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,