Compare commits

...

8 Commits

@ -381,6 +381,12 @@
"clickHere": "انقر هنا",
"iAgreeToTheTermsAndConditions": "أوافق على الشروط والأحكام ",
"iAgreeToTheTermsAndConditionsSubtitle": "أوافق على الشروط والأحكام ",
"iAgreeAndAcceptTermsAndConditions": "أوافق وأقبل ",
"termsAndConditions": "الشروط والأحكام",
"iAgreeHealthInfoCanBeUsed": "أوافق على استخدام معلوماتي الصحية للمقابلة. مزيد من المعلومات في ",
"privacyPolicyText": "سياسة الخصوصية",
"outPatient": "العيادات الخارجيه",
"thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة لاسترداد الأموال",
"save": "حفظ",
"userAgreement": "اتفاقية المستخدم",
"updateSuccessfully": "تم التحديث بنجاح",
@ -677,11 +683,20 @@
"emrgNo": "رقم جهة الاتصال في حالات الطوارئ",
"modes": "الأوضاع",
"vibration": "اهتزاز عند اللمس",
"blindModes": "أوضاع للمكفوفين جزئيًا",
"blindModes": "وضع للمكفوفين جزئياً",
"invertTheme": "عكس",
"offTheme": "إيقاف",
"dimTheme": "خافت",
"bwTheme": "أسود وأبيض",
"highContrastTheme": "تباين عالي",
"fontSize": "حجم الخط",
"fontSizeSmall": "صغير",
"fontSizeMedium": "متوسط",
"fontSizeLarge": "كبير",
"saveChanges": "حفظ التغييرات",
"settingsSavedSuccessfully": "تم حفظ الإعدادات بنجاح",
"cardHeading": "عنوان البطاقة",
"buttonText": "نص الزر",
"permissions": "إذن",
"cameraPermission": "الكاميرا",
"locationPermission": "الموقع",
@ -689,7 +704,7 @@
"outOfStockMsg": "لقد أضفت منتجًا غير متوفر حاليًا، يرجى إزالته!",
"noArabicLetters": "لا يُسمح باستخدام الحروف العربية",
"noOffersAvailable": "لا توجد عروض متاحة!",
"accessibility": "وضع الوصول",
"accessibility": "إمكانية الوصول",
"orderStatus": "حالة الطلب",
"findUs": "فروعنا",
"liveChat": "محادثة مباشرة",
@ -1440,6 +1455,9 @@
"symptomsSelector": "محدد الأعراض",
"possibleSymptomsRelatedTo": "أعراض مرتبطة بـ",
"suggestionAIListDescription": "هذه قائمة بالأعراض المقترحة من قبل الذكاء الاصطناعي، بناءً على المعلومات التي تم جمعها حتى الآن خلال المقابلة",
"whyAmIBeingAskedThis": "لماذا يُطرح علي هذا السؤال؟",
"weAreAskingThisQuestionToBetterUnderstand": "نطرح هذا السؤال لفهم {symptoms} بشكل أفضل وتحسين دقة التقييم.",
"analyzingYourSymptoms": "أعراضك",
"analyzing": "جاري التحليل...",
"seekMedicalAdvice": "اطلب المشورة الطبية",
"monitorDescription": "لا حاجة لطلب المشورة الطبية. فقط حافظ على روتين صحي.",
@ -1458,6 +1476,30 @@
"areYouSureYouWantToExitProgress": "هل تود الخروج؟ ستفقد تقدمك الحالي.",
"noQuestionAvailable": "لا يوجد سؤال متاح",
"possibleSymptom": "تشخيص محتمل",
"triageLevelEmergencyAmbulance": "طوارئ - اتصل بالإسعاف",
"triageLevelEmergency": "طوارئ",
"triageLevelConsultation24": "استشارة خلال 24 ساعة",
"triageLevelConsultation": "استشارة طبية",
"triageLevelSelfCare": "رعاية ذاتية",
"triageLevelEmergencyAmbulanceDesc": "الأعراض المبلغ عنها خطيرة جدًا وقد تحتاج إلى رعاية طارئة. يجب عليك الاتصال بالإسعاف الآن.",
"triageLevelEmergencyDesc": "تبدو الأدلة المبلغ عنها خطيرة ويجب عليك الذهاب إلى قسم الطوارئ. إذا لم تتمكن من ال<D8A7><D984>صول إلى أقرب قسم طوارئ، اتصل بالإسعاف.",
"triageLevelConsultation24Desc": "يجب عليك زيارة الطبيب خلال 24 ساعة. إذا ساءت الأعراض فجأة، توجه إلى أقرب قسم طوارئ.",
"triageLevelConsultationDesc": "قد تحتاج إلى تقييم طبي وقد تحتاج إلى تحديد موعد مع طبيب. إذا ساءت الأعراض، راجع الطبيب على الفور.",
"triageLevelSelfCareDesc": "قد لا تتطلب الأعراض المعلنة تقييمًا طبيًا وعادة ما تزول من تلقاء نفسها. في بعض الأحيان يمكن علاجها من خلال طرق الرعاية الذاتية. راقب أعراضك واستشر الطبيب إذا ساءت الأعراض أو ظهرت أعراض جديدة.",
"rootCauseEmergencyEvidencePresent": "تم الإبلاغ عن دليل طوارئ",
"rootCauseSeriousEvidencePresent": "تم الإبلاغ عن دليل خطير",
"rootCauseEmergencyConditionLikely": "حالة تهدد الحياة محتملة",
"rootCauseEmergencyConditionPossible": "حالة تهدد الحياة ممكنة",
"rootCauseConsultationConditionLikely": "استشارة طبية مطلوبة",
"rootCauseSelfCareSufficient": "لا يوجد سبب محدد للتقييم الطبي",
"rootCauseDiagnosisUnknown": "التقييم غير ممكن",
"teleconsultationAvailable": "الاستشارة عن بُعد متاحة",
"seriousSymptoms": "أعراض خطيرة",
"howTo": "كيفية القياس؟",
"explainWhy": "اشرح، لماذا؟",
"whyThisCondition": "لماذا هذه الحالة؟",
"supportingEvidence": "الأدلة الداعمة",
"explanationDescription": "بناءً على الأعراض التي أبلغت عنها، تم تحديد هذه الحالة كتطابق محتمل. الأعراض التالية تدعم هذا التقييم:",
"symptomsCheckerFindingScore": "نسبة التطابق",
"notSet": "غير محدد",
"years": "سنوات",
@ -1907,6 +1949,8 @@
"advanceNumber": "رقم الدفعة المقدمة",
"amountOnly": "المبلغ",
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"invoiceHistory": "سجل الفواتير",
"thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة للاسترداد",
"refund": "استرداد"
}

@ -377,6 +377,12 @@
"clickHere": "Click here",
"iAgreeToTheTermsAndConditions": "I agree to the terms and conditions ",
"iAgreeToTheTermsAndConditionsSubtitle": "I agree to the terms and conditions ",
"iAgreeAndAcceptTermsAndConditions": "I agree and accept ",
"termsAndConditions": "Terms and Conditions",
"iAgreeHealthInfoCanBeUsed": "I agree that my health information can be used for the interview. More information in the ",
"privacyPolicyText": "Privacy Policy",
"outPatient": "Out Patient",
"thisInvoiceIsNotEligibleForRefund": "This invoice is not eligible for refund",
"save": "Save",
"userAgreement": "User Agreement",
"updateSuccessfully": "Update Successfully",
@ -667,11 +673,20 @@
"emrgNo": "Emergency Contact Number",
"modes": "Modes",
"vibration": "Vibration Touch Feedback",
"blindModes": "Modes for Partially Blind",
"blindModes": "Mode for partially blind",
"invertTheme": "Invert",
"offTheme": "Off",
"dimTheme": "Dim",
"bwTheme": "Black and White",
"highContrastTheme": "High Contrast",
"fontSize": "Font Size",
"fontSizeSmall": "Small",
"fontSizeMedium": "Medium",
"fontSizeLarge": "Large",
"saveChanges": "Save Changes",
"settingsSavedSuccessfully": "Settings saved successfully",
"cardHeading": "Card Heading",
"buttonText": "Button Text",
"permissions": "Permission",
"cameraPermission": "Camera",
"locationPermission": "Location",
@ -679,7 +694,7 @@
"outOfStockMsg": "You have added product which is out of stock now, Please remove that!",
"noArabicLetters": "It Is Not Allow to Use Arabic Letters",
"noOffersAvailable": "No Offers Available!",
"accessibility": "Accessibility Mode",
"accessibility": "Accessibility",
"orderStatus": "Order Status",
"findUs": "Find Us",
"liveChat": "Live Chat",
@ -1432,6 +1447,9 @@
"symptomsSelector": "Symptoms Selector",
"possibleSymptomsRelatedTo": "Possible symptoms related to",
"suggestionAIListDescription": "This is a list of symptoms suggested by our AI, based on the information gathered so far during the interview",
"whyAmIBeingAskedThis": "Why am I being asked this?",
"weAreAskingThisQuestionToBetterUnderstand": "We are asking this question to better understand your {symptoms} and improve the accuracy of the assessment.",
"analyzingYourSymptoms": "your symptoms",
"analyzing": "Analyzing...",
"seekMedicalAdvice": "Seek Medical Advice",
"monitorDescription": "No need to seek medical advice. Just keep healthy routine.",
@ -1450,6 +1468,30 @@
"areYouSureYouWantToExitProgress": "Are you sure you want to exit? Your progress will be lost.",
"noQuestionAvailable": "No question available",
"possibleSymptom": "Possible symptom: ",
"triageLevelEmergencyAmbulance": "Emergency - Call Ambulance",
"triageLevelEmergency": "Emergency",
"triageLevelConsultation24": "Consultation Within 24 Hours",
"triageLevelConsultation": "Medical Consultation",
"triageLevelSelfCare": "Self-Care",
"triageLevelEmergencyAmbulanceDesc": "The reported symptoms are very serious and you may require emergency care. You should call an ambulance right now.",
"triageLevelEmergencyDesc": "The reported evidence appears serious and you should go to an emergency department. If you can't get to the nearest emergency department, call an ambulance.",
"triageLevelConsultation24Desc": "You should see a doctor within 24 hours. If the symptoms suddenly get worse, go to the nearest emergency department.",
"triageLevelConsultationDesc": "You may require medical evaluation and may need to schedule an appointment with a doctor. If the symptoms get worse, see a doctor immediately.",
"triageLevelSelfCareDesc": "The declared symptoms may not require a medical evaluation and they usually resolve on their own. Sometimes they can be treated through self-care methods. Observe your symptoms and consult a doctor if the symptoms get worse or new ones appear.",
"rootCauseEmergencyEvidencePresent": "Emergency evidence reported",
"rootCauseSeriousEvidencePresent": "Serious evidence reported",
"rootCauseEmergencyConditionLikely": "Life-threatening condition likely",
"rootCauseEmergencyConditionPossible": "Life-threatening condition possible",
"rootCauseConsultationConditionLikely": "Medical consultation required",
"rootCauseSelfCareSufficient": "No identified reason for medical evaluation",
"rootCauseDiagnosisUnknown": "Assessment not possible",
"teleconsultationAvailable": "Teleconsultation Available",
"seriousSymptoms": "Serious Symptoms",
"howTo": "How to?",
"explainWhy": "Explain, Why?",
"whyThisCondition": "Why this condition?",
"supportingEvidence": "Supporting Evidence",
"explanationDescription": "Based on your reported symptoms, this condition has been identified as a possible match. The following symptoms support this assessment:",
"symptomsCheckerFindingScore": "- Symptoms checker finding score",
"notSet": "Not set",
"years": "Years",
@ -1900,7 +1942,6 @@
"advanceNumber": "Advance Number",
"amountOnly": "Amount",
"invoiceHistory": "Invoice History"
}

@ -207,7 +207,7 @@ class ApiClientImp implements ApiClient {
if (url == 'https://uat.hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo') {
url = "https://hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo";
body['TokenID'] = "@dm!n";
}
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 4774167;
@ -259,7 +259,7 @@ class ApiClientImp implements ApiClient {
body: requestBody,
headers: headers,
);
// log("response: ${response.body.toString()}");
log("response: ${response.body.toString()}");
} on SocketException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));

@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT
@ -12,7 +12,7 @@ class ApiConsts {
static String rcBaseUrl = 'https://rc.hmg.com/'; // dRC API URL PROD
static String hmgPharmacyApiBaseUrl = 'https://hmgpharmacyapi.hmg.com/'; // symptoms API URL PROD
static String symptomsCheckerApiUAT = '${hmgPharmacyApiBaseUrl}symptomsapi/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerApiUAT = '${hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerApiLive = '${hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerApi = '${hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerScheduleAppointment = '$symptomsCheckerApi/ScheduleAppointment';
@ -211,6 +211,9 @@ class ApiConsts {
static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis';
static final String explain = '$symptomsCheckerApi/ExplainDiagnosisResult';
static final String getClinicFromCondition = '$symptomsCheckerApi/GetClinicsByCondition?condition=';
static final String getRationale = '$symptomsCheckerApi/GetRationale';
static final String getTriage = '$symptomsCheckerApi/GetTriage';
static final String getExplanations = '$symptomsCheckerApi/GetExplanations';
//E-REFERRAL SERVICES
static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes";
@ -1013,33 +1016,9 @@ class ApiKeyConstants {
static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng';
}
//flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_InsertUserActivity
// flutter: {"IdentificationNo":"2530976584","MobileNumber":"504278212","QuantityIntake":200,"VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":200.00,"PercentageConsumed":9.41,"PercentageLeft":90.59,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[{"Quantity":200.000,"CreatedDate":"\/Date(1766911222217+0300)\/"}],"VerificationCode":null,"isSMSSent":false}
// URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2o_UndoUserActivity
// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
// Progress":2 means weekly data
// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress
// flutter: {"Progress":2,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":null,"UserProgressForWeekData":[{"DayNumber":1,"DayDate":null,"DayName":"Sunday","PercentageConsumed":0},{"DayNumber":7,"DayDate":null,"DayName":"Saturday","PercentageConsumed":0},{"DayNumber":6,"DayDate":null,"DayName":"Friday","PercentageConsumed":0},{"DayNumber":5,"DayDate":null,"DayName":"Thursday","PercentageConsumed":0},{"DayNumber":4,"DayDate":null,"DayName":"Wednesday","PercentageConsumed":0},{"DayNumber":3,"DayDate":null,"DayName":"Tuesday","PercentageConsumed":0},{"DayNumber":2,"DayDate":null,"DayName":"Monday","PercentageConsumed":0}],"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
// Progress":1 means daily data
//URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress
// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
// Progress":1 means monthly data
// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress
// flutter: {"Progress":3,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417}
// flutter: response.body:
// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":[{"MonthNumber":1,"MonthName":"January","PercentageConsumed":0},{"MonthNumber":2,"MonthName":"February","PercentageConsumed":0},{"MonthNumber":3,"MonthName":"March","PercentageConsumed":0},{"MonthNumber":4,"MonthName":"April","PercentageConsumed":0},{"MonthNumber":5,"MonthName":"May","PercentageConsumed":0},{"MonthNumber":6,"MonthName":"June","PercentageConsumed":0},{"MonthNumber":7,"MonthName":"July","PercentageConsumed":0},{"MonthNumber":8,"MonthName":"August","PercentageConsumed":0},{"MonthNumber":9,"MonthName":"September","PercentageConsumed":0},{"MonthNumber":10,"MonthName":"October","PercentageConsumed":0},{"MonthNumber":11,"MonthName":"November","PercentageConsumed":0},{"MonthNumber":12,"MonthName":"December","PercentageConsumed":0}],"UserProgressForTodayData":null,"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false}
class AppUrls {
static final hmgTermsUrlEn = 'https://hmg.com/en/Pages/Terms.aspx';
static final hmgPrivacyUrlEn = 'https://hmg.com/en/Pages/Privacy.aspx';
static final hmgTermsUrlAr = 'https://hmg.com/ar/Pages/Terms.aspx';
static final hmgPrivacyUrlAr = 'https://hmg.com/ar/Pages/Privacy.aspx';
}

@ -26,6 +26,8 @@ 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/dialogs/confirm_dialog.dart';
import 'package:hmg_patient_app_new/widgets/loading_dialog.dart';
import 'package:hmg_patient_app_new/widgets/accessibility/preserve_image_colors.dart';
import 'package:hmg_patient_app_new/widgets/accessibility/preserve_svg_colors.dart';
import 'package:lottie/lottie.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -120,10 +122,8 @@ class Utils {
// ),
// ));
return !isAddHours
? DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US")
.format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.toLocal())
: DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US")
.format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.add(
? DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US").format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.toLocal())
: DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US").format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.add(
Duration(
hours: isAddHours ? 3 : 0,
),
@ -356,12 +356,9 @@ class Utils {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: isSmallWidget ? 0.h : 48.h),
Lottie.asset(AppAnimations.noData,
repeat: false, reverse: false, frameRate: FrameRate(60), width: width.w, height: height.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.noData, repeat: false, reverse: false, frameRate: FrameRate(60), width: width.w, height: height.h, fit: BoxFit.fill),
SizedBox(height: 16.h),
(noDataText ?? LocaleKeys.noDataAvailable.tr())
.toText14(isBold: true, color: AppColors.greyTextColor, isCenter: true)
.paddingSymmetrical(64.w, 0.h),
(noDataText ?? LocaleKeys.noDataAvailable.tr()).toText14(isBold: true, color: AppColors.greyTextColor, isCenter: true).paddingSymmetrical(64.w, 0.h),
SizedBox(height: 16.h),
callToActionButton
],
@ -373,8 +370,7 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.loadingAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor, isCenter: true, isBold: true),
SizedBox(height: 8.h),
@ -400,8 +396,7 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 8.h),
@ -502,8 +497,7 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.warningAnimation,
repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.warningAnimation, repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(loadingText ?? LocaleKeys.loadingText.tr()).toText15(color: AppColors.blackColor, letterSpacing: 0),
SizedBox(height: 16.h),
@ -623,10 +617,8 @@ class Utils {
}
// Replace HTML line breaks with newlines
var withLineBreaks = htmlString
.replaceAll(RegExp(r'<br\s*\/?>', multiLine: true), '\n')
.replaceAll(RegExp(r'<\/p>', multiLine: true), '\n')
.replaceAll(RegExp(r'<divider>', multiLine: true), '\n');
var withLineBreaks =
htmlString.replaceAll(RegExp(r'<br\s*\/?>', multiLine: true), '\n').replaceAll(RegExp(r'<\/p>', multiLine: true), '\n').replaceAll(RegExp(r'<divider>', multiLine: true), '\n');
// Remove all other HTML tags
var withoutTags = withLineBreaks.replaceAll(RegExp(r'<[^>]*>'), '');
@ -704,20 +696,7 @@ class Utils {
final year = parts[0];
// Map month number to short month name (Hijri months)
const hijriMonthNames = [
'Muharram',
'Safar',
'Rabi I',
'Rabi II',
'Jumada I',
'Jumada II',
'Rajab',
'Sha\'ban',
'Ramadan',
'Shawwal',
'Dhu al-Qi\'dah',
'Dhu al-Hijjah'
];
const hijriMonthNames = ['Muharram', 'Safar', 'Rabi I', 'Rabi II', 'Jumada I', 'Jumada II', 'Rajab', 'Sha\'ban', 'Ramadan', 'Shawwal', 'Dhu al-Qi\'dah', 'Dhu al-Hijjah'];
final monthIndex = int.tryParse(parts[1]) ?? 1;
final month = hijriMonthNames[monthIndex - 1];
@ -774,21 +753,23 @@ class Utils {
String? errorAsset,
}) {
final Color? resolvedColor = iconColor ?? (applyThemeColor && AppColors.isDarkMode ? AppColors.textColor : null);
return SvgPicture.asset(
icon,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withOpacity(0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
errorBuilder: errorAsset != null
? (context, error, stackTrace) => SvgPicture.asset(
errorAsset,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withOpacity(0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
)
: null,
return PreserveSvgColors(
child: SvgPicture.asset(
icon,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withValues(alpha: 0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
errorBuilder: errorAsset != null
? (context, error, stackTrace) => SvgPicture.asset(
errorAsset,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withValues(alpha: 0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
)
: null,
),
);
}
@ -802,14 +783,16 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return SvgPicture.network(
url,
colorFilter: ColorFilter.mode(
isDisabled ? iconColor.withOpacity(0.5) : iconColor,
BlendMode.srcIn,
return PreserveSvgColors(
child: SvgPicture.network(
url,
colorFilter: ColorFilter.mode(
isDisabled ? iconColor.withValues(alpha: 0.5) : iconColor,
BlendMode.srcIn,
),
width: iconW,
height: iconH,
),
width: iconW,
height: iconH,
);
}
@ -825,14 +808,15 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return Container(
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: border != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
return PreserveImageColors(
child: Container(
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: border != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
),
child: Image.asset(icon, width: iconW, height: iconH, fit: fit),
),
child: Image.asset(icon, width: iconW, height: iconH, fit: fit),
);
// return Image.asset(icon, width: width, height: height, fit: fit, );
}
static Widget buildImgWithAssetsWithBorderRadius({
@ -847,21 +831,23 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return Container(
width: iconW,
height: iconH,
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: borderRadius != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
image: DecorationImage(
image: AssetImage(
icon,
),
fit: fit)),
return PreserveImageColors(
child: Container(
width: iconW,
height: iconH,
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: BorderRadius.circular(borderRadius ?? 12.r),
image: DecorationImage(
image: AssetImage(
icon,
),
fit: fit)),
),
);
}
/// Widget to build an SVG from network
/// Widget to build an image from network - automatically preserves colors in visual modes
static Widget buildImgWithNetwork({
required String url,
required Color iconColor,
@ -873,16 +859,18 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return Image.network(
url,
width: iconW,
height: iconH,
fit: fit,
errorBuilder: errorBuilder ??
(_, __, ___) {
//todo_section change the error builder icon that it is returning
return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.no_visit_icon);
},
return PreserveImageColors(
child: Image.network(
url,
width: iconW,
height: iconH,
fit: fit,
errorBuilder: errorBuilder ??
(_, __, ___) {
//todo_section change the error builder icon that it is returning
return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.no_visit_icon);
},
),
);
}
@ -891,26 +879,37 @@ class Utils {
spacing: spacing.w,
mainAxisSize: MainAxisSize.max,
children: [
Image.asset(AppAssets.mada, width: width.h, height: height.h),
Image.asset(
AppAssets.tamaraEng,
width: width.h,
height: height.h,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
debugPrint('Failed to load Tamara PNG in payment methods: $error');
return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 35.h, height: 35.h, fit: BoxFit.contain);
},
PreserveImageColors(
child: Image.asset(AppAssets.mada, width: width.h, height: height.h),
),
PreserveImageColors(
child: Image.asset(
AppAssets.tamaraEng,
width: width.h,
height: height.h,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
debugPrint('Failed to load Tamara PNG in payment methods: $error');
return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 35.h, height: 35.h, fit: BoxFit.contain);
},
),
),
PreserveImageColors(
child: Image.asset(AppAssets.visa, width: width.h, height: height.h),
),
Image.asset(AppAssets.visa, width: width.h, height: height.h),
Image.asset(AppAssets.mastercard, width: width.h, height: height.h),
Platform.isIOS ? Image.asset(AppAssets.applePay, width: width.h, height: height.h) : SizedBox.shrink(),
PreserveImageColors(
child: Image.asset(AppAssets.mastercard, width: width.h, height: height.h),
),
Platform.isIOS
? PreserveImageColors(
child: Image.asset(AppAssets.applePay, width: width.h, height: height.h),
)
: SizedBox.shrink(),
],
);
}
static Widget getPaymentAmountWithSymbol(Widget paymentAmountWidget, Color iconColor, double iconSize,
{bool isSaudiCurrency = true, bool isExpanded = true}) {
static Widget getPaymentAmountWithSymbol(Widget paymentAmountWidget, Color iconColor, double iconSize, {bool isSaudiCurrency = true, bool isExpanded = true}) {
return Row(
mainAxisAlignment: isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
children: [
@ -965,13 +964,7 @@ class Utils {
// text: NumberFormat.currency(locale: 'en_US', symbol: " ", decimalDigits: 0).format(habibWalletAmount),
// text: " ${(NumberFormat.decimalPattern().format(habibWalletAmount)).toStringAsFixed(2)}",
text: " ${(NumberFormat.decimalPattern().format(habibWalletAmount))}",
style: TextStyle(
color: resolvedTextColor,
fontSize: fontSize ?? 32.f,
letterSpacing: letterSpacing ?? -4,
fontFamily: "Poppins",
fontWeight: fontWeight ?? FontWeight.w600,
height: 1),
style: TextStyle(color: resolvedTextColor, fontSize: fontSize ?? 32.f, letterSpacing: letterSpacing ?? -4, fontFamily: "Poppins", fontWeight: fontWeight ?? FontWeight.w600, height: 1),
),
],
),
@ -1035,12 +1028,7 @@ class Utils {
static PatientDoctorAppointmentList? convertToPatientDoctorAppointmentList(HospitalsModel? hospital) {
if (hospital == null) return null;
return PatientDoctorAppointmentList(
filterName: hospital.name,
distanceInKMs: hospital.distanceInKilometers?.toString(),
projectTopName: hospital.name,
projectBottomName: hospital.name,
model: hospital,
isHMC: hospital.isHMC);
filterName: hospital.name, distanceInKMs: hospital.distanceInKilometers?.toString(), projectTopName: hospital.name, projectBottomName: hospital.name, model: hospital, isHMC: hospital.isHMC);
}
static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) {

@ -127,6 +127,7 @@ extension EmailValidator on String {
Widget toText12(
{bool isEnglishOnly = false,
Color? color,
Color? decorationColor,
bool isUnderLine = false,
TextAlign textAlignment = TextAlign.start,
bool isBold = false,
@ -147,7 +148,7 @@ extension EmailValidator on String {
color: color ?? AppColors.blackColor,
letterSpacing: letterSpacing ?? 0,
height: height,
decorationColor: isUnderLine ? AppColors.blackColor : null,
decorationColor: decorationColor ?? AppColors.blackColor,
decoration: isUnderLine ? TextDecoration.underline : null,
fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins',
),

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
class LaserBodyParts {
static final LaserBodyParts _instance = LaserBodyParts._internal();
@ -9,11 +9,11 @@ class LaserBodyParts {
factory LaserBodyParts() => _instance;
static Widget image(String assetUri) {
return new SvgPicture.asset(assetUri, fit: BoxFit.cover, width: 88, height: 88);
return Utils.buildSvgWithAssets(icon: assetUri, fit: BoxFit.cover, width: 88, height: 88, applyThemeColor: false);
}
static Widget imagePng(String assetUri) {
return Image.asset(assetUri, fit: BoxFit.cover, width: 88, height: 88);
return Utils.buildImgWithAssets(icon: assetUri, fit: BoxFit.cover, width: 88, height: 88);
}
Map<String, Widget> maleBodyMap = {

@ -16,6 +16,8 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
class ProfileSettingsViewModel extends ChangeNotifier {
static const String _darkModeKey = 'is_dark_mode';
static const String _fontSizeStepKey = 'accessibility_font_size_step';
static const String _visualModeKey = 'accessibility_visual_mode';
final CacheService _cacheService;
final ProfileSettingsRepo profileSettingsRepo;
@ -25,6 +27,13 @@ class ProfileSettingsViewModel extends ChangeNotifier {
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
// Accessibility settings
int _fontSizeStep = 2; // 0: Small, 1: Small-Medium, 2: Medium (default), 3: Medium-Large, 4: Large
int get fontSizeStep => _fontSizeStep;
String _visualMode = 'off'; // off, invert, dim, highContrast
String get visualMode => _visualMode;
// Update email state
bool isUpdateEmailLoading = false;
bool isUpdateEmailSuccess = false;
@ -163,6 +172,47 @@ class ProfileSettingsViewModel extends ChangeNotifier {
notifyListeners();
}
// Accessibility settings
/// Load accessibility settings at app startup
void loadAccessibilitySettings() {
_fontSizeStep = _cacheService.getInt(key: _fontSizeStepKey) ?? 2;
_visualMode = _cacheService.getString(key: _visualModeKey) ?? 'off';
// No notifyListeners() here we are called before build.
}
/// Update font size step and save to cache
void setFontSizeStep(int step) {
_fontSizeStep = step;
_cacheService.saveInt(key: _fontSizeStepKey, value: step);
notifyListeners();
}
/// Update visual mode and save to cache
void setVisualMode(String mode) {
_visualMode = mode;
_cacheService.saveString(key: _visualModeKey, value: mode);
notifyListeners();
}
/// Get font scale multiplier based on current font size step
double getFontScaleMultiplier() {
switch (_fontSizeStep) {
case 0:
return 0.85; // Small
case 1:
return 0.95; // Small-Medium
case 2:
return 1.0; // Medium (default)
case 3:
return 1.15; // Medium-Large
case 4:
return 1.3; // Large
default:
return 1.0;
}
}
// Update patient info
Future<void> updatePatientInfo({

@ -0,0 +1,147 @@
class ExplanationsResponseModel {
final ExplanationsDataDetails? dataDetails;
ExplanationsResponseModel({this.dataDetails});
factory ExplanationsResponseModel.fromJson(Map<String, dynamic> json) {
return ExplanationsResponseModel(
dataDetails: json['dataDetails'] != null
? ExplanationsDataDetails.fromJson(json['dataDetails'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class ExplanationsDataDetails {
final List<ExplanationEvidence>? supportingEvidence;
final List<ExplanationEvidence>? conflictingEvidence;
final List<ExplanationEvidence>? unconfirmedEvidence;
final String? message;
final List<String>? errorList;
final int? id;
final String? language;
final String? generalId;
final String? createDate;
final String? lastEditDate;
final String? createdBy;
final String? lastEditBy;
final bool? active;
final int? sortOrder;
final int? userType;
final String? userId;
ExplanationsDataDetails({
this.supportingEvidence,
this.conflictingEvidence,
this.unconfirmedEvidence,
this.message,
this.errorList,
this.id,
this.language,
this.generalId,
this.createDate,
this.lastEditDate,
this.createdBy,
this.lastEditBy,
this.active,
this.sortOrder,
this.userType,
this.userId,
});
factory ExplanationsDataDetails.fromJson(Map<String, dynamic> json) {
return ExplanationsDataDetails(
supportingEvidence: json['supporting_evidence'] != null
? (json['supporting_evidence'] as List)
.map((item) => ExplanationEvidence.fromJson(item))
.toList()
: null,
conflictingEvidence: json['conflicting_evidence'] != null
? (json['conflicting_evidence'] as List)
.map((item) => ExplanationEvidence.fromJson(item))
.toList()
: null,
unconfirmedEvidence: json['unconfirmed_evidence'] != null
? (json['unconfirmed_evidence'] as List)
.map((item) => ExplanationEvidence.fromJson(item))
.toList()
: null,
message: json['Message'],
errorList: json['ErrorList'] != null
? List<String>.from(json['ErrorList'])
: null,
id: json['Id'],
language: json['language'],
generalId: json['generalId'],
createDate: json['CreateDate'],
lastEditDate: json['LastEditDate'],
createdBy: json['CreatedBy'],
lastEditBy: json['LastEditBy'],
active: json['Active'],
sortOrder: json['SortOrder'],
userType: json['userType'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'supporting_evidence': supportingEvidence?.map((item) => item.toJson()).toList(),
'conflicting_evidence': conflictingEvidence?.map((item) => item.toJson()).toList(),
'unconfirmed_evidence': unconfirmedEvidence?.map((item) => item.toJson()).toList(),
'Message': message,
'ErrorList': errorList,
'Id': id,
'language': language,
'generalId': generalId,
'CreateDate': createDate,
'LastEditDate': lastEditDate,
'CreatedBy': createdBy,
'LastEditBy': lastEditBy,
'Active': active,
'SortOrder': sortOrder,
'userType': userType,
'userId': userId,
};
}
}
class ExplanationEvidence {
final String? id;
final String? name;
final String? commonName;
ExplanationEvidence({
this.id,
this.name,
this.commonName,
});
factory ExplanationEvidence.fromJson(Map<String, dynamic> json) {
return ExplanationEvidence(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -0,0 +1,176 @@
class RationaleResponseModel {
final RationaleDataDetails? dataDetails;
RationaleResponseModel({this.dataDetails});
factory RationaleResponseModel.fromJson(Map<String, dynamic> json) {
return RationaleResponseModel(
dataDetails: json['dataDetails'] != null
? RationaleDataDetails.fromJson(json['dataDetails'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class RationaleDataDetails {
final String? type;
final List<RationaleObservationParam>? observationParams;
final List<RationaleConditionParam>? conditionParams;
final String? message;
final List<String>? errorList;
final int? id;
final String? language;
final String? generalId;
final String? createDate;
final String? lastEditDate;
final String? createdBy;
final String? lastEditBy;
final bool? active;
final int? sortOrder;
final int? userType;
final String? userId;
RationaleDataDetails({
this.type,
this.observationParams,
this.conditionParams,
this.message,
this.errorList,
this.id,
this.language,
this.generalId,
this.createDate,
this.lastEditDate,
this.createdBy,
this.lastEditBy,
this.active,
this.sortOrder,
this.userType,
this.userId,
});
factory RationaleDataDetails.fromJson(Map<String, dynamic> json) {
return RationaleDataDetails(
type: json['type'],
observationParams: json['observation_params'] != null
? (json['observation_params'] as List)
.map((item) => RationaleObservationParam.fromJson(item))
.toList()
: null,
conditionParams: json['condition_params'] != null
? (json['condition_params'] as List)
.map((item) => RationaleConditionParam.fromJson(item))
.toList()
: null,
message: json['Message'],
errorList: json['ErrorList'] != null
? List<String>.from(json['ErrorList'])
: null,
id: json['Id'],
language: json['language'],
generalId: json['generalId'],
createDate: json['CreateDate'],
lastEditDate: json['LastEditDate'],
createdBy: json['CreatedBy'],
lastEditBy: json['LastEditBy'],
active: json['Active'],
sortOrder: json['SortOrder'],
userType: json['userType'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'type': type,
'observation_params': observationParams?.map((item) => item.toJson()).toList(),
'condition_params': conditionParams?.map((item) => item.toJson()).toList(),
'Message': message,
'ErrorList': errorList,
'Id': id,
'language': language,
'generalId': generalId,
'CreateDate': createDate,
'LastEditDate': lastEditDate,
'CreatedBy': createdBy,
'LastEditBy': lastEditBy,
'Active': active,
'SortOrder': sortOrder,
'userType': userType,
'userId': userId,
};
}
}
class RationaleObservationParam {
final String? id;
final String? name;
final String? commonName;
RationaleObservationParam({
this.id,
this.name,
this.commonName,
});
factory RationaleObservationParam.fromJson(Map<String, dynamic> json) {
return RationaleObservationParam(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}
class RationaleConditionParam {
final String? id;
final String? name;
final String? commonName;
RationaleConditionParam({
this.id,
this.name,
this.commonName,
});
factory RationaleConditionParam.fromJson(Map<String, dynamic> json) {
return RationaleConditionParam(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -0,0 +1,151 @@
class TriageLevelResponseModel {
final TriageLevelDataDetails? dataDetails;
TriageLevelResponseModel({this.dataDetails});
factory TriageLevelResponseModel.fromJson(Map<String, dynamic> json) {
return TriageLevelResponseModel(
dataDetails: json['dataDetails'] != null
? TriageLevelDataDetails.fromJson(json['dataDetails'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class TriageLevelDataDetails {
final String? triageLevel;
final List<SeriousEvidence>? serious;
final String? rootCause;
final bool? teleconsultationApplicable;
final String? message;
final List<String>? errorList;
final int? id;
final String? language;
final String? generalId;
final String? createDate;
final String? lastEditDate;
final String? createdBy;
final String? lastEditBy;
final bool? active;
final int? sortOrder;
final int? userType;
final String? userId;
TriageLevelDataDetails({
this.triageLevel,
this.serious,
this.rootCause,
this.teleconsultationApplicable,
this.message,
this.errorList,
this.id,
this.language,
this.generalId,
this.createDate,
this.lastEditDate,
this.createdBy,
this.lastEditBy,
this.active,
this.sortOrder,
this.userType,
this.userId,
});
factory TriageLevelDataDetails.fromJson(Map<String, dynamic> json) {
return TriageLevelDataDetails(
triageLevel: json['triage_level'],
serious: json['serious'] != null
? (json['serious'] as List)
.map((item) => SeriousEvidence.fromJson(item))
.toList()
: null,
rootCause: json['root_cause'],
teleconsultationApplicable: json['teleconsultation_applicable'],
message: json['Message'],
errorList: json['ErrorList'] != null
? List<String>.from(json['ErrorList'])
: null,
id: json['Id'],
language: json['language'],
generalId: json['generalId'],
createDate: json['CreateDate'],
lastEditDate: json['LastEditDate'],
createdBy: json['CreatedBy'],
lastEditBy: json['LastEditBy'],
active: json['Active'],
sortOrder: json['SortOrder'],
userType: json['userType'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'triage_level': triageLevel,
'serious': serious?.map((item) => item.toJson()).toList(),
'root_cause': rootCause,
'teleconsultation_applicable': teleconsultationApplicable,
'Message': message,
'ErrorList': errorList,
'Id': id,
'language': language,
'generalId': generalId,
'CreateDate': createDate,
'LastEditDate': lastEditDate,
'CreatedBy': createdBy,
'LastEditBy': lastEditBy,
'Active': active,
'SortOrder': sortOrder,
'userType': userType,
'userId': userId,
};
}
}
class SeriousEvidence {
final String? id;
final String? name;
final String? commonName;
final String? seriousness;
final bool? isEmergency;
SeriousEvidence({
this.id,
this.name,
this.commonName,
this.seriousness,
this.isEmergency,
});
factory SeriousEvidence.fromJson(Map<String, dynamic> json) {
return SeriousEvidence(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
seriousness: json['seriousness'],
isEmergency: json['is_emergency'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
'seriousness': seriousness,
'is_emergency': isEmergency,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -91,18 +91,34 @@ class TriageDataDetails {
class TriageQuestion {
final int? type;
final String? text;
final String? explication;
final List<String>? instruction;
final List<TriageQuestionItem>? items;
TriageQuestion({
this.type,
this.text,
this.explication,
this.instruction,
this.items,
});
factory TriageQuestion.fromJson(Map<String, dynamic> json) {
// TESTING: Add dummy instructions to every question for testing UI
final dummyInstructions = [
"Clean the thermometer using soap and cold water.",
"For rectal insertion, apply a small amount of petroleum jelly to the end of the thermometer.",
"Insert the silver tip into the anus or mouth.",
"Hold the thermometer in place for 2 minutes, or as long as recommended by the manufacturer.",
];
final dummyExplication = "The most accurate measurement is rectal temperature. You can also measure the temperature under the armpit or use contactless thermometers. However, using the contact thermometer is more accurate. Follow the thermometer manufacturer's instructions. A temperature above 37°C or 98.6°F is classified as a fever.";
return TriageQuestion(
type: json['type'],
text: json['text'],
explication: json['explication'] ?? dummyExplication, // Use dummy if null
instruction: json['instruction'] != null ? List<String>.from(json['instruction']) : dummyInstructions, // Use dummy if null
items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null,
);
}
@ -111,9 +127,18 @@ class TriageQuestion {
return {
'type': type,
'text': text,
'explication': explication,
'instruction': instruction,
'items': items?.map((item) => item.toJson()).toList(),
};
}
/// Check if instruction OR explication is available
bool get hasInstructions {
final hasInstruction = instruction != null && instruction!.isNotEmpty;
final hasExplication = explication != null && explication!.isNotEmpty;
return hasInstruction || hasExplication;
}
}
class TriageQuestionItem {

@ -8,10 +8,13 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/explanations_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/rationale_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -40,7 +43,7 @@ abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> evidenceList,
required String language,
required String userSessionToken,
required String sessionId,
@ -49,7 +52,7 @@ abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> initialEvidenceList,
List<Map<String, String>>? triageEvidence,
required String language,
required String userSessionToken,
@ -66,6 +69,34 @@ abstract class SymptomsCheckerRepo {
required ScheduleAppointmentRequestModel request,
required String userSessionToken,
});
Future<Either<Failure, GenericApiModel<RationaleResponseModel>>> getRationale({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
});
Future<Either<Failure, GenericApiModel<TriageLevelResponseModel>>> getTriage({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
});
Future<Either<Failure, GenericApiModel<ExplanationsResponseModel>>> getExplanations({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String target,
required String generalId,
required String userSessionToken,
});
}
class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
@ -201,12 +232,20 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
required String userSessionToken,
required String sessionId,
}) async {
// Build evidence list with "initial" source for symptoms
final List<Map<String, dynamic>> evidenceList = evidenceIds.map((id) {
return {
"id": id,
"source": "initial",
};
}).toList();
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"evidence": evidenceList,
"language": language,
"generalId": sessionId,
};
@ -265,21 +304,19 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> initialEvidenceList,
List<Map<String, String>>? triageEvidence, // Additional triage-specific evidence
required String language,
required String userSessionToken,
required String sessionId,
}) async {
// Build evidence list: combine initial symptoms with triage evidence
// Build evidence list: combine initial evidence (with sources) with triage evidence
List<Map<String, dynamic>> evidenceList = [];
// Add initial evidence as simple IDs
for (var id in evidenceIds) {
evidenceList.add({"id": id});
}
// Add initial evidence (already has sources: initial, red_flags, suggest)
evidenceList.addAll(initialEvidenceList);
// Add triage evidence with proper format (id, choice_id, source)
// Add triage evidence with proper format (id, choice_id, source=triage)
if (triageEvidence != null && triageEvidence.isNotEmpty) {
evidenceList.addAll(triageEvidence);
}
@ -351,7 +388,7 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> evidenceList,
required String language,
required String userSessionToken,
required String sessionId,
@ -361,7 +398,7 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
"value": age,
},
"gender": gender,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"evidence": evidenceList,
"language": language,
"generalId": sessionId,
};
@ -529,4 +566,210 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RationaleResponseModel>>> getRationale({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidence,
"language": language,
"generalId": generalId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<RationaleResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRationale,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetRationale API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
RationaleResponseModel rationaleResponse = RationaleResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<RationaleResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: rationaleResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetRationale response: $e");
loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getRationale: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<TriageLevelResponseModel>>> getTriage({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidence,
"language": language,
"generalId": generalId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<TriageLevelResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getTriage,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetTriage API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
TriageLevelResponseModel triageResponse = TriageLevelResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<TriageLevelResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: triageResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetTriage response: $e");
loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getTriage: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<ExplanationsResponseModel>>> getExplanations({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String target,
required String generalId,
required String userSessionToken,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidence,
"language": language,
"target": target,
"generalId": generalId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<ExplanationsResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getExplanations,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetExplanations API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
ExplanationsResponseModel explanationsResponse = ExplanationsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<ExplanationsResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: explanationsResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetExplanations response: $e");
loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getExplanations: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
}

@ -8,10 +8,13 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/explanations_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/rationale_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -66,6 +69,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
bool isSuggestionsLoading = false;
bool isTriageDiagnosisLoading = false;
bool isScheduleAppointmentLoading = false;
bool isRationaleLoading = false;
bool isTriageLevelLoading = false;
bool isExplanationsLoading = false;
// Flag to track if appointment is being booked via symptoms checker flow
bool isBookingFromSymptomsChecker = false;
@ -76,6 +82,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
RiskAndSuggestionsResponseModel? riskFactorsResponse;
RiskAndSuggestionsResponseModel? suggestionsResponse;
TriageDataDetails? triageDataDetails;
RationaleResponseModel? rationaleResponse;
TriageLevelResponseModel? triageLevelResponse;
ExplanationsResponseModel? explanationsResponse;
// Triage state
int? _selectedTriageChoiceIndex; // Deprecated - keeping for backward compatibility
@ -87,6 +96,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
String? _selectedSingleItemId; // Store which item was selected
int? _selectedSingleChoiceIndex; // Store which choice was selected
// Triage question history tracking
final List<TriageDataDetails> _triageQuestionHistory = []; // Stack of previous questions
final List<List<Map<String, String>>> _triageEvidenceHistory = []; // Stack of evidence states at each question
// Selected risk factors tracking
final Set<String> _selectedRiskFactorIds = {};
@ -178,6 +191,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Get the number of triage questions answered
int get triageQuestionCount => _triageQuestionCount;
/// Check if this is the first triage question (no history)
bool get isFirstTriageQuestion => _triageQuestionHistory.isEmpty;
List<GetClinicDetailsResponseModel> clinicDetailsList = [];
/// Get choice index for a specific item
@ -745,6 +761,44 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return evidenceIds;
}
/// Get all evidence with their sources based on selection step
/// Source values:
/// - "initial" for symptoms (used in GetRiskFactors)
/// - "red_flags" for risk factors (used in GetSuggestions)
/// - "suggest" for suggestions (used in GetDiagnosis)
List<Map<String, dynamic>> getAllEvidenceWithSources({String defaultSource = "initial"}) {
List<Map<String, dynamic>> evidenceList = [];
// Add selected symptoms with source "initial"
final selectedSymptoms = getAllSelectedSymptoms();
for (var symptom in selectedSymptoms.where((s) => s.id != null)) {
evidenceList.add({
"id": symptom.id!,
"source": "initial",
});
}
// Add selected risk factors with source "red_flags" (excluding "not_applicable")
final selectedRiskFactors = getAllSelectedRiskFactors();
for (var riskFactor in selectedRiskFactors.where((rf) => rf.id != null && rf.id != "not_applicable")) {
evidenceList.add({
"id": riskFactor.id!,
"source": "red_flags",
});
}
// Add selected suggestions with source "suggest" (excluding "not_applicable")
final selectedSuggestions = getAllSelectedSuggestions();
for (var suggestion in selectedSuggestions.where((s) => s.id != null && s.id != "not_applicable")) {
evidenceList.add({
"id": suggestion.id!,
"source": "suggest",
});
}
return evidenceList;
}
/// Fetch risk factors based on selected symptoms
Future<void> fetchSuggestions({
Function()? onSuccess,
@ -768,21 +822,30 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return;
}
// Extract symptom IDs
List<String> evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList();
// Build evidence list with proper sources
List<Map<String, dynamic>> evidenceList = [];
// Get all selected symptoms
final selectedRisks = getAllSelectedRiskFactors();
// Add selected symptoms with source "initial"
for (var symptom in selectedSymptoms.where((s) => s.id != null)) {
evidenceList.add({
"id": symptom.id!,
"source": "initial",
});
}
if (selectedRisks.isNotEmpty) {
List<String> evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList();
evidenceIds.addAll(evidenceRisksIds);
// Get selected risk factors and add with source "red_flags"
final selectedRisks = getAllSelectedRiskFactors();
for (var riskFactor in selectedRisks.where((rf) => rf.id != null && rf.id != "not_applicable")) {
evidenceList.add({
"id": riskFactor.id!,
"source": "red_flags",
});
}
await getSuggestions(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidenceIds: evidenceIds,
evidenceList: evidenceList,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
@ -801,7 +864,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
Future<void> getSuggestions({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> evidenceList,
required String language,
Function(RiskAndSuggestionsResponseModel)? onSuccess,
Function(String)? onError,
@ -812,7 +875,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
final result = await symptomsCheckerRepo.getSuggestions(
age: age,
gender: gender,
evidenceIds: evidenceIds,
evidenceList: evidenceList,
language: language,
sessionId: currentSessionId,
userSessionToken: currentSessionAuthToken,
@ -863,19 +926,27 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
Future<void> getDiagnosisForTriage({
required int age,
required int gender,
required List<String> evidenceIds,
List<Map<String, String>>? triageEvidence,
required List<Map<String, dynamic>> initialEvidenceList, // Evidence with sources (initial, red_flags, suggest)
List<Map<String, String>>? triageEvidence, // Triage-specific evidence (id, choice_id, source=triage)
required String language,
Function(TriageDataDetails)? onSuccess,
Function(String)? onError,
bool isGoingBack = false, // Flag to indicate if we're going back in history
}) async {
// Save current question and evidence to history BEFORE making new API call
// (but only if we're moving forward, not going back)
if (!isGoingBack && triageDataDetails != null) {
_triageQuestionHistory.add(triageDataDetails!);
_triageEvidenceHistory.add(List<Map<String, String>>.from(_triageEvidenceList));
}
isTriageDiagnosisLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getDiagnosisForTriage(
age: age,
gender: gender,
evidenceIds: evidenceIds,
initialEvidenceList: initialEvidenceList,
triageEvidence: triageEvidence,
language: language,
sessionId: currentSessionId,
@ -923,10 +994,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return;
}
// Get all evidence IDs
final evidenceIds = getAllEvidenceIds();
// Get all evidence with sources
final evidenceList = getAllEvidenceWithSources();
if (evidenceIds.isEmpty) {
if (evidenceList.isEmpty) {
if (onError != null) {
onError('No evidence selected');
}
@ -936,7 +1007,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
await getDiagnosisForTriage(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidenceIds: evidenceIds,
initialEvidenceList: evidenceList,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
@ -993,7 +1064,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_triageEvidenceList.add({
"id": itemId,
"choice_id": choiceId,
"source": "triage",
"source": "suggest",
});
notifyListeners();
}
@ -1009,6 +1080,39 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
/// Go back to previous triage question
/// Returns true if successfully went back, false if already at first question
bool goBackToPreviousTriageQuestion() {
if (_triageQuestionHistory.isEmpty) {
return false; // Already at first question
}
// Pop the last question from history
final previousQuestion = _triageQuestionHistory.removeLast();
final previousEvidence = _triageEvidenceHistory.removeLast();
// Restore the previous question
triageDataDetails = previousQuestion;
// Restore the previous evidence state
_triageEvidenceList.clear();
_triageEvidenceList.addAll(previousEvidence);
// Decrement question count
if (_triageQuestionCount > 0) {
_triageQuestionCount--;
}
// Clear current selections
_selectedTriageChoiceIndex = null;
_selectedTriageChoicesByItemId.clear();
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
notifyListeners();
return true;
}
void reset() {
_currentView = BodyView.front;
_selectedOrganIds.clear();
@ -1018,6 +1122,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_triageEvidenceList.clear();
_selectedTriageChoicesByItemId.clear();
_triageQuestionCount = 0; // Reset question count
_triageQuestionHistory.clear(); // Clear question history
_triageEvidenceHistory.clear(); // Clear evidence history
_currentZoomScale = 1.0; // Reset zoom scale
_symptomSearchQuery = ''; // Reset search query
_filteredOrganSymptomsResults.clear(); // Clear filtered results
@ -1054,6 +1160,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_selectedRiskFactorIds.clear();
_selectedSuggestionsIds.clear();
_triageEvidenceList.clear();
_triageQuestionHistory.clear(); // Clear question history
_triageEvidenceHistory.clear(); // Clear evidence history
_selectedTriageChoicesByItemId.clear();
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
@ -1066,6 +1174,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
/// Clear selections when reaching possible conditions screen
/// This clears organs, symptoms, risk factors, suggestions, and questions
void clearSelectionsForPossibleConditions() {
_selectedOrganIds.clear();
_selectedSymptomsByOrgan.clear();
_selectedRiskFactorIds.clear();
_selectedSuggestionsIds.clear();
_triageEvidenceList.clear();
_triageQuestionHistory.clear();
_triageEvidenceHistory.clear();
_selectedTriageChoicesByItemId.clear();
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
_triageQuestionCount = 0;
_symptomSearchQuery = '';
_filteredOrganSymptomsResults.clear();
_isBottomSheetExpanded = false;
_tooltipTimer?.cancel();
_tooltipOrganId = null;
notifyListeners();
}
// User Info Flow Methods
/// Set current page in user info flow
@ -1170,6 +1300,34 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
};
}
// User Consent Methods
bool isTermsAccepted = false;
bool isPrivacyAccepted = false;
/// Toggle terms acceptance
void toggleTermsAcceptance() {
isTermsAccepted = !isTermsAccepted;
notifyListeners();
}
/// Toggle privacy policy acceptance
void togglePrivacyAcceptance() {
isPrivacyAccepted = !isPrivacyAccepted;
notifyListeners();
}
void setTermsAcceptance(bool value) {
isTermsAccepted = value;
notifyListeners();
}
/// Toggle privacy policy acceptance
void setPrivacyAcceptance(bool value) {
isPrivacyAccepted = value;
notifyListeners();
}
Future<void> getSymptomsUserDetails({
required String userName,
required String password,
@ -1352,6 +1510,170 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
);
}
/// Get Rationale for symptoms
/// This API shows which symptoms and conditions are being considered
Future<void> fetchRationale({
required List<Map<String, dynamic>> evidence,
Function(RationaleResponseModel)? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
isRationaleLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getRationale(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidence: evidence,
language: appState.isArabic() ? 'ar' : 'en',
generalId: currentSessionId,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isRationaleLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isRationaleLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
rationaleResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch rationale');
}
}
},
);
}
/// Get Triage Level for symptoms
/// This API determines the urgency level and serious symptoms
Future<void> fetchTriageLevel({
required List<Map<String, dynamic>> evidence,
Function(TriageLevelResponseModel)? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
isTriageLevelLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getTriage(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidence: evidence,
language: appState.isArabic() ? 'ar' : 'en',
generalId: currentSessionId,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isTriageLevelLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isTriageLevelLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
triageLevelResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch triage level');
}
}
},
);
}
/// Get Explanations for a specific condition
/// This API explains why a condition was suggested based on evidence
Future<void> fetchExplanations({
required String targetConditionId,
required List<Map<String, dynamic>> evidence,
Function(ExplanationsResponseModel)? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
isExplanationsLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getExplanations(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidence: evidence,
language: appState.isArabic() ? 'ar' : 'en',
target: targetConditionId,
generalId: currentSessionId,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isExplanationsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isExplanationsLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
explanationsResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch explanations');
}
}
},
);
}
@override
void dispose() {
_tooltipTimer?.cancel();

File diff suppressed because it is too large Load Diff

@ -382,6 +382,12 @@ abstract class LocaleKeys {
static const clickHere = 'clickHere';
static const iAgreeToTheTermsAndConditions = 'iAgreeToTheTermsAndConditions';
static const iAgreeToTheTermsAndConditionsSubtitle = 'iAgreeToTheTermsAndConditionsSubtitle';
static const iAgreeAndAcceptTermsAndConditions = 'iAgreeAndAcceptTermsAndConditions';
static const termsAndConditions = 'termsAndConditions';
static const iAgreeHealthInfoCanBeUsed = 'iAgreeHealthInfoCanBeUsed';
static const privacyPolicyText = 'privacyPolicyText';
static const outPatient = 'outPatient';
static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund';
static const save = 'save';
static const userAgreement = 'userAgreement';
static const updateSuccessfully = 'updateSuccessfully';
@ -681,6 +687,15 @@ abstract class LocaleKeys {
static const offTheme = 'offTheme';
static const dimTheme = 'dimTheme';
static const bwTheme = 'bwTheme';
static const highContrastTheme = 'highContrastTheme';
static const fontSize = 'fontSize';
static const fontSizeSmall = 'fontSizeSmall';
static const fontSizeMedium = 'fontSizeMedium';
static const fontSizeLarge = 'fontSizeLarge';
static const saveChanges = 'saveChanges';
static const settingsSavedSuccessfully = 'settingsSavedSuccessfully';
static const cardHeading = 'cardHeading';
static const buttonText = 'buttonText';
static const permissions = 'permissions';
static const cameraPermission = 'cameraPermission';
static const locationPermission = 'locationPermission';
@ -1371,7 +1386,6 @@ abstract class LocaleKeys {
static const medicalFiles = 'medicalFiles';
static const addANewFamilyMember = 'addANewFamilyMember';
static const viewInvoiceDetails = 'viewInvoiceDetails';
static const outPatient = 'outPatient';
static const invoiceDetails = 'invoiceDetails';
static const sendingEmailPleaseWait = 'sendingEmailPleaseWait';
static const emailSentSuccessfullyMessage = 'emailSentSuccessfullyMessage';
@ -1437,6 +1451,9 @@ abstract class LocaleKeys {
static const symptomsSelector = 'symptomsSelector';
static const possibleSymptomsRelatedTo = 'possibleSymptomsRelatedTo';
static const suggestionAIListDescription = 'suggestionAIListDescription';
static const whyAmIBeingAskedThis = 'whyAmIBeingAskedThis';
static const weAreAskingThisQuestionToBetterUnderstand = 'weAreAskingThisQuestionToBetterUnderstand';
static const analyzingYourSymptoms = 'analyzingYourSymptoms';
static const analyzing = 'analyzing';
static const seekMedicalAdvice = 'seekMedicalAdvice';
static const monitorDescription = 'monitorDescription';
@ -1454,6 +1471,30 @@ abstract class LocaleKeys {
static const areYouSureYouWantToExitProgress = 'areYouSureYouWantToExitProgress';
static const noQuestionAvailable = 'noQuestionAvailable';
static const possibleSymptom = 'possibleSymptom';
static const triageLevelEmergencyAmbulance = 'triageLevelEmergencyAmbulance';
static const triageLevelEmergency = 'triageLevelEmergency';
static const triageLevelConsultation24 = 'triageLevelConsultation24';
static const triageLevelConsultation = 'triageLevelConsultation';
static const triageLevelSelfCare = 'triageLevelSelfCare';
static const triageLevelEmergencyAmbulanceDesc = 'triageLevelEmergencyAmbulanceDesc';
static const triageLevelEmergencyDesc = 'triageLevelEmergencyDesc';
static const triageLevelConsultation24Desc = 'triageLevelConsultation24Desc';
static const triageLevelConsultationDesc = 'triageLevelConsultationDesc';
static const triageLevelSelfCareDesc = 'triageLevelSelfCareDesc';
static const rootCauseEmergencyEvidencePresent = 'rootCauseEmergencyEvidencePresent';
static const rootCauseSeriousEvidencePresent = 'rootCauseSeriousEvidencePresent';
static const rootCauseEmergencyConditionLikely = 'rootCauseEmergencyConditionLikely';
static const rootCauseEmergencyConditionPossible = 'rootCauseEmergencyConditionPossible';
static const rootCauseConsultationConditionLikely = 'rootCauseConsultationConditionLikely';
static const rootCauseSelfCareSufficient = 'rootCauseSelfCareSufficient';
static const rootCauseDiagnosisUnknown = 'rootCauseDiagnosisUnknown';
static const teleconsultationAvailable = 'teleconsultationAvailable';
static const seriousSymptoms = 'seriousSymptoms';
static const howTo = 'howTo';
static const explainWhy = 'explainWhy';
static const whyThisCondition = 'whyThisCondition';
static const supportingEvidence = 'supportingEvidence';
static const explanationDescription = 'explanationDescription';
static const symptomsCheckerFindingScore = 'symptomsCheckerFindingScore';
static const notSet = 'notSet';
static const years = 'years';
@ -1546,7 +1587,6 @@ abstract class LocaleKeys {
static const waterConsumption = 'waterConsumption';
static const selectNumberOfReminders = 'selectNumberOfReminders';
static const h2oSettings = 'h2oSettings';
static const settingsSavedSuccessfully = 'settingsSavedSuccessfully';
static const yourName = 'yourName';
static const ageYears = 'ageYears';
static const numberOfRemindersInADay = 'numberOfRemindersInADay';
@ -1893,6 +1933,5 @@ abstract class LocaleKeys {
static const amountOnly = 'amountOnly';
static const invoiceHistory = 'invoiceHistory';
static const refund = 'refund';
static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund';
}

@ -134,6 +134,9 @@ Future<void> callInitializations() async {
// Restore persisted dark-mode preference before the first frame.
getIt.get<ProfileSettingsViewModel>().loadDarkMode();
// Restore persisted accessibility settings before the first frame.
getIt.get<ProfileSettingsViewModel>().loadAccessibilitySettings();
}
void main() async {
@ -282,6 +285,55 @@ void main() async {
class MyApp extends StatelessWidget {
const MyApp({super.key});
/// Apply visual mode filter to the entire app
Widget _applyVisualMode(String visualMode, Widget child) {
switch (visualMode) {
case 'invert':
// Invert colors app-wide (red becomes cyan for colorblind users)
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255, // Red channel inverted
0, -1, 0, 0, 255, // Green channel inverted
0, 0, -1, 0, 255, // Blue channel inverted
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: child,
);
case 'dim':
// Apply dim effect with opacity overlay
return Stack(
children: [
child,
Positioned.fill(
child: IgnorePointer(
child: Container(
color: Colors.black.withValues(alpha: 0.3), // 30% black overlay for dimming
),
),
),
],
);
case 'highContrast':
// Apply desaturation for high contrast (removes colors, leaves black and white)
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0, // Red channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Green channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale)
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: child,
);
case 'off':
default:
// No filter applied
return child;
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
@ -300,13 +352,16 @@ class MyApp extends StatelessWidget {
// title: 'Dr. AlHabib',
title: 'Dr. AlHabib Beta',
builder: (context, mchild) {
return MediaQuery(
Widget child = MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
textScaler: TextScaler.linear(profileVm.getFontScaleMultiplier()),
alwaysUse24HourFormat: true,
),
child: mchild!,
);
// Apply visual mode filters app-wide
return _applyVisualMode(profileVm.visualMode, child);
},
showSemanticsDebugger: false,
debugShowCheckedModeBanner: false,

@ -570,8 +570,9 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(24.r),
// Todo: what is this???? Api Key??? 😲
child: Image.network(
"https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=${(MediaQuery.of(context).size.width * 1.5).toInt()}x${(MediaQuery.of(context).size.height * 0.35).toInt()}&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
child: Utils.buildImgWithNetwork(
url: "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=${(MediaQuery.of(context).size.width * 1.5).toInt()}x${(MediaQuery.of(context).size.height * 0.35).toInt()}&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
iconColor: AppColors.transparent,
fit: BoxFit.cover,
width: double.infinity,
),

@ -112,7 +112,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h)
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
@ -230,9 +230,9 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
@ -352,7 +352,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h)
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context)

@ -139,8 +139,9 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -261,8 +262,9 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
(doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
Utils.buildImgWithNetwork(
url: (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
iconColor: AppColors.transparent,
width: 24.w,
height: 24.h,
fit: BoxFit.cover,

@ -227,10 +227,11 @@ class _AppointmentCardState extends State<AppointmentCard> {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
widget.isLoading
Utils.buildImgWithNetwork(
url: widget.isLoading
? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png'
: widget.patientAppointmentHistoryResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -280,8 +281,9 @@ class _AppointmentCardState extends State<AppointmentCard> {
SizedBox(width: 12.w),
(widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&
widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty)
? Image.network(
widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
? Utils.buildImgWithNetwork(
url: widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
iconColor: AppColors.transparent,
width: 20.h,
height: 15.h,
fit: BoxFit.cover,

@ -58,8 +58,9 @@ class AppointmentDoctorCard extends StatelessWidget {
children: [
Column(
children: [
Image.network(
patientAppointmentHistoryResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: patientAppointmentHistoryResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -10,6 +10,9 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'dart:ui' as ui;
import '../../../core/utils/utils.dart';
import '../../../theme/colors.dart';
class BuildDoctorRowAppointmentRating extends StatelessWidget {
final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel;
@ -22,8 +25,9 @@ class BuildDoctorRowAppointmentRating extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
patientAppointmentHistoryResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: patientAppointmentHistoryResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -86,8 +86,9 @@ class _DoctorResponsePageState extends State<DoctorResponsePage> {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.blackBgColor,
width: 50.w,
height: 50.h,
fit: BoxFit.cover,

@ -87,8 +87,9 @@ class AskDoctorAppointmentCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
askDoctorAppointmentHistoryList.doctorImageURL!,
Utils.buildImgWithNetwork(
url: askDoctorAppointmentHistoryList.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -28,8 +28,9 @@ class DoctorResponseTransactions extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.blackBgColor,
width: 50.w,
height: 50.h,
fit: BoxFit.cover,
@ -85,7 +86,11 @@ class DoctorResponseTransactions extends StatelessWidget {
SizedBox(height: 16.h),
Row(
children: [
Image.asset(getIt.get<AppState>().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 42.w, height: 42.h),
Utils.buildImgWithAssets(
icon: getIt.get<AppState>().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 42.w,
height: 42.h,
),
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.all(8.w),
@ -116,10 +121,16 @@ class DoctorResponseTransactions extends StatelessWidget {
borderRadius: BorderRadius.circular(8.r),
),
child: doctorResponseModel.transactions![index].doctorResponse!.toText12(color: AppColors.textColor, isBold: true),
),
SizedBox(width: 8.w),
Image.network("https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png", fit: BoxFit.cover, width: 42.w, height: 42.h).circle(100.r),
],
),
SizedBox(width: 8.w),
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
width: 42.w,
height: 42.h,
).circle(100.r),
],
),
],
);

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
@ -95,7 +96,13 @@ class _RegisterNew extends State<RegisterNew> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.w, height: 200.h, fit: BoxFit.cover, repeat: true),
Utils.showLottie(
context: context,
assetPath: 'assets/animations/lottie/register.json',
width: 200.w,
height: 200.h,
fit: BoxFit.cover,
repeat: true),
SizedBox(height: 16.h),
LocaleKeys.prepareToElevate.tr(context: context).toText32(isBold: true),
SizedBox(height: 24.h),
@ -134,8 +141,10 @@ class _RegisterNew extends State<RegisterNew> {
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card,
fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.nationalIdError, // Show error message if exists
hasError: false,
// Don't show individual field border
errorMessage: authVm.nationalIdError,
// Show error message if exists
onChange: (value) {
// Clear error when user starts typing
authVm.clearNationalIdError();
@ -162,8 +171,9 @@ class _RegisterNew extends State<RegisterNew> {
authVm.clearDobError();
},
fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
hasError: false,
// Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
).withVerticalPadding(8),
],
),
@ -202,7 +212,7 @@ class _RegisterNew extends State<RegisterNew> {
onTap: () {
// Navigate to terms and conditions page
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
},
child: Text(

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
@ -15,18 +16,13 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_blood_group_widget.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_city_widget.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_gender_widget.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:lottie/lottie.dart';
import 'package:provider/provider.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart';
class BloodDonationPage extends StatefulWidget {
BloodDonationPage({super.key});
@ -60,7 +56,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
onPressed: () {
if (bloodDonationVM.isUserAuthanticated()) {
bloodDonationVM.fetchHospitalsList().then((value) {
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: Consumer<BloodDonationViewModel>(builder: (_, data, __) {
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false,
child: Consumer<BloodDonationViewModel>(builder: (_, data, __) {
return HospitalBottomSheetBodySelection(
isHideTitle: true,
onUserHospitalSelection: (BdGetProjectsHaveBdClinic userChoice) {
@ -77,7 +74,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.errorAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(LocaleKeys.loginToUseService.tr()).toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),
@ -131,7 +129,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
padding: EdgeInsets.all(24.w),
child: SingleChildScrollView(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
decoration:
RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
@ -150,7 +149,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
LocaleKeys.city.tr().toText16(color: AppColors.textColor, isBold: true),
(appState.isArabic()
? (bloodDonationVM.selectedCity?.descriptionN ?? LocaleKeys.selectBloodDonationCity.tr())
: bloodDonationVM.selectedCity?.description ?? LocaleKeys.selectBloodDonationCity.tr(context: context))
: bloodDonationVM.selectedCity?.description ??
LocaleKeys.selectBloodDonationCity.tr(context: context))
.toText14(color: AppColors.greyTextColor, isBold: true),
],
),
@ -160,7 +160,10 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
],
).onPress(() async {
showCommonBottomSheetWithoutHeight(context,
title: LocaleKeys.selectCity.tr(context: context), isDismissible: true, child: SelectCityWidget(bloodDonationViewModel: bloodDonationVM), callBackFunc: () {});
title: LocaleKeys.selectCity.tr(context: context),
isDismissible: true,
child: SelectCityWidget(bloodDonationViewModel: bloodDonationVM),
callBackFunc: () {});
}),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
@ -254,17 +257,17 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 12.w),
Expanded(
child: LocaleKeys.aboutBloodDonation.tr(context: context).toText16(
color: AppColors.primaryRedColor,
isBold: true,
),
color: AppColors.primaryRedColor,
isBold: true,
),
),
],
),
SizedBox(height: 12.h),
LocaleKeys.bloodDonationDescription.tr(context: context).toText14(
color: AppColors.greyTextColor,
isBold: true,
),
color: AppColors.greyTextColor,
isBold: true,
),
SizedBox(height: 12.h),
Row(
children: [
@ -279,9 +282,9 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 8.w),
Expanded(
child: LocaleKeys.bloodDonationBenefit1.tr(context: context).toText13(
color: AppColors.textColor,
isBold: true,
),
color: AppColors.textColor,
isBold: true,
),
),
],
),
@ -299,9 +302,9 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 8.w),
Expanded(
child: LocaleKeys.bloodDonationBenefit2.tr(context: context).toText13(
color: AppColors.textColor,
isBold: true,
),
color: AppColors.textColor,
isBold: true,
),
),
],
),
@ -319,9 +322,9 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 8.w),
Expanded(
child: LocaleKeys.bloodDonationBenefit3.tr(context: context).toText13(
color: AppColors.textColor,
isBold: true,
),
color: AppColors.textColor,
isBold: true,
),
),
],
),
@ -376,10 +379,10 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
// Navigator.of(context).pushNamed('/terms');
appState.isArabic()
? Utils.openWebView(
url: 'https://hmg.com/ar/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlAr,
)
: Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
},
child: Text(
@ -419,7 +422,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.errorAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(LocaleKeys.loginToUseService.tr()).toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),

@ -132,8 +132,9 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,
@ -191,8 +192,9 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,
@ -383,8 +385,9 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!,
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,

@ -83,8 +83,9 @@ class DoctorProfilePage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -109,8 +110,9 @@ class DoctorProfilePage extends StatelessWidget {
),
],
),
Image.network(
bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!,
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!,
iconColor: AppColors.transparent,
width: 32.h,
height: 32.h,
fit: BoxFit.cover,

@ -66,8 +66,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
padding: EdgeInsets.all(16.h),
child: Row(
children: [
Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
Utils.buildImgWithAssets(
icon: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h,
height: 52.h,
),

@ -106,7 +106,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -187,9 +187,9 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -271,7 +271,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),
],

@ -84,9 +84,10 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
children: [
Row(
children: [
Image.network(
bookAppointmentsViewModel.selectedDoctor.doctorImageURL ??
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.selectedDoctor.doctorImageURL ??
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.transparent,
width: 50.h,
height: 50.h,
fit: BoxFit.cover,
@ -104,9 +105,10 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
SizedBox(width: 12.w),
(bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null &&
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty)
? Image.network(
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ??
? Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ??
"https://hmgwebservices.com/Images/flag/SAU.png",
iconColor: AppColors.transparent,
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
@ -334,12 +336,10 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
// Show default image (no image data or user has no uploaded image)
print('📷 Showing default avatar for user $currentPatientId');
return Image.asset(
defaultImage,
key: ValueKey('default_$currentPatientId'),
return Utils.buildImgWithAssets(
icon: defaultImage,
width: 52.w,
height: 52.h,
gaplessPlayback: true,
);
}

@ -116,7 +116,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -197,9 +197,9 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -281,7 +281,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),
],

@ -52,10 +52,11 @@ class DoctorCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
isLoading
Utils.buildImgWithNetwork(
url: isLoading
? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png"
: doctorsListResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -120,15 +121,16 @@ class DoctorCard extends StatelessWidget {
.toString()
.toText12(isBold: true, color: AppColors.textColor, maxLine: 2, isEnglishOnly: true)
.toShimmer2(isShow: isLoading),
),
SizedBox(width: 6.w),
Image.network(
isLoading ? "https://hmgwebservices.com/Images/flag/SYR.png" : doctorsListResponseModel.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SYR.png",
width: 20.h,
height: 15.h,
fit: BoxFit.fill,
).toShimmer2(isShow: isLoading),
],
),
SizedBox(width: 6.w),
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/flag/SYR.png" : doctorsListResponseModel.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SYR.png",
iconColor: AppColors.transparent,
width: 20.h,
height: 15.h,
fit: BoxFit.fill,
).toShimmer2(isShow: isLoading),
],
),
SizedBox(height: 6.h),
Wrap(

@ -301,7 +301,7 @@ class _MyChildVaccinePageState extends State<MyChildVaccinePage> {
),
child: Row(
children: [
Image.asset(children[index]['image'], width: 24.h, height: 24.h),
Utils.buildImgWithAssets(icon: children[index]['image'], width: 24.h, height: 24.h),
SizedBox(width: 8.w),
Text(
"${children[index]['name']} (${children[index]['gender']})",

@ -282,8 +282,8 @@ class _ComprehensiveCheckupPageState extends State<ComprehensiveCheckupPage> {
final String imagePath = appStateLocal.isArabic() ? AppAssets.comprehensiveCheckupAr : AppAssets.comprehensiveCheckupEn;
return Stack(
children: [
Image.asset(
imagePath,
Utils.buildImgWithAssets(
icon: imagePath,
width: double.infinity,
fit: BoxFit.cover,
).paddingAll(16.w),

@ -259,8 +259,9 @@ class FeedbackPage extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!,
Utils.buildImgWithNetwork(
url: contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -16,6 +16,8 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart';
import 'package:provider/provider.dart';
import '../../core/utils/utils.dart';
class FindUsPage extends StatelessWidget {
FindUsPage({super.key});
@ -120,8 +122,9 @@ class FindUsPage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -70,8 +70,9 @@ class LiveChatPage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -78,8 +78,9 @@ class FindUsItemCard extends StatelessWidget {
: SizedBox.shrink(),
Row(
children: [
Image.network(
getHMGLocationsModel.projectImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: getHMGLocationsModel.projectImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 40.h,
height: 40.h,
fit: BoxFit.cover,

@ -211,7 +211,7 @@ class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -252,9 +252,9 @@ class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -296,21 +296,11 @@ class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
AppAssets.tamaraEng,
Utils.buildImgWithAssets(
icon: AppAssets.tamaraEng,
width: 72.h,
height: 25.h,
fit: BoxFit.contain,
// If PNG fails to load for any reason, log and fallback to SVG asset
errorBuilder: (context, error, stackTrace) {
debugPrint('Failed to load Tamara PNG asset: $error');
return Utils.buildSvgWithAssets(
icon: AppAssets.tamara,
width: 72.h,
height: 25.h,
fit: BoxFit.contain,
);
},
),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),

@ -102,7 +102,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -188,9 +188,9 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -277,7 +277,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),
],

@ -85,7 +85,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h).toShimmer2(isShow: false),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h).toShimmer2(isShow: false),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true).toShimmer2(isShow: false),
],
@ -183,9 +183,9 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 40.h, height: 40.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
).toShimmer2(isShow: false),
SizedBox(height: 16.h),

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
@ -820,7 +821,7 @@ class _ServicesPageState extends State<ServicesPage> {
),
).onPress(() {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
}),
),
@ -845,7 +846,7 @@ class _ServicesPageState extends State<ServicesPage> {
),
).onPress(() {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Privacy.aspx',
url: AppUrls.hmgPrivacyUrlEn,
);
}),
)

@ -13,6 +13,8 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import '../../core/utils/utils.dart';
class ServiceInfoPage extends StatelessWidget {
final String serviceName;
final String serviceHeader;
@ -37,8 +39,8 @@ class ServiceInfoPage extends StatelessWidget {
borderRadius: BorderRadius.circular(24.r),
child: Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Image.asset(
serviceImage,
child: Utils.buildImgWithAssets(
icon: serviceImage,
fit: BoxFit.fitHeight,
// height: 480.h,
// width: 520.w,

@ -56,8 +56,8 @@ class LargeServiceCard extends StatelessWidget {
topLeft: Radius.circular(24.r),
topRight: Radius.circular(24.r),
),
child: Image.asset(
serviceCardData.largeCardIcon,
child: Utils.buildImgWithAssets(
icon: serviceCardData.largeCardIcon,
fit: BoxFit.cover,
width: double.infinity,
height: isFoldable ? 190.h : (isTablet ? 200.h : 180.h),
@ -72,7 +72,7 @@ class LargeServiceCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
isPNG
? Image.asset(serviceCardData.icon, width: 35.h, height: 35.h)
? Utils.buildImgWithAssets(icon: serviceCardData.icon, width: 35.h, height: 35.h)
: Container(
height: 48.h,
width: 48.h,
@ -204,7 +204,7 @@ class FadedLargeServiceCard extends StatelessWidget {
borderRadius: BorderRadius.circular(24.r),
child: Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Image.asset(serviceCardData.largeCardIcon, fit: BoxFit.cover, width: 520.w, height: 250.h)),
child: Utils.buildImgWithAssets(icon: serviceCardData.largeCardIcon, fit: BoxFit.cover, width: 520.w, height: 250.h)),
),
Positioned(
top: 0,
@ -234,7 +234,7 @@ class FadedLargeServiceCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
isPNG
? Image.asset(serviceCardData.icon, width: 32.h, height: 32.h).circle(100.h)
? Utils.buildImgWithAssets(icon: serviceCardData.icon, width: 32.h, height: 32.h).circle(100.h)
: Container(
height: 32.h,
width: 32.h,

@ -63,7 +63,7 @@ class WelcomeWidget extends StatelessWidget {
),
imageWidget ??
(imageUrl != null
? Image.asset(imageUrl!, width: 40, height: 40)
? Utils.buildImgWithAssets(icon: imageUrl!, width: 40, height: 40)
: SizedBox(width: 40, height: 40)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.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/insurance/insurance_view_model.dart';
@ -82,8 +83,9 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
insuranceApprovalResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: insuranceApprovalResponseModel.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -106,8 +106,9 @@ class InsuranceApprovalCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -55,8 +55,9 @@ class LabResultItemView extends StatelessWidget {
SizedBox(height: 8.h),
Row(
children: [
Image.network(
isLoading ? "" : labOrder!.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "" : labOrder!.doctorImageURL!,
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,
fit: BoxFit.cover,

@ -1,9 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_order_response_by_ai_model.dart';
@ -37,7 +37,13 @@ class AiAnalysisWidget extends StatelessWidget {
Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(gradient: AppColors.aiLinearGradient, borderRadius: BorderRadius.circular(8.r)),
child: SvgPicture.asset(AppAssets.aiOverView, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), height: 18.r, width: 18.r),
child: Utils.buildSvgWithAssets(
icon: AppAssets.aiOverView,
iconColor: Colors.white,
height: 18.r,
width: 18.r,
applyThemeColor: false,
),
),
],
),

@ -908,8 +908,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
child: FadeInAnimation(
child: Row(
children: [
Image.network(
prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 40.h,
height: 40.h,
fit: BoxFit.cover,
@ -1059,8 +1060,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,
@ -1106,8 +1108,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,

@ -139,8 +139,9 @@ class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
model.patientSickLeaveList[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: model.patientSickLeaveList[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,

@ -83,8 +83,9 @@ class _VaccineListPageState extends State<VaccineListPage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -163,9 +163,10 @@ class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard>
children: [
Row(
children: [
Image.network(
widget.patientAppointmentHistoryResponseModel.doctorImageURL ??
Utils.buildImgWithNetwork(
url: widget.patientAppointmentHistoryResponseModel.doctorImageURL ??
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 30.h,
height: 30.h,
fit: BoxFit.fill,

@ -56,8 +56,9 @@ class PatientSickLeaveCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 30.h,
height: 30.h,
fit: BoxFit.cover,

@ -241,8 +241,9 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
report.doctorImageURL!,
Utils.buildImgWithNetwork(
url: report.doctorImageURL!,
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,

@ -42,8 +42,9 @@ class PatientMedicalReportCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.fill,

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
@ -82,7 +83,8 @@ class MonthlyReport extends StatelessWidget {
LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
child: "${appState.getAuthenticatedUser()!.emailAddress}".toText16(color: AppColors.textColor, isBold: true, maxlines: 2),
child: "${appState.getAuthenticatedUser()!.emailAddress}"
.toText16(color: AppColors.textColor, isBold: true, maxlines: 2),
),
],
),
@ -98,7 +100,9 @@ class MonthlyReport extends StatelessWidget {
Utils.buildSvgWithAssets(icon: AppAssets.prescription_remarks_icon, width: 18.w, height: 18.h),
SizedBox(width: 9.h),
Expanded(
child: LocaleKeys.monthlyHealthSummaryReportDisclaimer.tr(context: context).toText10(isBold: true, color: AppColors.greyTextColorLight),
child: LocaleKeys.monthlyHealthSummaryReportDisclaimer
.tr(context: context)
.toText10(isBold: true, color: AppColors.greyTextColorLight),
),
],
),
@ -135,9 +139,11 @@ class MonthlyReport extends StatelessWidget {
),
SizedBox(width: 10.w),
"I agree to the ".toText14(isBold: true, letterSpacing: -1.0),
"terms and conditions".toText14(isBold: true, letterSpacing: -1.0, color: AppColors.primaryRedColor, isUnderLine: true).onPress(() {
"terms and conditions"
.toText14(isBold: true, letterSpacing: -1.0, color: AppColors.primaryRedColor, isUnderLine: true)
.onPress(() {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
})
],

@ -149,8 +149,9 @@ class _MyInvoicesDetailsPageState extends State<MyInvoicesDetailsPage> {
children: [
Column(
children: [
Image.network(
widget.getInvoiceDetailsResponseModel.doctorImageURL ?? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png',
Utils.buildImgWithNetwork(
url: widget.getInvoiceDetailsResponseModel.doctorImageURL ?? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png',
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -24,9 +24,9 @@ import 'package:provider/provider.dart';
class InvoiceListCard extends StatelessWidget {
final GetInvoicesListResponseModel getInvoicesListResponseModel;
Function? onTap;
final Function? onTap;
InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap});
const InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap});
void _handleRefundInvoiceTap(BuildContext context) {
final invoiceNo = getInvoicesListResponseModel.invoiceNo;
@ -70,7 +70,6 @@ class InvoiceListCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
AppState appState = getIt<AppState>();
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: Padding(
@ -91,7 +90,7 @@ class InvoiceListCard extends StatelessWidget {
textColor: AppColors.textColor,
),
AppCustomChipWidget(
labelText: appState.isArabic() ? "العيادات الخارجيه" : "Out Patient",
labelText: LocaleKeys.outPatient.tr(context: context),
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1),
textColor: AppColors.warningColorYellow,
),
@ -103,8 +102,9 @@ class InvoiceListCard extends StatelessWidget {
children: [
Column(
children: [
Image.network(
getInvoicesListResponseModel.doctorImageUrl ?? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png',
Utils.buildImgWithNetwork(
url: getInvoicesListResponseModel.doctorImageUrl ?? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png',
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -71,8 +71,9 @@ class PharmacyInvoiceCard extends StatelessWidget {
children: [
Column(
children: [
Image.network(
pharmacyInvoice.doctorImageURL ?? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png',
Utils.buildImgWithNetwork(
url: pharmacyInvoice.doctorImageURL ?? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png',
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -84,24 +84,9 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
flipX: getIt.get<AppState>().isArabic(),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: Image.asset(
_promoImages[index],
child: Utils.buildImgWithAssets(
icon: _promoImages[index],
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16.r),
),
child: Center(
child: Icon(
Icons.image_outlined,
size: 100.h,
color: Colors.white.withValues(alpha: 0.5),
),
),
);
},
),
),
),

@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/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/offers_and_discounts/models/offers_and_discounts_response_model.dart';
@ -92,8 +93,9 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(24.h),
child: Image.network(
offer.imageUrl!,
child: Utils.buildImgWithNetwork(
url: offer.imageUrl!,
iconColor: AppColors.blackBgColor,
width: double.infinity,
height: 250.h,
fit: BoxFit.cover,

@ -158,8 +158,9 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
child: SizedBox.expand(
child: Image.network(
offersAndDiscountVM.filteredOffers[index].imageUrl!,
child: Utils.buildImgWithNetwork(
url: offersAndDiscountVM.filteredOffers[index].imageUrl!,
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(

@ -5,6 +5,8 @@ import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:smooth_corner/smooth_corner.dart';
import '../../../core/utils/utils.dart';
class OffersAndDiscountsCarousel extends StatefulWidget {
const OffersAndDiscountsCarousel({super.key});
@ -112,19 +114,9 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(11.r),
child: Image.asset(
imagePath,
child: Utils.buildImgWithAssets(
icon: imagePath,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColors.greyColor,
child: Icon(
Icons.image,
color: AppColors.textColorLight,
size: 32.h,
),
);
},
),
),
),

@ -5,6 +5,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/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/generated/locale_keys.g.dart';
@ -110,8 +111,8 @@ class _ParkingPageState extends State<ParkingPage> {
borderRadius: BorderRadius.circular(24.r),
child: Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Image.asset(
AppAssets.carParkingService,
child: Utils.buildImgWithAssets(
icon: AppAssets.carParkingService,
fit: BoxFit.fitHeight,
height: 480.h,
width: 520.w,

@ -61,8 +61,9 @@ class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget {
borderRadius: BorderRadius.all(
Radius.circular(5.r),
),
child: Image.network(
prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl!,
child: Utils.buildImgWithNetwork(
url: prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl!,
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
width: 60.w,
height: 70.h,
@ -99,8 +100,9 @@ class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget {
ClipRRect(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(20.r),
child: Image.network(
"https://maps.googleapis.com/maps/api/staticmap?center=${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat},${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng}&zoom=15&size=350x165&maptype=roadmap&markers=color:red%7C${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat},${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng}&key=AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng",
child: Utils.buildImgWithNetwork(
url: "https://maps.googleapis.com/maps/api/staticmap?center=${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat},${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng}&zoom=15&size=350x165&maptype=roadmap&markers=color:red%7C${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lat},${prescriptionsViewModel.locationGeocodeResponse.results.first.geometry.location.lng}&key=AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng",
iconColor: AppColors.blackBgColor,
fit: BoxFit.contain,
),
),

@ -125,8 +125,9 @@ class _PrescriptionDetailPageState extends State<PrescriptionDetailPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
widget.prescriptionsResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: widget.prescriptionsResponseModel.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,

@ -42,8 +42,9 @@ class PrescriptionItemView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8.h,
children: [
Image.network(
isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].imageThumbUrl!,
Utils.buildImgWithNetwork(
url: isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].imageThumbUrl!,
iconColor: AppColors.blackBgColor,
width: 60.h,
height: 60.h,
errorBuilder: (cxt, child, tr) {

@ -150,8 +150,9 @@ class _PrescriptionsListPageState extends State<PrescriptionsListPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
model.patientPrescriptionOrders[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: model.patientPrescriptionOrders[index].doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,

@ -0,0 +1,476 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.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/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:provider/provider.dart';
enum VisualMode { off, invert, dim, highContrast }
class AccessibilityPage extends StatefulWidget {
const AccessibilityPage({super.key});
@override
State<AccessibilityPage> createState() => _AccessibilityPageState();
}
class _AccessibilityPageState extends State<AccessibilityPage> {
late ProfileSettingsViewModel profileSettingsViewModel;
int _currentFontSizeStep = 2; // 0: Small, 1: Small-Medium, 2: Medium (default), 3: Medium-Large, 4: Large
VisualMode _selectedMode = VisualMode.off;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
// Load saved settings from ViewModel
_loadSavedSettings();
}
});
}
void _loadSavedSettings() {
setState(() {
_currentFontSizeStep = profileSettingsViewModel.fontSizeStep;
_selectedMode = _visualModeFromString(profileSettingsViewModel.visualMode);
});
}
VisualMode _visualModeFromString(String mode) {
switch (mode) {
case 'off':
return VisualMode.off;
case 'invert':
return VisualMode.invert;
case 'dim':
return VisualMode.dim;
case 'highContrast':
return VisualMode.highContrast;
default:
return VisualMode.off;
}
}
String _visualModeToString(VisualMode mode) {
switch (mode) {
case VisualMode.off:
return 'off';
case VisualMode.invert:
return 'invert';
case VisualMode.dim:
return 'dim';
case VisualMode.highContrast:
return 'highContrast';
}
}
@override
void dispose() {
super.dispose();
}
double _getFontSizeForStep(int step) {
const double mediumSize = 40.0; // Medium is baseline
switch (step) {
case 0:
return mediumSize * 0.5; // Small: 20.0
case 1:
return mediumSize * 0.75; // Small-Medium: 30.0
case 2:
return mediumSize; // Medium (default): 40.0
case 3:
return mediumSize * 1.5; // Medium-Large: 60.0
case 4:
return mediumSize * 1.75; // Large: 70.0
default:
return mediumSize;
}
}
@override
Widget build(BuildContext context) {
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
return CollapsingListView(
title: LocaleKeys.accessibility.tr(context: context),
isClose: true,
bottomChild: Container(
padding: EdgeInsets.all(24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: CustomButton(
height: 56.h,
icon: AppAssets.checkmark_icon,
iconColor: AppColors.whiteColor,
text: LocaleKeys.saveChanges.tr(context: context),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
onPressed: () {
// Save accessibility settings to ViewModel
profileSettingsViewModel.setFontSizeStep(_currentFontSizeStep);
profileSettingsViewModel.setVisualMode(_visualModeToString(_selectedMode));
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: LocaleKeys.settingsSavedSuccessfully.tr(context: context).toText14(
color: AppColors.whiteColor,
),
backgroundColor: AppColors.successColor,
duration: const Duration(seconds: 2),
),
);
// Pop the page
Navigator.pop(context);
},
),
),
child: SingleChildScrollView(
padding: EdgeInsets.only(top: 16.h, bottom: 24.h, left: 24.w, right: 24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Font Size Card
_buildFontSizeCard(),
SizedBox(height: 24.h),
// Mode for Partially Blind Card
_buildVisualModeCard(),
],
),
),
);
}
Widget _buildFontSizeCard() {
return Container(
padding: EdgeInsets.all(24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Card Label
LocaleKeys.fontSize.tr(context: context).toText18(isBold: true),
SizedBox(height: 24.h),
// Center Aa heading that changes size
Container(
height: 70.h,
child: Center(
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontSize: _getFontSizeForStep(_currentFontSizeStep),
fontWeight: FontWeight.bold,
color: AppColors.blackBgColor,
),
child: const Text("Aa"),
),
),
),
SizedBox(height: 32.h),
// Stepped slider
Column(
children: [
// Stack to position circles on top of the bar
Stack(
alignment: Alignment.center,
children: [
// Grey bar
Container(
height: 4.h,
decoration: BoxDecoration(
color: AppColors.greyTextColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(2.r),
),
),
// Step indicator with dots positioned on top of the bar
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(5, (index) {
bool isSelected = index == _currentFontSizeStep;
return GestureDetector(
onTap: () {
setState(() {
_currentFontSizeStep = index;
});
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsets.all(8.w), // Increased tap zone
child: Container(
width: isSelected ? 20.w : 12.w,
height: isSelected ? 20.w : 12.w,
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor : Colors.transparent,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.greyTextColor.withValues(alpha: 0.3),
width: 2.w,
),
),
),
),
);
}),
),
],
),
SizedBox(height: 12.h),
// Labels
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.fontSizeSmall.tr(context: context).toText12(color: AppColors.greyTextColor),
LocaleKeys.fontSizeMedium.tr(context: context).toText12(color: AppColors.greyTextColor),
LocaleKeys.fontSizeLarge.tr(context: context).toText12(color: AppColors.greyTextColor),
],
),
],
),
],
),
);
}
Widget _buildVisualModeCard() {
return Container(
padding: EdgeInsets.all(24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Card Label
LocaleKeys.blindModes.tr(context: context).toText18(isBold: true),
SizedBox(height: 24.h),
// Two columns layout
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column - Radio buttons
Expanded(
flex: 2,
child: Column(
children: [
_buildRadioOption(VisualMode.off, LocaleKeys.offTheme.tr(context: context)),
SizedBox(height: 12.h),
_buildRadioOption(VisualMode.invert, LocaleKeys.invertTheme.tr(context: context)),
SizedBox(height: 12.h),
_buildRadioOption(VisualMode.dim, LocaleKeys.dimTheme.tr(context: context)),
SizedBox(height: 12.h),
_buildRadioOption(VisualMode.highContrast, LocaleKeys.highContrastTheme.tr(context: context)),
],
),
),
SizedBox(width: 16.w),
// Right Column - Preview Card
Expanded(
flex: 3,
child: _buildPreviewCard(),
),
],
),
],
),
);
}
Widget _buildRadioOption(VisualMode mode, String label) {
bool isSelected = _selectedMode == mode;
return GestureDetector(
onTap: () {
setState(() {
_selectedMode = mode;
});
},
child: Row(
children: [
Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.greyTextColor,
width: 2.w,
),
color: Colors.transparent,
),
child: isSelected
? Center(
child: Container(
width: 10.w,
height: 10.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primaryRedColor,
),
),
)
: null,
),
SizedBox(width: 8.w),
label.toText14(
isBold: isSelected,
color: isSelected ? AppColors.blackBgColor : AppColors.greyTextColor,
),
],
),
);
}
Widget _buildPreviewCard() {
// Always build preview content with base "Off" mode colors
Widget previewContent = Container(
padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.r,
isCustomShadow: [
BoxShadow(
color: const Color(0xff000000).withValues(alpha: 0.1),
blurRadius: 16,
spreadRadius: 16 * 0.1, // 10% of blur radius
offset: const Offset(0, 0), // Uniform shadow on all sides
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top row with heading and arrow
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LocaleKeys.cardHeading.tr(context: context).toText14(
isBold: true,
color: AppColors.blackBgColor, // Always use base color
),
Transform.scale(
scaleX: Utils.appState.isArabic() ? -1 : 1,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
iconColor: AppColors.blackBgColor, // Always use base color
),
),
],
),
SizedBox(height: 16.h),
// Price with SAR symbol
Row(
children: [
"1234".toText24(
isBold: true,
color: AppColors.blackBgColor, // Always use base color
),
SizedBox(width: 4.w),
LocaleKeys.riyal.tr(context: context).toText14(
color: AppColors.blackBgColor, // Always use base color
),
SizedBox(width: 4.w),
Utils.buildSvgWithAssets(
icon: AppAssets.saudi_riyal_icon,
iconColor: AppColors.blackBgColor, // Always use base color
width: 16.w,
height: 16.w,
),
],
),
SizedBox(height: 16.h),
// Button - always show with base "Off" colors
CustomButton(
height: 44.h,
icon: AppAssets.add_icon,
iconColor: AppColors.whiteColor,
// Always use base color
text: LocaleKeys.buttonText.tr(context: context),
backgroundColor: AppColors.primaryRedColor,
// Always use base color
borderColor: AppColors.primaryRedColor,
// Always use base color
textColor: AppColors.whiteColor,
// Always use base color
fontSize: 14.f,
onPressed: () {},
),
],
),
);
// Apply visual mode filters to preview
switch (_selectedMode) {
case VisualMode.invert:
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255, // Red channel inverted
0, -1, 0, 0, 255, // Green channel inverted
0, 0, -1, 0, 255, // Blue channel inverted
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: previewContent,
);
case VisualMode.dim:
return Stack(
children: [
previewContent,
Positioned.fill(
child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
color: AppColors.blackBgColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20.r),
),
),
),
),
],
);
case VisualMode.highContrast:
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0, // Red channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Green channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale)
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: previewContent,
);
case VisualMode.off:
return previewContent;
}
}
}

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_swiper_view/flutter_swiper_view.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
@ -22,6 +23,7 @@ import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_v
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/accessibility_page.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/profile_picture_widget.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_email_widget.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_emergency_contact_widget.dart';
@ -253,6 +255,10 @@ class ProfileSettingsState extends State<ProfileSettings> {
);
},
),
1.divider,
actionItem(AppAssets.bell, LocaleKeys.accessibility.tr(context: context), () {
Navigator.of(context).push(CustomPageRoute(page: const AccessibilityPage()));
}),
],
),
),
@ -347,13 +353,13 @@ class ProfileSettingsState extends State<ProfileSettings> {
1.divider,
actionItem(AppAssets.privacy_terms, LocaleKeys.privacyPolicy.tr(context: context), () {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Privacy.aspx',
url: AppUrls.hmgPrivacyUrlEn,
);
}, isExternalLink: true),
1.divider,
actionItem(AppAssets.privacy_terms, LocaleKeys.termsConditoins.tr(context: context), () {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
}, isExternalLink: true),
1.divider,
@ -599,7 +605,13 @@ class _FamilyCardWidgetState extends State<FamilyCardWidget> {
],
).expanded,
// Icon(Icons.qr_code, size: 56.h)
Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${widget.profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w)
Utils.buildImgWithNetwork(
url: "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${widget.profile.responseId.toString()}",
iconColor: AppColors.transparent,
fit: BoxFit.contain,
height: 56.h,
width: 56.w,
)
],
),
SizedBox(height: 4.h),

@ -128,12 +128,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
// Show default image (no image data or user has no uploaded image)
print('📷 Showing default avatar for user $currentPatientId');
return Image.asset(
defaultImage,
key: ValueKey('default_$currentPatientId'),
return Utils.buildImgWithAssets(
icon: defaultImage,
width: 136.w,
height: 136.h,
gaplessPlayback: true,
);
}

@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/accessibility/preserve_svg_colors.dart';
class RadiologyAiAnalysisWidget extends StatelessWidget {
final RadiologyAiAnalysisResponse data;
@ -47,7 +48,7 @@ class RadiologyAiAnalysisWidget extends StatelessWidget {
Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(gradient: AppColors.aiLinearGradient, borderRadius: BorderRadius.circular(8.r)),
child: SvgPicture.asset(AppAssets.aiOverView, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), height: 18.r, width: 18.r),
child: PreserveSvgColors(child: SvgPicture.asset(AppAssets.aiOverView, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), height: 18.r, width: 18.r)),
),
],
),

@ -401,8 +401,9 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
group.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: group.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 24.w,
height: 24.h,
fit: BoxFit.cover,

@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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/my_appointments/appointment_rating_view_model.dart';
@ -96,7 +96,13 @@ class _RateAppointmentClinicState extends State<RateAppointmentClinic> {
});
},
iconSize: 35,
icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg', colorFilter: getColors(rating)),
icon: Utils.buildSvgWithAssets(
icon: 'assets/images/svg/rate_${index + 1}.svg',
iconColor: getRatingColor(rating),
width: 35,
height: 35,
applyThemeColor: false,
),
),
)
: IconButton(
@ -106,7 +112,12 @@ class _RateAppointmentClinicState extends State<RateAppointmentClinic> {
});
},
iconSize: 35,
icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg'),
icon: Utils.buildSvgWithAssets(
icon: 'assets/images/svg/rate_${index + 1}.svg',
width: 35,
height: 35,
applyThemeColor: false,
),
),
),
],
@ -181,6 +192,23 @@ class _RateAppointmentClinicState extends State<RateAppointmentClinic> {
);
}
Color getRatingColor(int rating) {
switch (rating) {
case 5:
return AppColors.bgGreenColor;
case 4:
return Colors.greenAccent;
case 3:
return AppColors.warningLightColor;
case 2:
return Colors.orange;
case 1:
return AppColors.primaryRedColor;
default:
return AppColors.greyColor;
}
}
ColorFilter getColors(int rating){
switch(rating){

@ -9,6 +9,9 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'dart:ui' as ui;
import '../../../core/utils/utils.dart';
import '../../../theme/colors.dart';
class BuildDoctorRow extends StatelessWidget {
bool isForClinic = false;
AppointmentDetails? appointmentDetails;
@ -21,8 +24,9 @@ class BuildDoctorRow extends StatelessWidget {
return Padding(padding: EdgeInsets.all(16),child:Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL ,
Utils.buildImgWithNetwork(
url: isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL ,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -81,7 +81,7 @@ class SmartwatchHomePage extends StatelessWidget {
),
child: Column(
children: [
Image.asset("assets/images/png/smartwatches/apple-watch-5.jpg", width: 136.h, height: 136.h).paddingSymmetrical(24.w, 8.h),
Utils.buildImgWithAssets(icon: "assets/images/png/smartwatches/apple-watch-5.jpg", width: 136.h, height: 136.h).paddingSymmetrical(24.w, 8.h),
"Apple Watch".needTranslation.toText16(isBold: true),
CustomButton(
text: LocaleKeys.selectSmartWatch.tr(context: context),
@ -115,7 +115,7 @@ class SmartwatchHomePage extends StatelessWidget {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
children: [
Image.asset("assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg", fit: BoxFit.contain, width: 136.w, height: 136.h)
Utils.buildImgWithAssets(icon: "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg", fit: BoxFit.contain, width: 136.w, height: 136.h)
.paddingSymmetrical(24.w, 8.h),
"Samsung Watch".needTranslation.toText16(isBold: true),
CustomButton(
@ -153,7 +153,7 @@ class SmartwatchHomePage extends StatelessWidget {
),
child: Column(
children: [
Image.asset("assets/images/png/smartwatches/Huawei_Watch.png", width: 136.w, height: 136.h).paddingSymmetrical(24.w, 8.h),
Utils.buildImgWithAssets(icon: "assets/images/png/smartwatches/Huawei_Watch.png", width: 136.w, height: 136.h).paddingSymmetrical(24.w, 8.h),
"Huawei Watch".needTranslation.toText16(isBold: true),
CustomButton(
text: LocaleKeys.selectSmartWatch.tr(context: context),
@ -187,7 +187,7 @@ class SmartwatchHomePage extends StatelessWidget {
),
child: Column(
children: [
Image.asset("assets/images/png/smartwatches/Whoop_Watch.png", width: 136.w, height: 136.h).paddingSymmetrical(24.w, 8.h),
Utils.buildImgWithAssets(icon: "assets/images/png/smartwatches/Whoop_Watch.png", width: 136.w, height: 136.h).paddingSymmetrical(24.w, 8.h),
"Whoop Watch".needTranslation.toText16(isBold: true),
CustomButton(
text: LocaleKeys.selectSmartWatch.tr(context: context),

@ -48,8 +48,8 @@ class SmartwatchInstructionsPage extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
spacing: 18.h,
children: [
Image.asset(
smartwatchDetails.watchIcon,
Utils.buildImgWithAssets(
icon: smartwatchDetails.watchIcon,
fit: BoxFit.contain,
height: 280.h,
width: 280.w,

@ -1,11 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:health/health.dart';
import 'package:fl_chart/fl_chart.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/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/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric.dart';
@ -53,12 +53,12 @@ class HealthMetricCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
metric.svgIcon,
Utils.buildSvgWithAssets(
icon: metric.svgIcon,
height: 30.h,
width: 30.h,
fit: BoxFit.contain,
// color: color,
applyThemeColor: false,
),
// Icon(
// metric.icon,

@ -23,6 +23,7 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_b
import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart';
import 'package:hmg_patient_app_new/presentation/emergency_services/nearest_er_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/triage_level_card.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@ -49,11 +50,46 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Clear selections once user reaches results page
symptomsCheckerViewModel.clearSelectionsKeepResults();
symptomsCheckerViewModel = context.read<SymptomsCheckerViewModel>();
// Call GetTriage API to fetch triage level
_fetchTriageLevel();
// NOTE: Do NOT clear selections here - we need the evidence for getExplanations API
// The evidence (symptoms, risk factors, suggestions) is needed when user taps "Explain, Why?" on condition cards
// symptomsCheckerViewModel.clearSelectionsForPossibleConditions();
});
}
void _fetchTriageLevel() {
// Build evidence from all selections
List<Map<String, dynamic>> evidence = [];
// Get all evidence with proper sources
final allEvidence = symptomsCheckerViewModel.getAllEvidenceWithSources();
// Convert to simple format (id + choice_id)
for (var item in allEvidence) {
evidence.add({
"id": item["id"] ?? "",
"choice_id": "present",
});
}
// Call Triage API
symptomsCheckerViewModel.fetchTriageLevel(
evidence: evidence,
onSuccess: (response) {
// Triage level data is now stored in ViewModel
// UI will update automatically via Consumer
},
onError: (error) {
// Silently fail - show conditions without triage card
debugPrint("Error fetching triage level: $error");
},
);
}
Widget _buildLoadingShimmer() {
return ListView.separated(
shrinkWrap: true,
@ -98,6 +134,12 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Triage Level Card at the top (if available)
if (symptomsCheckerViewModel.triageLevelResponse?.dataDetails != null)
TriageLevelCard(
triageLevelData: symptomsCheckerViewModel.triageLevelResponse!.dataDetails!,
),
SizedBox(height: 16.h),
ListView.separated(
padding: EdgeInsets.zero,
@ -128,8 +170,6 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
return;
}
log("isBookingFromSymptomsChecker: ${symptomsCheckerViewModel.isBookingFromSymptomsChecker}");
// For non-emergency cases, continue with normal booking flow
LoaderBottomSheet.showLoader();
symptomsCheckerViewModel.getClinicConditionsFromCategory(
@ -164,7 +204,11 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
loadingText: LocaleKeys.areYouSureYouWantToExitProgress.tr(context: context),
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
onConfirmTap: () {
// Clear all evidence before exiting
symptomsCheckerViewModel.clearSelectionsForPossibleConditions();
onConfirm();
},
),
callBackFunc: () {},
isFullScreen: false,
@ -301,7 +345,6 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
initiateBookAppointmentFlow(BuildContext context) {
// Set flag to indicate booking is from symptoms checker
symptomsCheckerViewModel.setBookingFromSymptomsChecker(true);
// Check if clinicDetailsList is not empty before accessing
if (symptomsCheckerViewModel.clinicDetailsList.isEmpty) {
// Show error if no clinic details available

@ -122,6 +122,7 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
Expanded(
child: LocaleKeys.suggestionAIListDescription.tr(context: context).toText12(
color: AppColors.greyInfoTextColor,
fontWeight: FontWeight.w600
),
)
],

@ -18,6 +18,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:lottie/lottie.dart';
import 'package:provider/provider.dart';
@ -158,7 +159,13 @@ class _TriagePageState extends State<TriagePage> {
}
void _onPreviousPressed() {
context.pop();
// Try to go back to previous question
bool wentBack = viewModel.goBackToPreviousTriageQuestion();
if (!wentBack) {
// If already at first question, go back to suggestions screen
context.pop();
}
}
void _onNextPressed() {
@ -225,15 +232,15 @@ class _TriagePageState extends State<TriagePage> {
}
}
// Get all evidence: initial symptoms + risk factors + suggestions + triage evidence
List<String> initialEvidenceIds = viewModel.getAllEvidenceIds();
// Get all evidence: initial symptoms + risk factors + suggestions (with sources) + triage evidence
List<Map<String, dynamic>> initialEvidenceList = viewModel.getAllEvidenceWithSources();
List<Map<String, String>> triageEvidence = viewModel.getTriageEvidence();
// Call API with updated evidence
viewModel.getDiagnosisForTriage(
age: viewModel.selectedAge!,
gender: viewModel.selectedGender!.toLowerCase() == "male" ? 1 : 2,
evidenceIds: initialEvidenceIds,
initialEvidenceList: initialEvidenceList,
triageEvidence: triageEvidence,
language: viewModel.appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
@ -245,6 +252,159 @@ class _TriagePageState extends State<TriagePage> {
);
}
void _showRationaleBottomSheet(BuildContext context) {
// Get all current evidence to send to the API
List<Map<String, dynamic>> evidence = [];
// Add all initial evidence (symptoms + risk factors + suggestions) with sources
final initialEvidence = viewModel.getAllEvidenceWithSources();
evidence.addAll(initialEvidence);
// Add triage evidence answered so far
final triageEvidence = viewModel.getTriageEvidence();
for (var triageItem in triageEvidence) {
evidence.add({
"id": triageItem["id"] ?? "",
"choice_id": triageItem["choice_id"] ?? "present",
"source": "initial",
});
}
// Show loading state
LoaderBottomSheet.showLoader();
// Call Rationale API
viewModel.fetchRationale(
evidence: evidence,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
// Get observation params (symptoms being analyzed)
final observationParams = response.dataDetails?.observationParams ?? [];
// Build symptom names string
String symptomNames = '';
if (observationParams.isNotEmpty) {
final names = observationParams.map((param) => param.getDisplayName()).where((name) => name.isNotEmpty).toList();
final isArabic = _appState.isArabic();
final andSeparator = isArabic ? ' و ' : ' and ';
if (names.length == 1) {
symptomNames = names.first;
} else if (names.length == 2) {
symptomNames = names.join(andSeparator);
} else if (names.length > 2) {
final lastItem = names.last;
final allButLast = names.sublist(0, names.length - 1);
symptomNames = '${allButLast.join(', ')}$andSeparator$lastItem';
}
}
if (symptomNames.isEmpty) {
symptomNames = LocaleKeys.analyzingYourSymptoms.tr(context: context);
}
// Show bottom sheet with rationale
_displayRationaleBottomSheet(context, symptomNames);
},
onError: (error) {
LoaderBottomSheet.hideLoader();
dialogService.showErrorBottomSheet(message: error);
},
);
}
void _displayRationaleBottomSheet(BuildContext context, String symptomNames) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.whyAmIBeingAskedThis.tr(context: context),
context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.weAreAskingThisQuestionToBetterUnderstand.tr(context: context, namedArgs: {'symptoms': symptomNames}).toText14(
color: AppColors.textColor,
),
SizedBox(height: 24.h),
CustomButton(
text: LocaleKeys.ok.tr(context: context),
onPressed: () => Navigator.pop(context),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
),
],
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
void _showInstructionsBottomSheet(BuildContext context, TriageQuestion question) {
final instructions = question.instruction ?? [];
final hasExplication = question.explication != null && question.explication!.isNotEmpty;
final hasInstructions = instructions.isNotEmpty;
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.howTo.tr(context: context),
context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Explication (if available)
if (hasExplication) ...[
(question.explication!).toText14(
color: AppColors.textColor,
),
if (hasInstructions) SizedBox(height: 16.h),
],
// Instructions list with bullets (if available)
if (hasInstructions) ...[
...instructions.asMap().entries.map((entry) {
final instruction = entry.value;
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 6.h, right: 12.w, left: 12.w),
width: 6.w,
height: 6.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primaryRedColor,
),
),
Expanded(
child: instruction.toText14(
color: AppColors.textColor,
),
),
],
),
);
}).toList(),
],
SizedBox(height: 24.h),
CustomButton(
text: LocaleKeys.ok.tr(context: context),
onPressed: () => Navigator.pop(context),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
),
],
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -401,8 +561,27 @@ class _TriagePageState extends State<TriagePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Main question text
(question.text ?? "").toText16(isBold: true, color: AppColors.textColor),
// Main question text with optional help icon
Row(
children: [
Expanded(
child: (question.text ?? "").toText16(isBold: true, color: AppColors.textColor),
),
// Show help icon if instructions are available
if (question.hasInstructions) ...[
SizedBox(width: 8.w),
GestureDetector(
onTap: () => _showInstructionsBottomSheet(context, question),
child: Utils.buildSvgWithAssets(
icon: AppAssets.alertSquare,
height: 24.h,
width: 24.h,
iconColor: AppColors.textColor,
),
),
],
],
),
SizedBox(height: 24.h),
// Type 1: Show items as checkboxes only (no choices displayed)
@ -474,6 +653,23 @@ class _TriagePageState extends State<TriagePage> {
);
}),
],
SizedBox(height: 12.h),
GestureDetector(
onTap: () => _showRationaleBottomSheet(context),
child: Row(
children: [
Expanded(
child: LocaleKeys.whyAmIBeingAskedThis.tr(context: context).toText12(
decorationColor: AppColors.primaryRedColor,
isUnderLine: true,
color: AppColors.primaryRedColor,
fontWeight: FontWeight.w600,
),
)
],
),
),
],
),
),
@ -537,7 +733,7 @@ class _TriagePageState extends State<TriagePage> {
children: [
RichText(
text: TextSpan(
text: "${LocaleKeys.possibleSymptom.tr(context: context)} ",
text: "${LocaleKeys.possibleConditions.tr(context: context)} ",
style: TextStyle(
color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'),
children: [
@ -587,8 +783,8 @@ class _TriagePageState extends State<TriagePage> {
Expanded(
child: CustomButton(
text: LocaleKeys.previous.tr(context: context),
onPressed: isFirstQuestion ? () {} : _onPreviousPressed,
isDisabled: isFirstQuestion || viewModel.isTriageDiagnosisLoading,
onPressed: _onPreviousPressed,
isDisabled: viewModel.isTriageDiagnosisLoading,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,

@ -0,0 +1,231 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
// Before using the checkup, please read the Terms of Service and remember:
//
// Checkup isn't a diagnosis. It's only for your information and not a qualified medical opinion.
// Checkup isn't for emergencies. Call your local emergency number right away when there's a health emergency.
// Your data is safe. Privacy measures ensure the information you share is protected.
class UserConsentScreen extends StatefulWidget {
const UserConsentScreen({super.key});
@override
State<UserConsentScreen> createState() => _UserConsentScreenState();
}
class _UserConsentScreenState extends State<UserConsentScreen> {
bool _isTermsAccepted = false;
bool _isPrivacyAccepted = false;
void _onTermsAndConditionsAgreed() {
setState(() {
_isTermsAccepted = !_isTermsAccepted;
});
}
void _onPrivacyPolicyAgreed() {
setState(() {
_isPrivacyAccepted = !_isPrivacyAccepted;
});
}
bool get _canProceed => _isTermsAccepted && _isPrivacyAccepted;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.termsService.tr(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Before using the checkup, please read the Terms of Service and remember:".toText14(),
SizedBox(height: 44.h),
"• Checkup isn't a diagnosis. It's only for your information and not a qualified medical opinion.".toText14(),
SizedBox(height: 12.h),
"• Checkup isn't for emergencies. Call your local emergency number right away when there's a health emergency.".toText14(),
SizedBox(height: 12.h),
"• Your data is safe. Privacy measures ensure the information you share is protected.".toText14(),
SizedBox(height: 24.h),
GestureDetector(
onTap: _onTermsAndConditionsAgreed,
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: _isTermsAccepted ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(5.r),
border: Border.all(
color: _isTermsAccepted ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor,
width: 1.w,
),
),
child: _isTermsAccepted ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
),
SizedBox(width: 12.w),
Expanded(
child: RichText(
text: TextSpan(
text: LocaleKeys.iAgreeAndAcceptTermsAndConditions.tr(context: context),
style: TextStyle(
fontSize: 16.f,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w500,
color: AppColors.textColor,
),
children: [
WidgetSpan(
child: GestureDetector(
onTap: () {
AppState appState = getIt.get<AppState>();
Utils.openWebView(
url: appState.isArabic() ? AppUrls.hmgTermsUrlAr : AppUrls.hmgTermsUrlEn,
);
},
child: Text(
LocaleKeys.termsAndConditions.tr(context: context),
style: TextStyle(
fontSize: 16.f,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w500,
color: AppColors.primaryRedColor,
decoration: TextDecoration.underline,
decorationColor: AppColors.primaryRedColor,
),
),
),
),
],
),
),
),
],
),
),
),
SizedBox(
height: 16.h,
),
GestureDetector(
onTap: _onPrivacyPolicyAgreed,
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: _isPrivacyAccepted ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(5.r),
border: Border.all(
color: _isPrivacyAccepted ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor,
width: 1.w,
),
),
child: _isPrivacyAccepted ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
),
SizedBox(width: 12.w),
Expanded(
child: RichText(
text: TextSpan(
text: LocaleKeys.iAgreeHealthInfoCanBeUsed.tr(context: context),
style: TextStyle(
fontSize: 16.f,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w500,
color: AppColors.textColor,
),
children: [
WidgetSpan(
child: GestureDetector(
onTap: () {
AppState appState = getIt.get<AppState>();
Utils.openWebView(
url: appState.isArabic() ? AppUrls.hmgPrivacyUrlAr : AppUrls.hmgPrivacyUrlEn,
);
},
child: Text(
LocaleKeys.privacyPolicyText.tr(context: context),
style: TextStyle(
fontSize: 16.f,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w500,
color: AppColors.primaryRedColor,
decorationColor: AppColors.primaryRedColor,
),
),
),
),
],
),
),
),
],
),
),
),
],
).paddingAll(24.w),
),
),
_buildStickyBottomCard(context),
],
),
);
}
Widget _buildStickyBottomCard(BuildContext context) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: LocaleKeys.next.tr(context: context),
onPressed: _canProceed ? () => context.navigateWithName(AppRoutes.userInfoSelection) : null,
isDisabled: !_canProceed,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
),
),
],
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0),
);
}
}

@ -17,14 +17,14 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:provider/provider.dart';
class UserInfoSelectionPage extends StatefulWidget {
const UserInfoSelectionPage({super.key});
class UserInfoSelectionScreen extends StatefulWidget {
const UserInfoSelectionScreen({super.key});
@override
State<UserInfoSelectionPage> createState() => _UserInfoSelectionPageState();
State<UserInfoSelectionScreen> createState() => _UserInfoSelectionScreenState();
}
class _UserInfoSelectionPageState extends State<UserInfoSelectionPage> {
class _UserInfoSelectionScreenState extends State<UserInfoSelectionScreen> {
@override
void initState() {
super.initState();
@ -202,10 +202,15 @@ class _UserInfoSelectionPageState extends State<UserInfoSelectionPage> {
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
child: Column(
children: [
LocaleKeys.helloIsYourInformationUpToDate.tr(namedArgs: {'name': name}).toText16(
weight: FontWeight.w600,
color: AppColors.textColor,
),
LocaleKeys.helloIsYourInformationUpToDate
.tr(namedArgs: {'name': name})
.toText16(
weight: FontWeight.w600,
color: AppColors.textColor,
)
.onPress(() {
context.navigateWithName(AppRoutes.userConsentScreen);
}),
SizedBox(height: 32.h),
_buildEditInfoTile(
context: context,

@ -8,11 +8,16 @@ import 'package:hmg_patient_app_new/core/enums.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/symptoms_checker/models/resp_models/triage_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:provider/provider.dart';
class ConditionCard extends StatelessWidget {
final TriageCondition condition;
@ -247,6 +252,16 @@ class ConditionCard extends StatelessWidget {
LocaleKeys.description.tr(context: context).toText14(weight: FontWeight.bold, color: AppColors.textColor),
SizedBox(height: 2.h),
description.toText12(isBold: true, color: AppColors.greyTextColor),
SizedBox(height: 8.h),
GestureDetector(
onTap: () => _showExplanationBottomSheet(context),
child: LocaleKeys.explainWhy.tr(context: context).toText12(
isBold: true,
color: AppColors.primaryRedColor,
isUnderLine: true,
decorationColor: AppColors.primaryRedColor,
),
),
if (severityEnum != PossibleConditionsSeverityEnum.monitorOnly)
CustomButton(
text: LocaleKeys.bookAppointment.tr(context: context),
@ -283,4 +298,107 @@ class ConditionCard extends StatelessWidget {
),
);
}
void _showExplanationBottomSheet(BuildContext context) {
final viewModel = context.read<SymptomsCheckerViewModel>();
final conditionId = condition.id ?? "";
if (conditionId.isEmpty) {
final dialogService = getIt<DialogService>();
dialogService.showErrorBottomSheet(
message: "Condition ID not available",
);
return;
}
// Get all evidence with proper sources
final allEvidenceWithSources = viewModel.getAllEvidenceWithSources();
// Convert to format expected by GetExplanations API (with choice_id)
final evidence = allEvidenceWithSources.map((item) {
return {
"id": item["id"] ?? "",
"choice_id": "present",
"source": item["source"] ?? "initial",
};
}).toList();
// Show loading
LoaderBottomSheet.showLoader();
// Call Explanations API
viewModel.fetchExplanations(
targetConditionId: conditionId,
evidence: evidence,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
final supportingEvidence = response.dataDetails?.supportingEvidence ?? [];
// Show bottom sheet with supporting evidence
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.whyThisCondition.tr(context: context),
context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Human-understandable description
LocaleKeys.explanationDescription.tr(context: context).toText14(
color: AppColors.textColor,
),
SizedBox(height: 12.h),
// Supporting Evidence List
if (supportingEvidence.isEmpty)
LocaleKeys.noDataAvailable.tr(context: context).toText14(
color: AppColors.greyTextColor,
)
else
...supportingEvidence.map((evidence) {
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 6.h, right: 12.w, left: 12.w),
width: 6.w,
height: 6.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.textColor,
),
),
Expanded(
child: (evidence.getDisplayName()).toText14(
color: AppColors.textColor,
),
),
],
),
);
}).toList(),
SizedBox(height: 24.h),
CustomButton(
text: LocaleKeys.ok.tr(context: context),
onPressed: () => Navigator.pop(context),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
),
],
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onError: (error) {
LoaderBottomSheet.hideLoader();
final dialogService = getIt<DialogService>();
dialogService.showErrorBottomSheet(message: error);
},
);
}
}

@ -2,6 +2,8 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.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/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart';
@ -113,11 +115,21 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
children: [
// Body image
Positioned.fill(
child: Image.asset(
widget.bodyImageAsset,
fit: BoxFit.fill,
color: widget.isBodyHidden ? Colors.black.withValues(alpha: 0.5) : null,
),
child: widget.isBodyHidden
? ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.5),
BlendMode.darken,
),
child: Utils.buildImgWithAssets(
icon: widget.bodyImageAsset,
fit: BoxFit.fill,
),
)
: Utils.buildImgWithAssets(
icon: widget.bodyImageAsset,
fit: BoxFit.fill,
),
),
// Organ dots

@ -0,0 +1,280 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class TriageLevelCard extends StatefulWidget {
final TriageLevelDataDetails triageLevelData;
const TriageLevelCard({super.key, required this.triageLevelData});
@override
State<TriageLevelCard> createState() => _TriageLevelCardState();
}
class _TriageLevelCardState extends State<TriageLevelCard> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _borderAnimation;
@override
void initState() {
super.initState();
// Create animation controller for border pulse effect
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
// Create animation that goes from 1.0 to 0.3 and back (for opacity)
_borderAnimation = Tween<double>(begin: 1.0, end: 0.3).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
// Start the animation and repeat
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// Get localized title based on triage level
String getTitle(BuildContext context) {
final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) {
case 'emergency_ambulance':
return LocaleKeys.triageLevelEmergencyAmbulance.tr(context: context);
case 'emergency':
return LocaleKeys.triageLevelEmergency.tr(context: context);
case 'consultation_24':
return LocaleKeys.triageLevelConsultation24.tr(context: context);
case 'consultation':
return LocaleKeys.triageLevelConsultation.tr(context: context);
case 'self_care':
return LocaleKeys.triageLevelSelfCare.tr(context: context);
default:
return LocaleKeys.triageLevelConsultation.tr(context: context);
}
}
/// Get localized description based on triage level
String getDescription(BuildContext context) {
final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) {
case 'emergency_ambulance':
return LocaleKeys.triageLevelEmergencyAmbulanceDesc.tr(context: context);
case 'emergency':
return LocaleKeys.triageLevelEmergencyDesc.tr(context: context);
case 'consultation_24':
return LocaleKeys.triageLevelConsultation24Desc.tr(context: context);
case 'consultation':
return LocaleKeys.triageLevelConsultationDesc.tr(context: context);
case 'self_care':
return LocaleKeys.triageLevelSelfCareDesc.tr(context: context);
default:
return LocaleKeys.triageLevelConsultationDesc.tr(context: context);
}
}
/// Get color based on triage level
Color getBackgroundColor() {
final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) {
case 'emergency_ambulance':
case 'emergency':
return AppColors.chipColorEmergency.withValues(alpha: 0.1);
case 'consultation_24':
case 'consultation':
return AppColors.chipColorSeekMedicalAdvice.withValues(alpha: 0.1);
case 'self_care':
return AppColors.chipColorMonitor.withValues(alpha: 0.1);
default:
return AppColors.chipColorSeekMedicalAdvice.withValues(alpha: 0.1);
}
}
/// Get border color based on triage level
Color getBorderColor() {
final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) {
case 'emergency_ambulance':
case 'emergency':
return AppColors.chipColorEmergency;
case 'consultation_24':
case 'consultation':
return AppColors.chipColorSeekMedicalAdvice;
case 'self_care':
return AppColors.chipColorMonitor;
default:
return AppColors.chipColorSeekMedicalAdvice;
}
}
/// Get icon based on triage level
IconData getIcon() {
final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) {
case 'emergency_ambulance':
return Icons.local_hospital;
case 'emergency':
return Icons.emergency;
case 'consultation_24':
return Icons.schedule;
case 'consultation':
return Icons.medical_services;
case 'self_care':
return Icons.self_improvement;
default:
return Icons.medical_services;
}
}
/// Get root cause text (localized)
String? getRootCauseText(BuildContext context) {
final rootCause = widget.triageLevelData.rootCause ?? '';
switch (rootCause) {
case 'emergency_evidence_present':
return LocaleKeys.rootCauseEmergencyEvidencePresent.tr(context: context);
case 'serious_evidence_present':
return LocaleKeys.rootCauseSeriousEvidencePresent.tr(context: context);
case 'emergency_condition_likely':
return LocaleKeys.rootCauseEmergencyConditionLikely.tr(context: context);
case 'emergency_condition_possible':
return LocaleKeys.rootCauseEmergencyConditionPossible.tr(context: context);
case 'consultation_condition_likely':
return LocaleKeys.rootCauseConsultationConditionLikely.tr(context: context);
case 'self_care_sufficient':
return LocaleKeys.rootCauseSelfCareSufficient.tr(context: context);
case 'diagnosis_unknown':
return LocaleKeys.rootCauseDiagnosisUnknown.tr(context: context);
default:
return null;
}
}
@override
Widget build(BuildContext context) {
final borderColor = getBorderColor();
final title = getTitle(context);
final description = getDescription(context);
final rootCauseText = getRootCauseText(context);
final seriousSymptoms = widget.triageLevelData.serious ?? [];
return AnimatedBuilder(
animation: _borderAnimation,
builder: (context, child) {
return Container(
width: double.infinity,
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 8.h),
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: borderColor.withValues(alpha: _borderAnimation.value),
width: 2.w,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 10.r,
offset: Offset(0, 2.h),
),
],
),
child: child,
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: title.toText18(isBold: true, color: AppColors.textColor),
),
],
),
SizedBox(height: 12.h),
// Description
description.toText14(color: AppColors.textColor),
// Root Cause (if available)
if (rootCauseText != null) ...[
SizedBox(height: 12.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 8.h),
decoration: BoxDecoration(
color: AppColors.whiteColor.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.info_outline,
size: 16.f,
color: borderColor,
),
SizedBox(width: 8.w),
Flexible(
child: rootCauseText.toText12(
color: AppColors.textColor,
isBold: true,
),
),
],
),
),
],
// Serious Symptoms (if any)
if (seriousSymptoms.isNotEmpty) ...[
SizedBox(height: 16.h),
LocaleKeys.seriousSymptoms.tr(context: context).toText14(
isBold: true,
color: borderColor,
),
SizedBox(height: 8.h),
...seriousSymptoms.map((symptom) {
return Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 6.h, right: 8.w, left: 8.w),
width: 6.w,
height: 6.h,
decoration: BoxDecoration(shape: BoxShape.circle, color: borderColor),
),
Expanded(
child: (symptom.getDisplayName()).toText13(color: AppColors.textColor),
),
],
),
);
}),
],
],
),
);
}
}

@ -1779,7 +1779,7 @@ class _CallScreenState extends State<CallScreen> {
children: [
IconButton(
onPressed: onPressAudio,
icon: isMuted.value ? Image.asset("assets/images/png/zoom/unmute@2x.png") : Image.asset("assets/images/png/zoom/mute@2x.png"),
icon: isMuted.value ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/unmute@2x.png") : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/mute@2x.png"),
iconSize: circleButtonSize,
tooltip: isMuted.value == true ? "Unmute" : "Mute",
),
@ -1791,7 +1791,7 @@ class _CallScreenState extends State<CallScreen> {
IconButton(
onPressed: onPressVideo,
iconSize: circleButtonSize,
icon: isVideoOn.value ? Image.asset("assets/images/png/zoom/video-off@2x.png") : Image.asset("assets/images/png/zoom/video-on@2x.png"),
icon: isVideoOn.value ? Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-off@2x.png") : Utils.buildImgWithAssets(icon: "assets/images/png/zoom/video-on@2x.png"),
),
Column(
children: [

@ -107,7 +107,7 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 50.h, height: 50.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true).toShimmer2(isShow: todoVM.isProcessingPayment),
],
@ -177,9 +177,9 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
).toShimmer2(isShow: todoVM.isProcessingPayment),
SizedBox(height: 16.h),

@ -224,8 +224,8 @@ class _AncillaryOrderDetailsListState extends State<AncillaryOrderDetailsList> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
Utils.buildImgWithAssets(
icon: gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 56.w,
height: 56.h,
),

@ -138,8 +138,9 @@ class AncillaryOrderCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!isLoading) ...[
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -235,12 +235,11 @@ class _VitalSignPageState extends State<VitalSignPage> {
child: Stack(
fit: StackFit.expand,
children: [
Image.asset(
getIt.get<AppState>().getAuthenticatedUser()?.gender == 1
Utils.buildImgWithAssets(
icon: getIt.get<AppState>().getAuthenticatedUser()?.gender == 1
? AppAssets.bmiFullBody
: AppAssets.bmiFullBodyFemale,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
Align(
alignment: Alignment.bottomCenter,

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart';
@ -24,7 +25,6 @@ import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart';
import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/presentation/service_price/services_price_list_page.dart';
import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_home_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart';
@ -33,8 +33,9 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/risk_factors_s
import 'package:hmg_patient_app_new/presentation/symptoms_checker/suggestions_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/symptoms_selector_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_consent_screen.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection_screen.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption_page.dart';
@ -85,6 +86,7 @@ class AppRoutes {
static const String possibleConditionsPage = '/possibleConditionsScreen';
static const String triagePage = '/triageProgressScreen';
static const String userInfoSelection = '/userInfoSelection';
static const String userConsentScreen = '/userConsentScreen';
static const String userInfoFlowManager = '/userInfoFlowManager';
static const String monthlyReports = '/monthlyReportsPage';
static const String qrParking = '/qrParkingPage';
@ -132,7 +134,8 @@ class AppRoutes {
bloodDonationPage: (context) => BloodDonationPage(),
myChildVaccine: (context) => MyChildVaccinePage(),
bookAppointmentPage: (context) => BookAppointmentPage(),
userInfoSelection: (context) => UserInfoSelectionPage(),
userInfoSelection: (context) => UserInfoSelectionScreen(),
userConsentScreen: (context) => UserConsentScreen(),
userInfoFlowManager: (context) => UserInfoFlowManager(),
smartWatches: (context) => SmartwatchHomePage(),
// huaweiHealthExample: (context) => HuaweiHealthExample(),

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.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/theme/colors.dart';
extension WithContainer on Widget {
@ -63,7 +63,7 @@ class DefaultButton extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (iconData != null) Icon(iconData, color: textColor),
if (svgIcon != null) SvgPicture.asset(svgIcon ?? ""),
if (svgIcon != null) Utils.buildSvgWithAssets(icon: svgIcon ?? "", applyThemeColor: false),
if (!isTextExpanded)
Padding(
padding: EdgeInsets.only(

@ -78,7 +78,7 @@ class AppCustomChipWidget extends StatelessWidget {
? Chip(
avatar: icon.isNotEmpty
? isIconPNG
? Image.asset(icon, width: iconS, height: iconS)
? Utils.buildImgWithAssets(icon: icon, width: iconS, height: iconS)
: Utils.buildSvgWithAssets(
icon: icon,
width: iconS,

@ -79,7 +79,7 @@ class ImageOptions {
}
},
onFilesTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
FilePickerResult? result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: [
'jpg',

@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:map_launcher/map_launcher.dart';
import '../../core/utils/utils.dart';
/// A reusable location map widget that displays a static map with address and directions button
/// Can be used in any review screen (HHC, CMC, etc.)
class LocationMapWidget extends StatelessWidget {
@ -127,24 +129,10 @@ class LocationMapWidget extends StatelessWidget {
ClipRRect(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(24.r),
child: Image.network(
staticMapUrl,
child: Utils.buildImgWithNetwork(
url: staticMapUrl,
iconColor: AppColors.transparent,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 165.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(24.r),
),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 165.h,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save