From 02e44ed7fbbcb6300a4403fe64c3c001d412c436 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Tue, 9 Jun 2026 16:27:30 +0300 Subject: [PATCH] added 3 apis --- assets/langs/ar-SA.json | 26 ++ assets/langs/en-US.json | 26 ++ lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 5 +- .../explanations_response_model.dart | 147 +++++++++++ .../resp_models/rationale_response_model.dart | 176 +++++++++++++ .../triage_level_response_model.dart | 151 +++++++++++ .../resp_models/triage_response_model.dart | 11 + .../symptoms_checker_repo.dart | 237 ++++++++++++++++++ .../symptoms_checker_view_model.dart | 173 +++++++++++++ lib/generated/locale_keys.g.dart | 27 ++ .../possible_conditions_screen.dart | 44 +++- .../symptoms_checker/suggestions_screen.dart | 1 + .../symptoms_checker/triage_screen.dart | 203 ++++++++++++++- .../widgets/condition_card.dart | 108 ++++++++ .../widgets/triage_level_card.dart | 225 +++++++++++++++++ pubspec.lock | 24 ++ 17 files changed, 1578 insertions(+), 8 deletions(-) create mode 100644 lib/features/symptoms_checker/models/resp_models/explanations_response_model.dart create mode 100644 lib/features/symptoms_checker/models/resp_models/rationale_response_model.dart create mode 100644 lib/features/symptoms_checker/models/resp_models/triage_level_response_model.dart create mode 100644 lib/presentation/symptoms_checker/widgets/triage_level_card.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index c2d4decf..a7e2c44d 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1446,6 +1446,9 @@ "symptomsSelector": "محدد الأعراض", "possibleSymptomsRelatedTo": "أعراض مرتبطة بـ", "suggestionAIListDescription": "هذه قائمة بالأعراض المقترحة من قبل الذكاء الاصطناعي، بناءً على المعلومات التي تم جمعها حتى الآن خلال المقابلة", + "whyAmIBeingAskedThis": "لماذا يُطرح علي هذا السؤال؟", + "weAreAskingThisQuestionToBetterUnderstand": "نطرح هذا السؤال لفهم {symptoms} بشكل أفضل وتحسين دقة التقييم.", + "analyzingYourSymptoms": "أعراضك", "analyzing": "جاري التحليل...", "seekMedicalAdvice": "اطلب المشورة الطبية", "monitorDescription": "لا حاجة لطلب المشورة الطبية. فقط حافظ على روتين صحي.", @@ -1464,6 +1467,29 @@ "areYouSureYouWantToExitProgress": "هل تود الخروج؟ ستفقد تقدمك الحالي.", "noQuestionAvailable": "لا يوجد سؤال متاح", "possibleSymptom": "تشخيص محتمل", + "triageLevelEmergencyAmbulance": "طوارئ - اتصل بالإسعاف", + "triageLevelEmergency": "طوارئ", + "triageLevelConsultation24": "استشارة خلال 24 ساعة", + "triageLevelConsultation": "استشارة طبية", + "triageLevelSelfCare": "رعاية ذاتية", + "triageLevelEmergencyAmbulanceDesc": "الأعراض المبلغ عنها خطيرة جدًا وقد تحتاج إلى رعاية طارئة. يجب عليك الاتصال بالإسعاف الآن.", + "triageLevelEmergencyDesc": "تبدو الأدلة المبلغ عنها خطيرة ويجب عليك الذهاب إلى قسم الطوارئ. إذا لم تتمكن من ال��صول إلى أقرب قسم طوارئ، اتصل بالإسعاف.", + "triageLevelConsultation24Desc": "يجب عليك زيارة الطبيب خلال 24 ساعة. إذا ساءت الأعراض فجأة، توجه إلى أقرب قسم طوارئ.", + "triageLevelConsultationDesc": "قد تحتاج إلى تقييم طبي وقد تحتاج إلى تحديد موعد مع طبيب. إذا ساءت الأعراض، راجع الطبيب على الفور.", + "triageLevelSelfCareDesc": "قد لا تتطلب الأعراض المعلنة تقييمًا طبيًا وعادة ما تزول من تلقاء نفسها. في بعض الأحيان يمكن علاجها من خلال طرق الرعاية الذاتية. راقب أعراضك واستشر الطبيب إذا ساءت الأعراض أو ظهرت أعراض جديدة.", + "rootCauseEmergencyEvidencePresent": "تم الإبلاغ عن دليل طوارئ", + "rootCauseSeriousEvidencePresent": "تم الإبلاغ عن دليل خطير", + "rootCauseEmergencyConditionLikely": "حالة تهدد الحياة محتملة", + "rootCauseEmergencyConditionPossible": "حالة تهدد الحياة ممكنة", + "rootCauseConsultationConditionLikely": "استشارة طبية مطلوبة", + "rootCauseSelfCareSufficient": "لا يوجد سبب محدد للتقييم الطبي", + "rootCauseDiagnosisUnknown": "التقييم غير ممكن", + "teleconsultationAvailable": "الاستشارة عن بُعد متاحة", + "seriousSymptoms": "أعراض خطيرة", + "howTo": "كيفية القياس؟", + "explainWhy": "اشرح، لماذا؟", + "whyThisCondition": "لماذا هذه الحالة؟", + "supportingEvidence": "الأدلة الداعمة", "symptomsCheckerFindingScore": "نسبة التطابق", "notSet": "غير محدد", "years": "سنوات", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 70293941..77d0e46e 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1438,6 +1438,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.", @@ -1456,6 +1459,29 @@ "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", "symptomsCheckerFindingScore": "- Symptoms checker finding score", "notSet": "Not set", "years": "Years", diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 3d10df4e..a28d72b3 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -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)); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 80f552a3..84f3acd0 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -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"; diff --git a/lib/features/symptoms_checker/models/resp_models/explanations_response_model.dart b/lib/features/symptoms_checker/models/resp_models/explanations_response_model.dart new file mode 100644 index 00000000..88273d33 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/explanations_response_model.dart @@ -0,0 +1,147 @@ +class ExplanationsResponseModel { + final ExplanationsDataDetails? dataDetails; + + ExplanationsResponseModel({this.dataDetails}); + + factory ExplanationsResponseModel.fromJson(Map json) { + return ExplanationsResponseModel( + dataDetails: json['dataDetails'] != null + ? ExplanationsDataDetails.fromJson(json['dataDetails']) + : null, + ); + } + + Map toJson() { + return { + 'dataDetails': dataDetails?.toJson(), + }; + } +} + +class ExplanationsDataDetails { + final List? supportingEvidence; + final List? conflictingEvidence; + final List? unconfirmedEvidence; + final String? message; + final List? 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 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.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 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 json) { + return ExplanationEvidence( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'common_name': commonName, + }; + } + + // Helper method to get display name + String getDisplayName() { + return commonName ?? name ?? ''; + } +} + diff --git a/lib/features/symptoms_checker/models/resp_models/rationale_response_model.dart b/lib/features/symptoms_checker/models/resp_models/rationale_response_model.dart new file mode 100644 index 00000000..33d31172 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/rationale_response_model.dart @@ -0,0 +1,176 @@ +class RationaleResponseModel { + final RationaleDataDetails? dataDetails; + + RationaleResponseModel({this.dataDetails}); + + factory RationaleResponseModel.fromJson(Map json) { + return RationaleResponseModel( + dataDetails: json['dataDetails'] != null + ? RationaleDataDetails.fromJson(json['dataDetails']) + : null, + ); + } + + Map toJson() { + return { + 'dataDetails': dataDetails?.toJson(), + }; + } +} + +class RationaleDataDetails { + final String? type; + final List? observationParams; + final List? conditionParams; + final String? message; + final List? 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 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.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 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 json) { + return RationaleObservationParam( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + ); + } + + Map 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 json) { + return RationaleConditionParam( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'common_name': commonName, + }; + } + + // Helper method to get display name + String getDisplayName() { + return commonName ?? name ?? ''; + } +} + diff --git a/lib/features/symptoms_checker/models/resp_models/triage_level_response_model.dart b/lib/features/symptoms_checker/models/resp_models/triage_level_response_model.dart new file mode 100644 index 00000000..7d8432bf --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/triage_level_response_model.dart @@ -0,0 +1,151 @@ +class TriageLevelResponseModel { + final TriageLevelDataDetails? dataDetails; + + TriageLevelResponseModel({this.dataDetails}); + + factory TriageLevelResponseModel.fromJson(Map json) { + return TriageLevelResponseModel( + dataDetails: json['dataDetails'] != null + ? TriageLevelDataDetails.fromJson(json['dataDetails']) + : null, + ); + } + + Map toJson() { + return { + 'dataDetails': dataDetails?.toJson(), + }; + } +} + +class TriageLevelDataDetails { + final String? triageLevel; + final List? serious; + final String? rootCause; + final bool? teleconsultationApplicable; + final String? message; + final List? 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 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.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 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 json) { + return SeriousEvidence( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + seriousness: json['seriousness'], + isEmergency: json['is_emergency'], + ); + } + + Map 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 ?? ''; + } +} + diff --git a/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart index da0f4b67..ea4d823b 100644 --- a/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart +++ b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart @@ -91,11 +91,15 @@ class TriageDataDetails { class TriageQuestion { final int? type; final String? text; + final String? explication; + final List? instruction; final List? items; TriageQuestion({ this.type, this.text, + this.explication, + this.instruction, this.items, }); @@ -103,6 +107,8 @@ class TriageQuestion { return TriageQuestion( type: json['type'], text: json['text'], + explication: json['explication'], + instruction: json['instruction'] != null ? List.from(json['instruction']) : null, items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null, ); } @@ -111,9 +117,14 @@ class TriageQuestion { return { 'type': type, 'text': text, + 'explication': explication, + 'instruction': instruction, 'items': items?.map((item) => item.toJson()).toList(), }; } + + /// Check if instruction is available + bool get hasInstructions => instruction != null && instruction!.isNotEmpty; } class TriageQuestionItem { diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 7a41149d..77d3a2fa 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -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'; @@ -66,6 +69,34 @@ abstract class SymptomsCheckerRepo { required ScheduleAppointmentRequestModel request, required String userSessionToken, }); + + Future>> getRationale({ + required int age, + required int gender, + required List> evidence, + required String language, + required String generalId, + required String userSessionToken, + }); + + Future>> getTriage({ + required int age, + required int gender, + required List> evidence, + required String language, + required String generalId, + required String userSessionToken, + }); + + Future>> getExplanations({ + required int age, + required int gender, + required List> evidence, + required String language, + required String target, + required String generalId, + required String userSessionToken, + }); } class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { @@ -535,4 +566,210 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getRationale({ + required int age, + required int gender, + required List> evidence, + required String language, + required String generalId, + required String userSessionToken, + }) async { + final Map body = { + "age": { + "value": age, + }, + "gender": gender, + "evidence": evidence, + "language": language, + "generalId": generalId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? 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 responseData = response is String ? jsonDecode(response) : response; + + RationaleResponseModel rationaleResponse = RationaleResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + 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>> getTriage({ + required int age, + required int gender, + required List> evidence, + required String language, + required String generalId, + required String userSessionToken, + }) async { + final Map body = { + "age": { + "value": age, + }, + "gender": gender, + "evidence": evidence, + "language": language, + "generalId": generalId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? 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 responseData = response is String ? jsonDecode(response) : response; + + TriageLevelResponseModel triageResponse = TriageLevelResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + 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>> getExplanations({ + required int age, + required int gender, + required List> evidence, + required String language, + required String target, + required String generalId, + required String userSessionToken, + }) async { + final Map body = { + "age": { + "value": age, + }, + "gender": gender, + "evidence": evidence, + "language": language, + "target": target, + "generalId": generalId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? 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 responseData = response is String ? jsonDecode(response) : response; + + ExplanationsResponseModel explanationsResponse = ExplanationsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + 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())); + } + } } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index 7d1deefa..18f6367e 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -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 @@ -1501,6 +1510,170 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); } + /// Get Rationale for symptoms + /// This API shows which symptoms and conditions are being considered + Future fetchRationale({ + required List> 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 fetchTriageLevel({ + required List> 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 fetchExplanations({ + required String targetConditionId, + required List> 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(); diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index b18de6db..035596a2 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1442,6 +1442,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'; @@ -1459,6 +1462,29 @@ 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 symptomsCheckerFindingScore = 'symptomsCheckerFindingScore'; static const notSet = 'notSet'; static const years = 'years'; @@ -1898,4 +1924,5 @@ abstract class LocaleKeys { static const amountOnly = 'amountOnly'; static const invoiceHistory = 'invoiceHistory'; static const refund = 'refund'; + } diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 12bd4c0c..59a03580 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -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,45 @@ class _PossibleConditionsPageState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { + symptomsCheckerViewModel = context.read(); + + // Call GetTriage API to fetch triage level + _fetchTriageLevel(); + // Clear all selections (organs, symptoms, risk factors, suggestions, questions) once user reaches possible conditions screen symptomsCheckerViewModel.clearSelectionsForPossibleConditions(); }); } + void _fetchTriageLevel() { + // Build evidence from all selections + List> 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 +133,12 @@ class _PossibleConditionsPageState extends State { 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 +169,6 @@ class _PossibleConditionsPageState extends State { return; } - log("isBookingFromSymptomsChecker: ${symptomsCheckerViewModel.isBookingFromSymptomsChecker}"); - // For non-emergency cases, continue with normal booking flow LoaderBottomSheet.showLoader(); symptomsCheckerViewModel.getClinicConditionsFromCategory( @@ -301,7 +340,6 @@ class _PossibleConditionsPageState extends State { 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 diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index 362d0eaf..dddb62db 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -122,6 +122,7 @@ class _SuggestionsScreenState extends State { Expanded( child: LocaleKeys.suggestionAIListDescription.tr(context: context).toText12( color: AppColors.greyInfoTextColor, + fontWeight: FontWeight.w600 ), ) ], diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 12b17353..93a4561f 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -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'; @@ -81,7 +82,8 @@ class _TriagePageState extends State { // Case 3: Probability >= 70% OR // Case 4: 30 or more questions answered // if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) { - if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30 || viewModel.shouldStopTriage) { + // todo: reset to 30 again + if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 5 || viewModel.shouldStopTriage) { // Navigate to results/possible conditions screen context.navigateWithName(AppRoutes.possibleConditionsPage); return; @@ -251,6 +253,155 @@ class _TriagePageState extends State { ); } + void _showRationaleBottomSheet(BuildContext context) { + // Get all current evidence to send to the API + List> 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: false, + ); + } + + void _showInstructionsBottomSheet(BuildContext context, TriageQuestion question) { + final instructions = question.instruction ?? []; + + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.howTo.tr(context: context), + context, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Explication (if available) + if (question.explication != null && question.explication!.isNotEmpty) ...[ + (question.explication!).toText14( + color: AppColors.textColor, + ), + SizedBox(height: 16.h), + ], + + // Instructions list with bullets + ...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: false, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -407,8 +558,33 @@ class _TriagePageState extends State { 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: Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: AppColors.primaryRedColor.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.help_outline, + size: 20.f, + color: AppColors.primaryRedColor, + ), + ), + ), + ], + ], + ), SizedBox(height: 24.h), // Type 1: Show items as checkboxes only (no choices displayed) @@ -480,6 +656,27 @@ class _TriagePageState extends State { ); }), ], + + SizedBox(height: 12.h), + GestureDetector( + onTap: () => _showRationaleBottomSheet(context), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.whyAmIBeingAskedThis + .tr(context: context) + .toText12(color: AppColors.greyInfoTextColor, fontWeight: FontWeight.w600), + ) + ], + ), + ), ], ), ), diff --git a/lib/presentation/symptoms_checker/widgets/condition_card.dart b/lib/presentation/symptoms_checker/widgets/condition_card.dart index d231f52f..b6d97fa4 100644 --- a/lib/presentation/symptoms_checker/widgets/condition_card.dart +++ b/lib/presentation/symptoms_checker/widgets/condition_card.dart @@ -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,14 @@ 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).toText14( + isBold: true, + color: AppColors.primaryRedColor, + ), + ), if (severityEnum != PossibleConditionsSeverityEnum.monitorOnly) CustomButton( text: LocaleKeys.bookAppointment.tr(context: context), @@ -283,4 +296,99 @@ class ConditionCard extends StatelessWidget { ), ); } + + void _showExplanationBottomSheet(BuildContext context) { + final viewModel = context.read(); + final conditionId = condition.id ?? ""; + + if (conditionId.isEmpty) { + final dialogService = getIt(); + dialogService.showErrorBottomSheet( + message: "Condition ID not available", + ); + return; + } + + // Get all evidence + final evidence = viewModel.getAllEvidenceWithSources(); + + // 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: [ + // Supporting Evidence Section + LocaleKeys.supportingEvidence.tr(context: context).toText16( + isBold: true, + 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.primaryRedColor, + ), + ), + 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: false, + ); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + final dialogService = getIt(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } } diff --git a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart new file mode 100644 index 00000000..7fcb93c4 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart @@ -0,0 +1,225 @@ +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/extensions/widget_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 StatelessWidget { + final TriageLevelDataDetails triageLevelData; + + const TriageLevelCard({super.key, required this.triageLevelData}); + + /// Get localized title based on triage level + String getTitle(BuildContext context) { + final triageLevel = 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 = 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 = 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 = 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 = 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 = 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 = triageLevelData.serious ?? []; + + 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: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + 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), + ), + ], + ), + ); + }), + ], + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e76aec05..4f2c35ac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.13.0" + clarity_flutter: + dependency: "direct main" + description: + name: clarity_flutter + sha256: b3a114fec6e873caf03cd59a32953f8cb4b1693bd3b4f749ef3030fea027ebce + url: "https://pub.dev" + source: hosted + version: "1.9.0" clock: dependency: transitive description: @@ -1041,6 +1049,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.12.0+301" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" image_picker: dependency: "direct main" description: @@ -2150,6 +2166,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" sdks: dart: ">=3.9.0 <4.0.0" flutter: ">=3.35.0"