Merge branch 'master' into faiz_dev

# Conflicts:
#	lib/core/api/api_client.dart
#	lib/core/api_consts.dart
#	lib/generated/locale_keys.g.dart
faiz_dev
faizatflutter 1 day ago
commit fd88e8f381

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": "معلومات الخدمة", "serviceInformation": "معلومات الخدمة",
"homeHealthCare": "الرعاية الصحية المنزلية", "homeHealthCare": "الرعاية الصحية المنزلية",
"noAppointmentAvailable": "لا توجد مواعيد متاحة", "noAppointmentAvailable": "لا توجد مواعيد متاحة",
"noScheduleAvailable": "لا يوجد جدول متاح حاليًا",
"loginRegister": "تسجيل الدخول / التسجيل", "loginRegister": "تسجيل الدخول / التسجيل",
"orderLog": "سجل الطلب", "orderLog": "سجل الطلب",
"infoLab": "تتيح لك هذه الخدمة عرض نتائج جميع الفحوصات المخبرية التي أجريت في مجموعة الحبيب الطبية بالإضافة إلى إرسال التقرير عبر البريد الإلكتروني.", "infoLab": "تتيح لك هذه الخدمة عرض نتائج جميع الفحوصات المخبرية التي أجريت في مجموعة الحبيب الطبية بالإضافة إلى إرسال التقرير عبر البريد الإلكتروني.",
@ -1971,5 +1972,12 @@
"cashRefundApprovalInstructions": "قم بزيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل بمجرد قيام الصراف بإنشاء معاملة الاسترداد. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", "cashRefundApprovalInstructions": "قم بزيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل بمجرد قيام الصراف بإنشاء معاملة الاسترداد. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.",
"packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية.", "packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية.",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:" "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
"schedule": "جدول",
"doctorSchedule": "جدول الطبيب",
"weeklyShift": "الفترات الأسبوعية",
"dayOff": "إجازة",
"notAvailable": "غير متاح"
} }

@ -460,6 +460,7 @@
"serviceInformation": "Service Information", "serviceInformation": "Service Information",
"homeHealthCare": "Home Health Care", "homeHealthCare": "Home Health Care",
"noAppointmentAvailable": "No Available Appointments", "noAppointmentAvailable": "No Available Appointments",
"noScheduleAvailable": "No schedule currently available",
"homeHealthCareText": "Clinical excellence and specialized care in the comfort of your home", "homeHealthCareText": "Clinical excellence and specialized care in the comfort of your home",
"loginRegister": "Login/Register", "loginRegister": "Login/Register",
"orderLog": "Order Log", "orderLog": "Order Log",
@ -1961,7 +1962,14 @@
"cashRefundApprovalInstructions": "Visit HMG Hospital or HMG Primary Care cashier to receive cash. At the cashier counter, you will receive an OTP on your registered mobile number once the cashier has created the refund transaction. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", "cashRefundApprovalInstructions": "Visit HMG Hospital or HMG Primary Care cashier to receive cash. At the cashier counter, you will receive an OTP on your registered mobile number once the cashier has created the refund transaction. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.",
"packageRefundValidationMessage": "Your package refund request required validation because of package use and related child invoices. We will review the process the request within the approved SLA after confirming eligibility.", "packageRefundValidationMessage": "Your package refund request required validation because of package use and related child invoices. We will review the process the request within the approved SLA after confirming eligibility.",
"parkingQR": "Parking QR Code", "parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:" "parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"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",
"notAvailable": "Not Available"
} }

@ -216,6 +216,8 @@ class ApiClientImp implements ApiClient {
// body['PatientID'] = 4776359; // body['PatientID'] = 4776359;
// body['PatientID'] = 4776466; // body['PatientID'] = 4776466;
// body['PatientID'] = 4769748;
// body['PatientID'] = 89754444;
// body['PatientTypeID'] = 1; // body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0; // body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q"; // body['SessionID'] = "45786230487560q";
@ -263,35 +265,83 @@ class ApiClientImp implements ApiClient {
body: requestBody, body: requestBody,
headers: headers, headers: headers,
); );
log("response: ${response.body.toString()}"); // log("response: ${response.body.toString()}");
} on SocketException catch (e) { } on SocketException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message)); onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "SocketException: $e"); _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_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody)); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.sendCustomEvent('api_failure'); 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; return;
} on http.ClientException catch (e) { } on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message)); onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "ClientException: $e"); _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; return;
} on TimeoutException catch (_) { } on TimeoutException catch (_) {
final message = LocaleKeys.networkTimeout.tr(); final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message)); onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "TimeoutException"); _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_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody)); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint));
Clarity.sendCustomEvent('api_timeout_failure'); Clarity.setCustomTag('api_err_method', 'POST');
if (_appState.isAuthenticated) {
Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString());
}
Clarity.sendCustomEvent('api_timeout');
return; return;
} catch (e) { } catch (e) {
final message = LocaleKeys.networkUnknownError.tr(); final message = LocaleKeys.networkUnknownError.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message)); onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "UnknownException: $e"); _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; return;
} }
@ -492,21 +542,74 @@ class ApiClientImp implements ApiClient {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1); onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "SocketException: $e"); _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; return;
} on http.ClientException catch (e) { } on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1); onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "ClientException: $e"); _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; return;
} on TimeoutException catch (_) { } on TimeoutException catch (_) {
final message = LocaleKeys.networkTimeout.tr(); final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1); onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "TimeoutException"); _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; return;
} catch (e) { } catch (e) {
final message = LocaleKeys.networkUnknownError.tr(); final message = LocaleKeys.networkUnknownError.tr();
onFailure(message, -1); onFailure(message, -1);
_analytics.errorTracking.log(endPoint, error: "UnknownException: $e"); _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; return;
} }
@ -516,6 +619,19 @@ class ApiClientImp implements ApiClient {
if (statusCode < 200 || statusCode >= 400) { if (statusCode < 200 || statusCode >= 400) {
onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode); onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode);
logApiEndpointError(endPoint, 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 { } else {
var parsed = json.decode(utf8.decode(response.bodyBytes)); var parsed = json.decode(utf8.decode(response.bodyBytes));
onSuccess(parsed, statusCode); onSuccess(parsed, statusCode);
@ -698,6 +814,15 @@ class ApiClientImp implements ApiClient {
return '...${url.substring(url.length - (clarityLimit - 3))}'; 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) { String _sanitizeRequestBody(String body) {
if (body.isEmpty) return ''; if (body.isEmpty) return '';

@ -311,22 +311,20 @@ class ApiConsts {
static final String sessionID = 'TMRhVmkGhOsvamErw'; static final String sessionID = 'TMRhVmkGhOsvamErw';
// PACKAGES and OFFERS // PACKAGES and OFFERS
static String exaCartApiBaseUrl = 'https://mdlaboratories.com/offersdiscounts'; var EXA_CART_API_BASE_URL = 'https://mdlaboratories.com/offersdiscounts';
// var EXA_CART_API_BASE_URL = 'http://10.200.101.75:9000'; // var EXA_CART_API_BASE_URL = 'http://10.200.101.75:9000';
static String packagesCategories = '/api/categories'; var PACKAGES_CATEGORIES = '/api/categories';
static String packagesStores = '/api/stores'; var PACKAGES_STORES = '/api/stores';
static String packagesToken = '/api/token'; var PACKAGES_TOKEN = '/api/token';
static String packagesProducts = '/api/products'; var PACKAGES_PRODUCTS = '/api/products';
static String packagesCustomer = '/api/customers'; var PACKAGES_CUSTOMER = '/api/customers';
static String packagesShoppingCart = '/api/shopping_cart_items'; var PACKAGES_SHOPPING_CART = '/api/shopping_cart_items';
static String packagesOrders = '/api/orders'; var PACKAGES_ORDERS = '/api/orders';
static String packagesOrderHistory = '/api/orders/items'; var PACKAGES_ORDER_HISTORY = '/api/orders/items';
static String packagesTamaraOpt = '/api/orders/paymentoptions/tamara'; var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara';
// var BASE_URL = 'http://10.50.100.198:2018/'; // var BASE_URL = 'http://10.50.100.198:2018/';
// var BASE_URL = 'https://uat.hmgwebservices.com/'; // var BASE_URL = 'https://uat.hmgwebservices.com/';
var BASE_URL = 'https://hmgwebservices.com/';
// var BASE_URL = 'http://10.201.204.103/'; // var BASE_URL = 'http://10.201.204.103/';
// var BASE_URL = 'https://orash.cloudsolutions.com.sa/'; // var BASE_URL = 'https://orash.cloudsolutions.com.sa/';
// var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/'; // var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/';
@ -341,8 +339,10 @@ class ApiConsts {
// var PHARMACY_BASE_URL = 'https://uat.hmgwebservices.com/epharmacy/api/'; // var PHARMACY_BASE_URL = 'https://uat.hmgwebservices.com/epharmacy/api/';
// // Pharmacy Production URLs // // Pharmacy Production URLs
static String pharmacyBaseUrl = 'https://mdlaboratories.com/exacartapi/api/'; var BASE_PHARMACY_URL = 'https://mdlaboratories.com/exacartapi/api/';
static String pharmacyRedirectUrl = 'https://bit.ly/AlhabibPharmacy'; var PHARMACY_BASE_URL = 'https://mdlaboratories.com/exacartapi/api/';
var PHARMACY_REDIRECT_URL = 'https://bit.ly/AlhabibPharmacy';
// Pharmacy VidaPlus URLs // Pharmacy VidaPlus URLs
// var BASE_PHARMACY_URL = 'https://mdlaboratories.com/exacartapitest/api/'; // var BASE_PHARMACY_URL = 'https://mdlaboratories.com/exacartapitest/api/';
@ -357,30 +357,28 @@ class ApiConsts {
// var RC_BASE_URL = 'https://ms.hmg.com/rc/'; // var RC_BASE_URL = 'https://ms.hmg.com/rc/';
static String pingService = 'Services/Weather.svc/REST/CheckConnectivity'; var PING_SERVICE = 'Services/Weather.svc/REST/CheckConnectivity';
static String getProjectsList = 'Services/Lists.svc/REST/GetProject'; var GET_PROJECT_LIST = 'Services/Lists.svc/REST/GetProject';
///Geofencing ///Geofencing
static String getGeoZones = 'Services/Patients.svc/REST/GeoF_GetAllPoints'; var GET_GEO_ZONES = 'Services/Patients.svc/REST/GeoF_GetAllPoints';
static String logGeoZones = 'Services/Patients.svc/REST/GeoF_InsertPatientFileInfo'; var LOG_GEO_ZONES = 'Services/Patients.svc/REST/GeoF_InsertPatientFileInfo';
// Delivery Driver // Delivery Driver
static String driverLocation = 'Services/Patients.svc/REST/PatientER_GetDriverLocation'; var DRIVER_LOCATION = 'Services/Patients.svc/REST/PatientER_GetDriverLocation';
//weather //weather
static String weatherIndicator = 'Services/Weather.svc/REST/GetCityInfo'; var WEATHER_INDICATOR = 'Services/Weather.svc/REST/GetCityInfo';
static String getPrivilege = 'Services/Patients.svc/REST/Service_Privilege'; var GET_PRIVILEGE = 'Services/Patients.svc/REST/Service_Privilege';
// Wifi Credentials // Wifi Credentials
static String wifiCredentials = "Services/Patients.svc/Hmg_SMS_Get_By_ProjectID_And_PatientID"; var WIFI_CREDENTIALS = "Services/Patients.svc/Hmg_SMS_Get_By_ProjectID_And_PatientID";
///Doctor
static String getMyDoctor = 'Services/Doctors.svc/REST/GetPatientDoctorAppointmentResult';
static String getDoctorProfiles = 'Services/Doctors.svc/REST/GetDocProfiles';
}
///Doctor
var GET_MY_DOCTOR = 'Services/Doctors.svc/REST/GetPatientDoctorAppointmentResult';
var GET_DOCTOR_PROFILE = 'Services/Doctors.svc/REST/GetDocProfiles';
var GET_DOCTOR_PRE_POST_IMAGES = 'Services/Doctors.svc/REST/GetDoctorPrePostImages'; var GET_DOCTOR_PRE_POST_IMAGES = 'Services/Doctors.svc/REST/GetDoctorPrePostImages';
var GET_DOCTOR_RATING_NOTES = 'Services/Doctors.svc/REST/dr_GetNotesDoctorRating'; var GET_DOCTOR_RATING_NOTES = 'Services/Doctors.svc/REST/dr_GetNotesDoctorRating';
var GET_DOCTOR_RATING_DETAILS = 'Services/Doctors.svc/REST/dr_GetDoctorRatingDetails'; var GET_DOCTOR_RATING_DETAILS = 'Services/Doctors.svc/REST/dr_GetDoctorRatingDetails';
@ -624,6 +622,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_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 GET_LIVECARE_HISTORY = 'Services/ER_VirtualCall.svc/REST/GetPatientErVirtualHistory';
var CANCEL_LIVECARE_REQUEST = 'Services/ER_VirtualCall.svc/REST/DeleteErRequest'; var CANCEL_LIVECARE_REQUEST = 'Services/ER_VirtualCall.svc/REST/DeleteErRequest';
var SEND_LIVECARE_INVOICE_EMAIL = 'Services/Notifications.svc/REST/SendInvoiceForLiveCare'; var SEND_LIVECARE_INVOICE_EMAIL = 'Services/Notifications.svc/REST/SendInvoiceForLiveCare';
@ -986,7 +986,10 @@ var IS_DOCTOR_AVAILABLE_BY_CALENDAR_SCHEDULE = 'Services/OUTPs.svc/REST/HIS_IsDo
//PAYFORT //PAYFORT
var getPayFortProjectDetails = "Services/PayFort_Serv.svc/REST/GetPayFortProjectDetails"; 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 payFortEnvironment = FortEnvironment.production;
// var applePayMerchantId = "merchant.com.hmgwebservices"; // var applePayMerchantId = "merchant.com.hmgwebservices";
// var payFortEnvironment = FortEnvironment.test; // var payFortEnvironment = FortEnvironment.test;

@ -193,6 +193,7 @@ class AppAssets {
static const String ic_rrt_vehicle = '$svgBasePath/ic_rrt_vehicle.svg'; 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_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_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_icon = '$svgBasePath/bookmark_icon.svg';
static const String bookmark_filled_icon = '$svgBasePath/bookmark_filled_icon.svg'; static const String bookmark_filled_icon = '$svgBasePath/bookmark_filled_icon.svg';
static const String waiting_appointment_icon = '$svgBasePath/waitingAppo.svg'; static const String waiting_appointment_icon = '$svgBasePath/waitingAppo.svg';

@ -57,7 +57,7 @@ class AppState {
void setIsPayfortHostedPageEnabled(bool value) { void setIsPayfortHostedPageEnabled(bool value) {
isPayfortHostedPageEnabled = value; isPayfortHostedPageEnabled = value;
// isPayfortHostedPageEnabled = true; // isPayfortHostedPageEnabled = false;
} }
int? _superUserID; int? _superUserID;

@ -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/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_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/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/dialog_service.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/services/firebase_service.dart'; import 'package:hmg_patient_app_new/services/firebase_service.dart';
@ -366,5 +367,9 @@ class AppDependencies {
)); ));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt())); getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt()));
getIt.registerLazySingleton<ClarityService>(
() => ClarityService(appState: getIt.get<AppState>()),
);
} }
} }

@ -1209,13 +1209,15 @@ class AuthenticationViewModel extends ChangeNotifier {
} }
// Fetch profile image after switching user // Fetch profile image after switching user
await _fetchProfileImage(activation.list!.first.patientId); // await _fetchProfileImage(activation.list!.first.patientId);
_fetchProfileImage(activation.list!.first.patientId);
} else { } else {
activation.list!.first.isParentUser = true; activation.list!.first.isParentUser = true;
_appState.addUserSwitchPayload(activation.list!.first.toJson(), isSuperUser: true); _appState.addUserSwitchPayload(activation.list!.first.toJson(), isSuperUser: true);
// Fetch profile image after login // 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.bloodGroup = activation.patientBloodType;
activation.list!.first.zipCode = selectedCountrySignup == CountryEnum.others ? '0' : selectedCountrySignup.countryCode; activation.list!.first.zipCode = selectedCountrySignup == CountryEnum.others ? '0' : selectedCountrySignup.countryCode;

@ -113,6 +113,8 @@ abstract class BookAppointmentsRepo {
Future<Either<Failure, GenericApiModel<dynamic>>> insertFavouriteDoctor( 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}); {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 { class BookAppointmentsRepoImp implements BookAppointmentsRepo {
@ -1228,4 +1230,49 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
return Left(UnknownFailure(e.toString())); 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;
}
}

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

@ -25,6 +25,9 @@ abstract class ImmediateLiveCareRepo {
{Function(dynamic)? onSuccess, Function(String)? onError}); {Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<List<PatientLiveCareHistory>>>> getPatientLiveCareHistory({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 { class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo {
@ -250,4 +253,44 @@ class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo {
return Left(UnknownFailure(e.toString())); 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 { Future<void> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
patientLiveCareHistoryList = []; patientLiveCareHistoryList = [];
patientHasPendingLiveCareRequest = false; patientHasPendingLiveCareRequest = false;

@ -588,7 +588,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
final result = await myAppointmentsRepo.confirmAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); final result = await myAppointmentsRepo.confirmAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel);
result.fold( 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) { (apiResponse) {
if (apiResponse.messageStatus == 2) { if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage!); onError!(apiResponse.errorMessage!);

@ -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/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_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_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/models/sdk_token_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.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}); {required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID});
Future<Either<Failure, GenericApiModel<GetTamaraInstallmentsDetailsResponseModel>>> getTamaraInstallmentsDetails(); 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 { 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:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/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/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/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_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_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/models/sdk_token_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.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:network_info_plus/network_info_plus.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'models/apple_pay_request_insert_model.dart';
class PayfortViewModel extends ChangeNotifier { class PayfortViewModel extends ChangeNotifier {
PayfortRepo payfortRepo; PayfortRepo payfortRepo;
ErrorHandlerService errorHandlerService; 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 { Future<void> checkPaymentStatus({required String transactionID, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.checkPaymentStatus(transactionID: transactionID); final result = await payfortRepo.checkPaymentStatus(transactionID: transactionID);
@ -291,13 +334,19 @@ class PayfortViewModel extends ChangeNotifier {
} }
Future<void> initiatePaymentWithCard( // var request, { Future<void> initiatePaymentWithCard( // var request, {
{ String paymentOption,
String liveServiceID,
String isScheduleLiveCare,
String appointmentDate,
String appointmentNo,
String doctorID, {
SucceededCallback? onSucceeded, SucceededCallback? onSucceeded,
FailedCallback? onFailed, FailedCallback? onFailed,
String countryIsoCode = "SA", String countryIsoCode = "SA",
String transactionID = "", String transactionID = "",
String amount = "0", String amount = "0",
String fileNumber = "0", String fileNumber = "0",
String orderDescription = "",
}) async { }) async {
dynamic paymentResult; dynamic paymentResult;
@ -325,6 +374,38 @@ class PayfortViewModel extends ChangeNotifier {
"customer_name": "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}", "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(); LoaderBottomSheet.hideLoader();
paymentResult = await FlutterAmazonpaymentservices.normalPay(request, ApiConsts.payFortEnvironmentType, isShowResponsePage: false); paymentResult = await FlutterAmazonpaymentservices.normalPay(request, ApiConsts.payFortEnvironmentType, isShowResponsePage: false);
@ -332,6 +413,27 @@ class PayfortViewModel extends ChangeNotifier {
String responseCode = paymentMap['response_code'] ?? ""; String responseCode = paymentMap['response_code'] ?? "";
print("Payment Success Data: ${paymentMap.toString()}"); 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 (responseCode.startsWith("14") || responseCode == "20064") {
if (onSucceeded != null) { if (onSucceeded != null) {
onSucceeded(PayFortResult.fromMap(paymentMap)); onSucceeded(PayFortResult.fromMap(paymentMap));

@ -13,11 +13,16 @@ enum Durations {
yearly("yearly"); yearly("yearly");
final String value; final String value;
const Durations(this.value); const Durations(this.value);
} }
class HealthDataTransformation { class HealthDataTransformation {
Map<String, List<DataPoint>> transformVitalsToDataPoints(VitalsWRTType vitals, String filterType, String selectedSection,) { Map<String, List<DataPoint>> transformVitalsToDataPoints(
VitalsWRTType vitals,
String filterType,
String selectedSection,
) {
final Map<String, List<DataPoint>> dataPointMap = {}; final Map<String, List<DataPoint>> dataPointMap = {};
Map<String, List<Vitals>> data = vitals.getVitals(); Map<String, List<Vitals>> data = vitals.getVitals();
// Group data based on the filter type // Group data based on the filter type
@ -32,31 +37,36 @@ class HealthDataTransformation {
for (var vital in item) { for (var vital in item) {
String key = ""; String key = "";
if (vital.value == "" || vital.timestamp == "") continue; if (vital.value == "" || vital.timestamp == "") continue;
var parseDate = DateTime.parse(vital.timestamp);
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()); var currentDate = normalizeToStartOfDay(DateTime.now());
if (filterType == Durations.daily.value) { if (filterType == Durations.daily.value) {
if(isBetweenInclusive(parseDate, currentDate, DateTime.now())) { if (isBetweenInclusive(parseDate, currentDate, DateTime.now())) {
key = DateFormat('yyyy-MM-dd HH').format(DateTime.parse(vital.timestamp)); key = DateFormat('yyyy-MM-dd HH').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital); groupedData.putIfAbsent(key, () => []).add(vital);
}// Group by hour } // Group by hour
} else if (filterType == Durations.weekly.value) { } else if (filterType == Durations.weekly.value) {
if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) { if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) {
key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); key = DateFormat('yyyy-MM-dd').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital); groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by day } // Group by day
} else if (filterType == Durations.monthly.value) { } else if (filterType == Durations.monthly.value) {
if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) { 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"); 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)); key = DateFormat('yyyy-MM-dd').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital); groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by day } // Group by day
} else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) { } 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())) { if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value ? 180 : 365)), DateTime.now())) {
key = DateFormat('yyyy-MM').format(DateTime.parse(vital.timestamp)); key = DateFormat('yyyy-MM').format(parseDate);
groupedData.putIfAbsent(key, () => []).add(vital); groupedData.putIfAbsent(key, () => []).add(vital);
} // Group by month } // Group by month
} else { } else {
throw ArgumentError('Invalid filter type'); throw ArgumentError('Invalid filter type');
@ -68,9 +78,9 @@ class HealthDataTransformation {
groupedData.forEach((key, values) { groupedData.forEach((key, values) {
double sum = values.fold(0, (acc, v) => acc + num.parse(v.value)); double sum = values.fold(0, (acc, v) => acc + num.parse(v.value));
double mean = sum / values.length; double mean = sum / values.length;
if(selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") { if (selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") {
mean = sum / values.length; mean = sum / values.length;
}else { } else {
mean = sum; mean = sum;
} }
@ -81,7 +91,7 @@ class HealthDataTransformation {
label: key, label: key,
actualValue: finalValue.toStringAsFixed(2), actualValue: finalValue.toStringAsFixed(2),
displayTime: key, displayTime: key,
unitOfMeasurement:values.first.unitOfMeasure , unitOfMeasurement: values.first.unitOfMeasure,
time: DateTime.parse(values.first.timestamp), time: DateTime.parse(values.first.timestamp),
)); ));
}); });
@ -109,10 +119,10 @@ class HealthDataTransformation {
final random = Random(); final random = Random();
// Smooth compression scaling // 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%) // 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; double result = baseScaled * randomFactor;
@ -122,6 +132,7 @@ class HealthDataTransformation {
DateTime normalizeToStartOfDay(DateTime date) { DateTime normalizeToStartOfDay(DateTime date) {
return DateTime(date.year, date.month, date.day); return DateTime(date.year, date.month, date.day);
} }
bool isBetweenInclusive( bool isBetweenInclusive(
DateTime target, DateTime target,
DateTime start, DateTime start,
@ -129,6 +140,4 @@ class HealthDataTransformation {
) { ) {
return !normalizeToStartOfDay(target).isBefore(start) && !normalizeToStartOfDay(target).isAfter(end); return !normalizeToStartOfDay(target).isBefore(start) && !normalizeToStartOfDay(target).isAfter(end);
} }
} }

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

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

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:async/src/result/result.dart'; import 'package:async/src/result/result.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -80,7 +81,10 @@ class HealthConnectHelper extends WatchHelper {
@override @override
Future<dynamic> retrieveData() async { Future<dynamic> retrieveData() async {
// String retData;
// retData = Result.value(getMappedData()) as String;
return Result.value(getMappedData()); return Result.value(getMappedData());
// return retData;
} }
@override @override
@ -151,9 +155,22 @@ class HealthConnectHelper extends WatchHelper {
for (var point in data) { for (var point in data) {
if (point.value is NumericHealthValue) { if (point.value is NumericHealthValue) {
final numericValue = (point.value as NumericHealthValue).numericValue; 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( Vitals vitals = Vitals(
value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), value: numericValue.toStringAsFixed(2),
timestamp: point.dateFrom.toString() timestamp: timestamp
); );
mappedData[s]?.add(vitals); mappedData[s]?.add(vitals);
} }
@ -165,17 +182,41 @@ class HealthConnectHelper extends WatchHelper {
for (var point in data) { for (var point in data) {
if (point.value is NumericHealthValue) { if (point.value is NumericHealthValue) {
final numericValue = (point.value as NumericHealthValue).numericValue; 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( point.value = NumericHealthValue(
numericValue: numericValue * 100, 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); mappedData[s]?.add(vitals);
} }
} }
} }
getMappedData() { 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 { getHeartData(DateTime startDate, DateTime endDate, HealthDataType types) async {

@ -469,6 +469,7 @@ abstract class LocaleKeys {
static const serviceInformation = 'serviceInformation'; static const serviceInformation = 'serviceInformation';
static const homeHealthCare = 'homeHealthCare'; static const homeHealthCare = 'homeHealthCare';
static const noAppointmentAvailable = 'noAppointmentAvailable'; static const noAppointmentAvailable = 'noAppointmentAvailable';
static const noScheduleAvailable = 'noScheduleAvailable';
static const loginRegister = 'loginRegister'; static const loginRegister = 'loginRegister';
static const orderLog = 'orderLog'; static const orderLog = 'orderLog';
static const infoLab = 'infoLab'; static const infoLab = 'infoLab';
@ -1953,4 +1954,11 @@ abstract class LocaleKeys {
static const packageRefundValidationMessage = 'packageRefundValidationMessage'; static const packageRefundValidationMessage = 'packageRefundValidationMessage';
static const parkingQR = 'parkingQR'; static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc'; 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';
} }

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

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

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

@ -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/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/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_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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -192,6 +194,17 @@ class DoctorProfilePage extends StatelessWidget {
isCloseButtonVisible: true, 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), 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,16 +186,27 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
CustomButton( CustomButton(
text: LocaleKeys.updateInsurance.tr(context: context), text: LocaleKeys.updateInsurance.tr(context: context),
onPressed: () { onPressed: () {
Navigator.of(context) showCommonBottomSheetWithoutHeight(
.push( title: LocaleKeys.notice.tr(context: context),
CustomPageRoute( context,
page: InsuranceHomePage(), child: Utils.getWarningWidget(
), loadingText: LocaleKeys.livecareUpdateInsurance.tr(),
) isShowActionButtons: true,
.then((val) async { onCancelTap: () {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingFeesPleaseWait.tr()); Navigator.pop(context);
await immediateLiveCareVM.getLiveCareImmediateAppointmentFees(onSuccess: (val) { },
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr());
await immediateLiveCareVM.addInsuranceUpdateRequestForLiveCare(onSuccess: (val) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.livecareUpdateInsuranceSent.tr()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) { }, onError: (err) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
@ -206,7 +217,33 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
isCloseButtonVisible: true, 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, backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.secondaryLightRedBorderColor, borderColor: AppColors.secondaryLightRedBorderColor,

@ -184,9 +184,16 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
integrationId: 1); integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"mada",
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0", amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
orderDescription: "LiveCare Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@ -328,9 +335,16 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
integrationId: 1); integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"visa",
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0", amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
orderDescription: "LiveCare Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));

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

@ -192,9 +192,16 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1); serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
DateTime.now().millisecondsSinceEpoch.toString(),
bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(), amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
@ -336,9 +343,16 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1); serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
DateTime.now().millisecondsSinceEpoch.toString(),
bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(), amount: bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!.toString(),
orderDescription: "Appointment Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader(); 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/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/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/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/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/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/theme/colors.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/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.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/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.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:lottie/lottie.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -25,11 +29,13 @@ class DoctorCard extends StatelessWidget {
super.key, super.key,
required this.doctorsListResponseModel, required this.doctorsListResponseModel,
required this.isLoading, required this.isLoading,
this.isDoctorNameSearch = false,
required this.bookAppointmentsViewModel, required this.bookAppointmentsViewModel,
}); });
final DoctorsListResponseModel doctorsListResponseModel; final DoctorsListResponseModel doctorsListResponseModel;
final bool isLoading; final bool isLoading;
final bool isDoctorNameSearch;
final BookAppointmentsViewModel bookAppointmentsViewModel; final BookAppointmentsViewModel bookAppointmentsViewModel;
@ -173,6 +179,29 @@ class DoctorCard extends StatelessWidget {
CustomButton( CustomButton(
text: LocaleKeys.bookAppo.tr(context: context), text: LocaleKeys.bookAppo.tr(context: context),
onPressed: () async { onPressed: () async {
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); bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel);
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
bookAppointmentsViewModel.isLiveCareSchedule bookAppointmentsViewModel.isLiveCareSchedule
@ -261,6 +290,7 @@ class DoctorCard extends StatelessWidget {
// isCloseButtonVisible: true, // isCloseButtonVisible: true,
// ); // );
}); });
}
}, },
backgroundColor: Color(0xffFEE9EA), backgroundColor: Color(0xffFEE9EA),
borderColor: 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/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/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/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/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/lab/lab_result_item_view.dart';
import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.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(), Spacer(),
LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white), LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white),
SizedBox(height: 4.h), SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) { Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount,
textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1) 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); .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), SizedBox(height: 16.h),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, 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/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.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/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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
@ -178,9 +179,16 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1); serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID, transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(), fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(), amount: habibWalletVM.walletRechargeAmount.toString(),
orderDescription: "Advance Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
@ -204,7 +212,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
habibWalletVM.setIsWalletAmountToBeLoaded(true); habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider(); habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount(); 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(); Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true); }, 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); serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID, transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(), fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(), amount: habibWalletVM.walletRechargeAmount.toString(),
orderDescription: "Advance Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
@ -380,7 +395,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
habibWalletVM.initHabibWalletProvider(); habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount(); 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(); // Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true); }, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
}, },
onError: (err) { 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);
}),
),
],
);
}
}

@ -448,31 +448,31 @@ class _LandingPageState extends State<LandingPage> {
: SizedBox.shrink(), : SizedBox.shrink(),
// Offers And Discounts Carousel - Auto-scrolling from right to left // Offers And Discounts Carousel - Auto-scrolling from right to left
appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel // appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel
? Column( // ? Column(
children: [ // children: [
SizedBox(height: 12.h), // SizedBox(height: 12.h),
Row( // Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ // children: [
("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true), // ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true),
Row( // Row(
children: [ // children: [
LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), // LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true),
SizedBox(width: 2.h), // SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), // Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
], // ],
), // ),
], // ],
).paddingSymmetrical(24.h, 0.h).onPress(() { // ).paddingSymmetrical(24.h, 0.h).onPress(() {
Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage())); // Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage()));
}), // }),
SizedBox(height: 16.h), // SizedBox(height: 16.h),
OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h), // OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h),
SizedBox(height: 18.h), // SizedBox(height: 18.h),
], // ],
) // )
: SizedBox.shrink(), // : SizedBox.shrink(),
appState.isAuthenticated appState.isAuthenticated
? Column( ? Column(
@ -516,7 +516,7 @@ class _LandingPageState extends State<LandingPage> {
), ),
).paddingSymmetrical(24.h, 16.h) ).paddingSymmetrical(24.h, 16.h)
: myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty
? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 && !immediateLiveCareVM.patientHasPendingLiveCareRequest
? Container( ? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
@ -536,9 +536,11 @@ class _LandingPageState extends State<LandingPage> {
height: isTablet ? 290.h : 255.h, height: isTablet ? 290.h : 255.h,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 itemCount: () {
? myAppointmentsVM.patientAppointmentsHistoryList.length final int baseCount =
: 3, myAppointmentsVM.patientAppointmentsHistoryList.length + (immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0);
return baseCount < 3 ? baseCount : 3;
}(),
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h), padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -571,10 +573,14 @@ class _LandingPageState extends State<LandingPage> {
height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space
child: Builder( child: Builder(
builder: (context) { 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 final int swiperItemCount = myAppointmentsVM.isMyAppointmentsLoading
? 3 ? 3
: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 : totalCount < 3
? myAppointmentsVM.patientAppointmentsHistoryList.length ? totalCount
: 3; : 3;
final bool isArabic = appState.isArabic(); final bool isArabic = appState.isArabic();
return Swiper( return Swiper(

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

@ -171,9 +171,16 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
); );
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"mada",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: widget.totalAmount.toString(), amount: widget.totalAmount.toString(),
orderDescription: "Ancillary Order Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
@ -286,9 +293,16 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
); );
payfortViewModel.initiatePaymentWithCard( payfortViewModel.initiatePaymentWithCard(
"visa",
"0",
"false",
DateUtil.convertDateToString(DateTime.now()),
"0",
"0",
transactionID: transID, transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(), fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: widget.totalAmount.toString(), amount: widget.totalAmount.toString(),
orderDescription: "Ancillary Order Payment",
onSucceeded: (paymentResult) async { onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}"); print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); 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:async';
import 'dart:io';
import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:flutter/foundation.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/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.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_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/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -91,6 +93,45 @@ class _SplashScreenState extends State<SplashPage> {
); );
Clarity.initialize(context, config); 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 { navigateToTeleConsult() async {

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

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; 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/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
@ -60,8 +61,28 @@ class CustomButton extends StatelessWidget {
final fontS = fontSize ?? (16.f); final fontS = fontSize ?? (16.f);
final isButtonDisabled = isDisabled || onPressed == null; final isButtonDisabled = isDisabled || onPressed == null;
return GestureDetector( return Semantics(
onTap: isButtonDisabled ? null : onPressed, 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( child: Container(
height: height == null height: height == null
? 56.h ? 56.h
@ -120,6 +141,7 @@ class CustomButton extends StatelessWidget {
), ),
], ],
), ),
)); )),
);
} }
} }

@ -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" description: "New HMG Patient App"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.37+36 #version: 0.0.38+37
#version: 0.0.6+3 version: 0.0.7+1
environment: environment:
sdk: ">=3.6.0 <4.0.0" sdk: ">=3.6.0 <4.0.0"
@ -87,7 +87,7 @@ dependencies:
open_filex: ^4.7.0 open_filex: ^4.7.0
flutter_swiper_view: flutter_swiper_view:
path: package/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 in_app_update: ^4.2.5
# device_preview: ^1.3.1 # device_preview: ^1.3.1
image_picker_android: ^0.8.13+16 image_picker_android: ^0.8.13+16

Loading…
Cancel
Save