diff --git a/assets/images/png/lakum_logo_white.png b/assets/images/png/lakum_logo_white.png new file mode 100644 index 00000000..b05c90a2 Binary files /dev/null and b/assets/images/png/lakum_logo_white.png differ diff --git a/assets/images/svg/hmg_pharmacy_logo.svg b/assets/images/svg/hmg_pharmacy_logo.svg new file mode 100644 index 00000000..e069d513 --- /dev/null +++ b/assets/images/svg/hmg_pharmacy_logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/svg/redeem_icon.svg b/assets/images/svg/redeem_icon.svg new file mode 100644 index 00000000..52eb8815 --- /dev/null +++ b/assets/images/svg/redeem_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index dab50baa..3c54f367 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1845,5 +1845,10 @@ "maxOneFileAllowed": "يمكن رفع ملف واحد فقط", "fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت", "completedPrescriptionOrder": "تمت الخدمة", - "submitEReferral": "تقديم الطلب" + "submitEReferral": "تقديم الطلب", + "redeem": "استبدال", + "points": "نقاط", + "transactions": "المعاملات", + "pharmacy": "الصيدلية", + "visitsOrders": "الزيارات/الطلبات" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index b4dd35f7..62cdd752 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -576,7 +576,7 @@ "active": "Active", "inactive": "InActive", "balance": "Balance", - "gained": "GAINED", + "gained": "Gained", "consumed": "Consumed", "transferred": "Transferred", "riyal": "RIYAL", @@ -1835,7 +1835,12 @@ "maxOneFileAllowed": "Only 1 file can be uploaded", "fileSizeExceedsLimit": "File size must not exceed 1 MB", "completedPrescriptionOrder": "Completed", - "submitEReferral": "Submit" + "submitEReferral": "Submit", + "redeem": "Redeem", + "points": "Points", + "transactions": "Transactions", + "pharmacy": "Pharmacy", + "visitsOrders": "Visits/Orders" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 64074ff8..b5d62899 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -208,8 +208,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + body['TokenID'] = "@dm!n"; + // body['PatientID'] = 5787639; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 74fa0ee7..301cb31c 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -281,7 +281,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.1; + static final double appVersionID = 21.2; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 2612bde9..f11de07f 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -377,6 +377,8 @@ class AppAssets { static const String gallery = '$svgBasePath/gallery.svg'; static const String files = '$svgBasePath/files.svg'; static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg'; + static const String hmg_pharmacy_logo = '$svgBasePath/hmg_pharmacy_logo.svg'; + static const String redeem_icon = '$svgBasePath/redeem_icon.svg'; //Health Calculators static const String bodyfatcalc = '$svgBasePath/bfc.svg'; @@ -418,6 +420,7 @@ class AppAssets { static const String maleIcon = '$pngBasePath/male_icon.png'; static const String femaleIcon = '$pngBasePath/female_icon.png'; static const String lakumLogo = '$pngBasePath/lakum_logo.png'; + static const String lakumLogoWhite = '$pngBasePath/lakum_logo_white.png'; static const String fullBodyFrontMale = '$pngBasePath/full_body_front_male.png'; static const String fullBodyBackMale = '$pngBasePath/full_body_back_male.png'; @@ -427,8 +430,6 @@ class AppAssets { static const String bmiFullBodyFemale = '$pngBasePath/female_bmi.png'; static const String defaultMEP = '$pngBasePath/default_MEP.png'; - - static const String ambulance_section = '$pngBasePath/ambulance_section.png'; static const String rrt_section = '$pngBasePath/rrt_section.png'; static const String er_checkin = '$pngBasePath/er_checkin.png'; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 1cf8094d..210970b1 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -99,7 +99,11 @@ class Utils { static bool isArabicText(String inputText) { bool isArabicText = false; - final arabic = RegExp(r'^[\u0621-\u064A]+'); + // Updated regex to include: + // - Arabic letters: \u0621-\u064A + // - Arabic numerals: \u0660-\u0669 (٠-٩) + // - Persian numerals: \u06F0-\u06F9 (۰-۹) + final arabic = RegExp(r'[\u0621-\u064A\u0660-\u0669\u06F0-\u06F9]'); if (arabic.hasMatch(inputText)) { isArabicText = true; } else { diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index 737df465..41ca087a 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; @@ -451,7 +452,9 @@ class _OTPVerificationScreenState extends State { super.initState(); _otpController = TextEditingController(); _startResendTimer(); - checkSignature(); + if(Platform.isAndroid) { + checkSignature(); + } } @override diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 7ac09686..c2f35308 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -1479,6 +1479,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { result.fold( (failure) async { + if(onError != null) { + onError(failure.message); + } onError!(LocaleKeys.noDoctorFound.tr()); }, (apiResponse) { diff --git a/lib/features/habib_wallet/habib_wallet_repo.dart b/lib/features/habib_wallet/habib_wallet_repo.dart index c89a85be..57d5af20 100644 --- a/lib/features/habib_wallet/habib_wallet_repo.dart +++ b/lib/features/habib_wallet/habib_wallet_repo.dart @@ -18,6 +18,10 @@ abstract class HabibWalletRepo { Future>> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference}); Future>> getPatientInfoByPatientID({required String patientID}); + + Future>> getLakumAccountInformation({required String identificationNumber}); + + Future>> getLakumAccountInquiryInformation({required String lakumAccountNumber}); } class HabibWalletRepoImp implements HabibWalletRepo { @@ -231,4 +235,75 @@ class HabibWalletRepoImp implements HabibWalletRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> getLakumAccountInformation({required String identificationNumber}) async { + Map requestBody = {"IdentificationNo": identificationNumber}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_LACUM_ACCOUNT_INFORMATION, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> getLakumAccountInquiryInformation({required String lakumAccountNumber}) async { + Map requestBody = { + "AccountNumber": lakumAccountNumber, + "IsDetailsRequired": true, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_LACUM_GROUP_INFORMATION, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 01dc2357..16997369 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -1,6 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.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/generated/locale_keys.g.dart'; @@ -43,6 +47,54 @@ class HabibWalletViewModel extends ChangeNotifier { String? get hospitalError => _hospitalError; String yahalaAccountNumber = ''; + bool isLakumAccountInfoLoading = false; + late LakumInquiryInformationResponseModel lakumAccountInfo; + + // Get all flattened transactions sorted by date descending + List getAllLakumTransactions() { + if (lakumAccountInfo.gainedPointsAmountPerYear == null) { + return []; + } + + List allTransactions = []; + + // Flatten the nested structure + for (var yearData in lakumAccountInfo.gainedPointsAmountPerYear!) { + if (yearData.pointsAmountPerMonth != null) { + for (var monthData in yearData.pointsAmountPerMonth!) { + if (monthData.pointsAmountPerday != null) { + for (var dayData in monthData.pointsAmountPerday!) { + if (dayData.pointsDetails != null) { + allTransactions.addAll(dayData.pointsDetails!); + } + } + } + } + } + } + + // Sort by transaction date DESCENDING (latest/most recent transactions first, older transactions last) + 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 (b.transactionDate == null) return -1; // a goes before b + + try { + DateTime dateA = DateUtil.convertStringToDate(a.transactionDate!); + DateTime dateB = DateUtil.convertStringToDate(b.transactionDate!); + + // Compare dateB to dateA (not dateA to dateB) for DESCENDING order + // This puts newer dates (larger values) at the top of the list + return dateB.compareTo(dateA); + } catch (e) { + // If date parsing fails, maintain current order + return 0; + } + }); + + return allTransactions; + } // Clear amount error void clearAmountError() { @@ -103,6 +155,7 @@ class HabibWalletViewModel extends ChangeNotifier { initHabibWalletProvider() { isWalletAmountLoading = true; isBottomSheetContentLoading = false; + isLakumAccountInfoLoading = true; habibWalletAmount = 0; walletRechargeAmount = 0; selectedRechargeType = 1; @@ -112,6 +165,7 @@ class HabibWalletViewModel extends ChangeNotifier { fileNumber = ''; depositorName = ''; mobileNumber = ''; + yahalaAccountNumber = ""; notifyListeners(); } @@ -148,6 +202,11 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + setYahalaAccountNumber(String accountNumber) { + yahalaAccountNumber = accountNumber; + notifyListeners(); + } + String getSelectedRechargeTypeValue() { switch (selectedRechargeType) { case 1: @@ -277,4 +336,46 @@ class HabibWalletViewModel extends ChangeNotifier { }, ); } + + Future getLakumAccountInformation({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isLakumAccountInfoLoading = true; + lakumAccountInfo = LakumInquiryInformationResponseModel(); + notifyListeners(); + + final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? ""); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) async { + if (apiResponse.messageStatus == 2) { + yahalaAccountNumber = "0"; + isLakumAccountInfoLoading = false; + notifyListeners(); + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data["YahalaAccountNo"] == 0 || apiResponse.data["YahalaAccountNo"] == "0") { + yahalaAccountNumber = "0"; + } else { + yahalaAccountNumber = apiResponse.data["YahalaAccountNo"].toString(); + + final lakumAccountResult = await habibWalletRepo.getLakumAccountInquiryInformation(lakumAccountNumber: yahalaAccountNumber); + lakumAccountResult.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (lakumApiResponse) async { + if (lakumApiResponse.messageStatus == 2) { + lakumAccountInfo = LakumInquiryInformationResponseModel(); + } else if (lakumApiResponse.messageStatus == 1) { + lakumAccountInfo = LakumInquiryInformationResponseModel.fromJson(lakumApiResponse.data["LakumInquiryInformationObjVersion"]); + } + }, + ); + } + isLakumAccountInfoLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/habib_wallet/models/lakum_inquiry_information_response_model.dart b/lib/features/habib_wallet/models/lakum_inquiry_information_response_model.dart new file mode 100644 index 00000000..621434fe --- /dev/null +++ b/lib/features/habib_wallet/models/lakum_inquiry_information_response_model.dart @@ -0,0 +1,318 @@ +class LakumInquiryInformationResponseModel { + int? accountNumber; + String? accountStatus; + String? barCode; + num? consumedPoints; + dynamic consumedPointsAmount; + dynamic consumedPointsAmountPerYear; + + // List? consumedPointsDetails; + String? createdDate; + num? expiredPoints; + String? expiryDate; + num? gainedPoints; + num? gainedPointsAmount; + List? gainedPointsAmountPerYear; + + // List? gainedPointsDetails; + String? lakumMessageStatus; + String? memberName; + String? memberUniversalId; + String? mobileNumber; + num? pointsBalance; + num? pointsBalanceAmount; + num? pointsWillBeExpired; + String? prefLang; + num? statusCode; + num? transferPoints; + dynamic transferPointsAmountPerYear; + + // List? transferPointsDetails; + num? waitingPoints; + num? loyalityAmount; + num? loyalityPoints; + num? purchaseRate; + + LakumInquiryInformationResponseModel( + {this.accountNumber, + this.accountStatus, + this.barCode, + this.consumedPoints, + this.consumedPointsAmount, + this.consumedPointsAmountPerYear, + // this.consumedPointsDetails, + this.createdDate, + this.expiredPoints, + this.expiryDate, + this.gainedPoints, + this.gainedPointsAmount, + this.gainedPointsAmountPerYear, + // this.gainedPointsDetails, + this.lakumMessageStatus, + this.memberName, + this.memberUniversalId, + this.mobileNumber, + this.pointsBalance, + this.pointsBalanceAmount, + this.pointsWillBeExpired, + this.prefLang, + this.statusCode, + this.transferPoints, + this.transferPointsAmountPerYear, + // this.transferPointsDetails, + this.waitingPoints, + this.loyalityAmount, + this.loyalityPoints, + this.purchaseRate}); + + LakumInquiryInformationResponseModel.fromJson(Map json) { + accountNumber = json['AccountNumber']; + accountStatus = json['AccountStatus']; + barCode = json['BarCode']; + consumedPoints = json['ConsumedPoints']; + consumedPointsAmount = json['ConsumedPointsAmount']; + consumedPointsAmountPerYear = json['ConsumedPointsAmountPerYear']; + // if (json['ConsumedPointsDetails'] != null) { + // consumedPointsDetails = []; + // json['ConsumedPointsDetails'].forEach((v) { + // consumedPointsDetails!.add(new Null.fromJson(v)); + // }); + // } + createdDate = json['CreatedDate']; + expiredPoints = json['ExpiredPoints']; + expiryDate = json['ExpiryDate']; + gainedPoints = json['GainedPoints']; + gainedPointsAmount = json['GainedPointsAmount']; + if (json['GainedPointsAmountPerYear'] != null) { + gainedPointsAmountPerYear = []; + json['GainedPointsAmountPerYear'].forEach((v) { + gainedPointsAmountPerYear!.add(new GainedPointsAmountPerYear.fromJson(v)); + }); + } + // if (json['GainedPointsDetails'] != null) { + // gainedPointsDetails = []; + // json['GainedPointsDetails'].forEach((v) { + // gainedPointsDetails!.add(new Null.fromJson(v)); + // }); + // } + lakumMessageStatus = json['LakumMessageStatus']; + memberName = json['MemberName']; + memberUniversalId = json['MemberUniversalId']; + mobileNumber = json['MobileNumber']; + pointsBalance = json['PointsBalance']; + pointsBalanceAmount = json['PointsBalanceAmount']; + pointsWillBeExpired = json['PointsWillBeExpired']; + prefLang = json['PrefLang']; + statusCode = json['StatusCode']; + transferPoints = json['TransferPoints']; + transferPointsAmountPerYear = json['TransferPointsAmountPerYear']; + // if (json['TransferPointsDetails'] != null) { + // transferPointsDetails = []; + // json['TransferPointsDetails'].forEach((v) { + // transferPointsDetails!.add(new Null.fromJson(v)); + // }); + // } + waitingPoints = json['WaitingPoints']; + loyalityAmount = json['loyalityAmount']; + loyalityPoints = json['loyalityPoints']; + purchaseRate = json['purchaseRate']; + } + + Map toJson() { + final Map data = new Map(); + data['AccountNumber'] = this.accountNumber; + data['AccountStatus'] = this.accountStatus; + data['BarCode'] = this.barCode; + data['ConsumedPoints'] = this.consumedPoints; + data['ConsumedPointsAmount'] = this.consumedPointsAmount; + data['ConsumedPointsAmountPerYear'] = this.consumedPointsAmountPerYear; + // if (this.consumedPointsDetails != null) { + // data['ConsumedPointsDetails'] = this.consumedPointsDetails!.map((v) => v.toJson()).toList(); + // } + data['CreatedDate'] = this.createdDate; + data['ExpiredPoints'] = this.expiredPoints; + data['ExpiryDate'] = this.expiryDate; + data['GainedPoints'] = this.gainedPoints; + data['GainedPointsAmount'] = this.gainedPointsAmount; + if (this.gainedPointsAmountPerYear != null) { + data['GainedPointsAmountPerYear'] = this.gainedPointsAmountPerYear!.map((v) => v.toJson()).toList(); + } + // if (this.gainedPointsDetails != null) { + // data['GainedPointsDetails'] = this.gainedPointsDetails!.map((v) => v.toJson()).toList(); + // } + data['LakumMessageStatus'] = this.lakumMessageStatus; + data['MemberName'] = this.memberName; + data['MemberUniversalId'] = this.memberUniversalId; + data['MobileNumber'] = this.mobileNumber; + data['PointsBalance'] = this.pointsBalance; + data['PointsBalanceAmount'] = this.pointsBalanceAmount; + data['PointsWillBeExpired'] = this.pointsWillBeExpired; + data['PrefLang'] = this.prefLang; + data['StatusCode'] = this.statusCode; + data['TransferPoints'] = this.transferPoints; + data['TransferPointsAmountPerYear'] = this.transferPointsAmountPerYear; + // if (this.transferPointsDetails != null) { + // data['TransferPointsDetails'] = this.transferPointsDetails!.map((v) => v.toJson()).toList(); + // } + data['WaitingPoints'] = this.waitingPoints; + data['loyalityAmount'] = this.loyalityAmount; + data['loyalityPoints'] = this.loyalityPoints; + data['purchaseRate'] = this.purchaseRate; + return data; + } +} + +class GainedPointsAmountPerYear { + num? amountPerYear; + List? pointsAmountPerMonth; + num? pointsPerYear; + int? year; + + GainedPointsAmountPerYear({this.amountPerYear, this.pointsAmountPerMonth, this.pointsPerYear, this.year}); + + GainedPointsAmountPerYear.fromJson(Map json) { + amountPerYear = json['AmountPerYear']; + if (json['PointsAmountPerMonth'] != null) { + pointsAmountPerMonth = []; + json['PointsAmountPerMonth'].forEach((v) { + pointsAmountPerMonth!.add(new PointsAmountPerMonth.fromJson(v)); + }); + } + pointsPerYear = json['PointsPerYear']; + year = json['Year']; + } + + Map toJson() { + final Map data = new Map(); + data['AmountPerYear'] = this.amountPerYear; + if (this.pointsAmountPerMonth != null) { + data['PointsAmountPerMonth'] = this.pointsAmountPerMonth!.map((v) => v.toJson()).toList(); + } + data['PointsPerYear'] = this.pointsPerYear; + data['Year'] = this.year; + return data; + } +} + +class PointsAmountPerMonth { + num? amountPerMonth; + String? month; + num? monthNumber; + List? pointsAmountPerday; + num? pointsPerMonth; + + PointsAmountPerMonth({this.amountPerMonth, this.month, this.monthNumber, this.pointsAmountPerday, this.pointsPerMonth}); + + PointsAmountPerMonth.fromJson(Map json) { + amountPerMonth = json['AmountPerMonth']; + month = json['Month']; + monthNumber = json['MonthNumber']; + if (json['PointsAmountPerday'] != null) { + pointsAmountPerday = []; + json['PointsAmountPerday'].forEach((v) { + pointsAmountPerday!.add(new PointsAmountPerday.fromJson(v)); + }); + } + pointsPerMonth = json['PointsPerMonth']; + } + + Map toJson() { + final Map data = new Map(); + data['AmountPerMonth'] = this.amountPerMonth; + data['Month'] = this.month; + data['MonthNumber'] = this.monthNumber; + if (this.pointsAmountPerday != null) { + data['PointsAmountPerday'] = this.pointsAmountPerday!.map((v) => v.toJson()).toList(); + } + data['PointsPerMonth'] = this.pointsPerMonth; + return data; + } +} + +class PointsAmountPerday { + num? amountPerDay; + String? day; + List? pointsDetails; + num? pointsPerDay; + String? transationDate; + + PointsAmountPerday({this.amountPerDay, this.day, this.pointsDetails, this.pointsPerDay, this.transationDate}); + + PointsAmountPerday.fromJson(Map json) { + amountPerDay = json['AmountPerDay']; + day = json['Day']; + if (json['PointsDetails'] != null) { + pointsDetails = []; + json['PointsDetails'].forEach((v) { + pointsDetails!.add(new PointsDetails.fromJson(v)); + }); + } + pointsPerDay = json['PointsPerDay']; + transationDate = json['TransationDate']; + } + + Map toJson() { + final Map data = new Map(); + data['AmountPerDay'] = this.amountPerDay; + data['Day'] = this.day; + if (this.pointsDetails != null) { + data['PointsDetails'] = this.pointsDetails!.map((v) => v.toJson()).toList(); + } + data['PointsPerDay'] = this.pointsPerDay; + data['TransationDate'] = this.transationDate; + return data; + } +} + +class PointsDetails { + int? accNumber; + String? accountStatus; + num? amount; + int? lineItemNo; + String? operationType; + num? points; + num? purchasePoints; + int? subTransactionType; + String? subTransactionTypeDescription; + String? transactionDate; + + PointsDetails( + {this.accNumber, + this.accountStatus, + this.amount, + this.lineItemNo, + this.operationType, + this.points, + this.purchasePoints, + this.subTransactionType, + this.subTransactionTypeDescription, + this.transactionDate}); + + PointsDetails.fromJson(Map json) { + accNumber = json['AccNumber']; + accountStatus = json['AccountStatus']; + amount = json['Amount']; + lineItemNo = json['LineItemNo']; + operationType = json['OperationType']; + points = json['Points']; + purchasePoints = json['PurchasePoints']; + subTransactionType = json['SubTransactionType']; + subTransactionTypeDescription = json['SubTransactionTypeDescription']; + transactionDate = json['TransactionDate']; + } + + Map toJson() { + final Map data = new Map(); + data['AccNumber'] = this.accNumber; + data['AccountStatus'] = this.accountStatus; + data['Amount'] = this.amount; + data['LineItemNo'] = this.lineItemNo; + data['OperationType'] = this.operationType; + data['Points'] = this.points; + data['PurchasePoints'] = this.purchasePoints; + data['SubTransactionType'] = this.subTransactionType; + data['SubTransactionTypeDescription'] = this.subTransactionTypeDescription; + data['TransactionDate'] = this.transactionDate; + return data; + } +} diff --git a/lib/features/my_invoices/my_invoices_view_model.dart b/lib/features/my_invoices/my_invoices_view_model.dart index e191b24d..a5d1e0b1 100644 --- a/lib/features/my_invoices/my_invoices_view_model.dart +++ b/lib/features/my_invoices/my_invoices_view_model.dart @@ -30,6 +30,8 @@ class MyInvoicesViewModel extends ChangeNotifier { String? downloadInvoicePDFBase64 = ""; + int selectedTabIndex = 0; + MyInvoicesViewModel({required this.myInvoicesRepo, required this.errorHandlerService, required this.navServices}); setInvoicesListLoading() { @@ -45,6 +47,11 @@ class MyInvoicesViewModel extends ChangeNotifier { notifyListeners(); } + void onTabChanged(int index) { + selectedTabIndex = index; + notifyListeners(); + } + Future getAllInvoicesList({Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await myInvoicesRepo.getAllInvoicesList(); diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index ad44d33f..475368f0 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -347,8 +347,11 @@ class PrescriptionsViewModel extends ChangeNotifier { result.fold( (failure) async { isPrescriptionsDeliveryOrdersLoading = false; + prescriptionsOrderList.clear(); notifyListeners(); - onError!(failure.message); + if(onError != null) { + onError(failure.message); + } }, (apiResponse) { if (apiResponse.messageStatus == 2) { diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index 7891357e..64db2481 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -40,7 +40,7 @@ class ProfileSettingsViewModel extends ChangeNotifier { bool isDeactivateAccountSuccess = false; String? deactivateAccountError; - late GetPatientInfoForUpdate getPatientInfoForUpdate; + GetPatientInfoForUpdate? getPatientInfoForUpdate; bool isPatientProfileLoading = false; // ── Profile image state ────────────────────────────────────────────── diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index ae8c6817..34787232 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1838,5 +1838,10 @@ abstract class LocaleKeys { static const fileSizeExceedsLimit = 'fileSizeExceedsLimit'; static const completedPrescriptionOrder = 'completedPrescriptionOrder'; static const submitEReferral = 'submitEReferral'; + static const redeem = 'redeem'; + static const points = 'points'; + static const transactions = 'transactions'; + static const pharmacy = 'pharmacy'; + static const visitsOrders = 'visitsOrders'; } diff --git a/lib/main.dart b/lib/main.dart index ffe2043e..25639e91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -98,7 +98,7 @@ Future callAppStateInitializations() async { PlatformDispatcher.instance.onError = (error, stack) { if (!kDebugMode) { FirebaseCrashlytics.instance.recordError(error, stack, - fatal: true, + fatal: false, printDetails: true, reason: "${appState.isAuthenticated ? "Authenticated User ID: ${appState.getAuthenticatedUser()!.patientId} - ${appState.getAuthenticatedUser()!.mobileNumber}" : "Unauthenticated User"} - Uncaught asynchronous error"); diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index c14897d8..2ec8f568 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -838,40 +838,42 @@ class _AppointmentDetailsPageState extends State { iconSize: 36.w, isLocked: false, ).toShimmer2() - : MedicalFileCard( - label: !((myAppointmentsVM.appointmentRatedResponseModel!).isAllowedToRate ?? false) - ? LocaleKeys.ratingSubmitted.tr(context: context) - : LocaleKeys.doctorRatingAppointment.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.appointmentRatingIcon, - isLargeText: true, - iconSize: 36.w, - isLocked: !(myAppointmentsVM.appointmentRatedResponseModel!.isAllowedToRate ?? false), - ).onPress(() { - if ((myAppointmentsVM.appointmentRatedResponseModel!.isAllowedToRate ?? true)) { - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.doctorRating.tr(context: context), - isCloseButtonVisible: true, - child: StatefulBuilder( - builder: (context, setState) { - return AppointmentRatingWidget( - patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, - ); - }, - ), - callBackFunc: () { - myAppointmentsViewModel.getDoctorsRatingCheck( - doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); - }, - isFullScreen: false, - ); - } - }), + : myAppointmentsVM.appointmentRatedResponseModel != null + ? MedicalFileCard( + label: !(myAppointmentsVM.appointmentRatedResponseModel!.isAllowedToRate ?? false) + ? LocaleKeys.ratingSubmitted.tr(context: context) + : LocaleKeys.doctorRatingAppointment.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.appointmentRatingIcon, + isLargeText: true, + iconSize: 36.w, + isLocked: !(myAppointmentsVM.appointmentRatedResponseModel!.isAllowedToRate ?? false), + ).onPress(() { + if (myAppointmentsVM.appointmentRatedResponseModel?.isAllowedToRate ?? false) { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.doctorRating.tr(context: context), + isCloseButtonVisible: true, + child: StatefulBuilder( + builder: (context, setState) { + return AppointmentRatingWidget( + patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + ); + }, + ), + callBackFunc: () { + myAppointmentsViewModel.getDoctorsRatingCheck( + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); + }, + isFullScreen: false, + ); + } + }) + : SizedBox.shrink(), ], ); }), diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 9b01ce8b..e17d52dd 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -274,9 +274,8 @@ class _AppointmentCardState extends State { children: [ (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), Expanded( - child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") - .toText16(isBold: true, maxlines: 2, - textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(30)}").toText16( + isBold: true, maxlines: 2, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), ), SizedBox(width: 12.w), (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 04111c8d..21f46bea 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -186,7 +186,6 @@ class LoginScreenState extends State { constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), backgroundColor: AppColors.transparent, child: StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { - // Helper function to validate phone number with error handling void validatePhoneAndProceed(OTPTypeEnum otpType) { // Use ViewModel validation method diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 137aaa09..044db504 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -170,7 +170,7 @@ class DoctorProfilePage extends StatelessWidget { icon: AppAssets.doctor_profile_reviews_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), LocaleKeys.reviews.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor), - NumberFormat.decimalPattern() + NumberFormat.decimalPattern("en_US") .format(bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate ?? 0) .toText16( isBold: true, diff --git a/lib/presentation/book_appointment/widgets/clinic_card.dart b/lib/presentation/book_appointment/widgets/clinic_card.dart index e7a6ca3a..fb3e2c79 100644 --- a/lib/presentation/book_appointment/widgets/clinic_card.dart +++ b/lib/presentation/book_appointment/widgets/clinic_card.dart @@ -22,6 +22,14 @@ class ClinicCard extends StatelessWidget { @override Widget build(BuildContext context) { AppState appState = getIt.get(); + + // Get clinic icon path with fallback + String getClinicIconPath() { + final clinicId = clinicsListResponseModel.clinicID ?? 1; + // Try to use specific clinic icon, fallback to default if not found + return "assets/images/clinicIcons/$clinicId.svg"; + } + return Container( padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -32,7 +40,19 @@ class ClinicCard extends StatelessWidget { child: Column( children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.buildSvgWithAssets(icon: "assets/images/clinicIcons/${clinicsListResponseModel.clinicID ?? 1}.svg", width: 24.w, height: 24.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading), + // Use Image.asset with error builder for fallback + SizedBox( + width: 24.w, + height: 24.h, + child: Utils.buildSvgWithAssets( + icon: getClinicIconPath(), + width: 24.w, + height: 24.h, + fit: BoxFit.contain, + // Note: Add error handling in the Utils.buildSvgWithAssets if possible + // or use a try-catch wrapper + ), + ).toShimmer2(isShow: isLoading), (clinicsListResponseModel.isLiveCareClinicAndOnline ?? true) ? Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.w, height: 32.h, fit: BoxFit.contain, applyThemeColor: false).toShimmer2(isShow: isLoading) : SizedBox.shrink(), diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index c21e7a62..42ad5984 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -33,6 +33,7 @@ class _HabibWalletState extends State { scheduleMicrotask(() async { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); + habibWalletVM.getLakumAccountInformation(); }); super.initState(); @@ -123,6 +124,7 @@ class _HabibWalletState extends State { .then((val) { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); + habibWalletVM.getLakumAccountInformation(); }); }, ), diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 151b3f21..3bd95a98 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; @@ -319,15 +320,13 @@ class _WalletPaymentConfirmPageState extends State { paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, onSuccess: (value) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getSuccessWidget(loadingText: "Payment Successful!"), + showCommonBottomSheetWithoutHeight(getIt.get().navigatorKey.currentContext!, child: Utils.getSuccessWidget(loadingText: "Payment Successful!"), callBackFunc: () { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, + Navigator.of(getIt.get().navigatorKey.currentContext!).pop(); + Navigator.of(getIt.get().navigatorKey.currentContext!).pop(); + }, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true diff --git a/lib/presentation/home/lakum_wallet_details.dart b/lib/presentation/home/lakum_wallet_details.dart new file mode 100644 index 00000000..a3f9f9f9 --- /dev/null +++ b/lib/presentation/home/lakum_wallet_details.dart @@ -0,0 +1,431 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/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/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/models/lakum_inquiry_information_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/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class LakumWalletDetails extends StatelessWidget { + const LakumWalletDetails({super.key}); + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + + return CollapsingListView( + title: LocaleKeys.lakumPoints.tr(context: context), + child: Consumer( + builder: (context, viewModel, child) { + // Check if loading + if (viewModel.isLakumAccountInfoLoading) { + return Center( + child: Padding( + padding: EdgeInsets.all(32.w), + child: CircularProgressIndicator( + color: AppColors.primaryRedColor, + ), + ), + ); + } + + // Check if account number is not available + if (viewModel.yahalaAccountNumber == "0" || viewModel.yahalaAccountNumber.isEmpty) { + return Utils.getNoDataWidget(context); + // return Center( + // child: Padding( + // padding: EdgeInsets.all(32.w), + // child: LocaleKeys.lakumMsg.tr().toText14( + // color: AppColors.greyTextColor, + // ), + // ), + // ); + } + + // Get all transactions + List transactions = viewModel.getAllLakumTransactions(); + + // Display main UI + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Main Lakum Card with gradient and user info + _buildLakumMainCard(viewModel.lakumAccountInfo, appState), + SizedBox(height: 24.h), + + // Redeem Button + _buildRedeemButton(context), + SizedBox(height: 24.h), + + // Transaction List Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.transactions.tr(context: context).toText16(isBold: true), + if (transactions.isNotEmpty) "${transactions.length} Records".toText12(color: AppColors.greyTextColor), + ], + ), + SizedBox(height: 12.h), + + // Transaction List Container + if (transactions.isEmpty) _buildEmptyTransactions() else _buildTransactionsList(transactions), + + SizedBox(height: 24.h), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildLakumMainCard(LakumInquiryInformationResponseModel accountInfo, AppState appState) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.habibPharmacyColor, + AppColors.habibPharmacyColor.withAlpha(220), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24.r), + boxShadow: [ + BoxShadow( + color: AppColors.habibPharmacyColor.withAlpha(76), + blurRadius: 16, + offset: Offset(0, 8), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Row: User Info and Grid Icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${appState.getAuthenticatedUser()?.firstName ?? ''} ${appState.getAuthenticatedUser()?.lastName ?? ''}".toText18( + isBold: true, + color: Colors.white, + ), + SizedBox(height: 4.h), + (accountInfo.accountNumber ?? 0).toString().toText14( + color: Colors.white.withAlpha(230), + ), + ], + ), + ), + Utils.buildImgWithAssets( + icon: AppAssets.lakumLogoWhite, + width: 80.h, + height: 80.h, + ), + ], + ), + // SizedBox(height: 24.h), + LocaleKeys.lakumPoints.tr().toText18(color: Colors.white.withAlpha(230), isBold: true), + SizedBox(height: 8.h), + + // Points Balance + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + (accountInfo.pointsBalance?.toString() ?? "0").toText44( + isBold: true, + color: Colors.white, + ), + SizedBox(width: 8.w), + LocaleKeys.points + .tr() + .toText18( + color: Colors.white.withAlpha(230), + isBold: true, + ) + .paddingOnly(bottom: 8.h), + ], + ), + SizedBox(height: 4.h), + + // Balance Amount in SAR + "≈ ${accountInfo.pointsBalanceAmount?.toStringAsFixed(2) ?? "0.00"} SAR".toText18( + color: Colors.white.withAlpha(204), + ), + SizedBox(height: 20.h), + + // Stats Row + Row( + children: [ + Expanded( + child: _buildStatChip( + LocaleKeys.gained.tr(), + accountInfo.gainedPoints?.toString() ?? "0", + Icons.arrow_circle_up, + ), + ), + SizedBox(width: 8.w), + Expanded( + child: _buildStatChip( + LocaleKeys.consumed.tr(), + accountInfo.consumedPoints?.toString() ?? "0", + Icons.arrow_circle_down, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStatChip(String label, String value, IconData icon) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + decoration: BoxDecoration( + color: Colors.white.withAlpha(51), + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: Colors.white, size: 16.f), + SizedBox(width: 6.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + value.toText14(color: Colors.white, isBold: true), + label.toText10(color: Colors.white.withAlpha(204)), + ], + ), + ], + ), + ); + } + + Widget _buildRedeemButton(BuildContext context) { + return CustomButton( + height: 46.h, + icon: AppAssets.redeem_icon, + iconColor: AppColors.whiteColor, + iconSize: 24.h, + backgroundColor: AppColors.habibPharmacyColor, + textColor: AppColors.whiteColor, + text: LocaleKeys.redeem.tr(context: context), + borderWidth: 0.w, + fontWeight: FontWeight.w600, + borderColor: AppColors.habibPharmacyColor.withAlpha(15), + padding: EdgeInsets.fromLTRB(4, 0, 12, 0), + fontSize: 14.f, + onPressed: () { + Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + launchUrl(uri, mode: LaunchMode.externalApplication); + }, + ); + } + + Widget _buildEmptyTransactions() { + return Container( + padding: EdgeInsets.all(32.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Center( + child: Column( + children: [ + Icon(Icons.receipt_long_outlined, size: 48.f, color: AppColors.greyTextColor), + SizedBox(height: 12.h), + "No transactions found".toText14( + color: AppColors.greyTextColor, + isCenter: true, + ), + ], + ), + ), + ); + } + + Widget _buildTransactionsList(List transactions) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.all(16.w), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: transactions.length, + separatorBuilder: (context, index) => Divider( + color: AppColors.dividerColor, + height: 24.h, + ), + itemBuilder: (context, index) { + return _buildTransactionItem(transactions[index]); + }, + ), + ); + } + + Widget _buildTransactionItem(PointsDetails transaction) { + // Determine if points were gained or used + bool isGained = transaction.operationType?.toLowerCase() == 'gain' || transaction.operationType?.toLowerCase() == 'credit' || (transaction.points ?? 0) > 0; + + Color transactionColor = isGained ? AppColors.habibPharmacyColor : AppColors.primaryRedColor; + String pointsText = "${isGained ? '+' : '-'}${transaction.points?.abs() ?? 0}"; + + // Format date + String formattedDate = ""; + if (transaction.transactionDate != null) { + try { + formattedDate = DateUtil.formatDateToDate( + DateUtil.convertStringToDate(transaction.transactionDate!), + false, + ); + } catch (e) { + formattedDate = transaction.transactionDate ?? ""; + } + } + + return Row( + children: [ + // Transaction Icon + Container( + width: 44.w, + height: 44.h, + decoration: BoxDecoration( + color: transactionColor.withAlpha(25), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + isGained ? Icons.add_rounded : Icons.remove_rounded, + color: transactionColor, + size: 24.f, + ), + ), + SizedBox(width: 12.w), + + // Transaction Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (transaction.subTransactionTypeDescription ?? transaction.operationType ?? "Transaction").toText14( + isBold: true, + maxlines: 2, + ), + SizedBox(height: 2.h), + formattedDate.toText12(color: AppColors.greyTextColor), + ], + ), + ), + + // Points Value + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + pointsText.toText18( + isBold: true, + color: transactionColor, + ), + if (transaction.amount != null && transaction.amount! > 0) + "${transaction.amount?.toStringAsFixed(2) ?? "0.00"} SAR".toText11( + color: AppColors.greyTextColor, + ), + ], + ), + ], + ); + } +} + +// Custom painter for wavy background pattern +class _WavyBackgroundPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + // Base color from AppColors + final baseColor = AppColors.habibPharmacyColor; + + final paint = Paint() + ..color = Colors.white.withAlpha(25) + ..style = PaintingStyle.fill; + + // Draw multiple wave patterns + final path1 = Path(); + path1.moveTo(0, size.height * 0.3); + path1.quadraticBezierTo( + size.width * 0.25, + size.height * 0.2, + size.width * 0.5, + size.height * 0.3, + ); + path1.quadraticBezierTo( + size.width * 0.75, + size.height * 0.4, + size.width, + size.height * 0.3, + ); + path1.lineTo(size.width, 0); + path1.lineTo(0, 0); + path1.close(); + canvas.drawPath(path1, paint); + + // Second wave with darker shade + paint.color = Colors.black.withAlpha(15); + final path2 = Path(); + path2.moveTo(0, size.height * 0.6); + path2.quadraticBezierTo( + size.width * 0.3, + size.height * 0.5, + size.width * 0.6, + size.height * 0.6, + ); + path2.quadraticBezierTo( + size.width * 0.8, + size.height * 0.7, + size.width, + size.height * 0.6, + ); + path2.lineTo(size.width, size.height); + path2.lineTo(0, size.height); + path2.close(); + canvas.drawPath(path2, paint); + + // Decorative circles with white overlay + paint.color = Colors.white.withAlpha(20); + canvas.drawCircle(Offset(size.width * 0.85, size.height * 0.15), 30, paint); + paint.color = Colors.white.withAlpha(10); + canvas.drawCircle(Offset(size.width * 0.15, size.height * 0.75), 40, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 5ea5b96b..b6d32f72 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -137,6 +137,7 @@ class _LandingPageState extends State { if (appState.isAuthenticated) { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); + habibWalletVM.getLakumAccountInformation(); todoSectionViewModel.initializeTodoSectionViewModel(); immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); @@ -147,7 +148,7 @@ class _LandingPageState extends State { notificationsViewModel.initNotificationsViewModel(); insuranceViewModel.initInsuranceProvider(); - if(!appState.isRatedVisible) { + if(appState.isRatedVisible) { appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, @@ -197,6 +198,7 @@ class _LandingPageState extends State { // Refresh Appointments Data habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); + habibWalletVM.getLakumAccountInformation(); // Refresh Ancillary Orders todoSectionViewModel.initializeTodoSectionViewModel(); @@ -280,7 +282,7 @@ class _LandingPageState extends State { await authVM.onLoginPressed(); } }), - (appState.isAuthenticated && (int.parse(todoSectionVM.notificationsCount ?? "0") > 0)) + (appState.isAuthenticated && todoSectionVM.notificationsCount != null && todoSectionVM.notificationsCount != "0") ? Positioned( // right: appState.isArabic() ? 8.w : -8.w, top: -8.h, @@ -295,7 +297,7 @@ class _LandingPageState extends State { borderRadius: BorderRadius.circular(20.r), ), child: Text( - todoSectionVM.notificationsCount.toString(), + todoSectionVM.notificationsCount ?? "0", style: TextStyle( color: Colors.white, fontFamily: "Poppins", diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 15aed11a..5ee07f15 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -86,6 +86,7 @@ class HabibWalletCard extends StatelessWidget { Consumer(builder: (context, habibWalletVM, child) { return Row( mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ diff --git a/lib/presentation/home/widgets/lakum_wallet_card.dart b/lib/presentation/home/widgets/lakum_wallet_card.dart index e693667c..599dc2e8 100644 --- a/lib/presentation/home/widgets/lakum_wallet_card.dart +++ b/lib/presentation/home/widgets/lakum_wallet_card.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -11,10 +12,12 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/lakum_wallet_details.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/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class LakumWalletCard extends StatelessWidget { const LakumWalletCard({super.key}); @@ -64,7 +67,7 @@ class LakumWalletCard extends StatelessWidget { // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // LocaleKeys.habibWallet.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), - "Lakum Wallet".toText16(isBold: true, letterSpacing: -0.2), + LocaleKeys.lakumPoints.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), // Container( // height: 40.h, // width: 40.h, @@ -84,7 +87,7 @@ class LakumWalletCard extends StatelessWidget { // ), // ], // ), - SizedBox(height: 4.h), + SizedBox(height: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -93,24 +96,19 @@ class LakumWalletCard extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.saudi_riyal_icon, - iconColor: AppColors.inputLabelTextColor, - width: 24.h, - height: 24.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.h), NumberFormat.decimalPattern() - .format(habibWalletVM.habibWalletAmount) + .format(habibWalletVM.isLakumAccountInfoLoading ? 123 : habibWalletVM.yahalaAccountNumber == "0" ? 0 : habibWalletVM.lakumAccountInfo.pointsBalance) .toString() .toText32(isBold: true, isEnglishOnly: true) - .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 40.h), + .toShimmer2(isShow: habibWalletVM.isLakumAccountInfoLoading, radius: 12.h, width: 80.h, height: 40.h), + SizedBox(width: 4.h), + LocaleKeys.points.tr().toText12(color: AppColors.inputLabelTextColor, isBold: true) ], ), - // Spacer(), - Utils.getPaymentMethods(), + Spacer(), + Utils.buildSvgWithAssets(icon: AppAssets.hmg_pharmacy_logo, width: 32.h, height: 32.h, fit: BoxFit.contain, applyThemeColor: false), ], ); }), @@ -123,19 +121,21 @@ class LakumWalletCard extends StatelessWidget { flex: 6, child: CustomButton( height: 40.h, - icon: AppAssets.recharge_icon, - iconColor: AppColors.infoColor, + icon: AppAssets.redeem_icon, + iconColor: AppColors.habibPharmacyColor, iconSize: 24.h, - backgroundColor: AppColors.infoColor.withAlpha(15), - textColor: AppColors.infoColor, - text: LocaleKeys.recharge.tr(context: context), + backgroundColor: AppColors.habibPharmacyColor.withAlpha(15), + textColor: AppColors.habibPharmacyColor, + text: LocaleKeys.redeem.tr(context: context), borderWidth: 0.w, fontWeight: FontWeight.w600, - borderColor: AppColors.infoColor.withAlpha(15), + borderColor: AppColors.habibPharmacyColor.withAlpha(15), padding: EdgeInsets.fromLTRB(4, 0, 12, 0), fontSize: 14.f, onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + launchUrl(uri, mode: LaunchMode.externalApplication); + // Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); }, ), ), @@ -165,7 +165,7 @@ class LakumWalletCard extends StatelessWidget { ).onPress(() { Navigator.of(context).push( CustomPageRoute( - page: HabibWalletPage(), + page: LakumWalletDetails(), ), ); }), diff --git a/lib/presentation/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart index 5a8e9c70..ae191d89 100644 --- a/lib/presentation/my_invoices/my_invoices_list.dart +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -18,6 +18,7 @@ import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_lis 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/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.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:open_filex/open_filex.dart'; @@ -124,194 +125,211 @@ class _MyInvoicesListState extends State { crossAxisAlignment: CrossAxisAlignment.start, 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.withOpacity(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.withOpacity(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.withOpacity(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.withOpacity(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, - ), + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + initialIndex: 0, + tabs: [ + CustomTabBarModel(null, LocaleKeys.visitsOrders.tr(context: context)), + CustomTabBarModel(null, LocaleKeys.pharmacy.tr(context: context)), ], + onTabChange: (index) { + myInvoicesVM.onTabChanged(index); + }, ).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) { + 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.withOpacity(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.withOpacity(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.withOpacity(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.withOpacity(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: "Cannot open file"), + child: Utils.getErrorWidget(loadingText: err), 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, - // ); - // }); - // }, + }, + 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), + ) + : Utils.getNoDataWidget(context); + }).paddingSymmetrical(24.w, 0.h), + ], + ) : Container(), ], ); }), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index c581bcd5..bf83a905 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -53,11 +53,17 @@ class ProfileSettingsState extends State { @override void initState() { - scheduleMicrotask(() { - insuranceViewModel.initInsuranceProvider(); - }); - _loadPermissions(); super.initState(); + _loadPermissions(); + + // Load data after first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + insuranceViewModel.initInsuranceProvider(); + // Load profile settings for preferred language + profileSettingsViewModel.getProfileSettings(); + } + }); } @override @@ -96,12 +102,14 @@ class ProfileSettingsState extends State { final SwiperController _controller = SwiperController(); late InsuranceViewModel insuranceViewModel; late ContactUsViewModel contactUsViewModel; + late ProfileSettingsViewModel profileSettingsViewModel; String _permissionsLabel = ""; @override Widget build(BuildContext context) { insuranceViewModel = Provider.of(context, listen: false); contactUsViewModel = Provider.of(context, listen: false); + profileSettingsViewModel = Provider.of(context, listen: false); return CollapsingListView( title: LocaleKeys.profileAndSettings.tr(context: context), logout: () { @@ -268,9 +276,9 @@ class ProfileSettingsState extends State { actionItem(AppAssets.language, LocaleKeys.communicationLanguage.tr(context: context), () { profileVm.openPreferredLanguageBottomSheet(); }, - trailingLabel: profileVm.isPatientProfileLoading + trailingLabel: profileVm.isPatientProfileLoading || profileVm.getPatientInfoForUpdate == null ? "English" - : (profileVm.getPatientInfoForUpdate.preferredLanguage! == "1" ? LocaleKeys.arabic.tr(context: context) : LocaleKeys.english.tr(context: context)), + : (profileVm.getPatientInfoForUpdate!.preferredLanguage! == "1" ? LocaleKeys.arabic.tr(context: context) : LocaleKeys.english.tr(context: context)), isShowLoading: profileVm.isPatientProfileLoading), 1.divider, actionItem(AppAssets.smart_phone_fill, LocaleKeys.emrgNo.tr(context: context), () { diff --git a/lib/presentation/profile_settings/widgets/preferred_language_widget.dart b/lib/presentation/profile_settings/widgets/preferred_language_widget.dart index 4cecc651..32dcc57b 100644 --- a/lib/presentation/profile_settings/widgets/preferred_language_widget.dart +++ b/lib/presentation/profile_settings/widgets/preferred_language_widget.dart @@ -29,7 +29,7 @@ class _PreferredLanguageWidgetState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { - selectedValue = profileSettingsViewModel.getPatientInfoForUpdate.preferredLanguage!; + selectedValue = profileSettingsViewModel.getPatientInfoForUpdate?.preferredLanguage!; }); }); } @@ -60,9 +60,9 @@ class _PreferredLanguageWidgetState extends State { profileSettingsViewModel!.updatePatientInfo( patientInfo: { "PreferredLanguage": selectedValue, - "EmailAddress": profileSettingsViewModel.getPatientInfoForUpdate.emailAddress, - "EmergencyContactName": profileSettingsViewModel.getPatientInfoForUpdate.emergencyContactName, - "EmergencyContactNo": profileSettingsViewModel.getPatientInfoForUpdate.emergencyContactNo, + "EmailAddress": profileSettingsViewModel.getPatientInfoForUpdate!.emailAddress, + "EmergencyContactName": profileSettingsViewModel.getPatientInfoForUpdate!.emergencyContactName, + "EmergencyContactNo": profileSettingsViewModel.getPatientInfoForUpdate!.emergencyContactNo, "IsEmailAlertRequired": true, "IsSMSAlertRequired": true, }, diff --git a/lib/presentation/profile_settings/widgets/update_email_widget.dart b/lib/presentation/profile_settings/widgets/update_email_widget.dart index e8e313f4..4f13726a 100644 --- a/lib/presentation/profile_settings/widgets/update_email_widget.dart +++ b/lib/presentation/profile_settings/widgets/update_email_widget.dart @@ -39,7 +39,7 @@ class _UpdateEmailDialogState extends State { // Set the text setState(() { - textController!.text = viewModel.getPatientInfoForUpdate.emailAddress ?? ""; + textController!.text = viewModel.getPatientInfoForUpdate!.emailAddress ?? ""; }); }); @@ -118,10 +118,10 @@ class _UpdateEmailDialogState extends State { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.updatingEmailAddress.tr(context: context)); profileSettingsViewModel!.updatePatientInfo( patientInfo: { - "PreferredLanguage": profileSettingsViewModel!.getPatientInfoForUpdate.preferredLanguage, + "PreferredLanguage": profileSettingsViewModel!.getPatientInfoForUpdate!.preferredLanguage, "EmailAddress": textController!.text, - "EmergencyContactName": profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactName, - "EmergencyContactNo": profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactNo, + "EmergencyContactName": profileSettingsViewModel!.getPatientInfoForUpdate!.emergencyContactName, + "EmergencyContactNo": profileSettingsViewModel!.getPatientInfoForUpdate!.emergencyContactNo, "IsEmailAlertRequired": true, "IsSMSAlertRequired": true, }, diff --git a/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart b/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart index b9a81afd..e1faadca 100644 --- a/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart +++ b/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart @@ -1,11 +1,13 @@ 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/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; @@ -39,7 +41,7 @@ class _UpdateEmergencyContactDialogState extends State().navigatorKey.currentContext!).pop(); profileSettingsViewModel!.getProfileSettings(); }, isFullScreen: false, isAutoDismiss: true); }, diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 3a113b1e..db60b7df 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -302,6 +302,18 @@ class _PossibleConditionsPageState extends State { // Set flag to indicate booking is from symptoms checker symptomsCheckerViewModel.setBookingFromSymptomsChecker(true); + // Check if clinicDetailsList is not empty before accessing + if (symptomsCheckerViewModel.clinicDetailsList.isEmpty) { + // Show error if no clinic details available + final dialogService = getIt(); + dialogService.showErrorBottomSheet( + message: appState.isArabic() + ? "لا توجد تفاصيل عيادة متاحة" + : "No clinic details available", + ); + return; + } + // bookAppointmentsViewModel.getLocation(); bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel( clinicID: symptomsCheckerViewModel.clinicDetailsList.first.clinicID, @@ -395,9 +407,17 @@ class _PossibleConditionsPageState extends State { //if the region screen is opened for the dental clinic then the project id will be in the hospital list as the list is formed form the get project api var id = ""; if (data.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { - id = regionalViewModel.selectedHospital?.hospitalList.first.iD?.toString() ?? ""; + // Check if hospitalList is not empty before accessing first + if (regionalViewModel.selectedHospital?.hospitalList != null && + regionalViewModel.selectedHospital!.hospitalList.isNotEmpty) { + id = regionalViewModel.selectedHospital!.hospitalList.first.iD?.toString() ?? ""; + } } else { - id = regionalViewModel.selectedHospital?.patientDoctorAppointmentList?.first.projectID?.toString() ?? ""; + // Check if patientDoctorAppointmentList is not empty before accessing first + if (regionalViewModel.selectedHospital?.patientDoctorAppointmentList != null && + regionalViewModel.selectedHospital!.patientDoctorAppointmentList!.isNotEmpty) { + id = regionalViewModel.selectedHospital!.patientDoctorAppointmentList!.first.projectID?.toString() ?? ""; + } } bookAppointmentsViewModel.setProjectID(id); return SizedBox.shrink(); diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 1e39eac8..1ecbcb61 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -40,6 +40,7 @@ class AppColors { static Color get lightGreenColor => isDarkMode ? dark.lightGreenColor : const Color(0xFF0ccedde); static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15); static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A); + static Color get habibPharmacyColor => const Color(0xFF2d9137); // ── Doctor Rating Colors ────────────────────────────────────────────────── static const Color ratingStarIconColor = Color(0xFFFFA726); // Orange-amber for star icons diff --git a/lib/widgets/bottomsheet/generic_bottom_sheet.dart b/lib/widgets/bottomsheet/generic_bottom_sheet.dart index dfc1584c..d232f207 100644 --- a/lib/widgets/bottomsheet/generic_bottom_sheet.dart +++ b/lib/widgets/bottomsheet/generic_bottom_sheet.dart @@ -150,6 +150,9 @@ class GenericBottomSheetState extends State { keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number, onChange: (value) { if (widget.onChange != null) { + if (Utils.isArabicText(value!)) { + value = Utils.toEnglishNumbers(value); + } widget.onChange!(value); } },