haroon_dev #315

Merged
Haroon6138 merged 5 commits from haroon_dev into master 19 hours ago

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.93712 2.25C5.05309 2.24995 4.28757 2.2499 3.67381 2.33562C3.01162 2.4281 2.38072 2.63711 1.87303 3.16449C1.37047 3.68654 1.17623 4.32619 1.08959 4.99561C1.00807 5.62545 1.0081 6.41382 1.00815 7.33738L1.00815 7.40177C1.00815 7.65552 1.00591 7.89931 1.00378 8.13142C0.998266 8.73244 0.993469 9.25515 1.0301 9.66981C1.08709 10.315 1.25466 10.9156 1.76284 11.4435C2.14586 11.8414 2.77891 11.8535 3.17679 11.4704C3.57468 11.0874 3.58672 10.4544 3.2037 10.0565C3.12657 9.97636 3.05658 9.88147 3.02234 9.49383C2.99474 9.18135 2.99819 8.80852 3.00318 8.27087C3.0055 8.02045 3.00815 7.73426 3.00815 7.40177C3.00815 6.39595 3.01012 5.73847 3.07304 5.25232C3.13273 4.79115 3.23113 4.63751 3.31389 4.55154C3.39151 4.47092 3.52427 4.37592 3.95045 4.31639C4.40942 4.25229 5.0335 4.25001 6.00488 4.25001L17.9951 4.25001C18.9664 4.25001 19.5905 4.25229 20.0495 4.3164C20.4757 4.37592 20.6084 4.47092 20.686 4.55155C20.7688 4.63751 20.8672 4.79115 20.9269 5.25232C20.9898 5.73847 20.9918 6.39595 20.9918 7.40177C20.9918 7.73233 20.9944 8.01713 20.9967 8.26653C21.0016 8.80612 21.005 9.18046 20.9773 9.4935C20.9429 9.88104 20.8729 9.97616 20.7956 10.0565C20.4125 10.4544 20.4246 11.0874 20.8225 11.4704C21.2203 11.8535 21.8534 11.8414 22.2364 11.4435C22.7444 10.9158 22.9123 10.3154 22.9695 9.67014C23.0063 9.25455 23.0016 8.73073 22.9961 8.12825C22.994 7.89711 22.9918 7.65439 22.9918 7.40177L22.9918 7.33739C22.9918 6.41387 22.9919 5.62543 22.9103 4.99561C22.8237 4.32619 22.6295 3.68654 22.1269 3.16449C21.6192 2.63711 20.9883 2.4281 20.3261 2.33562C19.7124 2.2499 18.9468 2.24995 18.0628 2.25L5.93712 2.25Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1978 6.25C14.9586 6.24998 15.598 6.24996 16.1143 6.31403C16.6617 6.38197 17.1602 6.5306 17.5912 6.89146C18.0222 7.25232 18.2562 7.71697 18.4193 8.24389C18.5731 8.74084 18.6855 9.3703 18.8192 10.1193L19.4895 13.8726C19.7725 15.4578 19.9993 16.7275 20.0348 17.7329C20.0715 18.7716 19.9118 19.6676 19.2925 20.4073C18.6731 21.1471 17.8191 21.4618 16.7902 21.6082C15.7942 21.75 14.5044 21.75 12.8942 21.75H11.1065C9.49624 21.75 8.20645 21.75 7.21049 21.6082C6.18153 21.4618 5.32758 21.1471 4.70822 20.4073C4.08886 19.6676 3.92921 18.7716 3.96588 17.7329C4.00137 16.7275 4.22812 15.4578 4.51121 13.8727L5.18145 10.1193C5.31517 9.37034 5.42756 8.74084 5.58139 8.24389C5.74449 7.71697 5.97845 7.25232 6.40946 6.89146C6.84047 6.5306 7.339 6.38197 7.8864 6.31403C8.40264 6.24996 9.04206 6.24998 9.80289 6.25H14.1978ZM12 9.75C12.4142 9.75 12.75 10.0858 12.75 10.5V15.6893L13.9697 14.4697C14.2626 14.1768 14.7374 14.1768 15.0303 14.4697C15.3232 14.7626 15.3232 15.2374 15.0303 15.5303L12.5303 18.0303C12.2374 18.3232 11.7626 18.3232 11.4697 18.0303L8.96967 15.5303C8.67678 15.2374 8.67678 14.7626 8.96967 14.4697C9.26256 14.1768 9.73744 14.1768 10.0303 14.4697L11.25 15.6893V10.5C11.25 10.0858 11.5858 9.75 12 9.75Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -1845,5 +1845,10 @@
"maxOneFileAllowed": "يمكن رفع ملف واحد فقط",
"fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت",
"completedPrescriptionOrder": "تمت الخدمة",
"submitEReferral": "تقديم الطلب"
"submitEReferral": "تقديم الطلب",
"redeem": "استبدال",
"points": "نقاط",
"transactions": "المعاملات",
"pharmacy": "الصيدلية",
"visitsOrders": "الزيارات/الطلبات"
}

@ -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"
}

@ -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";

@ -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;

@ -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';

@ -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 {

@ -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<OTPVerificationScreen> {
super.initState();
_otpController = TextEditingController();
_startResendTimer();
checkSignature();
if(Platform.isAndroid) {
checkSignature();
}
}
@override

@ -1479,6 +1479,9 @@ class BookAppointmentsViewModel extends ChangeNotifier {
result.fold(
(failure) async {
if(onError != null) {
onError(failure.message);
}
onError!(LocaleKeys.noDoctorFound.tr());
},
(apiResponse) {

@ -18,6 +18,10 @@ abstract class HabibWalletRepo {
Future<Either<Failure, GenericApiModel<dynamic>>> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference});
Future<Either<Failure, GenericApiModel<dynamic>>> getPatientInfoByPatientID({required String patientID});
Future<Either<Failure, GenericApiModel<dynamic>>> getLakumAccountInformation({required String identificationNumber});
Future<Either<Failure, GenericApiModel<dynamic>>> getLakumAccountInquiryInformation({required String lakumAccountNumber});
}
class HabibWalletRepoImp implements HabibWalletRepo {
@ -231,4 +235,75 @@ class HabibWalletRepoImp implements HabibWalletRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel>> getLakumAccountInformation({required String identificationNumber}) async {
Map<String, dynamic> requestBody = {"IdentificationNo": identificationNumber};
try {
GenericApiModel<dynamic>? 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<dynamic>(
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<Either<Failure, GenericApiModel>> getLakumAccountInquiryInformation({required String lakumAccountNumber}) async {
Map<String, dynamic> requestBody = {
"AccountNumber": lakumAccountNumber,
"IsDetailsRequired": true,
};
try {
GenericApiModel<dynamic>? 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<dynamic>(
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()));
}
}
}

@ -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<PointsDetails> getAllLakumTransactions() {
if (lakumAccountInfo.gainedPointsAmountPerYear == null) {
return [];
}
List<PointsDetails> 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<void> getLakumAccountInformation({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isLakumAccountInfoLoading = true;
lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners();
final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get<AppState>().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);
}
}
},
);
}
}

@ -0,0 +1,318 @@
class LakumInquiryInformationResponseModel {
int? accountNumber;
String? accountStatus;
String? barCode;
num? consumedPoints;
dynamic consumedPointsAmount;
dynamic consumedPointsAmountPerYear;
// List<Null>? consumedPointsDetails;
String? createdDate;
num? expiredPoints;
String? expiryDate;
num? gainedPoints;
num? gainedPointsAmount;
List<GainedPointsAmountPerYear>? gainedPointsAmountPerYear;
// List<Null>? gainedPointsDetails;
String? lakumMessageStatus;
String? memberName;
String? memberUniversalId;
String? mobileNumber;
num? pointsBalance;
num? pointsBalanceAmount;
num? pointsWillBeExpired;
String? prefLang;
num? statusCode;
num? transferPoints;
dynamic transferPointsAmountPerYear;
// List<Null>? 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<String, dynamic> json) {
accountNumber = json['AccountNumber'];
accountStatus = json['AccountStatus'];
barCode = json['BarCode'];
consumedPoints = json['ConsumedPoints'];
consumedPointsAmount = json['ConsumedPointsAmount'];
consumedPointsAmountPerYear = json['ConsumedPointsAmountPerYear'];
// if (json['ConsumedPointsDetails'] != null) {
// consumedPointsDetails = <Null>[];
// 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 = <GainedPointsAmountPerYear>[];
json['GainedPointsAmountPerYear'].forEach((v) {
gainedPointsAmountPerYear!.add(new GainedPointsAmountPerYear.fromJson(v));
});
}
// if (json['GainedPointsDetails'] != null) {
// gainedPointsDetails = <Null>[];
// 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 = <Null>[];
// json['TransferPointsDetails'].forEach((v) {
// transferPointsDetails!.add(new Null.fromJson(v));
// });
// }
waitingPoints = json['WaitingPoints'];
loyalityAmount = json['loyalityAmount'];
loyalityPoints = json['loyalityPoints'];
purchaseRate = json['purchaseRate'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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>? pointsAmountPerMonth;
num? pointsPerYear;
int? year;
GainedPointsAmountPerYear({this.amountPerYear, this.pointsAmountPerMonth, this.pointsPerYear, this.year});
GainedPointsAmountPerYear.fromJson(Map<String, dynamic> json) {
amountPerYear = json['AmountPerYear'];
if (json['PointsAmountPerMonth'] != null) {
pointsAmountPerMonth = <PointsAmountPerMonth>[];
json['PointsAmountPerMonth'].forEach((v) {
pointsAmountPerMonth!.add(new PointsAmountPerMonth.fromJson(v));
});
}
pointsPerYear = json['PointsPerYear'];
year = json['Year'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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>? pointsAmountPerday;
num? pointsPerMonth;
PointsAmountPerMonth({this.amountPerMonth, this.month, this.monthNumber, this.pointsAmountPerday, this.pointsPerMonth});
PointsAmountPerMonth.fromJson(Map<String, dynamic> json) {
amountPerMonth = json['AmountPerMonth'];
month = json['Month'];
monthNumber = json['MonthNumber'];
if (json['PointsAmountPerday'] != null) {
pointsAmountPerday = <PointsAmountPerday>[];
json['PointsAmountPerday'].forEach((v) {
pointsAmountPerday!.add(new PointsAmountPerday.fromJson(v));
});
}
pointsPerMonth = json['PointsPerMonth'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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>? pointsDetails;
num? pointsPerDay;
String? transationDate;
PointsAmountPerday({this.amountPerDay, this.day, this.pointsDetails, this.pointsPerDay, this.transationDate});
PointsAmountPerday.fromJson(Map<String, dynamic> json) {
amountPerDay = json['AmountPerDay'];
day = json['Day'];
if (json['PointsDetails'] != null) {
pointsDetails = <PointsDetails>[];
json['PointsDetails'].forEach((v) {
pointsDetails!.add(new PointsDetails.fromJson(v));
});
}
pointsPerDay = json['PointsPerDay'];
transationDate = json['TransationDate'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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;
}
}

@ -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<void> getAllInvoicesList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await myInvoicesRepo.getAllInvoicesList();

@ -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) {

@ -40,7 +40,7 @@ class ProfileSettingsViewModel extends ChangeNotifier {
bool isDeactivateAccountSuccess = false;
String? deactivateAccountError;
late GetPatientInfoForUpdate getPatientInfoForUpdate;
GetPatientInfoForUpdate? getPatientInfoForUpdate;
bool isPatientProfileLoading = false;
// Profile image state

@ -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';
}

@ -98,7 +98,7 @@ Future<void> 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");

@ -838,40 +838,42 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
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(),
],
);
}),

@ -274,9 +274,8 @@ class _AppointmentCardState extends State<AppointmentCard> {
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 &&

@ -186,7 +186,6 @@ class LoginScreenState extends State<LoginScreen> {
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

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

@ -22,6 +22,14 @@ class ClinicCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
// 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(),

@ -33,6 +33,7 @@ class _HabibWalletState extends State<HabibWalletPage> {
scheduleMicrotask(() async {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();
});
super.initState();
@ -123,6 +124,7 @@ class _HabibWalletState extends State<HabibWalletPage> {
.then((val) {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();
});
},
),

@ -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<WalletPaymentConfirmPage> {
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,
onSuccess: (value) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: "Payment Successful!"),
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().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<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
},
isFullScreen: false,
isCloseButtonVisible: true,
isAutoDismiss: true

@ -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<AppState>();
return CollapsingListView(
title: LocaleKeys.lakumPoints.tr(context: context),
child: Consumer<HabibWalletViewModel>(
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<PointsDetails> 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<PointsDetails> 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;
}

@ -137,6 +137,7 @@ class _LandingPageState extends State<LandingPage> {
if (appState.isAuthenticated) {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();
todoSectionViewModel.initializeTodoSectionViewModel();
immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory();
@ -147,7 +148,7 @@ class _LandingPageState extends State<LandingPage> {
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<LandingPage> {
// Refresh Appointments Data
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();
// Refresh Ancillary Orders
todoSectionViewModel.initializeTodoSectionViewModel();
@ -280,7 +282,7 @@ class _LandingPageState extends State<LandingPage> {
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<LandingPage> {
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
todoSectionVM.notificationsCount.toString(),
todoSectionVM.notificationsCount ?? "0",
style: TextStyle(
color: Colors.white,
fontFamily: "Poppins",

@ -86,6 +86,7 @@ class HabibWalletCard extends StatelessWidget {
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [

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

@ -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<MyInvoicesList> {
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(),
],
);
}),

@ -53,11 +53,17 @@ class ProfileSettingsState extends State<ProfileSettings> {
@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<ProfileSettings> {
final SwiperController _controller = SwiperController();
late InsuranceViewModel insuranceViewModel;
late ContactUsViewModel contactUsViewModel;
late ProfileSettingsViewModel profileSettingsViewModel;
String _permissionsLabel = "";
@override
Widget build(BuildContext context) {
insuranceViewModel = Provider.of<InsuranceViewModel>(context, listen: false);
contactUsViewModel = Provider.of<ContactUsViewModel>(context, listen: false);
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
return CollapsingListView(
title: LocaleKeys.profileAndSettings.tr(context: context),
logout: () {
@ -268,9 +276,9 @@ class ProfileSettingsState extends State<ProfileSettings> {
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), () {

@ -29,7 +29,7 @@ class _PreferredLanguageWidgetState extends State<PreferredLanguageWidget> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
selectedValue = profileSettingsViewModel.getPatientInfoForUpdate.preferredLanguage!;
selectedValue = profileSettingsViewModel.getPatientInfoForUpdate?.preferredLanguage!;
});
});
}
@ -60,9 +60,9 @@ class _PreferredLanguageWidgetState extends State<PreferredLanguageWidget> {
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,
},

@ -39,7 +39,7 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
// Set the text
setState(() {
textController!.text = viewModel.getPatientInfoForUpdate.emailAddress ?? "";
textController!.text = viewModel.getPatientInfoForUpdate!.emailAddress ?? "";
});
});
@ -118,10 +118,10 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
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,
},

@ -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<UpdateEmergencyContactDia
// Set the text
setState(() {
textController!.text = viewModel.getPatientInfoForUpdate.emergencyContactNo ?? "";
textController!.text = viewModel.getPatientInfoForUpdate!.emergencyContactNo ?? "";
});
});
@ -118,9 +120,9 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
profileSettingsViewModel!.updatePatientInfo(
patientInfo: {
"PreferredLanguage": profileSettingsViewModel!.getPatientInfoForUpdate.preferredLanguage,
"EmailAddress": profileSettingsViewModel!.getPatientInfoForUpdate.emailAddress,
"EmergencyContactName": profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactName,
"PreferredLanguage": profileSettingsViewModel!.getPatientInfoForUpdate!.preferredLanguage,
"EmailAddress": profileSettingsViewModel!.getPatientInfoForUpdate!.emailAddress,
"EmergencyContactName": profileSettingsViewModel!.getPatientInfoForUpdate!.emergencyContactName,
"EmergencyContactNo": textController!.text,
"IsEmailAlertRequired": true,
"IsSMSAlertRequired": true,
@ -130,7 +132,7 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
profileSettingsViewModel!.clearEmergencyContactError();
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()),
callBackFunc: () async {
Navigator.of(context).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
profileSettingsViewModel!.getProfileSettings();
}, isFullScreen: false, isAutoDismiss: true);
},

@ -302,6 +302,18 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
// 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>();
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<PossibleConditionsPage> {
//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();

@ -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

@ -150,6 +150,9 @@ class GenericBottomSheetState extends State<GenericBottomSheet> {
keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number,
onChange: (value) {
if (widget.onChange != null) {
if (Utils.isArabicText(value!)) {
value = Utils.toEnglishNumbers(value);
}
widget.onChange!(value);
}
},

Loading…
Cancel
Save