Compare commits

...

31 Commits

Author SHA1 Message Date
Aamir Muhammad 558a957101 Offers And Discounts 1 month ago
Aamir Muhammad 90aaed84ed Merge branch 'refs/heads/master' into offers_discounts
# Conflicts:
#	assets/langs/ar-SA.json
#	assets/langs/en-US.json
#	lib/generated/locale_keys.g.dart
#	lib/presentation/home/landing_page.dart
1 month ago
Haroon6138 e44856167f Merge pull request 'haroon_dev' (#343) from haroon_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/343
1 month ago
haroon amjad b793426dd6 Implemented CloudFlare TurnStile 1 month ago
haroon amjad 1a6975fd8a Update to stores VersionID 21.6 1 month ago
haroon amjad 836965a6e0 Merge branch 'master' into haroon_dev 1 month ago
haroon amjad adde5c2036 updates 1 month ago
Haroon6138 765137ce3e Merge pull request 'haroon_dev' (#342) from haroon_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/342
1 month ago
haroon amjad 4972295606 update 1 month ago
haroon amjad d353eb1006 Merge branch 'master' into haroon_dev
# Conflicts:
#	lib/core/api/api_client.dart
1 month ago
Haroon6138 6c4cf1ffb9 Merge pull request 'lab result changes' (#340) from lab_result_changes into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/340
1 month ago
haroon amjad 1ee07da155 Merge branch 'master' into haroon_dev 1 month ago
haroon amjad 9d8744ec17 Updates 1 month ago
tahaalam be78a662c7 hardcoded admin removed 1 month ago
tahaalam 7025d33193 LAB RESULT CHANGES ADDED 1 month ago
tahaalam 780cf271f2 LAB RESULT CHANGES ADDED 1 month ago
Haroon6138 c2cce5cba1 Merge pull request 'haroon_dev' (#339) from haroon_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/339
2 months ago
haroon amjad 753968d3fa updates for dental & laser clinics 2 months ago
Haroon Amjad 13f2898a8b Fixes for Apple Watch integration 2 months ago
haroon amjad e867de7b71 updates 2 months ago
haroon amjad 948416da74 Update to stores VersionID 21.4 2 months ago
Haroon6138 66240df2f4 Merge pull request 'dev_sultan' (#338) from dev_sultan into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/338
2 months ago
Sultan khan be00b29bbb no message 2 months ago
Sultan khan 860e7db5f6 Merge branch 'master' of http://34.17.182.140/Haroon6138/HMG_Patient_App_New into dev_sultan
* 'master' of http://34.17.182.140/Haroon6138/HMG_Patient_App_New:
  updates
  updates
  updates
  Payfort request & response insert
  Clarity implementation, LiveCare update insurance API implemented
  Clarity implementation updates
  updates & fixes
  Updates
2 months ago
Haroon6138 89645645ba Merge pull request 'haroon_dev' (#337) from haroon_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/337
2 months ago
haroon amjad eafb7aa105 updates 2 months ago
haroon amjad b5d95a5a74 updates 2 months ago
haroon amjad 4f6dbb79cd Merge branch 'master' into haroon_dev 2 months ago
haroon amjad e64f147f49 updates 2 months ago
haroon amjad 3dd4665f6e Payfort request & response insert 2 months ago
haroon amjad 6abcdf135e Clarity implementation, LiveCare update insurance API implemented 2 months ago

File diff suppressed because it is too large Load Diff

@ -0,0 +1,9 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="10" fill="#FEE9EA"/>
<path d="M20 24C19.4477 24 19 24.4477 19 25C19 25.5523 19.4477 26 20 26H20.009C20.5613 26 21.009 25.5523 21.009 25C21.009 24.4477 20.5613 24 20.009 24H20Z" fill="#ED1C2B"/>
<path d="M23.9955 24C23.4432 24 22.9955 24.4477 22.9955 25C22.9955 25.5523 23.4432 26 23.9955 26H24.0045C24.5568 26 25.0045 25.5523 25.0045 25C25.0045 24.4477 24.5568 24 24.0045 24H23.9955Z" fill="#ED1C2B"/>
<path d="M27.991 24C27.4387 24 26.991 24.4477 26.991 25C26.991 25.5523 27.4387 26 27.991 26H28C28.5523 26 29 25.5523 29 25C29 24.4477 28.5523 24 28 24H27.991Z" fill="#ED1C2B"/>
<path d="M20 28C19.4477 28 19 28.4477 19 29C19 29.5523 19.4477 30 20 30H20.009C20.5613 30 21.009 29.5523 21.009 29C21.009 28.4477 20.5613 28 20.009 28H20Z" fill="#ED1C2B"/>
<path d="M23.9955 28C23.4432 28 22.9955 28.4477 22.9955 29C22.9955 29.5523 23.4432 30 23.9955 30H24.0045C24.5568 30 25.0045 29.5523 25.0045 29C25.0045 28.4477 24.5568 28 24.0045 28H23.9955Z" fill="#ED1C2B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.75 14C18.75 13.5858 18.4142 13.25 18 13.25C17.5858 13.25 17.25 13.5858 17.25 14V14.6348C16.4452 14.8629 15.7665 15.2336 15.2015 15.8444C14.4236 16.6853 14.0778 17.7479 13.9122 19.0795C13.75 20.3843 13.75 22.0565 13.75 24.19V24.81C13.75 26.9435 13.75 28.6157 13.9122 29.9205C14.0778 31.2521 14.4236 32.3147 15.2015 33.1556C15.9871 34.0049 16.9928 34.3901 18.2513 34.573C19.4693 34.75 21.0255 34.75 22.9892 34.75H25.0108C26.9745 34.75 28.5307 34.75 29.7487 34.573C31.0072 34.3901 32.0129 34.0049 32.7985 33.1556C33.5764 32.3147 33.9222 31.2521 34.0878 29.9205C34.25 28.6157 34.25 26.9435 34.25 24.8099V24.1901C34.25 22.0565 34.25 20.3843 34.0878 19.0795C33.9222 17.7479 33.5764 16.6853 32.7985 15.8444C32.2335 15.2336 31.5548 14.8629 30.75 14.6348V14C30.75 13.5858 30.4142 13.25 30 13.25C29.5858 13.25 29.25 13.5858 29.25 14V14.3659C28.1139 14.25 26.716 14.25 25.0108 14.25L22.9892 14.25C21.284 14.25 19.8861 14.25 18.75 14.3659V14ZM17.2786 16.2058C17.3681 16.52 17.6572 16.75 18 16.75C18.4142 16.75 18.75 16.4142 18.75 16V15.8746C19.811 15.7516 21.1824 15.75 23.05 15.75L24.95 15.75C26.8176 15.75 28.189 15.7516 29.25 15.8746V16C29.25 16.4142 29.5858 16.75 30 16.75C30.3428 16.75 30.6319 16.52 30.7214 16.2058C31.1287 16.3679 31.438 16.5826 31.6973 16.8629C32.1704 17.3744 32.4505 18.0759 32.5982 19.2564C32.5661 19.2522 32.5333 19.25 32.5 19.25L15.5 19.25C15.4667 19.25 15.4339 19.2522 15.4018 19.2564C15.5495 18.0759 15.8296 17.3744 16.3027 16.8629C16.562 16.5826 16.8713 16.3679 17.2786 16.2058ZM15.2901 20.7202C15.2506 21.6832 15.25 22.8363 15.25 24.2432V24.7568C15.25 26.9551 15.2514 28.5339 15.4008 29.7354C15.5482 30.9208 15.8285 31.6244 16.3027 32.1371C16.7692 32.6414 17.3973 32.9331 18.467 33.0886C19.5648 33.2481 21.0119 33.25 23.05 33.25H24.95C26.9881 33.25 28.4352 33.2481 29.533 33.0886C30.6027 32.9331 31.2308 32.6414 31.6973 32.1371C32.1715 31.6244 32.4518 30.9208 32.5992 29.7354C32.7486 28.5339 32.75 26.9551 32.75 24.7568V24.2432C32.75 22.8363 32.7494 21.6832 32.7099 20.7202C32.6433 20.7396 32.5728 20.75 32.5 20.75L15.5 20.75C15.4272 20.75 15.3567 20.7396 15.2901 20.7202Z" fill="#ED1C2B"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@ -468,6 +468,7 @@
"serviceInformation": "معلومات الخدمة",
"homeHealthCare": "الرعاية الصحية المنزلية",
"noAppointmentAvailable": "لا توجد مواعيد متاحة",
"noScheduleAvailable": "لا يوجد جدول متاح حاليًا",
"loginRegister": "تسجيل الدخول / التسجيل",
"orderLog": "سجل الطلب",
"infoLab": "تتيح لك هذه الخدمة عرض نتائج جميع الفحوصات المخبرية التي أجريت في مجموعة الحبيب الطبية بالإضافة إلى إرسال التقرير عبر البريد الإلكتروني.",
@ -767,7 +768,7 @@
"viewTerms": "لعرض الشروط والأحكام",
"wantConnectHmgNetwork": "عزيزي العميل، لا يوجد وصول إلى الإنترنت، هل ترغب في الاتصال بشبكة مجموعة الحبيب الطبية لاستخدام تطبيقنا، تأكد من أنك في نطاق شبكة مجموعة الحبيب الطبية",
"failedToAccessHmgServices": "متصل بشبكة مجموعة الحبيب الطبية،\n\nلكن فشل في الوصول إلى خدمات مجموعة الحبيب الطبية",
"offerAndPackages": "عروض وباقات",
"offerAndPackages": "العروض و الباقات",
"offerAndPackagesDetails": "تتيح لك هذه الخدمة عرض جميع عروض مجموعة الحبيب الطبية:",
"invoiceNo": "رقم الفاتورة",
"invoiceDate": "تاريخ الفاتورة",
@ -1957,10 +1958,18 @@
"refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
"schedule": "جدول",
"doctorSchedule": "جدول الطبيب",
"weeklyShift": "الفترات الأسبوعية",
"dayOff": "إجازة",
"notAvailable": "غير متاح",
"hide": "إخفاء",
"paymentSuccessful": "تم الدفع بنجاح",
"typeAnyOffer": "اكتب أي عرض",
"validTill": "صالح حتى",
"buyNow": "اشتري الآن",
"typeAnyOffer": "ابحث عن العروض و الباقات",
"validTill": "صالح لغاية",
"buyNow": "اشترِ الآن",
"availableAt": "متاح في",
"includedServices": "الخدمات المشمولة",
"off": "خصم",
@ -1968,8 +1977,8 @@
"orderLabel": "الطلب",
"purchased": "تم الشراء",
"validUntil": "صالح حتى",
"viewOfferDetails": "عرض تفاصيل العرض",
"viewOrderDetails": "عرض تفاصيل الطلب",
"viewOfferDetails": "تفاصيل العرض",
"viewOrderDetails": "تفاصيل الطلب",
"hospitalNA": "المستشفى: غير متوفر",
"hospitalLabel": "المستشفى",
"showLess": "عرض أقل",
@ -1992,10 +2001,12 @@
"errorLoadingQRCode": "خطأ في تحميل رمز الاستجابة السريعة",
"noOfferDetailsAvailable": "لا توجد تفاصيل عرض متاحة",
"noImagesAvailable": "لا توجد صور متاحة",
"checkOutThisAmazingOffer": "تحقق من هذا العرض المذهل!",
"checkOutThisAmazingOffer": "تحقق من هذا العرض من مستشفى سلمان الحبيب",
"offerPayment": "دفع العرض",
"unableToCreateOrder": "غير قادر على إنشاء الطلب. معلومات العرض أو المستخدم مفقودة.",
"failedToCreateOrder": "فشل في إنشاء الطلب. يرجى المحاولة مرة أخرى.",
"unableToCompletePayment": "غير قادر على إكمال الدفع. معلومات الطلب مفقودة.",
"paymentConfirmationFailed": "تم تسجيل الدفع لكن التأكيد فشل. يرجى الاتصال بالدعم."
"paymentConfirmationFailed": "تم تسجيل الدفع لكن التأكيد فشل. يرجى الاتصال بالدعم.",
"offerAndPackageDetails": "تفاصيل العروض والباقات",
"myOrders": "طلباتي"
}

@ -460,6 +460,7 @@
"serviceInformation": "Service Information",
"homeHealthCare": "Home Health Care",
"noAppointmentAvailable": "No Available Appointments",
"noScheduleAvailable": "No schedule currently available",
"homeHealthCareText": "Clinical excellence and specialized care in the comfort of your home",
"loginRegister": "Login/Register",
"orderLog": "Order Log",
@ -1948,7 +1949,60 @@
"invoiceHistory": "Invoice History",
"parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"paymentSuccessful": "Payment SuccessFul"
"livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
"livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?",
"schedule": "Schedule",
"doctorSchedule": "Doctor Schedule",
"weeklyShift": "Weekly Shift",
"dayOff": "Off",
"hide": "Hide",
"paymentSuccessful": "Payment SuccessFul",
"typeAnyOffer": "Type any offer",
"validTill": "Valid till",
"buyNow": "Buy Now",
"availableAt": "Available at",
"includedServices": "Included Services",
"off": "OFF",
"failedToLoadOfferDetails": "Failed to load offer details",
"orderLabel": "Order",
"purchased": "Purchased",
"validUntil": "Valid until",
"viewOfferDetails": "View Offer Details",
"viewOrderDetails": "View Order Details",
"hospitalNA": "Hospital: N/A",
"hospitalLabel": "Hospital",
"showLess": "Show less",
"redeemed": "Redeemed",
"partiallyRedeemed": "Partially Redeemed",
"available": "Available",
"saveQRCode": "Save QR Code",
"voucherNo": "Voucher No",
"expiresOn": "Expires on",
"saveTheQRCode": "Save the QR Code",
"pleaseShowQRCodeInHospital": "Please save it to your device and show it in hospital to proceed.",
"voucherCodeCopied": "Voucher code copied to clipboard",
"qrCodeNotAvailableToShare": "QR Code not available to share",
"failedToShareQRCode": "Failed to share QR Code",
"failedToSaveQRCode": "Failed to save QR Code",
"qrCodeSavedSuccessfully": "QR Code Saved Successfully",
"qrCodeSavedToDevice": "Your QR code has been saved to your device",
"viewOrderHistory": "View Order History",
"qrCodeNotAvailable": "QR Code not available",
"errorLoadingQRCode": "Error loading QR Code",
"noOfferDetailsAvailable": "No offer details available",
"noImagesAvailable": "No images available",
"checkOutThisAmazingOffer": "Checkout this Offer From HMG",
"offerPayment": "Offer Payment",
"unableToCreateOrder": "Unable to create order. Missing offer or user information.",
"failedToCreateOrder": "Failed to create order. Please try again.",
"unableToCompletePayment": "Unable to complete payment. Missing order information.",
"paymentConfirmationFailed": "Payment recorded but confirmation failed. Please contact support.",
"offerAndPackageDetails": "Offers and Package Detail",
"myOrders": "My Orders"
}

@ -208,11 +208,11 @@ class ApiClientImp implements ApiClient {
if (url == 'https://uat.hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo') {
url = "https://hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo";
body['TokenID'] = "@dm!n";
}
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 4772993;
// body['PatientID'] = 89754444;
// body['PatientID'] = 2122445;
// body['PatientID'] = 53320;
// body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q";
@ -266,29 +266,77 @@ class ApiClientImp implements ApiClient {
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "SocketException: $e");
// Enhanced Clarity logging for network errors
Clarity.setCustomTag('api_err_type', 'socket_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
Clarity.sendCustomEvent('api_failure');
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'POST');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_socket_exception');
return;
} on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "ClientException: $e");
// Enhanced Clarity logging
Clarity.setCustomTag('api_err_type', 'client_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'POST');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_client_exception');
return;
} on TimeoutException catch (_) {
final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "TimeoutException");
// Enhanced Clarity logging for timeouts
Clarity.setCustomTag('api_err_type', 'timeout');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
Clarity.sendCustomEvent('api_timeout_failure');
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'POST');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_timeout');
return;
} catch (e) {
final message = LocaleKeys.networkUnknownError.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "UnknownException: $e");
// Enhanced Clarity logging for unknown errors
Clarity.setCustomTag('api_err_type', 'unknown_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'POST');
// Truncate exception message for Clarity
final exceptionMsg = e.toString();
final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg;
Clarity.setCustomTag('api_err_exception', truncatedMsg);
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_unknown_exception');
return;
}
@ -489,21 +537,74 @@ class ApiClientImp implements ApiClient {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "SocketException: $e");
// Clarity logging for GET requests
Clarity.setCustomTag('api_err_type', 'socket_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'GET');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_get_socket_exception');
return;
} on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "ClientException: $e");
Clarity.setCustomTag('api_err_type', 'client_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'GET');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_get_client_exception');
return;
} on TimeoutException catch (_) {
final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "TimeoutException");
Clarity.setCustomTag('api_err_type', 'timeout');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'GET');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_get_timeout');
return;
} catch (e) {
final message = LocaleKeys.networkUnknownError.tr();
onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "UnknownException: $e");
Clarity.setCustomTag('api_err_type', 'unknown_exception');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'GET');
final exceptionMsg = e.toString();
final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg;
Clarity.setCustomTag('api_err_exception', truncatedMsg);
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_get_unknown_exception');
return;
}
@ -513,6 +614,19 @@ class ApiClientImp implements ApiClient {
if (statusCode < 200 || statusCode >= 400) {
onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode);
logApiEndpointError(endPoint, LocaleKeys.networkErrorWhileFetching.tr(), statusCode);
// Clarity logging for GET status code errors
Clarity.setCustomTag('api_err_type', 'status_code_error');
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.setCustomTag('api_err_method', 'GET');
Clarity.setCustomTag('api_err_status', statusCode.toString());
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_get_status_error');
} else {
var parsed = json.decode(utf8.decode(response.bodyBytes));
onSuccess(parsed, statusCode);
@ -695,6 +809,15 @@ class ApiClientImp implements ApiClient {
return '...${url.substring(url.length - (clarityLimit - 3))}';
}
String _getEndpointName(String endpoint) {
// Extract the last part of the endpoint path for cleaner tracking
const int clarityLimit = 100;
final cleanEndpoint = endpoint.split('/').last;
if (cleanEndpoint.length <= clarityLimit) return cleanEndpoint;
return '...${cleanEndpoint.substring(cleanEndpoint.length - (clarityLimit - 3))}';
}
String _sanitizeRequestBody(String body) {
if (body.isEmpty) return '';

@ -310,7 +310,7 @@ class ApiConsts {
static String pomsPaymentResult(int orderId) => '$pomsBaseUrl/api/v1/mobile/orders/$orderId/payment-result';
// ************ static values for Api ****************
static final double appVersionID = 21.3;
static final double appVersionID = 21.6;
// static final double appVersionID = 50.7;
static final int appChannelId = 3;
@ -632,6 +632,8 @@ var GET_ER_APPOINTMENT_TIME = 'Services/ER_VirtualCall.svc/REST/GetRestTime';
var ADD_NEW_CALL_FOR_PATIENT_ER = 'Services/DoctorApplication.svc/REST/NewCallForPatientER';
var ADD_LIVECARE_INSURANCE_UPDATE_REQUEST = 'Services/ER_VirtualCall.svc/REST/ER_InsertRequests';
var GET_LIVECARE_HISTORY = 'Services/ER_VirtualCall.svc/REST/GetPatientErVirtualHistory';
var CANCEL_LIVECARE_REQUEST = 'Services/ER_VirtualCall.svc/REST/DeleteErRequest';
var SEND_LIVECARE_INVOICE_EMAIL = 'Services/Notifications.svc/REST/SendInvoiceForLiveCare';
@ -995,7 +997,10 @@ var IS_DOCTOR_AVAILABLE_BY_CALENDAR_SCHEDULE = 'Services/OUTPs.svc/REST/HIS_IsDo
//PAYFORT
var getPayFortProjectDetails = "Services/PayFort_Serv.svc/REST/GetPayFortProjectDetails";
var addPayFortApplePayResponse = "Services/PayFort_Serv.svc/REST/AddResponse";
var ADD_PAYFORT_REQUEST = "Services/PayFort_Serv.svc/REST/AddRequest";
var ADD_PAYFORT_RESPONSE = "Services/PayFort_Serv.svc/REST/AddResponse";
// var payFortEnvironment = FortEnvironment.production;
// var applePayMerchantId = "merchant.com.hmgwebservices";
// var payFortEnvironment = FortEnvironment.test;

@ -193,6 +193,7 @@ class AppAssets {
static const String ic_rrt_vehicle = '$svgBasePath/ic_rrt_vehicle.svg';
static const String doctor_profile_rating_icon = '$svgBasePath/doctor_profile_rating_icon.svg';
static const String doctor_profile_reviews_icon = '$svgBasePath/doctor_profile_reviews_icon.svg';
static const String doctor_profile_schedule_icon = '$svgBasePath/doctor_profile_schedule_icon.svg';
static const String bookmark_icon = '$svgBasePath/bookmark_icon.svg';
static const String bookmark_filled_icon = '$svgBasePath/bookmark_filled_icon.svg';
static const String waiting_appointment_icon = '$svgBasePath/waitingAppo.svg';

@ -75,6 +75,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
import 'package:hmg_patient_app_new/services/clarity_service.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/services/firebase_service.dart';
@ -83,6 +84,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/notification_service.dart';
import 'package:hmg_patient_app_new/services/permission_service.dart';
import 'package:hmg_patient_app_new/core/services/turnstile_service.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:local_auth/local_auth.dart';
@ -154,6 +156,7 @@ class AppDependencies {
);
getIt.registerLazySingleton<PermissionService>(() => PermissionService());
getIt.registerLazySingleton<TurnstileService>(() => TurnstileService(getIt<LoggerService>()));
// Repositories
getIt.registerLazySingleton<CommonRepo>(() => CommonRepoImp(loggerService: getIt()));
@ -254,7 +257,8 @@ class AppDependencies {
dialogService: getIt(),
appState: getIt(),
errorHandlerService: getIt(),
localAuthService: getIt()),
localAuthService: getIt(),
turnstileService: getIt()),
);
getIt.registerLazySingleton<ProfileSettingsViewModel>(() => ProfileSettingsViewModel(
@ -366,5 +370,9 @@ class AppDependencies {
));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt()));
getIt.registerLazySingleton<ClarityService>(
() => ClarityService(appState: getIt.get<AppState>()),
);
}
}

@ -0,0 +1,56 @@
import 'dart:async';
import 'package:cloudflare_turnstile/cloudflare_turnstile.dart';
import 'package:flutter/foundation.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
/// Service to handle Cloudflare Turnstile captcha verification
class TurnstileService {
final LoggerService _loggerService;
// Replace with your Cloudflare Turnstile site key
static const String _siteKey = '0x4AAAAAAAkmbvEy63pcxBBt';
TurnstileService(this._loggerService);
/// Get Turnstile token using invisible mode
/// Returns the token on success, null on failure
Future<String?> getTurnstileToken() async {
// Initialize an instance of invisible Cloudflare Turnstile with your site key
final turnstile = CloudflareTurnstile.invisible(
siteKey: _siteKey,
);
try {
// Get the Turnstile token
final token = await turnstile.getToken();
_loggerService.logInfo('Turnstile token retrieved successfully');
return token; // Return the token upon success
} on TurnstileException catch (e) {
// Handle Turnstile failure
_loggerService.logError('Turnstile challenge failed: ${e.message}');
if (kDebugMode) {
print('Challenge failed: ${e.message}');
}
} catch (e) {
// Handle any other exceptions
_loggerService.logError('Unexpected error during Turnstile challenge: $e');
if (kDebugMode) {
print('Unexpected error: $e');
}
} finally {
// Ensure the Turnstile instance is properly disposed of
turnstile.dispose();
}
// Return null if the token couldn't be generated
return null;
}
/// Get the site key for Turnstile widget
String getSiteKey() => _siteKey;
/// Validate if Turnstile is properly configured
bool isConfigured() {
return _siteKey.isNotEmpty && _siteKey != '0x4AAAAAAAkmbvEy63pcxBBt';
}
}

@ -18,7 +18,8 @@ class DateUtil {
final endIndex = date.indexOf(end, startIndex + start.length);
return DateTime.fromMillisecondsSinceEpoch(int.parse(
date.substring(startIndex + start.length, endIndex),
), isUtc: true).add(Duration(hours: 3));
// ), isUtc: true).add(Duration(hours: 3));
), isUtc: true).toLocal();
}
static DateTime convertStringToDateSaudiTimezone(String date, int projectId) {

@ -147,6 +147,9 @@ class AuthenticationRepoImp implements AuthenticationRepo {
bool isRegister = false,
bool isFormFamilyFile = false,
}) async {
int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1;
sendActivationCodeReq.patientOutSA = isOutKsa;
sendActivationCodeReq.isDentalAllowedBackend = false;

@ -42,6 +42,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/services/localauth_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/core/services/turnstile_service.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
@ -60,6 +61,7 @@ class AuthenticationViewModel extends ChangeNotifier {
final DialogService _dialogService;
final NavigationService _navigationService;
final LocalAuthService _localAuthService;
final TurnstileService _turnstileService;
AuthenticationViewModel({
required AppState appState,
@ -69,12 +71,14 @@ class AuthenticationViewModel extends ChangeNotifier {
required NavigationService navigationService,
required CacheService cacheService,
required LocalAuthService localAuthService,
required TurnstileService turnstileService,
}) : _navigationService = navigationService,
_dialogService = dialogService,
_errorHandlerService = errorHandlerService,
_appState = appState,
_authenticationRepo = authenticationRepo,
_localAuthService = localAuthService {}
_localAuthService = localAuthService,
_turnstileService = turnstileService {}
final TextEditingController nationalIdController = TextEditingController(),
phoneNumberController = TextEditingController(),
dobController = TextEditingController(),
@ -974,6 +978,15 @@ class AuthenticationViewModel extends ChangeNotifier {
// TODO: GET APP SMS SIGNATURE HERE
request.sMSSignature = await getSignature();
// Get Turnstile token if configured
final turnstileToken = await _turnstileService.getTurnstileToken();
if (turnstileToken != null) {
request.turnstileToken = turnstileToken;
} else {
request.turnstileToken = "";
}
if (isNeedLoading) {
LoaderBottomSheet.showLoader();
}
@ -1209,13 +1222,15 @@ class AuthenticationViewModel extends ChangeNotifier {
}
// Fetch profile image after switching user
await _fetchProfileImage(activation.list!.first.patientId);
// await _fetchProfileImage(activation.list!.first.patientId);
_fetchProfileImage(activation.list!.first.patientId);
} else {
activation.list!.first.isParentUser = true;
_appState.addUserSwitchPayload(activation.list!.first.toJson(), isSuperUser: true);
// Fetch profile image after login
await _fetchProfileImage(activation.list!.first.patientId);
// await _fetchProfileImage(activation.list!.first.patientId);
_fetchProfileImage(activation.list!.first.patientId);
}
activation.list!.first.bloodGroup = activation.patientBloodType;
activation.list!.first.zipCode = selectedCountrySignup == CountryEnum.others ? '0' : selectedCountrySignup.countryCode;

@ -30,6 +30,7 @@ class SendActivationRequest {
int? status;
int? familyRegionID;
bool? isPatientExcluded;
String? turnstileToken;
SendActivationRequest(
{this.patientMobileNumber,
@ -62,7 +63,8 @@ class SendActivationRequest {
this.responseID,
this.status,
this.familyRegionID,
this.isPatientExcluded});
this.isPatientExcluded,
this.turnstileToken});
SendActivationRequest.fromJson(Map<String, dynamic> json) {
patientMobileNumber = json['PatientMobileNumber'];
@ -96,6 +98,7 @@ class SendActivationRequest {
status = json['Status'];
familyRegionID = json['FamilyRegionID'];
isPatientExcluded = json['IsPatientExcluded'];
turnstileToken = json['TurnstileToken'];
}
Map<String, dynamic> toJson() {
@ -135,6 +138,9 @@ class SendActivationRequest {
data['Status'] = status;
data['FamilyRegionID'] = familyRegionID;
data['IsPatientExcluded'] = isPatientExcluded;
if (turnstileToken != null) {
data['TurnstileToken'] = turnstileToken;
}
return data;
}
}

@ -113,6 +113,8 @@ abstract class BookAppointmentsRepo {
Future<Either<Failure, GenericApiModel<dynamic>>> insertFavouriteDoctor(
{required int patientID, required int projectID, required int clinicID, required int doctorID, required bool isActive, Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<dynamic>>> getDoctorSchedule({required int doctorID, required int projectID, required int clinicID, int doctorWorkingHoursDays = 90, Function(dynamic)? onSuccess, Function(String)? onError});
}
class BookAppointmentsRepoImp implements BookAppointmentsRepo {
@ -1228,4 +1230,49 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> getDoctorSchedule({required int doctorID, required int projectID, required int clinicID, int doctorWorkingHoursDays = 90, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Map<String, dynamic> mapRequest = {
"DoctorID": doctorID,
"ProjectID": projectID,
"ClinicID": clinicID,
"DoctorWorkingHoursDays": doctorWorkingHoursDays,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
DOCTOR_SCHEDULE_URL,
body: mapRequest,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
if (onError != null) {
onError(error);
}
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: response,
);
if (onSuccess != null) {
onSuccess(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()));
}
}
}

@ -1816,4 +1816,48 @@ class BookAppointmentsViewModel extends ChangeNotifier {
},
);
}
bool isDoctorScheduleLoading = false;
List<String> doctorFreeSlotDates = [];
dynamic doctorScheduleResponse;
Future<void> getDoctorSchedule({required int doctorID, required int projectID, required int clinicID, int doctorWorkingHoursDays = 90, Function(dynamic)? onSuccess, Function(String)? onError}) async {
isDoctorScheduleLoading = true;
notifyListeners();
final result = await bookAppointmentsRepo.getDoctorSchedule(
doctorID: doctorID,
projectID: projectID,
clinicID: clinicID,
doctorWorkingHoursDays: doctorWorkingHoursDays,
);
result.fold(
(failure) async {
isDoctorScheduleLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
isDoctorScheduleLoading = false;
if (apiResponse.messageStatus == 2) {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to fetch doctor schedule");
}
} else if (apiResponse.messageStatus == 1) {
doctorScheduleResponse = apiResponse.data;
if (doctorScheduleResponse != null && doctorScheduleResponse['FreeTimeSlots'] != null) {
doctorFreeSlotDates = List<String>.from(doctorScheduleResponse['FreeTimeSlots']);
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data);
}
}
},
);
}
}

@ -0,0 +1,26 @@
class DoctorScheduleResponseModel {
String? date;
String? dayName;
String? workingHours;
DoctorScheduleResponseModel({
this.date,
this.dayName,
this.workingHours,
});
DoctorScheduleResponseModel.fromJson(Map<String, dynamic> json) {
date = json['Date'];
dayName = json['DayName'];
workingHours = json['WorkingHours'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['Date'] = date;
data['DayName'] = dayName;
data['WorkingHours'] = workingHours;
return data;
}
}

@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_p
import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_status_coc_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:permission_handler/permission_handler.dart';
class ContactUsViewModel extends ChangeNotifier {
@ -308,7 +309,15 @@ class ContactUsViewModel extends ChangeNotifier {
final result = await contactUsRepo.insertCOCItem(requestInsertCOCItem: requestInsertCOCItem, patientSelectedAppointment: patientFeedbackSelectedAppointment);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
LoaderBottomSheet.hideLoader();
await errorHandlerService.handleError(failure: failure);
},
// (failure) async {
// if(onError != null) {
// onError(failure.toString());
// }
// },
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});

@ -335,7 +335,6 @@ class HabibWalletViewModel extends ChangeNotifier {
walletRechargeAmount = 0;
selectedRechargeType = 1;
advancePaymentHospitals.clear();
habibWalletBalanceList.clear();
selectedHospital = null;
fileNumber = '';
depositorName = '';
@ -417,6 +416,7 @@ class HabibWalletViewModel extends ChangeNotifier {
if (!isWalletAmountToBeLoaded) {
return;
}
habibWalletBalanceList.clear();
isWalletAmountLoading = true;
notifyListeners();

@ -25,6 +25,9 @@ abstract class ImmediateLiveCareRepo {
{Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<List<PatientLiveCareHistory>>>> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<dynamic>>> addInsuranceUpdateRequestForLiveCare(String name, String mobileNumber, String fileNumber,
{Function(dynamic)? onSuccess, Function(String)? onError});
}
class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo {
@ -250,4 +253,44 @@ class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel>> addInsuranceUpdateRequestForLiveCare(String name, String mobileNumber, String fileNumber,
{Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async {
Map<String, dynamic> mapDevice = {
"PatientName": name,
"MobileNumber": mobileNumber,
"FileNumber": fileNumber,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
ADD_LIVECARE_INSURANCE_UPDATE_REQUEST,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
// onError!(error);
},
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()));
}
}
}

@ -245,6 +245,28 @@ class ImmediateLiveCareViewModel extends ChangeNotifier {
);
}
Future<void> addInsuranceUpdateRequestForLiveCare({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await immediateLiveCareRepo.addInsuranceUpdateRequestForLiveCare("${_appState.getAuthenticatedUser()?.firstName ?? ''} ${_appState.getAuthenticatedUser()?.lastName ?? ''}",
_appState.getAuthenticatedUser()!.mobileNumber ?? "", _appState.getAuthenticatedUser()!.patientId.toString() ?? "");
result.fold(
(failure) async {
onError!(failure.message);
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage ?? "Unknown error occurred");
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientLiveCareHistoryList = [];
patientHasPendingLiveCareRequest = false;

@ -29,6 +29,7 @@ abstract class LabRepo {
Future<Either<Failure, GenericApiModel<LabOrdersResponseByAi>>> getPatientAiOverViewLabOrders({required int langId, required dynamic payload});
Future<Either<Failure, GenericApiModel<LabOrderResponseByAi>>> getPatientAiOverViewLabOrder(var payloa);
Map<String, List<LabResult>> groupLabResultsByPackageName(List<LabResult> labResults);
}
class LabRepoImp implements LabRepo {
@ -37,6 +38,55 @@ class LabRepoImp implements LabRepo {
LabRepoImp({required this.loggerService, required this.apiClient});
/// Groups a list of LabResult objects by their PackageName
/// Returns a Map where the key is the PackageName and the value is a List of LabResult objects
/// Both the keys and the lists are sorted alphabetically
///
/// Example usage:
/// ```dart
/// List<LabResult> labResults = [/* your lab results */];
/// Map<String, List<LabResult>> groupedResults = labRepo.groupLabResultsByPackageName(labResults);
///
/// // Access results by package name
/// groupedResults.forEach((packageName, results) {
/// print('Package: $packageName has ${results.length} results');
/// });
/// ```
Map<String, List<LabResult>> groupLabResultsByPackageName(List<LabResult> labResults) {
Map<String, List<LabResult>> groupedResults = {};
// Group results by package name
for (var labResult in labResults) {
String packageName = labResult.PackageName ?? 'Unknown Package';
if (groupedResults.containsKey(packageName)) {
// Package name exists, add to existing list
groupedResults[packageName]!.add(labResult);
} else {
// Package name doesn't exist, create new key with new list
groupedResults[packageName] = [labResult];
}
}
// Sort each list alphabetically by description
groupedResults.forEach((key, list) {
list.sort((a, b) {
String descA = (a.description ?? '').toLowerCase();
String descB = (b.description ?? '').toLowerCase();
return descA.compareTo(descB);
});
});
// Sort the map keys alphabetically and return a new sorted map
var sortedKeys = groupedResults.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
Map<String, List<LabResult>> sortedGroupedResults = {};
for (var key in sortedKeys) {
sortedGroupedResults[key] = groupedResults[key]!;
}
return sortedGroupedResults;
}
@override
Future<Either<Failure, GenericApiModel<List<PatientLabOrdersResponseModel>>>> getPatientLabOrders() async {
Map<String, dynamic> mapDevice = {};

@ -57,7 +57,11 @@ class LabViewModel extends ChangeNotifier {
PatientLabOrdersResponseModel? currentlySelectedPatientOrder;
List<LabResult> mainLabResultsByHospitals = [];
// Flat list for UI compatibility
List<LabResult> mainLabResultsByHospitalsList = [];
// Grouped map by package name for efficient lookups
Map<String, List<LabResult>> mainLabResultsByPackageName = {};
List<LabResult> mainLabResults = [];
List<DataPoint> mainGraphPoints = [];
@ -383,7 +387,8 @@ class LabViewModel extends ChangeNotifier {
Future<void> getPatientLabResultByHospital(PatientLabOrdersResponseModel laborder) async {
isLabResultByHospitalLoading = true;
notifyListeners();
mainLabResultsByHospitals.clear();
mainLabResultsByHospitalsList.clear();
mainLabResultsByPackageName.clear();
final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")));
@ -397,7 +402,9 @@ class LabViewModel extends ChangeNotifier {
isLabResultByHospitalLoading = false;
if (apiResponse.messageStatus == 2) {
} else if (apiResponse.messageStatus == 1) {
mainLabResultsByHospitals = apiResponse.data ?? [];
mainLabResultsByHospitalsList = apiResponse.data ?? [];
// Group the results by package name for efficient lookups
mainLabResultsByPackageName = labRepo.groupLabResultsByPackageName(mainLabResultsByHospitalsList);
notifyListeners();
}
},
@ -842,6 +849,25 @@ class LabViewModel extends ChangeNotifier {
notifyListeners();
}
// /// Finds a matching LabResult from the grouped map by package name
// /// Searches through all packages to find a result matching the test code or description
// LabResult? _findMatchingLabResult(TestDetails testDetail) {
// // Search through all packages in the grouped map
// for (var packageResults in mainLabResultsByPackageName.values) {
// try {
// var matchingResult = packageResults.firstWhere(
// (labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description,
// );
// return matchingResult;
// } catch (e) {
// // Continue searching in other packages
// continue;
// }
// }
// // Return null if no match found
// return LabResult();
// }
Future<void> getAiOverviewLabOrders({required int langId, required PatientLabOrdersResponseModel labOrder, required String loadingText}) async {
isLabAIAnalysisNeedsToBeShown = true;
notifyListeners();
@ -858,7 +884,7 @@ class LabViewModel extends ChangeNotifier {
if (labOrder.testDetails != null && labOrder.testDetails!.isNotEmpty) {
for (var testDetail in labOrder.testDetails!) {
LabResult? matchingResult =
mainLabResultsByHospitals.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult());
mainLabResultsByHospitalsList.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult());
testResults.add({
"test_name": testDetail.description ?? "",
"result_value": matchingResult.resultValue ?? "",

@ -28,6 +28,7 @@ class LabResult {
String? packageShortDescription;
int? testId;
String? testShortDescription;
String? PackageName;
LabResult(
{this.description,
@ -56,6 +57,7 @@ class LabResult {
this.referenceLow,
this.testId,
this.testShortDescription,
this.PackageName
});
LabResult.fromJson(Map<String, dynamic> json) {
@ -88,6 +90,7 @@ class LabResult {
resultTypeID = json['ResultTypeID'];
testId = json['TestID'];
testShortDescription = json['TestShortDescription'];
PackageName = json['PackageName'];
}
Map<String, dynamic> toJson() {
@ -118,6 +121,7 @@ class LabResult {
data['ReferenceLow'] = this.referenceLow;
data['TestID'] = this.testId;
data['TestShortDescription'] = this.testShortDescription;
data['PackageName'] = this.PackageName;
return data;
}

@ -42,7 +42,7 @@ class MedicalFileViewModel extends ChangeNotifier {
bool isSickLeavesSortByClinic = true;
// bool isSickLeavesDataNeedsReloading = true;
bool isSickLeavesDataNeedsReloading = true;
List<GetAllergiesResponseModel> patientAllergiesList = [];
@ -165,15 +165,15 @@ class MedicalFileViewModel extends ChangeNotifier {
}
setIsPatientSickLeaveListLoading(bool val) {
// if (val && isSickLeavesDataNeedsReloading) {
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
if (val && isSickLeavesDataNeedsReloading) {
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
patientSickLeavesByHospital.clear();
patientSickLeavesViewList.clear();
patientSickLeavePDFBase64 = "";
isSickLeavesSortByClinic = true;
isPatientSickLeaveListLoading = val;
// }
}
notifyListeners();
}
@ -282,9 +282,9 @@ class MedicalFileViewModel extends ChangeNotifier {
}
Future<void> getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isSickLeavesDataNeedsReloading) {
// return;
// }
if (!isSickLeavesDataNeedsReloading) {
return;
}
patientSickLeaveList.clear();
notifyListeners();
@ -300,6 +300,7 @@ class MedicalFileViewModel extends ChangeNotifier {
// ),
(failure) async {
isPatientSickLeaveListLoading = false;
isSickLeavesDataNeedsReloading = true;
notifyListeners();
},
(apiResponse) {
@ -330,7 +331,7 @@ class MedicalFileViewModel extends ChangeNotifier {
}
}
patientSickLeavesViewList = patientSickLeavesByClinic;
// isSickLeavesDataNeedsReloading = false;
isSickLeavesDataNeedsReloading = false;
notifyListeners();
if (onSuccess != null) {

@ -53,7 +53,7 @@ abstract class MyAppointmentsRepo {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointmentsForTimeLine();
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList();
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList({bool isTop8 = false});
Future<Either<Failure, GenericApiModel<List<GetFavoriteDoctorsListModel>>>> getFavouriteDoctorsList();
@ -514,11 +514,12 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
}
@override
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList() async {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList({bool isTop8 = false}) async {
Map<String, dynamic> mapDevice = {
"Top": 0,
"beforeDays": 0,
"exludType": 4,
"IsTop8": isTop8,
};
try {

@ -54,6 +54,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
bool isFavouriteDoctorsDataFetched = false;
bool isAppointmentDataToBeLoaded = true;
bool isMyDoctorsDataToBeLoaded = true;
bool isEyeMeasurementsAppointmentsLoading = false;
@ -210,9 +211,15 @@ class MyAppointmentsViewModel extends ChangeNotifier {
notifyListeners();
}
setIsMyDoctorsDataToBeLoaded(bool val) {
isMyDoctorsDataToBeLoaded = val;
notifyListeners();
}
setIsAppointmentDataToBeLoaded(bool val) {
if (val) {
isMyAppointmentsLoading = true;
isMyDoctorsDataToBeLoaded = true;
}
isAppointmentDataToBeLoaded = val;
notifyListeners();
@ -588,7 +595,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
final result = await myAppointmentsRepo.confirmAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
// (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
if(onError != null) {
onError(failure.message);
}
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage!);
@ -668,13 +680,13 @@ class MyAppointmentsViewModel extends ChangeNotifier {
);
}
Future<void> getPatientMyDoctors({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isAppointmentDataToBeLoaded) return;
Future<void> getPatientMyDoctors({bool isTop8 = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (!isMyDoctorsDataToBeLoaded) return;
isPatientMyDoctorsLoading = true;
patientMyDoctorsList.clear();
notifyListeners();
final result = await myAppointmentsRepo.getPatientDoctorsList();
final result = await myAppointmentsRepo.getPatientDoctorsList(isTop8: isTop8);
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
@ -687,6 +699,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
} else if (apiResponse.messageStatus == 1) {
patientMyDoctorsList = apiResponse.data!;
isPatientMyDoctorsLoading = false;
isMyDoctorsDataToBeLoaded = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);

@ -0,0 +1,128 @@
class PayfortRequestInsertModel {
String? accessCode;
String? commandType;
num? amount;
String? customerEmail;
String? orderDescription;
String? serviceID;
String? returnURL;
String? paymentOption;
String? clientRequestID;
String? projectID;
String? fileNumber;
String? customerName;
String? currency;
String? liveServiceID;
String? longitude;
String? latitude;
String? merchantReference;
String? merchantIdentifier;
String? deviceToken;
String? sHARequestPhase;
String? sHAResponsePhase;
String? signature;
String? language;
String? integrationId;
String? iPAdress;
String? isSchedule;
String? appointmentDate;
String? appointmentNo;
String? doctorID;
PayfortRequestInsertModel(
{this.accessCode,
this.commandType,
this.amount,
this.customerEmail,
this.orderDescription,
this.serviceID,
this.returnURL,
this.paymentOption,
this.clientRequestID,
this.projectID,
this.fileNumber,
this.customerName,
this.currency,
this.liveServiceID,
this.longitude,
this.latitude,
this.merchantReference,
this.merchantIdentifier,
this.deviceToken,
this.sHARequestPhase,
this.sHAResponsePhase,
this.signature,
this.language,
this.integrationId,
this.iPAdress,
this.isSchedule,
this.appointmentDate,
this.appointmentNo,
this.doctorID});
PayfortRequestInsertModel.fromJson(Map<String, dynamic> json) {
accessCode = json['Access_code'];
commandType = json['CommandType'];
amount = json['Amount'];
customerEmail = json['CustomerEmail'];
orderDescription = json['OrderDescription'];
serviceID = json['Service_ID'];
returnURL = json['ReturnURL'];
paymentOption = json['PaymentOption'];
clientRequestID = json['ClientRequestID'];
projectID = json['ProjectID'];
fileNumber = json['FileNumber'];
customerName = json['CustomerName'];
currency = json['Currency'];
liveServiceID = json['Live_ServiceID'];
longitude = json['Longitude'];
latitude = json['Latitude'];
merchantReference = json['Merchant_Reference'];
merchantIdentifier = json['Merchant_Identifier'];
deviceToken = json['DeviceToken'];
sHARequestPhase = json['SHA_RequestPhase'];
sHAResponsePhase = json['SHA_ResponsePhase'];
signature = json['Signature'];
language = json['Language'];
integrationId = json['Integration_Id'];
iPAdress = json['IPAdress'];
isSchedule = json['IsSchedule'];
appointmentDate = json['AppointmentDate'];
appointmentNo = json['AppointmentNo'];
doctorID = json['DoctorID'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['Access_code'] = this.accessCode;
data['CommandType'] = this.commandType;
data['Amount'] = this.amount;
data['CustomerEmail'] = this.customerEmail;
data['OrderDescription'] = this.orderDescription;
data['Service_ID'] = this.serviceID;
data['ReturnURL'] = this.returnURL;
data['PaymentOption'] = this.paymentOption;
data['ClientRequestID'] = this.clientRequestID;
data['ProjectID'] = this.projectID;
data['FileNumber'] = this.fileNumber;
data['CustomerName'] = this.customerName;
data['Currency'] = this.currency;
data['Live_ServiceID'] = this.liveServiceID;
data['Longitude'] = this.longitude;
data['Latitude'] = this.latitude;
data['Merchant_Reference'] = this.merchantReference;
data['Merchant_Identifier'] = this.merchantIdentifier;
data['DeviceToken'] = this.deviceToken;
data['SHA_RequestPhase'] = this.sHARequestPhase;
data['SHA_ResponsePhase'] = this.sHAResponsePhase;
data['Signature'] = this.signature;
data['Language'] = this.language;
data['Integration_Id'] = this.integrationId;
data['IPAdress'] = this.iPAdress;
data['IsSchedule'] = this.isSchedule;
data['AppointmentDate'] = this.appointmentDate;
data['AppointmentNo'] = this.appointmentNo;
data['DoctorID'] = this.doctorID;
return data;
}
}

@ -0,0 +1,77 @@
class PayfortResponseInsertModel {
String? fortId;
String? commandType;
num? amount;
String? sadadOLP;
String? paymentOption;
String? customerIP;
String? eCI;
String? responseMessage;
String? cardNumber;
String? cardBin;
int? installmentsNumber;
String? status;
String? merchantRef;
bool? isRefund;
bool? remmeberMe;
String? reconciliationReference;
PayfortResponseInsertModel({
this.fortId,
this.commandType,
this.amount,
this.sadadOLP,
this.paymentOption,
this.customerIP,
this.eCI,
this.responseMessage,
this.cardNumber,
this.cardBin,
this.installmentsNumber,
this.status,
this.merchantRef,
this.isRefund,
this.remmeberMe,
this.reconciliationReference,
});
PayfortResponseInsertModel.fromJson(Map<String, dynamic> json) {
fortId = json['Fort_id'];
commandType = json['CommandType'];
amount = json['Amount'];
sadadOLP = json['Sadad_OLP'];
paymentOption = json['Payment_Option'];
customerIP = json['Customer_IP'];
eCI = json['ECI'];
responseMessage = json['Response_Message'];
cardNumber = json['Card_Number'];
cardBin = json['Card_Bin'];
installmentsNumber = json['Installments_Number'];
status = json['Status'];
merchantRef = json['Merchant_Ref'];
isRefund = json['IsRefund'];
remmeberMe = json['RemmeberMe'];
reconciliationReference = json['Reconciliation_Reference'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = Map<String, dynamic>();
data['Fort_id'] = this.fortId;
data['CommandType'] = this.commandType;
data['Amount'] = this.amount;
data['Sadad_OLP'] = this.sadadOLP;
data['Payment_Option'] = this.paymentOption;
data['Customer_IP'] = this.customerIP;
data['ECI'] = this.eCI;
data['Response_Message'] = this.responseMessage;
data['Card_Number'] = this.cardNumber;
data['Card_Bin'] = this.cardBin;
data['Installments_Number'] = this.installmentsNumber;
data['Status'] = this.status;
data['Merchant_Ref'] = this.merchantRef;
data['IsRefund'] = this.isRefund;
data['RemmeberMe'] = this.remmeberMe;
data['Reconciliation_Reference'] = this.reconciliationReference;
return data;
}
}

@ -8,6 +8,8 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_project_details_resp_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/sdk_token_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -28,6 +30,10 @@ abstract class PayfortRepo {
{required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID});
Future<Either<Failure, GenericApiModel<GetTamaraInstallmentsDetailsResponseModel>>> getTamaraInstallmentsDetails();
Future<Either<Failure, GenericApiModel<dynamic>>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel});
Future<Either<Failure, GenericApiModel<dynamic>>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel});
}
class PayfortRepoImp implements PayfortRepo {
@ -291,4 +297,57 @@ class PayfortRepoImp implements PayfortRepo {
}
}
@override
Future<Either<Failure, GenericApiModel>> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel}) async {
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(ADD_PAYFORT_REQUEST, body: payfortRequestInsertModel.toJson(), 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());
}
}, isAllowAny: true, isPaymentServices: true);
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>> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel}) async {
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(ADD_PAYFORT_RESPONSE, body: payfortResponseInsertModel.toJson(), 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());
}
}, isAllowAny: true, isPaymentServices: true);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}

@ -8,11 +8,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_project_details_resp_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/sdk_token_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -20,6 +23,8 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:crypto/crypto.dart';
import 'models/apple_pay_request_insert_model.dart';
class PayfortViewModel extends ChangeNotifier {
PayfortRepo payfortRepo;
ErrorHandlerService errorHandlerService;
@ -92,6 +97,44 @@ class PayfortViewModel extends ChangeNotifier {
);
}
Future<void> payfortRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
// payfortProjectDetailsRespModel = apiResponse.data!;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> payfortResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
// payfortProjectDetailsRespModel = apiResponse.data!;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> checkPaymentStatus({required String transactionID, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.checkPaymentStatus(transactionID: transactionID);
@ -291,13 +334,19 @@ class PayfortViewModel extends ChangeNotifier {
}
Future<void> initiatePaymentWithCard( // var request, {
{
String paymentOption,
String liveServiceID,
String isScheduleLiveCare,
String appointmentDate,
String appointmentNo,
String doctorID, {
SucceededCallback? onSucceeded,
FailedCallback? onFailed,
String countryIsoCode = "SA",
String transactionID = "",
String amount = "0",
String fileNumber = "0",
String orderDescription = "",
}) async {
dynamic paymentResult;
@ -325,6 +374,38 @@ class PayfortViewModel extends ChangeNotifier {
"customer_name": "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}",
};
PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel();
payfortRequestInsertModel.accessCode = payfortProjectDetailsRespModel!.accessCode;
payfortRequestInsertModel.commandType = "PURCHASE";
payfortRequestInsertModel.amount = num.parse(amount);
payfortRequestInsertModel.customerEmail = getIt.get<AppState>().getAuthenticatedUser()!.emailAddress;
payfortRequestInsertModel.orderDescription = orderDescription;
payfortRequestInsertModel.serviceID = payfortProjectDetailsRespModel!.servID.toString();
payfortRequestInsertModel.paymentOption = paymentOption;
payfortRequestInsertModel.clientRequestID = transactionID;
payfortRequestInsertModel.projectID = payfortProjectDetailsRespModel!.projectID.toString();
payfortRequestInsertModel.fileNumber = getIt.get<AppState>().getAuthenticatedUser()!.patientId.toString();
payfortRequestInsertModel.customerName = "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}";
payfortRequestInsertModel.currency = getIt.get<AppState>().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED";
payfortRequestInsertModel.liveServiceID = liveServiceID;
payfortRequestInsertModel.latitude = getIt.get<AppState>().userLat.toString();
payfortRequestInsertModel.longitude = getIt.get<AppState>().userLong.toString();
payfortRequestInsertModel.merchantReference = transactionID;
payfortRequestInsertModel.merchantIdentifier = payfortProjectDetailsRespModel!.merchantIdentifier;
payfortRequestInsertModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken);
payfortRequestInsertModel.sHARequestPhase = payfortProjectDetailsRespModel!.shaRequest;
payfortRequestInsertModel.sHAResponsePhase = payfortProjectDetailsRespModel!.shaResponse;
payfortRequestInsertModel.signature = sdkTokenResponse!.signature;
payfortRequestInsertModel.language = getIt.get<AppState>().isArabic() ? "ar" : "en";
payfortRequestInsertModel.integrationId = "1";
payfortRequestInsertModel.isSchedule = isScheduleLiveCare;
payfortRequestInsertModel.appointmentDate = appointmentDate;
payfortRequestInsertModel.appointmentNo = appointmentNo;
payfortRequestInsertModel.doctorID = doctorID;
await payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel);
LoaderBottomSheet.hideLoader();
paymentResult = await FlutterAmazonpaymentservices.normalPay(request, ApiConsts.payFortEnvironmentType, isShowResponsePage: false);
@ -332,6 +413,27 @@ class PayfortViewModel extends ChangeNotifier {
String responseCode = paymentMap['response_code'] ?? "";
print("Payment Success Data: ${paymentMap.toString()}");
PayfortResponseInsertModel payfortResponseInsertModel = PayfortResponseInsertModel();
payfortResponseInsertModel.fortId = paymentMap['fort_id'] ?? "";
payfortResponseInsertModel.commandType = paymentMap['command'] ?? "";
payfortResponseInsertModel.amount = num.tryParse(paymentMap['amount'] ?? "0") ?? 0;
payfortResponseInsertModel.sadadOLP = "";
payfortResponseInsertModel.paymentOption = paymentMap['payment_option'] ?? "";
payfortResponseInsertModel.customerIP = paymentMap['customer_ip'] ?? "";
payfortResponseInsertModel.eCI = paymentMap['eci'] ?? "";
payfortResponseInsertModel.responseMessage = paymentMap['response_message'] ?? "";
payfortResponseInsertModel.cardNumber = paymentMap['card_number'] ?? "";
payfortResponseInsertModel.cardBin = paymentMap['card_bin'] ?? "";
payfortResponseInsertModel.installmentsNumber = int.tryParse(paymentMap['installments_number'] ?? "0") ?? 0;
payfortResponseInsertModel.status = paymentMap['status'] ?? "";
payfortResponseInsertModel.merchantRef = paymentMap['merchant_reference'] ?? "";
payfortResponseInsertModel.isRefund = false;
payfortResponseInsertModel.remmeberMe = true;
payfortResponseInsertModel.reconciliationReference = "";
await payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel);
if (responseCode.startsWith("14") || responseCode == "20064") {
if (onSucceeded != null) {
onSucceeded(PayFortResult.fromMap(paymentMap));

@ -64,7 +64,7 @@ class PrescriptionsViewModel extends ChangeNotifier {
PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService, required this.navServices});
initPrescriptionsViewModel() {
// if (isPrescriptionsDataNeedsReloading) {
if (isPrescriptionsDataNeedsReloading) {
patientPrescriptionOrders.clear();
patientPrescriptionOrdersByClinic.clear();
patientPrescriptionOrdersByHospital.clear();
@ -72,7 +72,7 @@ class PrescriptionsViewModel extends ChangeNotifier {
prescriptionsOrderList.clear();
isPrescriptionsOrdersLoading = true;
getPatientPrescriptionOrders();
// }
}
isSortByClinic = true;
isPrescriptionsDeliveryOrdersLoading = true;
notifyListeners();
@ -138,9 +138,9 @@ class PrescriptionsViewModel extends ChangeNotifier {
}
Future<void> getPatientPrescriptionOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isPrescriptionsDataNeedsReloading) {
// return;
// }
if (!isPrescriptionsDataNeedsReloading) {
return;
}
final result = await prescriptionsRepo.getPatientPrescriptionOrders(patientId: "1231755");
@ -235,11 +235,16 @@ class PrescriptionsViewModel extends ChangeNotifier {
result.fold(
(failure) async {
onError!(failure.message);
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage!);
if (onError != null) {
onError(apiResponse.errorMessage ?? "An error occurred while fetching the prescription instructions PDF.");
}
// onError!(apiResponse.errorMessage!);
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
prescriptionInstructionsPDFLink = apiResponse.data;

@ -13,80 +13,90 @@ enum Durations {
yearly("yearly");
final String value;
const Durations(this.value);
}
class HealthDataTransformation {
Map<String, List<DataPoint>> transformVitalsToDataPoints(VitalsWRTType vitals, String filterType, String selectedSection,) {
final Map<String, List<DataPoint>> dataPointMap = {};
Map<String, List<DataPoint>> transformVitalsToDataPoints(
VitalsWRTType vitals,
String filterType,
String selectedSection,
) {
final Map<String, List<DataPoint>> dataPointMap = {};
Map<String, List<Vitals>> data = vitals.getVitals();
// Group data based on the filter type
Map<String, List<Vitals>> groupedData = {};
// List<List<Vitals> > items = data.values.toList();
List<String> keys = data.keys.toList();
var count = 0;
List<Vitals> item = data[selectedSection] ?? [];
List<Vitals> item = data[selectedSection] ?? [];
// for(var item in items) {
List<DataPoint> dataPoints = [];
for (var vital in item) {
String key = "";
if (vital.value == "" || vital.timestamp == "") continue;
var parseDate = DateTime.parse(vital.timestamp);
var currentDate = normalizeToStartOfDay(DateTime.now());
if (filterType == Durations.daily.value) {
if(isBetweenInclusive(parseDate, currentDate, DateTime.now())) {
key = DateFormat('yyyy-MM-dd HH').format(DateTime.parse(vital.timestamp));
groupedData.putIfAbsent(key, () => []).add(vital);
}// Group by hour
} else if (filterType == Durations.weekly.value) {
if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) {
key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp));
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by day
} else if (filterType == Durations.monthly.value) {
if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) {
print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate");
key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp));
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by day
} else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) {
if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value?180: 365)), DateTime.now())) {
key = DateFormat('yyyy-MM').format(DateTime.parse(vital.timestamp));
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by month
} else {
throw ArgumentError('Invalid filter type');
}
List<DataPoint> dataPoints = [];
for (var vital in item) {
String key = "";
if (vital.value == "" || vital.timestamp == "") continue;
DateTime parseDate;
try {
parseDate = DateTime.parse(vital.timestamp);
} catch (e) {
print('Error parsing timestamp "${vital.timestamp}": $e');
continue; // Skip this vital if timestamp is invalid
}
var currentDate = normalizeToStartOfDay(DateTime.now());
if (filterType == Durations.daily.value) {
if (isBetweenInclusive(parseDate, currentDate, DateTime.now())) {
key = DateFormat('yyyy-MM-dd HH').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by hour
} else if (filterType == Durations.weekly.value) {
if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) {
key = DateFormat('yyyy-MM-dd').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by day
} else if (filterType == Durations.monthly.value) {
if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) {
print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate");
key = DateFormat('yyyy-MM-dd').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by day
} else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) {
if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value ? 180 : 365)), DateTime.now())) {
key = DateFormat('yyyy-MM').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by month
} else {
throw ArgumentError('Invalid filter type');
}
print("the size of groupData is ${groupedData.values.length}");
// Process grouped data
groupedData.forEach((key, values) {
double sum = values.fold(0, (acc, v) => acc + num.parse(v.value));
double mean = sum / values.length;
if(selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") {
mean = sum / values.length;
}else {
mean = sum;
}
double finalValue = mean;
print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}");
dataPoints.add(DataPoint(
value: smartScale(finalValue),
label: key,
actualValue: finalValue.toStringAsFixed(2),
displayTime: key,
unitOfMeasurement:values.first.unitOfMeasure ,
time: DateTime.parse(values.first.timestamp),
));
});
dataPointMap[filterType] = dataPoints;
}
print("the size of groupData is ${groupedData.values.length}");
// Process grouped data
groupedData.forEach((key, values) {
double sum = values.fold(0, (acc, v) => acc + num.parse(v.value));
double mean = sum / values.length;
if (selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") {
mean = sum / values.length;
} else {
mean = sum;
}
double finalValue = mean;
print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}");
dataPoints.add(DataPoint(
value: smartScale(finalValue),
label: key,
actualValue: finalValue.toStringAsFixed(2),
displayTime: key,
unitOfMeasurement: values.first.unitOfMeasure,
time: DateTime.parse(values.first.timestamp),
));
});
dataPointMap[filterType] = dataPoints;
// }
return dataPointMap;
}
@ -109,10 +119,10 @@ class HealthDataTransformation {
final random = Random();
// Smooth compression scaling
double baseScaled = number <20 ? number:100 * (number / (number + 100));
double baseScaled = number < 20 ? number : 100 * (number / (number + 100));
// Random factor between 0.9 and 1.1 (±10%)
double randomFactor = number <20 ? random.nextDouble() * 1.5: 0.9 + random.nextDouble() * 0.2;
double randomFactor = number < 20 ? random.nextDouble() * 1.5 : 0.9 + random.nextDouble() * 0.2;
double result = baseScaled * randomFactor;
@ -122,13 +132,12 @@ class HealthDataTransformation {
DateTime normalizeToStartOfDay(DateTime date) {
return DateTime(date.year, date.month, date.day);
}
bool isBetweenInclusive(
DateTime target,
DateTime start,
DateTime end,
) {
DateTime target,
DateTime start,
DateTime end,
) {
return !normalizeToStartOfDay(target).isBefore(start) && !normalizeToStartOfDay(target).isAfter(end);
}
}
}

@ -172,12 +172,12 @@ class HealthService {
}
}
void addWatchHelper(SmartWatchTypes watchType){
watchHelper = CreateWatchHelper.getWatchName(watchType) ;
void addWatchHelper(SmartWatchTypes watchType) {
watchHelper = CreateWatchHelper.getWatchName(watchType);
}
Future<Result<bool>> initDevice() async {
if(watchHelper == null){
if (watchHelper == null) {
return Result.error('No watch helper found');
}
return await watchHelper!.initDevice();
@ -197,14 +197,14 @@ class HealthService {
await watchHelper!.getDistance();
await watchHelper!.getBloodOxygen();
Result<dynamic> data = await watchHelper!.retrieveData();
if(data.isError) {
if (data.isError) {
print('Unable to get the data');
}
var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "")??"");
VitalsWRTType vitals = VitalsWRTType.fromMap(response);
log("the data is ${vitals}");
var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "") ?? "");
VitalsWRTType vitals = VitalsWRTType.fromMap(response);
// log("the data is ${vitals}");
return vitals;
}catch(e){
} catch (e) {
print('Error getting heart rate: $e');
}
return null;

@ -9,10 +9,22 @@ class Vitals {
this.unitOfMeasure = "",
});
Map<String, dynamic> toJson() {
return {
'value': value,
'timestamp': timestamp,
'uom': unitOfMeasure,
};
}
factory Vitals.fromMap(Map<dynamic, dynamic> map) {
String timestamp = map['timestamp'] ?? map['timeStamp'] ?? "";
if (timestamp.isEmpty) {
timestamp = DateTime.now().toIso8601String();
}
return Vitals(
value: map['value'] ?? "",
timestamp: map['timeStamp'] ?? "",
timestamp: timestamp,
unitOfMeasure: map['uom'] ?? "",
);
}

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:async/src/result/result.dart';
import 'package:flutter/material.dart';
@ -80,7 +81,10 @@ class HealthConnectHelper extends WatchHelper {
@override
Future<dynamic> retrieveData() async {
// String retData;
// retData = Result.value(getMappedData()) as String;
return Result.value(getMappedData());
// return retData;
}
@override
@ -151,9 +155,22 @@ class HealthConnectHelper extends WatchHelper {
for (var point in data) {
if (point.value is NumericHealthValue) {
final numericValue = (point.value as NumericHealthValue).numericValue;
String timestamp = DateTime.now().toIso8601String();
try {
if (point.dateFrom != null) {
String tempTimestamp = point.dateFrom.toIso8601String();
if (tempTimestamp.isNotEmpty) {
timestamp = tempTimestamp;
}
}
} catch (e) {
debugPrint('Error converting dateFrom to ISO8601 for $s: $e');
}
Vitals vitals = Vitals(
value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2),
timestamp: point.dateFrom.toString()
value: numericValue.toStringAsFixed(2),
timestamp: timestamp
);
mappedData[s]?.add(vitals);
}
@ -165,17 +182,41 @@ class HealthConnectHelper extends WatchHelper {
for (var point in data) {
if (point.value is NumericHealthValue) {
final numericValue = (point.value as NumericHealthValue).numericValue;
String timestamp = DateTime.now().toIso8601String();
try {
if (point.dateFrom != null) {
String tempTimestamp = point.dateFrom.toIso8601String();
if (tempTimestamp.isNotEmpty) {
timestamp = tempTimestamp;
}
}
} catch (e) {
debugPrint('Error converting dateFrom to ISO8601 for $s: $e');
}
point.value = NumericHealthValue(
numericValue: numericValue * 100,
);
Vitals vitals = Vitals(value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), timestamp: point.dateFrom.toString());
Vitals vitals = Vitals(
value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2),
timestamp: timestamp
);
mappedData[s]?.add(vitals);
}
}
}
getMappedData() {
return " { \"heartRate\": ${mappedData["heartRate"] ?? []}, \"sleep\": ${mappedData["sleep"] ?? []}, \"steps\": ${mappedData["steps"] ?? []}, \"activity\": ${mappedData["activity"] ?? []}, \"bloodOxygen\": ${mappedData["bloodOxygen"] ?? []}, \"bodyTemperature\": ${mappedData["bodyTemperature"] ?? []}, \"distance\": ${mappedData["distance"] ?? []} }";
return jsonEncode({
"heartRate": (mappedData["heartRate"] ?? []).map((v) => v.toJson()).toList(),
"sleep": (mappedData["sleep"] ?? []).map((v) => v.toJson()).toList(),
"steps": (mappedData["steps"] ?? []).map((v) => v.toJson()).toList(),
"activity": (mappedData["activity"] ?? []).map((v) => v.toJson()).toList(),
"bloodOxygen": (mappedData["bloodOxygen"] ?? []).map((v) => v.toJson()).toList(),
"bodyTemperature": (mappedData["bodyTemperature"] ?? []).map((v) => v.toJson()).toList(),
"distance": (mappedData["distance"] ?? []).map((v) => v.toJson()).toList(),
});
}
getHeartData(DateTime startDate, DateTime endDate, HealthDataType types) async {

@ -469,6 +469,7 @@ abstract class LocaleKeys {
static const serviceInformation = 'serviceInformation';
static const homeHealthCare = 'homeHealthCare';
static const noAppointmentAvailable = 'noAppointmentAvailable';
static const noScheduleAvailable = 'noScheduleAvailable';
static const loginRegister = 'loginRegister';
static const orderLog = 'orderLog';
static const infoLab = 'infoLab';
@ -1940,6 +1941,13 @@ abstract class LocaleKeys {
static const refund = 'refund';
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent';
static const livecareUpdateInsurance = 'livecareUpdateInsurance';
static const schedule = 'schedule';
static const doctorSchedule = 'doctorSchedule';
static const weeklyShift = 'weeklyShift';
static const dayOff = 'dayOff';
static const hide = 'hide';
static const paymentSuccessful = 'paymentSuccessful';
static const typeAnyOffer = 'typeAnyOffer';
static const validTill = 'validTill';

@ -63,6 +63,7 @@ import 'core/utils/size_utils.dart';
import 'features/monthly_reports/terms_conditions_view_model.dart';
import 'firebase_options.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@ -109,11 +110,6 @@ Future<void> callInitializations() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
// Download the latest JSONs from your server
// await TranslationService.downloadTranslations();
// await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
try {
// Attempt to get the default app. If it exists, this avoids the error.
await Firebase.app();

@ -40,6 +40,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appoin
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/bell_switch.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart';
import 'package:hmg_patient_app_new/services/clarity_service.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';
@ -1215,6 +1216,16 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
isDismissible: false,
isFullScreen: false,
isSuccessDialog: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
// LoaderBottomSheet.hideLoader();
case 15:

@ -228,8 +228,15 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false",
widget.patientAppointmentHistoryResponseModel.appointmentDate!,
widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
widget.patientAppointmentHistoryResponseModel.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "Appointment Payment",
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
@ -444,9 +451,16 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false",
widget.patientAppointmentHistoryResponseModel.appointmentDate!,
widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
widget.patientAppointmentHistoryResponseModel.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));

@ -43,8 +43,10 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
@override
void initState() {
scheduleMicrotask(() {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
if (!myAppointmentsViewModel.isMyAppointmentsLoading) {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
}
});
super.initState();
}

@ -39,7 +39,7 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
@override
void initState() {
scheduleMicrotask(() {
myAppointmentsViewModel.getPatientMyDoctors();
myAppointmentsViewModel.getPatientMyDoctors(isTop8: false);
});
super.initState();
}

@ -277,7 +277,7 @@ class _AppointmentCardState extends State<AppointmentCard> {
(widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1),
Expanded(
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")),
isBold: true, maxlines: 1, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
),
SizedBox(width: 12.w),
(widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&

@ -101,7 +101,7 @@ class AppointmentDoctorCard extends StatelessWidget {
Row(
children: [
Expanded(
child: patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(
child: (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText16(
isBold: true,
isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""),
textOverflow: TextOverflow.ellipsis,

@ -57,9 +57,12 @@ class SelectGenderWidget extends StatelessWidget {
),
],
).paddingSymmetrical(16.h, 16.h).onPress(() {
final genderValue = isArabic
? GenderTypeEnum.values[index].typeAr
: GenderTypeEnum.values[index].type;
final genderValue =
// isArabic
// ? GenderTypeEnum.values[index].typeAr
// :
GenderTypeEnum.values[index].type;
bloodDonationViewModel.onGenderChange(genderValue);
Navigator.of(context).pop();
}));

@ -66,14 +66,14 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
// bookAppointmentsViewModel.getLocation();
immediateLiveCareViewModel.initImmediateLiveCare();
if (appState.isAuthenticated) {
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors();
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors(isTop8: true);
getIt.get<MyAppointmentsViewModel>().getPatientFavouriteDoctors();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (bookAppointmentsViewModel.selectedTabIndex == 1) {
if (appState.isAuthenticated) {
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors();
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors(isTop8: true);
getIt.get<MyAppointmentsViewModel>().getPatientFavouriteDoctors();
showUnKnownClinicBottomSheet();
}
@ -166,6 +166,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
],
).paddingSymmetrical(24.h, 0.h).onPress(() {
myAppointmentsVM.setIsMyDoctorsDataToBeLoaded(true);
Navigator.of(context).push(
CustomPageRoute(
page: MyDoctorsPage(),

@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_rating_details.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_schedule_page.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.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';
@ -192,6 +194,17 @@ class DoctorProfilePage extends StatelessWidget {
isCloseButtonVisible: true,
);
}),
SizedBox(width: 36.w),
Column(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_schedule_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false),
SizedBox(height: 16.h),
LocaleKeys.schedule.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor),
LocaleKeys.view.tr(context: context).toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"),
],
).onPress(() {
_openDoctorSchedule(context);
})
],
),
SizedBox(height: 16.h),
@ -294,4 +307,75 @@ class DoctorProfilePage extends StatelessWidget {
),
);
}
// Guard against double-tap opening two bottom sheets simultaneously
static bool _scheduleInFlight = false;
void _openDoctorSchedule(BuildContext context) async {
if (_scheduleInFlight) return;
_scheduleInFlight = true;
final bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingDoctorSchedulePleaseWait.tr(context: context));
// Yield to the frame scheduler so the loader is fully painted
// before the API calls start (prevents the "loader appears late" glitch).
await Future<void>.delayed(Duration.zero);
if (bookAppointmentsViewModel.freeSlotsResponse == null || bookAppointmentsViewModel.freeSlotsResponse.isEmpty) {
await bookAppointmentsViewModel.getDoctorFreeSlots(
isBookingForLiveCare: false,
onSuccess: (dynamic respData) async {
await _fetchDoctorSchedule(context, bookAppointmentsViewModel);
},
onError: (err) {
_scheduleInFlight = false;
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: true,
isCloseButtonVisible: true,
);
},
);
} else {
await _fetchDoctorSchedule(context, bookAppointmentsViewModel);
}
}
Future<void> _fetchDoctorSchedule(BuildContext context, BookAppointmentsViewModel bookAppointmentsViewModel) async {
await bookAppointmentsViewModel.getDoctorSchedule(
doctorID: bookAppointmentsViewModel.selectedDoctor.doctorID!,
projectID: bookAppointmentsViewModel.selectedDoctor.projectID!,
clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!,
doctorWorkingHoursDays: 90,
onSuccess: (dynamic respData) async {
_scheduleInFlight = false;
LoaderBottomSheet.hideLoader();
if (context.mounted) {
await Navigator.of(context).push(
CustomPageRoute(
page: const DoctorSchedulePage(),
fullScreenDialog: true,
direction: AxisDirection.down,
),
);
}
},
onError: (err) {
_scheduleInFlight = false;
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
}
}

@ -186,27 +186,64 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
CustomButton(
text: LocaleKeys.updateInsurance.tr(context: context),
onPressed: () {
Navigator.of(context)
.push(
CustomPageRoute(
page: InsuranceHomePage(),
),
)
.then((val) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingFeesPleaseWait.tr());
await immediateLiveCareVM.getLiveCareImmediateAppointmentFees(onSuccess: (val) {
LoaderBottomSheet.hideLoader();
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
});
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.livecareUpdateInsurance.tr(),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr());
await immediateLiveCareVM.addInsuranceUpdateRequestForLiveCare(onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.livecareUpdateInsuranceSent.tr()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
// Navigator.of(context)
// .push(
// CustomPageRoute(
// page: InsuranceHomePage(),
// ),
// )
// .then((val) async {
// LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingFeesPleaseWait.tr());
// await immediateLiveCareVM.getLiveCareImmediateAppointmentFees(onSuccess: (val) {
// LoaderBottomSheet.hideLoader();
// }, onError: (err) {
// LoaderBottomSheet.hideLoader();
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getErrorWidget(loadingText: err),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// );
// });
// });
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.secondaryLightRedBorderColor,

@ -184,9 +184,16 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
orderDescription: "LiveCare Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@ -328,9 +335,16 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"visa",
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
orderDescription: "LiveCare Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@ -530,12 +544,12 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
if (selectedPaymentMethod == "TAMARA") {
if (Platform.isAndroid) {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['status']!;
tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!;
tamaraPaymentStatus = uri.queryParameters['status'] ?? "";
tamaraOrderID = uri.queryParameters['AuthorizePaymentId'] ?? "";
} else {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['paymentStatus']!;
tamaraOrderID = uri.queryParameters['orderId']!;
tamaraPaymentStatus = uri.queryParameters['paymentStatus'] ?? "";
tamaraOrderID = uri.queryParameters['orderId'] ?? "";
}
}

@ -212,6 +212,7 @@ class _SearchDoctorByNameState extends State<SearchDoctorByName> {
doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index],
isLoading: false,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isDoctorNameSearch: true,
).paddingSymmetrical(16.h, 0.h).onPress(() async {
bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]);
LoaderBottomSheet.showLoader();

@ -192,9 +192,16 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
DateTime.now().millisecondsSinceEpoch.toString(),
bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader();
@ -336,9 +343,16 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
DateTime.now().millisecondsSinceEpoch.toString(),
bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader();

@ -10,13 +10,17 @@ 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/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/laser/laser_appointment.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.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:lottie/lottie.dart';
import 'package:url_launcher/url_launcher.dart';
@ -25,11 +29,13 @@ class DoctorCard extends StatelessWidget {
super.key,
required this.doctorsListResponseModel,
required this.isLoading,
this.isDoctorNameSearch = false,
required this.bookAppointmentsViewModel,
});
final DoctorsListResponseModel doctorsListResponseModel;
final bool isLoading;
final bool isDoctorNameSearch;
final BookAppointmentsViewModel bookAppointmentsViewModel;
@ -173,78 +179,47 @@ class DoctorCard extends StatelessWidget {
CustomButton(
text: LocaleKeys.bookAppo.tr(context: context),
onPressed: () async {
bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel);
LoaderBottomSheet.showLoader();
bookAppointmentsViewModel.isLiveCareSchedule
? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots(
isBookingForLiveCare: true,
onSuccess: (dynamic respData) async {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.pickADate.tr(context: context),
context,
child: AppointmentCalendar(),
isFullScreen: false,
isCloseButtonVisible: true,
callBackFunc: () {},
);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
})
: await bookAppointmentsViewModel.getDoctorFreeSlots(
isBookingForLiveCare: false,
onSuccess: (dynamic respData) async {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.pickADate.tr(context: context),
context,
child: AppointmentCalendar(),
isFullScreen: false,
isCloseButtonVisible: true,
callBackFunc: () {},
);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
if (bookAppointmentsViewModel.selectedClinic.clinicID == 23) {
if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 17) {
GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel(
clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0);
bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString());
bookAppointmentsViewModel.setSelectedClinic(selectedClinic);
bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true);
Navigator.of(context).push(
CustomPageRoute(
page: DentalChiefComplaintsPage(),
),
);
} else if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 253) {
GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel(
clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0);
bookAppointmentsViewModel.setSelectedClinic(selectedClinic);
bookAppointmentsViewModel.getLaserClinic();
bookAppointmentsViewModel.setIsDoctorsListLoading(true);
Navigator.of(context).push(
CustomPageRoute(
page: LaserAppointment(),
),
);
} else {
bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel);
LoaderBottomSheet.showLoader();
bookAppointmentsViewModel.isLiveCareSchedule
? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots(
isBookingForLiveCare: true,
onSuccess: (dynamic respData) async {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.pickADate.tr(context: context),
context,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
("For booking appointments in the Physiotherapy clinic, please contact: 920066666").toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),
CustomButton(
height: 40.h,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
text: LocaleKeys.callNow.tr(),
textColor: AppColors.whiteColor,
iconColor: AppColors.whiteColor,
onPressed: () {
launchUrl(
Uri.parse("tel://920066666"),
);
},
icon: AppAssets.call),
],
).center,
callBackFunc: () {},
child: AppointmentCalendar(),
isFullScreen: false,
isCloseButtonVisible: true,
callBackFunc: () {},
);
} else {
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
@ -252,15 +227,70 @@ class DoctorCard extends StatelessWidget {
isFullScreen: false,
isCloseButtonVisible: true,
);
}
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getErrorWidget(loadingText: err),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// );
});
})
: await bookAppointmentsViewModel.getDoctorFreeSlots(
isBookingForLiveCare: false,
onSuccess: (dynamic respData) async {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.pickADate.tr(context: context),
context,
child: AppointmentCalendar(),
isFullScreen: false,
isCloseButtonVisible: true,
callBackFunc: () {},
);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
if (bookAppointmentsViewModel.selectedClinic.clinicID == 23) {
showCommonBottomSheetWithoutHeight(
context,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
("For booking appointments in the Physiotherapy clinic, please contact: 920066666").toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),
CustomButton(
height: 40.h,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
text: LocaleKeys.callNow.tr(),
textColor: AppColors.whiteColor,
iconColor: AppColors.whiteColor,
onPressed: () {
launchUrl(
Uri.parse("tel://920066666"),
);
},
icon: AppAssets.call),
],
).center,
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
// showCommonBottomSheetWithoutHeight(
// context,
// child: Utils.getErrorWidget(loadingText: err),
// callBackFunc: () {},
// isFullScreen: false,
// isCloseButtonVisible: true,
// );
});
}
},
backgroundColor: Color(0xffFEE9EA),
borderColor: Color(0xffFEE9EA),

@ -0,0 +1,400 @@
import 'dart:ui' as ui;
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/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_schedule_response_model.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.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:provider/provider.dart';
class DoctorScheduleBottomSheet extends StatefulWidget {
const DoctorScheduleBottomSheet({super.key});
@override
State<DoctorScheduleBottomSheet> createState() => _DoctorScheduleBottomSheetState();
}
class _DoctorScheduleBottomSheetState extends State<DoctorScheduleBottomSheet>
with SingleTickerProviderStateMixin {
late PageController _pageController;
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
double currentPage = 0;
List<List<Map<String, dynamic>>> weeks = [];
List<DoctorScheduleResponseModel> doctorScheduleResponse = [];
List<String> freeSlots = [];
/// True while weeks are being computed off the build thread
bool _isInitializing = false;
late BookAppointmentsViewModel bookAppointmentsViewModel;
late AppState appState;
@override
void initState() {
super.initState();
_pageController = PageController();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_fadeAnimation = CurvedAnimation(parent: _fadeController, curve: Curves.easeIn);
}
@override
void dispose() {
_pageController.dispose();
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: true);
appState = getIt.get<AppState>();
final isArabic = appState.isArabic();
// Trigger initialization after the first frame so the UI never freezes
final responseReady = bookAppointmentsViewModel.doctorScheduleResponse != null;
if (responseReady && weeks.isEmpty && !_isInitializing) {
WidgetsBinding.instance.addPostFrameCallback((_) => _triggerInit());
}
// API loading
if (bookAppointmentsViewModel.isDoctorScheduleLoading || _isInitializing) {
return Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
strokeWidth: 2.5,
),
);
}
// No data
if (weeks.isEmpty) {
return Center(
child: Utils.getNoDataWidget(
context,
noDataText: LocaleKeys.noScheduleAvailable.tr(context: context),
),
);
}
// Schedule ready fade in
return FadeTransition(
opacity: _fadeAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context, isArabic),
SizedBox(height: 20.h),
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: weeks.length,
onPageChanged: (index) => setState(() => currentPage = index.toDouble()),
itemBuilder: (context, weekIndex) => _buildWeekView(weekIndex, isArabic),
),
),
],
),
);
}
// Initialization
void _triggerInit() {
if (!mounted || _isInitializing || weeks.isNotEmpty) return;
setState(() => _isInitializing = true);
// Defer heavy work to the next microtask so the spinner is painted first
Future.microtask(() {
_buildWeeksFromResponse();
if (mounted) {
setState(() => _isInitializing = false);
_fadeController.forward();
}
});
}
void _buildWeeksFromResponse() {
final response = bookAppointmentsViewModel.doctorScheduleResponse;
if (response == null) return;
final scheduleList = response['List_DoctorWorkingHoursTable'] as List?;
if (scheduleList == null || scheduleList.isEmpty) return;
doctorScheduleResponse =
scheduleList.map((item) => DoctorScheduleResponseModel.fromJson(item)).toList();
// Resolve free-slot dates
if (bookAppointmentsViewModel.doctorFreeSlotDates.isNotEmpty) {
freeSlots = bookAppointmentsViewModel.doctorFreeSlotDates;
} else if (bookAppointmentsViewModel.freeSlotsResponse != null &&
bookAppointmentsViewModel.freeSlotsResponse.isNotEmpty) {
freeSlots = List<String>.from(bookAppointmentsViewModel.freeSlotsResponse);
} else if (response['FreeTimeSlots'] != null) {
freeSlots = List<String>.from(response['FreeTimeSlots']);
}
_generateWeeksSchedules();
}
/// Splits ALL API days into 7-day chunks one chunk per page.
/// Supports full 3-month schedules (~13 weeks) returned by the API.
void _generateWeeksSchedules() {
weeks.clear();
// Convert every API day into a flat map and mark free slots
final allDays = doctorScheduleResponse.map((workDay) {
return <String, dynamic>{
'Date': workDay.date ?? '',
'DayName': workDay.dayName ?? '',
'WorkingHours': workDay.workingHours ?? '',
'fullDay': !_hasFreeSlot(workDay.date ?? ''),
};
}).toList();
// Slice into pages of 7 days each
const daysPerPage = 7;
for (var i = 0; i < allDays.length; i += daysPerPage) {
final end = (i + daysPerPage).clamp(0, allDays.length);
weeks.add(allDays.sublist(i, end));
}
}
// Header
Widget _buildHeader(BuildContext context, bool isArabic) {
final currentWeek = weeks[currentPage.round()];
final firstDate = DateUtil.convertStringToDate(currentWeek.first['Date'] as String);
final lastDate = DateUtil.convertStringToDate(currentWeek.last['Date'] as String);
final dateRangeLabel = isArabic
? '${firstDate.day} ${DateUtil.getMonthArabic(firstDate.month).trim()} '
'${lastDate.day} ${DateUtil.getMonthArabic(lastDate.month).trim()}, ${lastDate.year}'
: '${DateFormat('MMM dd', 'en_US').format(firstDate)} '
'${DateFormat('MMM dd', 'en_US').format(lastDate)}, ${lastDate.year}';
return Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// "Doctor Schedule",
// style: TextStyle(
// fontSize: 24.h,
// fontWeight: FontWeight.bold,
// color: const Color(0xFF1A1A1A),
// ),
// ),
SizedBox(height: 16.h),
// Navigation: < date-pill >
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavArrow(
icon: Icons.chevron_left,
enabled: currentPage.round() > 0,
onTap: () => _animateToPage(currentPage.round() - 1),
),
SizedBox(width: 12.w),
Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
decoration: BoxDecoration(
color: AppColors.secondaryLightRedColor,
borderRadius: BorderRadius.circular(12.r),
),
child: dateRangeLabel.toText14(
weight: FontWeight.w600,
color: AppColors.primaryRedColor,
),
),
SizedBox(width: 12.w),
_buildNavArrow(
icon: Icons.chevron_right,
enabled: currentPage.round() < weeks.length - 1,
onTap: () => _animateToPage(currentPage.round() + 1),
),
],
),
SizedBox(height: 20.h),
LocaleKeys.weeklyShift.tr(context: context).toText18(
weight: FontWeight.w600,
color: AppColors.textColor,
),
],
),
);
}
void _animateToPage(int index) {
_pageController.animateToPage(
index.clamp(0, weeks.length - 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
Widget _buildNavArrow({
required IconData icon,
required bool enabled,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: enabled ? onTap : null,
child: Container(
width: 34.w,
height: 34.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.h),
color: enabled ? AppColors.secondaryLightRedColor : AppColors.greyF7Color,
),
child: Icon(
icon,
size: 28.h,
color: enabled ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor,
),
),
);
}
// Week / day views
Widget _buildWeekView(int weekIndex, bool isArabic) {
final weekSchedule = weeks[weekIndex];
return Directionality(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
child: ListView.builder(
// primary: false detaches this ListView from the PrimaryScrollController
// so it doesn't fight with CollapsingListView's outer CustomScrollView.
primary: false,
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.only(top: 8.h, bottom: 24.h),
itemCount: weekSchedule.length,
itemBuilder: (context, dayIndex) => _buildDayCard(context, weekSchedule[dayIndex]),
),
);
}
Widget _buildDayCard(BuildContext context, Map<String, dynamic> daySchedule) {
final date = DateUtil.convertStringToDate(daySchedule['Date'] as String);
final hasFreeSlots = !(daySchedule['fullDay'] as bool? ?? true);
final workingHours = (daySchedule['WorkingHours'] as String?) ?? '';
final isOff = workingHours.isEmpty ||
workingHours.toLowerCase().contains('off') ||
workingHours.toLowerCase().contains('not available');
final shifts = workingHours.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
return Material(
color: Colors.transparent,
child: InkWell(
onTap: hasFreeSlots ? () => getIt.get<NavigationService>().pop() : null,
borderRadius: BorderRadius.circular(20.r),
child: Container(
margin: EdgeInsets.only(bottom: 12.h, left: 5.w, right: 5.w),
padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
blurRadius: 16,
spreadRadius: 0,
color: Colors.black.withValues(alpha: 0.02),
offset: const Offset(0, 6),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Left day name + short date
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
daySchedule['DayName'].toString().toText16(
weight: FontWeight.w700,
color: AppColors.textColor,
),
SizedBox(height: 4.h),
_getDayMonth(date).toText13(
color: AppColors.greyTextColorLight,
),
],
),
),
// Right time slots or "Off"
if (isOff)
LocaleKeys.dayOff.tr(context: context).toText14(
weight: FontWeight.w500,
color: AppColors.greyTextColorLight,
)
else
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < shifts.length; i++) ...[
shifts[i].toText14(
weight: FontWeight.w500,
color: AppColors.textColor,
),
if (i < shifts.length - 1) SizedBox(height: 6.h),
],
],
),
),
],
),
),
),
);
}
// Helpers
bool _hasFreeSlot(String jsonDate) {
if (jsonDate.isEmpty) return false;
final date = DateUtil.convertStringToDate(jsonDate);
final scheduleDay = DateTime(date.year, date.month, date.day);
for (final event in freeSlots) {
try {
final eventDate = DateUtil.convertStringToDate(event);
final nDate = DateTime(eventDate.year, eventDate.month, eventDate.day);
if (nDate.millisecondsSinceEpoch == scheduleDay.millisecondsSinceEpoch) return true;
} catch (_) {
continue;
}
}
return false;
}
String _getDayMonth(DateTime dateTime) {
final isArabic = appState.isArabic();
if (isArabic) {
return '${dateTime.day} ${DateUtil.getMonthArabic(dateTime.month)}';
}
return '${dateTime.day} ${DateUtil.getMonth(dateTime.month).substring(0, 3)}';
}
}

@ -0,0 +1,39 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_schedule_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
class DoctorSchedulePage extends StatelessWidget {
const DoctorSchedulePage({super.key});
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.doctorSchedule.tr(),
isClose: true,
child: Builder(
builder: (ctx) {
final mq = MediaQuery.of(ctx);
// SliverToBoxAdapter is unbounded give explicit height so
// the PageView (Expanded) inside DoctorScheduleBottomSheet works.
final height = mq.size.height - mq.padding.top - 56.h;
return SizedBox(
height: height,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
// Stop scroll notifications from bubbling up to CollapsingListView's
// outer CustomScrollView, which would steal vertical scroll gestures.
child: NotificationListener<ScrollNotification>(
onNotification: (_) => true,
child: const DoctorScheduleBottomSheet(),
),
),
);
},
),
);
}
}

@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode
import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/wallet_balance_list.dart';
import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart';
import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.dart';
@ -97,15 +98,39 @@ class _HabibWalletState extends State<HabibWalletPage> {
Spacer(),
LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white),
SizedBox(height: 4.h),
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount,
textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1)
.toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h);
}),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount,
textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1)
.toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h);
}),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
height: 24.h,
width: 24.h,
iconColor: AppColors.whiteColor,
),
),
],
),
],
),
),
),
).onPress(() {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.habibWallet.tr(context: context),
context,
// child: Utils.getSuccessWidget(loadingText: LocaleKeys.erAppointmentBookedSuccess.tr(context: context)),
child: HabibWalletBalanceList(),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,

@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.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';
@ -178,9 +179,16 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(),
orderDescription: "Advance Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader();
@ -204,7 +212,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
// Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
@ -353,9 +361,16 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(),
orderDescription: "Advance Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader();
@ -380,7 +395,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
// Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
onError: (err) {

@ -0,0 +1,54 @@
import 'package:flutter/material.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/theme/colors.dart';
import 'package:provider/provider.dart';
class HabibWalletBalanceList extends StatelessWidget {
const HabibWalletBalanceList({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return ListView.separated(
itemCount: habibWalletVM.habibWalletBalanceList.length,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(top: 8, bottom: 8),
shrinkWrap: true,
itemBuilder: (context, index) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: habibWalletVM.habibWalletBalanceList[index].projectDescription!.toText16(isBold: true, color: AppColors.textColor)),
Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value
textColor: AppColors.textColor,
iconColor: AppColors.textColor,
iconSize: 18.h,
isExpanded: false,
fontSize: 28.f,
fontWeight: FontWeight.w600,
letterSpacing: -1),
],
).paddingSymmetrical(0, 12.h);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(height: 1, color: AppColors.textColor.withAlpha(20));
},
).paddingSymmetrical(16.h, 24.h).toShimmer2(isShow: habibWalletVM.isWalletAmountLoading);
}),
),
],
);
}
}

@ -543,8 +543,6 @@ class _LandingPageState extends State<LandingPage> {
).paddingSymmetrical(24.w, 0.h)
: SizedBox.shrink(),
// Offers And Discounts Carousel - Auto-scrolling from right to left
//appState.isAuthenticated &&
appState.isAuthenticated
? Column(
@ -587,7 +585,7 @@ class _LandingPageState extends State<LandingPage> {
),
).paddingSymmetrical(24.h, 16.h)
: myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty
? myAppointmentsVM.patientAppointmentsHistoryList.length == 1
? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 && !immediateLiveCareVM.patientHasPendingLiveCareRequest
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
@ -607,7 +605,10 @@ class _LandingPageState extends State<LandingPage> {
height: isTablet ? 290.h : 255.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 ? myAppointmentsVM.patientAppointmentsHistoryList.length : 3,
itemCount: () {
final int baseCount = myAppointmentsVM.patientAppointmentsHistoryList.length + (immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0);
return baseCount < 3 ? baseCount : 3;
}(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {
@ -640,10 +641,14 @@ class _LandingPageState extends State<LandingPage> {
height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space
child: Builder(
builder: (context) {
final int baseAppointmentCount = myAppointmentsVM.patientAppointmentsHistoryList.length;
final int liveCareCount = immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0;
final int totalCount = baseAppointmentCount + liveCareCount;
final int swiperItemCount = myAppointmentsVM.isMyAppointmentsLoading
? 3
: myAppointmentsVM.patientAppointmentsHistoryList.length < 3
? myAppointmentsVM.patientAppointmentsHistoryList.length
: totalCount < 3
? totalCount
: 3;
final bool isArabic = appState.isArabic();
return Swiper(

File diff suppressed because one or more lines are too long

@ -1,45 +1,275 @@
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/core/app_state.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/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart';
import 'package:provider/provider.dart' show Selector, Provider, ReadContext;
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:provider/provider.dart';
import '../../../core/dependencies.dart' show getIt;
import '../../../core/utils/size_utils.dart' show ResponsiveExtension;
import '../../../generated/locale_keys.g.dart' show LocaleKeys;
import '../../../widgets/date_range_selector/viewmodel/date_range_view_model.dart';
class LabResultList extends StatefulWidget {
@override
State<LabResultList> createState() => _LabResultListState();
}
class LabResultList extends StatelessWidget {
class _LabResultListState extends State<LabResultList> {
late LabViewModel model;
int expandedIndex = -1;
Map<int, bool> descriptionExpandedMap = {};
@override
Widget build(BuildContext context) {
model = Provider.of<LabViewModel>(context);
return Selector<LabViewModel, List<LabResult>>(
selector: (_, model) => model.mainLabResultsByHospitals,
builder: (__, list, ___) {
if (list.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) {
return Selector<LabViewModel, Map<String, List<LabResult>>>(
selector: (_, model) => model.mainLabResultsByPackageName,
builder: (__, groupedResults, ___) {
if (groupedResults.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) {
// return Utils.getNoDataWidget(context,
// noDataText: LocaleKeys.noLabResults.tr(context: context));
return Container();
} else {
// Convert map entries to a list for indexed access
final packageEntries = groupedResults.entries.toList();
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) {
var labItem = list[index];
return LabOrderResultItem(onTap: () {
context.read<DateRangeSelectorRangeViewModel>().flush();
model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription ?? "", labItem.uOM ?? "");
},
tests: labItem,
index: index,
iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"),
severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N"));
});
shrinkWrap: true,
itemCount: packageEntries.length,
itemBuilder: (context, packageIndex) {
final packageName = packageEntries[packageIndex].key;
final labResults = packageEntries[packageIndex].value;
final isExpanded = expandedIndex == packageIndex;
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Package name as title/header
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.0.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.h,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: packageName.toText18(
isBold: true
),
),
AnimatedRotation(
turns: isExpanded ? 0.5 : 0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Icon(Icons.expand_more),
),
],
),
if(labResults.first.packageShortDescription != null)
_ExpandableDescriptionText(
description: labResults.first.packageShortDescription!,
isExpanded: descriptionExpandedMap[packageIndex] ?? false,
onToggle: () {
setState(() {
descriptionExpandedMap[packageIndex] = !(descriptionExpandedMap[packageIndex] ?? false);
});
},
)
else
SizedBox.shrink()
],
),),
// Lab results under this package with animation
AnimatedSize(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: isExpanded
? Column(
children: labResults.asMap().entries.map((entry) {
final index = entry.key;
final labItem = entry.value;
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300 + (index * 50)),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: Column(
children: [
LabOrderResultItem(
onTap: () {
context.read<DateRangeSelectorRangeViewModel>().flush();
model.getPatientLabResult(
model.currentlySelectedPatientOrder!,
labItem.description ?? "",
labItem.testShortDescription ?? "",
labItem.uOM ?? ""
);
},
tests: labItem,
index: index,
iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"),
severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N")
),
if(labResults.length>1)
...[SizedBox(
height: 8.h,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.h),
child: Divider(
color: AppColors.dividerColor,
thickness: 1.h,
),
)]
],
),
);
}).toList(),
)
: SizedBox.shrink(),
),
],
),
).onPress((){
setState(() {
expandedIndex = isExpanded ? -1 : packageIndex;
});
});
}
);
}
},
);
}
}
class _ExpandableDescriptionText extends StatelessWidget {
final String description;
final bool isExpanded;
final VoidCallback onToggle;
const _ExpandableDescriptionText({
required this.description,
required this.isExpanded,
required this.onToggle,
});
static TextStyle get textStyle => TextStyle(
fontSize: 14.f,
color: AppColors.textColorLight,
// or Colors.black87
// fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal,
height: 1.4,
letterSpacing: 0.0,
decoration: TextDecoration.none,
);
@override
Widget build(BuildContext context) {
final readMoreText = isExpanded
? ' ${LocaleKeys.hide.tr()}'
: ' ${LocaleKeys.readMore.tr(context: context)}';
final readMoreStyle = TextStyle(
fontSize: 12.f,
color: AppColors.primaryRedColor,
// fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
);
return LayoutBuilder(
builder: (context, constraints) {
// Check if description alone overflows 2 lines
final descriptionPainter = TextPainter(
text: TextSpan(text: description, style: textStyle),
maxLines: 2,
textDirection: Directionality.of(context),
)..layout(maxWidth: constraints.maxWidth);
final isOverflowing = descriptionPainter.didExceedMaxLines;
// No overflow, or already expanded -> show full text
if (!isOverflowing || isExpanded) {
return GestureDetector(
onTap: isOverflowing ? onToggle : null,
child: Text.rich(
TextSpan(
style: textStyle,
children: [
TextSpan(text: description, style: textStyle),
if (isOverflowing)
TextSpan(text: readMoreText, style: readMoreStyle),
],
),
),
);
}
// Binary search for max chars that fit in 2 lines with "Read more" appended
int low = 0, high = description.length;
while (low < high) {
final mid = (low + high + 1) ~/ 2;
final testPainter = TextPainter(
text: TextSpan(
style: textStyle,
children: [
TextSpan(text: '${description.substring(0, mid)}...'),
TextSpan(text: readMoreText, style: readMoreStyle),
],
),
maxLines: 2,
textDirection: Directionality.of(context),
)..layout(maxWidth: constraints.maxWidth);
if (testPainter.didExceedMaxLines) {
high = mid - 1;
} else {
low = mid;
}
}
final truncated = description.substring(0, low).trimRight();
return GestureDetector(
onTap: onToggle,
child: Text.rich(
TextSpan(
style: textStyle,
children: [
TextSpan(text: '$truncated...'),
TextSpan(text: readMoreText, style: readMoreStyle),
],
),
),
);
},
);
}
}
}

@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.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/enums.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';
@ -12,6 +13,7 @@ import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.d
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/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:provider/provider.dart';
import 'dart:ui' as ui;
@ -24,7 +26,16 @@ class LabOrderResultItem extends StatelessWidget {
final bool isExpanded;
final Color iconColor;
const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false, required this.iconColor, required this.severityText});
const LabOrderResultItem({
super.key,
required this.onTap,
this.tests,
required this.index,
this.isLoading = false,
this.isExpanded = false,
required this.iconColor,
required this.severityText
});
@override
build(BuildContext context) {
@ -32,10 +43,10 @@ class LabOrderResultItem extends StatelessWidget {
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
child: Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 4.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -84,7 +95,20 @@ class LabOrderResultItem extends StatelessWidget {
),
softWrap: true,
),
)
),
Visibility(
visible: tests?.notes != null && tests?.notes?.isNotEmpty == true,
child: Text(
tests?.notes??"",
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
),
),
],
),
),
@ -104,7 +128,9 @@ class LabOrderResultItem extends StatelessWidget {
severityText.tr().toText10(isBold: true, color: AppColors.greyTextColor),
Utils.buildSvgWithAssets(icon: AppAssets.lab_result_indicator, width: 21, height: 23, iconColor: iconColor),
],
),
).onPress((){
_buildSeverityDetailsBottomsheet(severity: tests?.calculatedResultFlag ?? "N", context: context);
}),
),
),
Flexible(
@ -136,4 +162,77 @@ class LabOrderResultItem extends StatelessWidget {
),
));
}
_buildSeverityDetailsBottomsheet({required String severity,required BuildContext context}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.colorScience.tr(context: context),
context,
child: Container(
padding: EdgeInsets.all(16.w),
width: double.infinity,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Column(
children: [
_buildInfoTile(severityValue: severity, context: context),
],
),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
Widget _buildInfoTile({required String severityValue, required BuildContext context}) {
String title = LocaleKeys.emergency.tr(context: context);
String description = LocaleKeys.emergencyDescription.tr(context: context);
Color dotColor = AppColors.chipColorEmergency;
switch (severityValue) {
case 'CL':
case 'LCL':
title = LocaleKeys.emergency.tr(context: context);
description = LocaleKeys.emergencyDescription.tr(context: context);
dotColor = AppColors.chipColorEmergency;
case 'L':
title = LocaleKeys.seekMedicalAdvice.tr(context: context);
description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context);
dotColor = AppColors.chipColorSeekMedicalAdvice;
case 'N':
title = LocaleKeys.monitor.tr(context: context);
description = LocaleKeys.monitorDescription.tr(context: context);
dotColor = AppColors.chipColorMonitor;
case 'H':
title = LocaleKeys.seekMedicalAdvice.tr(context: context);
description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context);
dotColor = AppColors.chipColorSeekMedicalAdvice;
case 'CH':
case 'HCH':
title = LocaleKeys.emergency.tr(context: context);
description = LocaleKeys.emergencyDescription.tr(context: context);
dotColor = AppColors.chipColorEmergency;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 14.h,
width: 14.w,
decoration: BoxDecoration(shape: BoxShape.circle, color: dotColor),
).paddingOnly(top: 3.h),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title.toText14(isBold: true, color: AppColors.textColor),
SizedBox(height: 4.h),
description.toText12(isBold: true, color: AppColors.greyTextColor),
],
),
),
],
);
}
}

@ -110,7 +110,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
appState = getIt.get<AppState>();
scheduleMicrotask(() {
if (appState.isAuthenticated) {
myAppointmentsViewModel.getPatientMyDoctors();
myAppointmentsViewModel.getPatientMyDoctors(isTop8: true);
prescriptionsViewModel.initPrescriptionsViewModel();
insuranceViewModel.initInsuranceProvider();
medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
@ -1046,6 +1046,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
],
).onPress(() {
// myAppointmentsViewModel.getPatientMyDoctors();
myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true);
Navigator.of(context).push(
CustomPageRoute(
page: MyDoctorsPage(),
@ -1099,7 +1100,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
duration: const Duration(milliseconds: 250),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(

@ -92,7 +92,7 @@ class MedicalReportCard extends StatelessWidget {
// fontWeight: FontWeight.w600,
isBold: true,
borderRadius: 12.r,
height: 48.h,
height: 40.h,
).toShimmer2(isShow: isLoading),
],
),

@ -242,7 +242,7 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildImgWithNetwork(
url: report.doctorImageURL!,
url: report.doctorImageURL ?? "",
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenshot_blocker/flutter_screenshot_blocker.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/extensions/string_extensions.dart';
@ -20,28 +21,38 @@ class NotificationDetailsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.notificationDetails.tr(),
// trailing: IconButton(
// icon: Icon(
// Icons.share_outlined,
// size: 24.h,
// color: AppColors.textColor,
// ),
// onPressed: () {
// _shareNotification();
// },
// ),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Notification content card
_buildNotificationCard(context),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0.h),
return ScreenshotBlockerWidget(
blockScreenshots: true, // makes this widget appear black in screenshots
detectScreenshots: true, // listen for screenshot attempts
onScreenshotDetected: (event) {
// event.type 'screenshot_taken' or 'screenshot_blocked'
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('Screenshots are not allowed!')),
// );
},
child: CollapsingListView(
title: LocaleKeys.notificationDetails.tr(),
// trailing: IconButton(
// icon: Icon(
// Icons.share_outlined,
// size: 24.h,
// color: AppColors.textColor,
// ),
// onPressed: () {
// _shareNotification();
// },
// ),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Notification content card
_buildNotificationCard(context),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0.h),
),
),
);
}
@ -109,9 +120,9 @@ class NotificationDetailsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.messageNotification.tr().toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
notification.message!.toText16(
weight: FontWeight.w400,
@ -123,8 +134,7 @@ class NotificationDetailsPage extends StatelessWidget {
),
// Notification image (if MessageType is "image")
if (notification.messageType != null &&
notification.messageType!.toLowerCase() == "image")
if (notification.messageType != null && notification.messageType!.toLowerCase() == "image")
Builder(
builder: (context) {
// Try to get image URL from videoURL or messageTypeData
@ -147,9 +157,9 @@ class NotificationDetailsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.attachedImage.tr().toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
ClipRRect(
borderRadius: BorderRadius.circular(12.h),
@ -177,8 +187,8 @@ class NotificationDetailsPage extends StatelessWidget {
),
SizedBox(height: 8.h),
LocaleKeys.failedToLoadImage.tr().toText12(
color: AppColors.greyTextColor,
),
color: AppColors.greyTextColor,
),
SizedBox(height: 4.h),
Text(
imageUrl!,
@ -205,10 +215,7 @@ class NotificationDetailsPage extends StatelessWidget {
),
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
),
),
);
@ -249,7 +256,8 @@ ${notification.message ?? 'Notification'}
Time: ${_formatTime(notification.isSentOn)}
Date: ${_formatDate(notification.isSentOn)}
${notification.notificationType != null ? '\nType: ${notification.notificationType}' : ''}
'''.trim();
'''
.trim();
await Share.share(shareText);
}
@ -274,4 +282,3 @@ ${notification.notificationType != null ? '\nType: ${notification.notificationTy
}
}
}

@ -61,7 +61,8 @@ class _NotificationsListPageState extends State<NotificationsListPage> {
child: ListView.builder(
itemCount: notificationsVM.isNotificationsLoading
? 4
: notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0),
// : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0),
: notificationsVM.notificationsList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,

@ -15,6 +15,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart' hide Share;
@ -288,37 +289,47 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
getIt<AuthenticationViewModel>().onLoginPressed();
});
} else {
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await vm.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
if (!isAuthenticated) {
return;
}
// Set the selected offer and navigate to payment page
if (vm.offerDetails != null) {
vm.setSelectedOffer(vm.offerDetails!);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
// Show loader during authentication
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await vm.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
// Hide loader on error
LoaderBottomSheet.hideLoader();
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
if (!isAuthenticated) {
// Hide loader if authentication failed
LoaderBottomSheet.hideLoader();
return;
}
// Hide loader on success
LoaderBottomSheet.hideLoader();
// Set the selected offer and navigate to payment page
if (vm.offerDetails != null) {
vm.setSelectedOffer(vm.offerDetails!);
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountPaymentPage(),
),
);
}
}
}
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,

@ -94,13 +94,16 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (selectedOffer == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64.h, color: AppColors.errorColor),
SizedBox(height: 16.h),
LocaleKeys.noDataAvailable.tr(context: context).toText16(),
],
child: Container(
color: AppColors.bgScaffoldColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64.h, color: AppColors.errorColor),
SizedBox(height: 16.h),
LocaleKeys.noDataAvailable.tr(context: context).toText16(),
],
),
),
);
}
@ -111,225 +114,240 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
child: CollapsingListView(
title: "${LocaleKeys.offerAndPackages.tr(context: context)} ${LocaleKeys.payment.tr(context: context)}",
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Offer Summary Card
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(appState.getLanguageID() == 1 ? selectedOffer.nameAr : selectedOffer.name).toText18(isBold: true),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.price.tr(context: context).toText14(color: AppColors.greyTextColor),
"${selectedOffer.vatExclusivePrice} ${selectedOffer.currency}".toText16(isBold: true, color: AppColors.primaryRedColor),
],
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
LocaleKeys.selectPaymentMethod.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h, fit: BoxFit.contain),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
),
SizedBox(width: 8.h),
const Spacer(),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 40.h,
height: 40.h,
fit: BoxFit.contain,
child: Container(
color: AppColors.bgScaffoldColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Offer Summary Card
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(appState.getLanguageID() == 1 ? selectedOffer.nameAr : selectedOffer.name).toText18(isBold: true),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.price.tr(context: context).toText14(color: AppColors.greyTextColor),
"${selectedOffer.vatExclusivePrice} ${selectedOffer.currency}".toText16(isBold: true, color: AppColors.primaryRedColor),
],
),
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
LocaleKeys.offerPayment.tr(context: context),
getPaymentAmount(),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
if (appState.isPayfortHostedPageEnabled) {
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: getPaymentAmount().toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
await completePaymentResult(paymentStatus: "success");
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
LocaleKeys.selectPaymentMethod.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h, fit: BoxFit.contain),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
),
SizedBox(width: 8.h),
const Spacer(),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 40.h,
height: 40.h,
fit: BoxFit.contain,
),
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
LocaleKeys.offerPayment.tr(context: context),
getPaymentAmount(),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.hideLoader();
openPaymentURL("mada", getPaymentAmount());
if (appState.isPayfortHostedPageEnabled) {
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
"mada",
ServiceTypeEnum.offerPayment.getIdFromServiceEnum().toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: getPaymentAmount().toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
await completePaymentResult(paymentStatus: "success");
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
LoaderBottomSheet.hideLoader();
openPaymentURL("mada", getPaymentAmount());
}
}
}
}),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
}),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
),
SizedBox(height: 16.h),
LocaleKeys.visaOrMastercard.tr(context: context).toText16(isBold: true),
],
),
SizedBox(width: 8.h),
const Spacer(),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 40.h,
height: 40.h,
fit: BoxFit.contain,
),
SizedBox(height: 16.h),
LocaleKeys.visaOrMastercard.tr(context: context).toText16(isBold: true),
],
),
SizedBox(width: 8.h),
const Spacer(),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon,
iconColor: AppColors.blackColor,
width: 40.h,
height: 40.h,
fit: BoxFit.contain,
),
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
LocaleKeys.offerPayment.tr(context: context),
getPaymentAmount(),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
if (appState.isPayfortHostedPageEnabled) {
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: getPaymentAmount().toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
await completePaymentResult(paymentStatus: "success");
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
LocaleKeys.offerPayment.tr(context: context),
getPaymentAmount(),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.hideLoader();
openPaymentURL("visa", getPaymentAmount());
if (appState.isPayfortHostedPageEnabled) {
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
"visa",
ServiceTypeEnum.offerPayment.getIdFromServiceEnum().toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: getPaymentAmount().toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
await completePaymentResult(paymentStatus: "success");
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
LoaderBottomSheet.hideLoader();
openPaymentURL("visa", getPaymentAmount());
}
}
}
}),
],
}),
],
),
),
),
),

@ -133,12 +133,18 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
getIt<AuthenticationViewModel>().onLoginPressed();
});
} else {
// Show loader during authentication
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await viewModel.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
// Hide loader on error
LoaderBottomSheet.hideLoader();
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
@ -150,8 +156,15 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
);
if (!isAuthenticated) {
// Hide loader if authentication failed
LoaderBottomSheet.hideLoader();
return;
}
// Hide loader on success
LoaderBottomSheet.hideLoader();
// Set the selected offer and navigate to payment page
viewModel.setSelectedOffer(viewModel.offerDetails!);
Navigator.of(context).push(
CustomPageRoute(
@ -359,10 +372,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
// Branches Section
if (offer.branches.isNotEmpty) ...[
LocaleKeys.availableAt.tr(context: context).toText18(
isBold: true,
weight: FontWeight.w700,
),
LocaleKeys.availableAt.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
SizedBox(height: 12.h),
Wrap(
spacing: 8.w,

@ -240,7 +240,8 @@ class _ActivityDetailsState extends State<ActivityDetails> {
return Selector<HealthProvider, Map<String, List<DataPoint>>?>(
selector: (_, model) => model.selectedData,
builder: (_, data, __) {
if (context.read<HealthProvider>().selectedData.values.toList().first?.isEmpty == true) return SizedBox();
// if (context.read<HealthProvider>().selectedData.values.toList().first.isEmpty == true) return SizedBox();
if (context.read<HealthProvider>().selectedData.values.toList().isEmpty == true) return SizedBox();
return CustomBarChart(
dataPoints: context.read<HealthProvider>().selectedData.values.toList().first,

@ -171,9 +171,16 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
);
payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: widget.totalAmount.toString(),
orderDescription: "Ancillary Order Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@ -286,9 +293,16 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
);
payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: widget.totalAmount.toString(),
orderDescription: "Ancillary Order Payment",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));

@ -0,0 +1,291 @@
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
/// Centralized service for Clarity tracking and analytics
/// Provides methods for tracking user actions, errors, and app state
class ClarityService {
final AppState _appState;
ClarityService({required AppState appState}) : _appState = appState;
/// Initialize Clarity with user context
void setUserContext() {
if (kDebugMode) return; // Skip in debug mode
try {
if (_appState.isAuthenticated) {
final user = _appState.getAuthenticatedUser();
if (user != null) {
// Set user ID
Clarity.setCustomUserId(user.patientId.toString());
// Set user segments for filtering
Clarity.setCustomTag('user_type', user.patientType?.toString() ?? 'unknown');
Clarity.setCustomTag('user_segment', user.outSa == 0 ? 'in_sa' : 'out_sa');
Clarity.setCustomTag('auth_status', 'authenticated');
}
} else {
Clarity.setCustomTag('auth_status', 'guest');
}
} catch (e) {
debugPrint('Failed to set Clarity user context: $e');
}
}
/// Track screen views / route changes
void trackScreenView(String screenName, {Map<String, String>? properties}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('current_screen', _truncate(screenName, 100));
if (properties != null) {
properties.forEach((key, value) {
Clarity.setCustomTag(key, _truncate(value, 250));
});
}
Clarity.sendCustomEvent('screen_view_$screenName');
} catch (e) {
debugPrint('Failed to track screen view: $e');
}
}
/// Track user actions (button clicks, form submissions, etc.)
void trackAction(String actionName, {Map<String, String>? properties}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('action_name', _truncate(actionName, 100));
if (properties != null) {
properties.forEach((key, value) {
Clarity.setCustomTag('action_$key', _truncate(value, 250));
});
}
Clarity.sendCustomEvent('user_action_$actionName');
} catch (e) {
debugPrint('Failed to track action: $e');
}
}
/// Track API calls with success/failure
void trackApiCall({
required String endpoint,
required String method,
required bool isSuccess,
int? statusCode,
String? errorType,
String? errorMessage,
}) {
if (kDebugMode) return;
try {
final eventType = isSuccess ? 'api_success' : 'api_failure';
Clarity.setCustomTag('api_endpoint', _getEndpointName(endpoint));
Clarity.setCustomTag('api_method', method);
Clarity.setCustomTag('api_status', isSuccess ? 'success' : 'failure');
if (statusCode != null) {
Clarity.setCustomTag('api_status_code', statusCode.toString());
}
if (errorType != null) {
Clarity.setCustomTag('api_error_type', _truncate(errorType, 100));
}
if (errorMessage != null) {
Clarity.setCustomTag('api_error_msg', _truncate(errorMessage, 250));
}
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent(eventType);
} catch (e) {
debugPrint('Failed to track API call: $e');
}
}
/// Track app errors and exceptions
void trackError({
required String errorType,
required String errorMessage,
String? stackTrace,
bool isFatal = false,
Map<String, String>? additionalContext,
}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('error_type', _truncate(errorType, 100));
Clarity.setCustomTag('error_fatal', isFatal.toString());
Clarity.setCustomTag('error_message', _truncate(errorMessage, 250));
if (stackTrace != null) {
// Only send first 250 chars of stack trace
Clarity.setCustomTag('error_stack', _truncate(stackTrace, 250));
}
if (additionalContext != null) {
additionalContext.forEach((key, value) {
Clarity.setCustomTag('error_$key', _truncate(value, 250));
});
}
if (_appState.isAuthenticated) {
Clarity.setCustomTag('error_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent(isFatal ? 'app_fatal_error' : 'app_error');
} catch (e) {
debugPrint('Failed to track error: $e');
}
}
/// Track user frustration events (rage clicks, dead clicks)
void trackFrustration({
required String frustrationType,
required String elementLabel,
Map<String, String>? context,
}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('frustration_type', frustrationType);
Clarity.setCustomTag('frustration_element', _truncate(elementLabel, 100));
if (context != null) {
context.forEach((key, value) {
Clarity.setCustomTag('frustration_$key', _truncate(value, 250));
});
}
Clarity.sendCustomEvent('user_frustration_$frustrationType');
} catch (e) {
debugPrint('Failed to track frustration: $e');
}
}
/// Track feature usage
void trackFeatureUsage(String featureName, {Map<String, String>? metadata}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('feature_name', _truncate(featureName, 100));
if (metadata != null) {
metadata.forEach((key, value) {
Clarity.setCustomTag('feature_$key', _truncate(value, 250));
});
}
if (_appState.isAuthenticated) {
Clarity.setCustomTag('feature_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('feature_used_$featureName');
} catch (e) {
debugPrint('Failed to track feature usage: $e');
}
}
/// Track payment events
void trackPayment({
required String paymentStatus,
String? paymentMethod,
String? amount,
String? transactionId,
}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('payment_status', paymentStatus);
if (paymentMethod != null) {
Clarity.setCustomTag('payment_method', paymentMethod);
}
if (amount != null) {
Clarity.setCustomTag('payment_amount', amount);
}
if (transactionId != null) {
Clarity.setCustomTag('payment_tx_id', _truncate(transactionId, 100));
}
if (_appState.isAuthenticated) {
Clarity.setCustomTag('payment_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('payment_$paymentStatus');
} catch (e) {
debugPrint('Failed to track payment: $e');
}
}
/// Track appointment booking flow
void trackAppointmentEvent({
required String eventType,
String? doctorId,
String? clinicId,
String? appointmentDate,
}) {
if (kDebugMode) return;
try {
Clarity.setCustomTag('appointment_event', eventType);
if (doctorId != null) {
Clarity.setCustomTag('appointment_doctor', doctorId);
}
if (clinicId != null) {
Clarity.setCustomTag('appointment_clinic', clinicId);
}
if (appointmentDate != null) {
Clarity.setCustomTag('appointment_date', appointmentDate);
}
if (_appState.isAuthenticated) {
Clarity.setCustomTag('appointment_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('appointment_$eventType');
} catch (e) {
debugPrint('Failed to track appointment event: $e');
}
}
/// Clear user context on logout
void clearUserContext() {
if (kDebugMode) return;
try {
Clarity.setCustomTag('auth_status', 'guest');
Clarity.setCustomTag('user_action', 'logout');
Clarity.sendCustomEvent('user_logged_out');
} catch (e) {
debugPrint('Failed to clear Clarity user context: $e');
}
}
/// Helper: Truncate string to fit Clarity's limits
String _truncate(String text, int maxLength) {
if (text.length <= maxLength) return text;
return '...${text.substring(text.length - (maxLength - 3))}';
}
/// Helper: Extract endpoint name from full URL
String _getEndpointName(String endpoint) {
final cleanEndpoint = endpoint.split('/').last;
return _truncate(cleanEndpoint, 100);
}
}

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:flutter/foundation.dart';
@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.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/utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -91,6 +93,45 @@ class _SplashScreenState extends State<SplashPage> {
);
Clarity.initialize(context, config);
// Set initial app context tags
try {
final appState = getIt.get<AppState>();
// Set device and app metadata
Clarity.setCustomTag('app_version', ApiConsts.appVersionID.toString());
Clarity.setCustomTag('device_type', appState.deviceTypeID ?? 'unknown');
Clarity.setCustomTag('platform', Platform.isIOS ? 'ios' : 'android');
// Set user context if authenticated
if (appState.isAuthenticated) {
final user = appState.getAuthenticatedUser();
if (user != null) {
// Set user ID for session tracking
Clarity.setCustomUserId(user.patientId.toString());
// Set custom tags for filtering sessions
Clarity.setCustomTag('user_type', user.patientType?.toString() ?? 'unknown');
Clarity.setCustomTag('user_segment', user.outSa == 0 ? 'in_sa' : 'out_sa');
Clarity.setCustomTag('auth_status', 'authenticated');
// Optional: Set other non-sensitive user properties
if (user.mobileNumber != null) {
// Mask phone number for privacy (show only last 4 digits)
final maskedPhone = '***${user.mobileNumber!.substring(user.mobileNumber!.length - 4)}';
Clarity.setCustomTag('user_phone_masked', maskedPhone);
}
}
} else {
Clarity.setCustomTag('auth_status', 'guest');
}
// Send initialization event
Clarity.sendCustomEvent('app_initialized');
} catch (e) {
debugPrint('Failed to set Clarity user context: $e');
}
}
navigateToTeleConsult() async {

@ -75,6 +75,7 @@ class _AppLanguageChangeState extends State<AppLanguageChange> {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.getLakumAccountInformation();

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -60,39 +61,73 @@ class CustomButton extends StatelessWidget {
final fontS = fontSize ?? (16.f);
final isButtonDisabled = isDisabled || onPressed == null;
return GestureDetector(
onTap: isButtonDisabled ? null : onPressed,
child: Container(
height: height == null
? 56.h
: (isFoldable || isTablet)
? height! + 10.h
: height,
width: width,
padding: padding,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor,
borderRadius: radius,
customBorder: BorderRadius.circular(radius),
side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null)
Padding(
padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(
icon: icon!,
iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor,
isDisabled: isButtonDisabled,
width: iconS,
height: iconS,
applyThemeColor: applyThemeColor),
),
if (text.isNotEmpty && icon != null)
Flexible(
child: Text(
return Semantics(
button: true,
enabled: !isButtonDisabled,
label: text,
hint: isButtonDisabled ? 'Button is disabled' : 'Tap to $text',
child: GestureDetector(
onTap: isButtonDisabled
? null
: () {
// Log button tap to Clarity before executing callback
try {
Clarity.setCustomTag('button_text', text.length > 100 ? text.substring(0, 100) : text);
Clarity.setCustomTag('button_action', 'tap');
Clarity.setCustomTag('button_disabled', 'false');
Clarity.sendCustomEvent('button_tapped');
} catch (e) {
// Silence Clarity errors to not interrupt user flow
}
// Execute the actual button callback
onPressed?.call();
},
child: Container(
height: height == null
? 56.h
: (isFoldable || isTablet)
? height! + 10.h
: height,
width: width,
padding: padding,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor,
borderRadius: radius,
customBorder: BorderRadius.circular(radius),
side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null)
Padding(
padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(
icon: icon!,
iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor,
isDisabled: isButtonDisabled,
width: iconS,
height: iconS,
applyThemeColor: applyThemeColor),
),
if (text.isNotEmpty && icon != null)
Flexible(
child: Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
style: context.dynamicTextStyle(
fontSize: fontS,
color: isButtonDisabled ? AppColors.greyTextColor : textColor,
letterSpacing: 0,
fontWeight: isBold ? FontWeight.w700 : fontWeight,
),
),
),
if (text.isNotEmpty && icon == null)
Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
@ -104,22 +139,9 @@ class CustomButton extends StatelessWidget {
fontWeight: isBold ? FontWeight.w700 : fontWeight,
),
),
),
if (text.isNotEmpty && icon == null)
Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
style: context.dynamicTextStyle(
fontSize: fontS,
color: isButtonDisabled ? AppColors.greyTextColor : textColor,
letterSpacing: 0,
fontWeight: isBold ? FontWeight.w700 : fontWeight,
),
),
],
),
));
],
),
)),
);
}
}

@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:clarity_flutter/clarity_flutter.dart';
/// A wrapper widget that tracks user interactions with Clarity for UX frustration analysis.
/// Helps identify rage clicks, dead clicks, and rapid scrolls.
///
/// Usage:
/// ```dart
/// ClarityInteractionTracker(
/// semanticLabel: 'Login Button',
/// interactionType: ClarityInteractionType.button,
/// onTap: () { /* your action */ },
/// child: YourWidget(),
/// )
/// ```
class ClarityInteractionTracker extends StatelessWidget {
final Widget child;
final String semanticLabel;
final VoidCallback? onTap;
final ClarityInteractionType interactionType;
final Map<String, String>? customTags;
final bool enabled;
const ClarityInteractionTracker({
super.key,
required this.child,
required this.semanticLabel,
this.onTap,
this.interactionType = ClarityInteractionType.button,
this.customTags,
this.enabled = true,
});
void _logInteraction(BuildContext context) {
if (!enabled) return;
try {
// Set common tags
Clarity.setCustomTag('interaction_type', interactionType.name);
Clarity.setCustomTag('interaction_label',
semanticLabel.length > 100 ? semanticLabel.substring(0, 100) : semanticLabel
);
Clarity.setCustomTag('interaction_enabled', enabled.toString());
// Set route/screen name if available
final route = ModalRoute.of(context);
if (route != null && route.settings.name != null) {
Clarity.setCustomTag('screen_route', route.settings.name!);
}
// Add custom tags if provided
if (customTags != null) {
customTags!.forEach((key, value) {
// Ensure key and value fit Clarity's 255 char limit
final truncatedValue = value.length > 250 ? value.substring(0, 250) : value;
Clarity.setCustomTag(key, truncatedValue);
});
}
// Send event to Clarity
Clarity.sendCustomEvent('user_interaction_${interactionType.name}');
} catch (e) {
// Silence Clarity errors to not interrupt user flow
debugPrint('Clarity interaction tracking error: $e');
}
}
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
button: interactionType == ClarityInteractionType.button,
enabled: enabled,
excludeSemantics: false,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: enabled && onTap != null
? () {
_logInteraction(context);
onTap?.call();
}
: null,
child: child,
),
);
}
}
/// Types of interactions that can be tracked
enum ClarityInteractionType {
button,
link,
card,
listItem,
icon,
image,
input,
custom,
}
/// Extension to easily wrap any widget with Clarity tracking
extension ClarityTrackingExtension on Widget {
/// Wraps this widget with Clarity interaction tracking
Widget trackWithClarity({
required String label,
VoidCallback? onTap,
ClarityInteractionType type = ClarityInteractionType.custom,
Map<String, String>? customTags,
bool enabled = true,
}) {
return ClarityInteractionTracker(
semanticLabel: label,
onTap: onTap,
interactionType: type,
customTags: customTags,
enabled: enabled,
child: this,
);
}
}
/// Wrapper for ScrollView to track rapid scrolling behavior
class ClarityScrollTracker extends StatefulWidget {
final Widget child;
final String scrollViewLabel;
final ScrollController? controller;
const ClarityScrollTracker({
super.key,
required this.child,
required this.scrollViewLabel,
this.controller,
});
@override
State<ClarityScrollTracker> createState() => _ClarityScrollTrackerState();
}
class _ClarityScrollTrackerState extends State<ClarityScrollTracker> {
late ScrollController _controller;
bool _isRapidScrolling = false;
DateTime? _lastScrollTime;
int _rapidScrollCount = 0;
@override
void initState() {
super.initState();
_controller = widget.controller ?? ScrollController();
_controller.addListener(_onScroll);
}
void _onScroll() {
final now = DateTime.now();
// Detect rapid scrolling (scroll events within 100ms)
if (_lastScrollTime != null) {
final timeDiff = now.difference(_lastScrollTime!).inMilliseconds;
if (timeDiff < 100) {
_rapidScrollCount++;
if (!_isRapidScrolling && _rapidScrollCount > 5) {
_isRapidScrolling = true;
_logRapidScroll();
}
} else if (timeDiff > 500) {
// Reset if scrolling slows down
_isRapidScrolling = false;
_rapidScrollCount = 0;
}
}
_lastScrollTime = now;
}
void _logRapidScroll() {
try {
Clarity.setCustomTag('scroll_type', 'rapid');
Clarity.setCustomTag('scroll_view', widget.scrollViewLabel);
Clarity.setCustomTag('scroll_position', _controller.position.pixels.toString());
Clarity.sendCustomEvent('rapid_scroll_detected');
} catch (e) {
debugPrint('Clarity scroll tracking error: $e');
}
}
@override
void dispose() {
if (widget.controller == null) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

@ -2,8 +2,8 @@ name: hmg_patient_app_new
description: "New HMG Patient App"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.37+36
#version: 0.0.6+3
version: 0.0.39+38
#version: 0.0.8+1
environment:
sdk: ">=3.6.0 <4.0.0"
@ -87,7 +87,7 @@ dependencies:
open_filex: ^4.7.0
flutter_swiper_view:
path: package/flutter_swiper_view
flutter_callkit_incoming: ^3.0.0
flutter_callkit_incoming: 3.0.0
in_app_update: ^4.2.5
# device_preview: ^1.3.1
image_picker_android: ^0.8.13+16
@ -108,6 +108,8 @@ dependencies:
clarity_flutter: 1.9.0
flutter_amazonpaymentservices: ^0.0.13
screen_brightness: ^1.0.1
flutter_screenshot_blocker: ^1.0.4
cloudflare_turnstile: ^3.7.2
dev_dependencies:
flutter_test:

Loading…
Cancel
Save