added 3 apis

pull/327/head
faizatflutter 1 day ago
parent c8378278c9
commit 02e44ed7fb

@ -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": "تبدو الأدلة المبلغ عنها خطيرة ويجب عليك الذهاب إلى قسم الطوارئ. إذا لم تتمكن من ال<D8A7><D984>صول إلى أقرب قسم طوارئ، اتصل بالإسعاف.",
"triageLevelConsultation24Desc": "يجب عليك زيارة الطبيب خلال 24 ساعة. إذا ساءت الأعراض فجأة، توجه إلى أقرب قسم طوارئ.",
"triageLevelConsultationDesc": "قد تحتاج إلى تقييم طبي وقد تحتاج إلى تحديد موعد مع طبيب. إذا ساءت الأعراض، راجع الطبيب على الفور.",
"triageLevelSelfCareDesc": "قد لا تتطلب الأعراض المعلنة تقييمًا طبيًا وعادة ما تزول من تلقاء نفسها. في بعض الأحيان يمكن علاجها من خلال طرق الرعاية الذاتية. راقب أعراضك واستشر الطبيب إذا ساءت الأعراض أو ظهرت أعراض جديدة.",
"rootCauseEmergencyEvidencePresent": "تم الإبلاغ عن دليل طوارئ",
"rootCauseSeriousEvidencePresent": "تم الإبلاغ عن دليل خطير",
"rootCauseEmergencyConditionLikely": "حالة تهدد الحياة محتملة",
"rootCauseEmergencyConditionPossible": "حالة تهدد الحياة ممكنة",
"rootCauseConsultationConditionLikely": "استشارة طبية مطلوبة",
"rootCauseSelfCareSufficient": "لا يوجد سبب محدد للتقييم الطبي",
"rootCauseDiagnosisUnknown": "التقييم غير ممكن",
"teleconsultationAvailable": "الاستشارة عن بُعد متاحة",
"seriousSymptoms": "أعراض خطيرة",
"howTo": "كيفية القياس؟",
"explainWhy": "اشرح، لماذا؟",
"whyThisCondition": "لماذا هذه الحالة؟",
"supportingEvidence": "الأدلة الداعمة",
"symptomsCheckerFindingScore": "نسبة التطابق",
"notSet": "غير محدد",
"years": "سنوات",

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

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

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

@ -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,11 +91,15 @@ 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,
});
@ -103,6 +107,8 @@ class TriageQuestion {
return TriageQuestion(
type: json['type'],
text: json['text'],
explication: json['explication'],
instruction: json['instruction'] != null ? List<String>.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 {

@ -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<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 {
@ -535,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
@ -1501,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();

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

@ -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<PossibleConditionsPage> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
symptomsCheckerViewModel = context.read<SymptomsCheckerViewModel>();
// 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<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 +133,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 +169,6 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
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<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';
@ -81,7 +82,8 @@ class _TriagePageState extends State<TriagePage> {
// 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<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: 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<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: 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<TriagePage> {
);
}),
],
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),
)
],
),
),
],
),
),

@ -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<SymptomsCheckerViewModel>();
final conditionId = condition.id ?? "";
if (conditionId.isEmpty) {
final dialogService = getIt<DialogService>();
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>();
dialogService.showErrorBottomSheet(message: error);
},
);
}
}

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

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

Loading…
Cancel
Save