Till Triage Completed

faiz_dev
faizatflutter 2 weeks ago
parent 597579e677
commit 49d091547a

@ -0,0 +1,31 @@
// ...existing imports...
import { apiService } from '../services/api';
// ...existing code...
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// ...existing state...
const login = async (nationalId: string, password: string) => {
try {
setIsLoading(true);
// Call the real API
const user = await apiService.getUserDetails(nationalId, password);
// Store user data
await AsyncStorage.setItem('user', JSON.stringify(user));
await AsyncStorage.setItem('authToken', user.authToken);
setUser(user);
} catch (error) {
console.error('Login error:', error);
throw new Error('Invalid credentials or network error');
} finally {
setIsLoading(false);
}
};
// ...existing code...
};

@ -825,11 +825,13 @@ class ApiConsts {
static final String updateHHCOrder = 'api/hhc/update';
static final String addHHCOrder = 'api/HHC/add';
// SYMPTOMS CHECKER
// SYMPTOMS CHECKER API
static final String symptomsUserLogin = '$symptomsCheckerApi/user_login';
static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName';
static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors';
static final String getSuggestions = '$symptomsCheckerApi/GetSuggestion';
static final String diagnosis = '$symptomsCheckerApi/diagnosis';
static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis';
static final String explain = '$symptomsCheckerApi/explain';
//E-REFERRAL SERVICES

@ -218,16 +218,6 @@ class Utils {
return await prefs.remove(key);
}
static void showLoading({bool isNeedBinding = true}) {
if (isNeedBinding) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showLoadingDialog();
});
} else {
showLoadingDialog();
}
}
static void showLoadingDialog() {
_isLoadingVisible = true;
showDialog(
@ -244,18 +234,6 @@ class Utils {
);
}
static void hideLoading() {
try {
if (_isLoadingVisible) {
_isLoadingVisible = false;
Navigator.of(navigationService.navigatorKey.currentContext!).pop();
}
_isLoadingVisible = false;
} catch (e) {
log("errr: ${e.toString()}");
}
}
static List<T> uniqueBy<T, K>(List<T> list, K Function(T) keySelector) {
final seenKeys = <K>{};
return list.where((item) => seenKeys.add(keySelector(item))).toList();

@ -0,0 +1,188 @@
class SymptomsUserDetailsResponseModel {
final TokenDetails? tokenDetails;
final UserDetails? userDetails;
final String? sessionId;
SymptomsUserDetailsResponseModel({
this.tokenDetails,
this.userDetails,
this.sessionId,
});
factory SymptomsUserDetailsResponseModel.fromJson(Map<String, dynamic> json) {
return SymptomsUserDetailsResponseModel(
tokenDetails: json['tokenDetails'] != null ? TokenDetails.fromJson(json['tokenDetails']) : null,
userDetails: json['userDetails'] != null ? UserDetails.fromJson(json['userDetails']) : null,
sessionId: json['sessionId'],
);
}
Map<String, dynamic> toJson() {
return {
'tokenDetails': tokenDetails?.toJson(),
'userDetails': userDetails?.toJson(),
'sessionId': sessionId,
};
}
}
class TokenDetails {
final String? id;
final String? authToken;
final int? expiresIn;
TokenDetails({
this.id,
this.authToken,
this.expiresIn,
});
factory TokenDetails.fromJson(Map<String, dynamic> json) {
return TokenDetails(
id: json['id'],
authToken: json['auth_token'],
expiresIn: json['expires_in'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'auth_token': authToken,
'expires_in': expiresIn,
};
}
}
class UserDetails {
final String? fileNo;
final String? nationalId;
final String? email;
final String? dateOfBirth;
final String? dateOfBirthHijri;
final int? age;
final UserName? name;
final int? maritalStatus;
final String? maritalStatusCode;
final String? nationality;
final String? nationalityIsoCode;
final String? occupation;
final int? idType;
final int? gender;
final String? jwtToken;
final String? countryDialCode;
final String? phoneNo;
UserDetails({
this.fileNo,
this.nationalId,
this.email,
this.dateOfBirth,
this.dateOfBirthHijri,
this.age,
this.name,
this.maritalStatus,
this.maritalStatusCode,
this.nationality,
this.nationalityIsoCode,
this.occupation,
this.idType,
this.gender,
this.jwtToken,
this.countryDialCode,
this.phoneNo,
});
factory UserDetails.fromJson(Map<String, dynamic> json) {
return UserDetails(
fileNo: json['FileNo'],
nationalId: json['national_id'],
email: json['email'],
dateOfBirth: json['date_of_birth'],
dateOfBirthHijri: json['date_of_birth_hijri'],
age: json['age'],
name: json['name'] != null ? UserName.fromJson(json['name']) : null,
maritalStatus: json['marital_status'],
maritalStatusCode: json['marital_status_code'],
nationality: json['nationality'],
nationalityIsoCode: json['nationality_iso_code'],
occupation: json['occupation'],
idType: json['id_type'],
gender: json['gender'],
jwtToken: json['jwt_token'],
countryDialCode: json['country_dial_code'],
phoneNo: json['phone_no'],
);
}
Map<String, dynamic> toJson() {
return {
'FileNo': fileNo,
'national_id': nationalId,
'email': email,
'date_of_birth': dateOfBirth,
'date_of_birth_hijri': dateOfBirthHijri,
'age': age,
'name': name?.toJson(),
'marital_status': maritalStatus,
'marital_status_code': maritalStatusCode,
'nationality': nationality,
'nationality_iso_code': nationalityIsoCode,
'occupation': occupation,
'id_type': idType,
'gender': gender,
'jwt_token': jwtToken,
'country_dial_code': countryDialCode,
'phone_no': phoneNo,
};
}
// Helper method to get full name
String getFullName(bool isArabic) {
if (name == null) return '';
if (isArabic) {
return '${name!.firstNameAr ?? ''} ${name!.middleNameAr ?? ''} ${name!.lastNameAr ?? ''}'.trim();
}
return '${name!.firstName ?? ''} ${name!.middleName ?? ''} ${name!.lastName ?? ''}'.trim();
}
}
class UserName {
final String? firstName;
final String? middleName;
final String? lastName;
final String? firstNameAr;
final String? middleNameAr;
final String? lastNameAr;
UserName({
this.firstName,
this.middleName,
this.lastName,
this.firstNameAr,
this.middleNameAr,
this.lastNameAr,
});
factory UserName.fromJson(Map<String, dynamic> json) {
return UserName(
firstName: json['first_name'],
middleName: json['middle_name'],
lastName: json['last_name'],
firstNameAr: json['first_name_ar'],
middleNameAr: json['middle_name_ar'],
lastNameAr: json['last_name_ar'],
);
}
Map<String, dynamic> toJson() {
return {
'first_name': firstName,
'middle_name': middleName,
'last_name': lastName,
'first_name_ar': firstNameAr,
'middle_name_ar': middleNameAr,
'last_name_ar': lastNameAr,
};
}
}

@ -0,0 +1,209 @@
class TriageDataDetails {
final TriageQuestion? question;
final List<TriageCondition>? conditions;
final bool? hasEmergencyEvidence;
final bool? shouldStop;
final String? interviewToken;
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;
TriageDataDetails({
this.question,
this.conditions,
this.hasEmergencyEvidence,
this.shouldStop,
this.interviewToken,
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 TriageDataDetails.fromJson(Map<String, dynamic> json) {
return TriageDataDetails(
question: json['question'] != null ? TriageQuestion.fromJson(json['question']) : null,
conditions: json['conditions'] != null ? (json['conditions'] as List).map((item) => TriageCondition.fromJson(item)).toList() : null,
hasEmergencyEvidence: json['has_emergency_evidence'],
shouldStop: json['should_stop'],
interviewToken: json['interview_token'],
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 {
'question': question?.toJson(),
'conditions': conditions?.map((item) => item.toJson()).toList(),
'has_emergency_evidence': hasEmergencyEvidence,
'should_stop': shouldStop,
'interview_token': interviewToken,
'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 TriageQuestion {
final int? type;
final String? text;
final List<TriageQuestionItem>? items;
TriageQuestion({
this.type,
this.text,
this.items,
});
factory TriageQuestion.fromJson(Map<String, dynamic> json) {
return TriageQuestion(
type: json['type'],
text: json['text'],
items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null,
);
}
Map<String, dynamic> toJson() {
return {
'type': type,
'text': text,
'items': items?.map((item) => item.toJson()).toList(),
};
}
}
class TriageQuestionItem {
final String? id;
final String? name;
final List<TriageChoice>? choices;
TriageQuestionItem({
this.id,
this.name,
this.choices,
});
factory TriageQuestionItem.fromJson(Map<String, dynamic> json) {
return TriageQuestionItem(
id: json['id'],
name: json['name'],
choices: json['choices'] != null ? (json['choices'] as List).map((item) => TriageChoice.fromJson(item)).toList() : null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'choices': choices?.map((item) => item.toJson()).toList(),
};
}
}
class TriageChoice {
final String? id;
final String? label;
TriageChoice({
this.id,
this.label,
});
factory TriageChoice.fromJson(Map<String, dynamic> json) {
return TriageChoice(
id: json['id'],
label: json['label'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'label': label,
};
}
}
class TriageCondition {
final String? id;
final String? name;
final String? commonName;
final double? probability;
final dynamic conditionDetails;
TriageCondition({
this.id,
this.name,
this.commonName,
this.probability,
this.conditionDetails,
});
factory TriageCondition.fromJson(Map<String, dynamic> json) {
return TriageCondition(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
probability: json['probability']?.toDouble(),
conditionDetails: json['condition_details'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
'probability': probability,
'condition_details': conditionDetails,
};
}
/// Get probability as percentage
String getProbabilityPercentage() {
if (probability == null) return '0%';
return '${(probability! * 100).toStringAsFixed(1)}%';
}
}

@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
@ -8,11 +7,20 @@ 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/resp_models/body_symptom_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/symptoms_user_details_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';
abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<SymptomsUserDetailsResponseModel>>> getUserDetails({
required String userName,
required String password,
});
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
required List<String> organNames,
required String userSessionToken,
required int gender,
});
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getRiskFactors({
@ -20,6 +28,9 @@ abstract class SymptomsCheckerRepo {
required String sex,
required List<String> evidenceIds,
required String language,
required String userSessionToken,
required int gender,
required String sessionId,
});
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
@ -27,6 +38,19 @@ abstract class SymptomsCheckerRepo {
required String sex,
required List<String> evidenceIds,
required String language,
required String userSessionToken,
required String sessionId,
required int gender,
});
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
required String userSessionToken,
required int gender,
required String sessionId,
});
}
@ -37,11 +61,71 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService});
@override
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({required List<String> organNames}) async {
log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}");
log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}");
Future<Either<Failure, GenericApiModel<SymptomsUserDetailsResponseModel>>> getUserDetails({
required String userName,
required String password,
}) async {
Map<String, String> body = {"userName": userName, "password": password};
try {
GenericApiModel<SymptomsUserDetailsResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.symptomsUserLogin,
body: body,
isExternal: true,
isAllowAny: true,
isBodyPlainText: false,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("getUserDetails API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
Map<String, String> headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'};
SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = SymptomsUserDetailsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<SymptomsUserDetailsResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: symptomsUserDetailsResponseModel,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing getUserDetails 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 getUserDetails: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
required List<String> organNames,
required String userSessionToken,
required int gender,
}) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
Map<String, dynamic> body = {
'bodyPartName': organNames,
'gender': gender,
};
try {
GenericApiModel<BodySymptomResponseModel>? apiResponse;
@ -50,21 +134,15 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
await apiClient.post(
ApiConsts.getBodySymptomsByName,
apiHeaders: headers,
body: jsonEncode(organNames),
body: body,
isExternal: true,
isAllowAny: true,
isBodyPlainText: true,
isBodyPlainText: false,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetBodySymptomsByName API Failed: $error");
log("GetBodySymptomsByName Failed: $error, Status: $statusCode");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("GetBodySymptomsByName Response Status: $statusCode");
loggerService.logInfo("GetBodySymptomsByName API Success: $response");
log("GetBodySymptomsByName Response: $response");
BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response);
apiResponse = GenericApiModel<BodySymptomResponseModel>(
@ -76,7 +154,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetBodySymptomsByName response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
failure = DataParsingFailure(e.toString());
}
},
@ -88,7 +165,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} catch (e, stackTrace) {
loggerService.logError("Exception in getBodySymptomsByName: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@ -99,6 +175,9 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
required String sex,
required List<String> evidenceIds,
required String language,
required String userSessionToken,
required int gender,
required String sessionId,
}) async {
final Map<String, dynamic> body = {
"age": {
@ -107,6 +186,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
"sex": sex,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"language": language,
"generalId": sessionId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
@ -115,17 +200,16 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
await apiClient.post(
ApiConsts.getRiskFactors,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
log("GetRiskFactors Failed: $error, Status: $statusCode");
loggerService.logError("GetRiskFactors API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("GetRiskFactors Response: $response");
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
@ -140,7 +224,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetRiskFactors response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
failure = DataParsingFailure(e.toString());
}
},
@ -152,7 +235,79 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} catch (e, stackTrace) {
loggerService.logError("Exception in getRiskFactors: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
required String userSessionToken,
required int gender,
required String sessionId,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"sex": sex,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"language": language,
"suggest_method": "diagnosis",
"generalId": sessionId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<TriageDataDetails>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.diagnosis,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("getDiagnosisForTriage API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
final updatedResponseData = responseData['dataDetails'];
TriageDataDetails riskFactorsResponse = TriageDataDetails.fromJson(updatedResponseData);
apiResponse = GenericApiModel<TriageDataDetails>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: riskFactorsResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing getDiagnosisForTriage 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 getDiagnosisForTriage: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
@ -163,6 +318,9 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
required String sex,
required List<String> evidenceIds,
required String language,
required String userSessionToken,
required String sessionId,
required int gender,
}) async {
final Map<String, dynamic> body = {
"age": {
@ -171,6 +329,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
"sex": sex,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"language": language,
"generalId": sessionId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
@ -179,17 +343,16 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
await apiClient.post(
ApiConsts.getSuggestions,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
log("getSuggestions Failed: $error, Status: $statusCode");
loggerService.logError("GetSuggestions API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
log("getSuggestions Response: $response");
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
@ -202,9 +365,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
data: riskFactorsResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing getSuggestions response: $e");
loggerService.logError("Error parsing GetSuggestions response: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Parse Error: $e");
failure = DataParsingFailure(e.toString());
}
},
@ -216,7 +378,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} catch (e, stackTrace) {
loggerService.logError("Exception in getSuggestions: $e");
loggerService.logError("StackTrace: $stackTrace");
log("Exception: $e");
return Left(UnknownFailure(e.toString()));
}
}

@ -7,6 +7,8 @@ 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/resp_models/body_symptom_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/symptoms_user_details_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';
@ -35,11 +37,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
bool isBodySymptomsLoading = false;
bool isRiskFactorsLoading = false;
bool isSuggestionsLoading = false;
bool isTriageDiagnosisLoading = false;
// API data storage - using API models directly
SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel;
BodySymptomResponseModel? bodySymptomResponse;
RiskAndSuggestionsResponseModel? riskFactorsResponse;
RiskAndSuggestionsResponseModel? suggestionsResponse;
TriageDataDetails? triageDataDetails;
// Triage state
int? _selectedTriageChoiceIndex;
// Selected risk factors tracking
final Set<String> _selectedRiskFactorIds = {};
@ -92,6 +100,23 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
String? get tooltipOrganId => _tooltipOrganId;
String get currentSessionAuthToken => symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? "";
String get currentSessionId => symptomsUserDetailsResponseModel?.sessionId ?? "";
// Triage-related getters
bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false;
bool get hasEmergencyEvidence => triageDataDetails?.hasEmergencyEvidence ?? false;
String? get currentInterviewToken => triageDataDetails?.interviewToken;
TriageQuestion? get currentTriageQuestion => triageDataDetails?.question;
List<TriageCondition>? get currentConditions => triageDataDetails?.conditions;
int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex;
/// Get organs for current view
List<OrganModel> get currentOrgans => OrganData.getOrgansForView(_currentView);
@ -391,6 +416,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
age: _selectedAge!,
sex: _selectedGender!.toLowerCase(),
evidenceIds: evidenceIds,
sessionId: currentSessionId,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
@ -409,6 +435,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
Future<void> getRiskFactors({
required int age,
required String sex,
required String sessionId,
required List<String> evidenceIds,
required String language,
Function(RiskAndSuggestionsResponseModel)? onSuccess,
@ -422,6 +449,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
sex: sex,
evidenceIds: evidenceIds,
language: language,
sessionId: sessionId,
userSessionToken: currentSessionAuthToken,
gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2,
);
result.fold(
@ -502,6 +532,25 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
/// Get all evidence IDs (symptoms + risk factors + suggestions) for triage/diagnosis
List<String> getAllEvidenceIds() {
List<String> evidenceIds = [];
// Add selected symptoms
final selectedSymptoms = getAllSelectedSymptoms();
evidenceIds.addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!));
// Add selected risk factors (excluding "not_applicable")
final selectedRiskFactors = getAllSelectedRiskFactors();
evidenceIds.addAll(selectedRiskFactors.where((rf) => rf.id != null && rf.id != "not_applicable").map((rf) => rf.id!));
// Add selected suggestions (excluding "not_applicable")
final selectedSuggestions = getAllSelectedSuggestions();
evidenceIds.addAll(selectedSuggestions.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!));
return evidenceIds;
}
/// Fetch risk factors based on selected symptoms
Future<void> fetchSuggestions({
Function()? onSuccess,
@ -571,6 +620,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
sex: sex,
evidenceIds: evidenceIds,
language: language,
sessionId: currentSessionId,
userSessionToken: currentSessionAuthToken,
gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2,
);
result.fold(
@ -612,6 +664,109 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
);
}
/// Call Diagnosis API for Triage - This is called iteratively until shouldStop is true
Future<void> getDiagnosisForTriage({
required int age,
required String sex,
required List<String> evidenceIds,
required String language,
Function(TriageDataDetails)? onSuccess,
Function(String)? onError,
}) async {
isTriageDiagnosisLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getDiagnosisForTriage(
age: age,
sex: sex,
evidenceIds: evidenceIds,
language: language,
sessionId: currentSessionId,
userSessionToken: currentSessionAuthToken,
gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2,
);
result.fold(
(failure) async {
isTriageDiagnosisLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isTriageDiagnosisLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
triageDataDetails = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch diagnosis');
}
}
},
);
}
/// Convenience method to start or continue the triage process
/// This automatically uses all selected evidence (symptoms + risk factors + suggestions)
Future<void> startOrContinueTriage({
Function()? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
// Get all evidence IDs
final evidenceIds = getAllEvidenceIds();
if (evidenceIds.isEmpty) {
if (onError != null) {
onError('No evidence selected');
}
return;
}
await getDiagnosisForTriage(
age: _selectedAge!,
sex: _selectedGender!.toLowerCase(),
evidenceIds: evidenceIds,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
onSuccess();
}
},
onError: (error) {
if (onError != null) {
onError(error);
}
},
);
}
/// Select a triage choice option
void selectTriageChoice(int choiceIndex) {
_selectedTriageChoiceIndex = choiceIndex;
notifyListeners();
}
/// Reset triage choice selection
void resetTriageChoice() {
_selectedTriageChoiceIndex = null;
notifyListeners();
}
void reset() {
_currentView = BodyView.front;
_selectedOrganIds.clear();
@ -621,6 +776,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
bodySymptomResponse = null;
riskFactorsResponse = null;
suggestionsResponse = null;
triageDataDetails = null;
isTriageDiagnosisLoading = false;
_selectedTriageChoiceIndex = null;
_isBottomSheetExpanded = false;
_tooltipTimer?.cancel();
_tooltipOrganId = null;
@ -725,6 +883,43 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
};
}
Future<void> getSymptomsUserDetails({
required String userName,
required String password,
Function()? onSuccess,
Function(String)? onError,
}) async {
isBodySymptomsLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getUserDetails(userName: userName, password: password);
result.fold(
(failure) async {
isBodySymptomsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isBodySymptomsLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
symptomsUserDetailsResponseModel = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess();
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms');
}
}
},
);
}
Future<void> getBodySymptomsByName({
required List<String> organNames,
Function(BodySymptomResponseModel)? onSuccess,
@ -735,6 +930,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
final result = await symptomsCheckerRepo.getBodySymptomsByName(
organNames: organNames,
userSessionToken: currentSessionAuthToken,
gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2,
);
result.fold(

@ -10,9 +10,11 @@ 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/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.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/loader/bottomsheet_loader.dart';
import 'package:provider/provider.dart';
class OrganSelectorPage extends StatefulWidget {
@ -24,25 +26,38 @@ class OrganSelectorPage extends StatefulWidget {
class _OrganSelectorPageState extends State<OrganSelectorPage> {
late final AppState _appState;
late final DialogService dialogService;
@override
void initState() {
super.initState();
_appState = getIt.get<AppState>();
dialogService = getIt<DialogService>();
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
void _onNextPressed(SymptomsCheckerViewModel viewModel) async {
if (!viewModel.validateSelection()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one organ'.needTranslation),
backgroundColor: AppColors.errorColor,
),
dialogService.showErrorBottomSheet(
message: 'Please select at least one organ'.needTranslation,
);
return;
}
LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation);
final String userName = 'guest_user';
final String password = '123456';
context.navigateWithName(AppRoutes.symptomsSelectorScreen);
await viewModel.getSymptomsUserDetails(
userName: userName,
password: password,
onSuccess: () {
LoaderBottomSheet.hideLoader();
context.navigateWithName(AppRoutes.symptomsSelectorScreen);
},
onError: (String error) {
LoaderBottomSheet.hideLoader();
},
);
}
@override

@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/models/conditions_
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_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';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
@ -44,7 +45,7 @@ class PossibleConditionsScreen extends StatelessWidget {
);
}
Widget _buildPredictionsList(List<ConditionsModel> conditions) {
Widget _buildPredictionsList(BuildContext context, List<ConditionsModel> conditions) {
if (conditions.isEmpty) {
return Center(
child: Padding(
@ -60,6 +61,8 @@ class PossibleConditionsScreen extends StatelessWidget {
);
}
final dialogService = getIt<DialogService>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -82,11 +85,8 @@ class PossibleConditionsScreen extends StatelessWidget {
description: conditionModel.description,
possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum,
onActionPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('We are not available for a week. May you Rest In Peace :('),
backgroundColor: AppColors.primaryRedColor,
),
dialogService.showErrorBottomSheet(
message: 'We are not available for a week. May you Rest In Peace :(',
);
},
);
@ -168,7 +168,7 @@ class PossibleConditionsScreen extends StatelessWidget {
if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) {
return _buildLoadingShimmer();
}
return _buildPredictionsList(dummyConditions);
return _buildPredictionsList(context, dummyConditions);
},
),
),

@ -2,11 +2,13 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -20,9 +22,12 @@ class RiskFactorsScreen extends StatefulWidget {
}
class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
late DialogService dialogService;
@override
void initState() {
super.initState();
dialogService = getIt<DialogService>();
// Fetch risk factors based on selected symptoms
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
@ -38,11 +43,8 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
if (viewModel.hasSelectedRiskFactors) {
context.navigateWithName(AppRoutes.suggestionsScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one risk before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
dialogService.showErrorBottomSheet(
message: 'Please select at least one risk before proceeding'.needTranslation,
);
}
}

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -19,9 +21,12 @@ class SuggestionsScreen extends StatefulWidget {
}
class _SuggestionsScreenState extends State<SuggestionsScreen> {
late DialogService dialogService;
@override
void initState() {
super.initState();
dialogService = getIt<DialogService>();
// Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
@ -40,11 +45,8 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
// Navigate to triage screen
context.navigateWithName(AppRoutes.triageScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one option before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
dialogService.showErrorBottomSheet(
message: 'Please select at least one option before proceeding'.needTranslation,
);
}
}

@ -1,6 +1,7 @@
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/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -8,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.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/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -23,9 +25,12 @@ class SymptomsSelectorScreen extends StatefulWidget {
}
class _SymptomsSelectorScreenState extends State<SymptomsSelectorScreen> {
late DialogService dialogService;
@override
void initState() {
super.initState();
dialogService = getIt<DialogService>();
// Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>();
@ -38,11 +43,8 @@ class _SymptomsSelectorScreenState extends State<SymptomsSelectorScreen> {
// Navigate to triage screen
context.navigateWithName(AppRoutes.riskFactorsScreen);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select at least one symptom before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
dialogService.showErrorBottomSheet(
message: 'Please select at least one symptom before proceeding'.needTranslation,
);
}
}

@ -1,18 +1,23 @@
import 'dart:developer';
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/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_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_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/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:provider/provider.dart';
class TriageScreen extends StatefulWidget {
const TriageScreen({super.key});
@ -22,114 +27,293 @@ class TriageScreen extends StatefulWidget {
}
class _TriageScreenState extends State<TriageScreen> {
late List<TriageQuestionModel> triageQuestions;
int currentQuestionIndex = 0;
List<String> answeredEvidenceIds = []; // Track user's answers
late SymptomsCheckerViewModel viewModel;
late DialogService dialogService;
@override
void initState() {
super.initState();
triageQuestions = TriageQuestionsData.getSampleTriageQuestions();
}
TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex];
bool get isFirstQuestion => currentQuestionIndex == 0;
viewModel = context.read<SymptomsCheckerViewModel>();
dialogService = getIt.get<DialogService>();
bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1;
void _onOptionSelected(int optionIndex) {
setState(() {
currentQuestion.selectOption(optionIndex);
// Start triage process when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) {
_startTriage();
});
}
void _onPreviousPressed() {
if (!isFirstQuestion) {
setState(() {
currentQuestionIndex--;
});
}
void _startTriage() {
viewModel.startOrContinueTriage(
onSuccess: () {
_handleTriageResponse();
},
onError: (error) {
dialogService.showErrorBottomSheet(
message: error,
onOkPressed: () => context.pop(),
);
},
);
}
void _onNextPressed() {
if (currentQuestion.isAnswered) {
currentQuestion.confirmSelection();
if (isLastQuestion) {
context.navigateWithName(AppRoutes.possibleConditionsScreen);
} else {
setState(() {
currentQuestionIndex++;
});
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please select an option before proceeding'.needTranslation),
backgroundColor: AppColors.errorColor,
),
);
void _handleTriageResponse() {
if (viewModel.hasEmergencyEvidence) {
_showEmergencyDialog();
return;
}
if (viewModel.shouldStopTriage) {
// Navigate to results/possible conditions screen
context.navigateWithName(AppRoutes.possibleConditionsScreen);
return;
}
// Question is loaded, reset selection for new question
viewModel.resetTriageChoice();
}
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
void _showEmergencyDialog() {
showCommonBottomSheetWithoutHeight(
context,
title: "Emergency".needTranslation,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
loadingText: "Emergency evidence detected. Please seek immediate medical attention.".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(),
onConfirmTap: () {
Navigator.pop(context);
context.pop();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
bool get isFirstQuestion => answeredEvidenceIds.isEmpty;
void _onOptionSelected(int choiceIndex) {
viewModel.selectTriageChoice(choiceIndex);
}
void _onPreviousPressed() {
context.pop();
}
void _onNextPressed() {
// Check if user has selected an option
if (viewModel.selectedTriageChoiceIndex == null) {
dialogService.showErrorBottomSheet(message: 'Please select an option before proceeding'.needTranslation);
return;
}
// Get the selected choice from the current question
final currentQuestion = viewModel.currentTriageQuestion;
if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) {
dialogService.showErrorBottomSheet(
message: 'No question items available'.needTranslation,
);
return;
}
final questionItem = currentQuestion.items!.first;
if (questionItem.choices == null || viewModel.selectedTriageChoiceIndex! >= questionItem.choices!.length) {
dialogService.showErrorBottomSheet(
message: 'Invalid choice selection'.needTranslation,
);
return;
}
final selectedChoice = questionItem.choices![viewModel.selectedTriageChoiceIndex!];
final evidenceId = selectedChoice.label ?? "";
if (evidenceId.isNotEmpty) {
answeredEvidenceIds.add(evidenceId);
}
// Get all previous evidence IDs
List<String> allEvidenceIds = viewModel.getAllEvidenceIds();
allEvidenceIds.addAll(answeredEvidenceIds);
log("allEvidences: ${allEvidenceIds.toString()}");
// Call API with updated evidence
viewModel.getDiagnosisForTriage(
age: viewModel.selectedAge!,
sex: viewModel.selectedGender!.toLowerCase(),
evidenceIds: allEvidenceIds,
language: viewModel.appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
_handleTriageResponse();
},
onError: (error) {
dialogService.showErrorBottomSheet(message: error);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
children: [
Expanded(
child: CollapsingListView(
title: "Triage".needTranslation,
// onLeadingTapped: () => _buildConfirmationBottomSheet(
// context: context,
// onConfirm: () => {
// context.pop(),
// context.pop(),
// }),
leadingCallback: () => context.pop(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
_buildQuestionCard(),
],
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, child) {
// Show normal question UI
return Column(
children: [
Expanded(
child: CollapsingListView(
title: "Triage".needTranslation,
leadingCallback: () => _showConfirmationBeforeExit(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
_buildQuestionCard(viewModel),
],
),
),
),
_buildStickyBottomCard(context, viewModel),
],
);
},
),
);
}
Widget _buildLoadingShimmer() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Create 2-3 shimmer cards
...List.generate(1, (index) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: _buildShimmerCard(),
);
}),
],
);
}
Widget _buildShimmerCard() {
return Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Shimmer title
Container(
height: 40.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
),
).toShimmer2(isShow: true, radius: 24.r),
SizedBox(height: 16.h),
// Shimmer chips
Wrap(
runSpacing: 12.h,
spacing: 8.w,
children: List.generate(4, (index) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(color: AppColors.bottomNAVBorder, width: 1),
),
child: Text(
'Not Applicable Suggestion',
style: TextStyle(fontSize: 14.f, color: AppColors.textColor),
),
).toShimmer2(isShow: true, radius: 24.r);
}),
),
_buildStickyBottomCard(context),
],
),
);
}
Widget _buildQuestionCard() {
Widget _buildErrorState() {
return CollapsingListView(
title: "Triage".needTranslation,
leadingCallback: () => context.pop(),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64.f, color: AppColors.errorColor),
SizedBox(height: 16.h),
"No question available".needTranslation.toText16(weight: FontWeight.w500),
SizedBox(height: 24.h),
CustomButton(
text: "Go Back".needTranslation,
onPressed: () => context.pop(),
backgroundColor: AppColors.primaryRedColor,
).paddingSymmetrical(48.w, 0),
],
),
),
);
}
void _showConfirmationBeforeExit(BuildContext context) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to exit? Your progress will be lost.".needTranslation,
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () {
Navigator.pop(context);
context.pop();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
Widget _buildQuestionCard(SymptomsCheckerViewModel viewModel) {
if (viewModel.isTriageDiagnosisLoading) {
return _buildLoadingShimmer();
}
if (viewModel.currentTriageQuestion == null) {
return Center(
child: "No question available".needTranslation.toText16(weight: FontWeight.w500),
);
}
final question = viewModel.currentTriageQuestion;
if (question == null || question.items == null || question.items!.isEmpty) {
return SizedBox.shrink();
}
final questionItem = question.items!.first;
final choices = questionItem.choices ?? [];
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
final offsetAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
));
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
),
);
return SlideTransition(
position: offsetAnimation,
@ -140,7 +324,7 @@ class _TriageScreenState extends State<TriageScreen> {
);
},
child: Container(
key: ValueKey<int>(currentQuestionIndex),
key: ValueKey<String>(questionItem.id ?? answeredEvidenceIds.length.toString()),
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
@ -148,11 +332,11 @@ class _TriageScreenState extends State<TriageScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
currentQuestion.question.toText16(weight: FontWeight.w500),
(question.text ?? "").toText16(weight: FontWeight.w500),
SizedBox(height: 24.h),
...List.generate(currentQuestion.options.length, (index) {
bool selected = currentQuestion.selectedOptionIndex == index;
return _buildOptionItem(index, selected, currentQuestion.options[index].text);
...List.generate(choices.length, (index) {
bool selected = viewModel.selectedTriageChoiceIndex == index;
return _buildOptionItem(index, selected, choices[index].label ?? "");
}),
],
),
@ -188,9 +372,21 @@ class _TriageScreenState extends State<TriageScreen> {
);
}
Widget _buildStickyBottomCard(BuildContext context) {
final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions);
final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore);
Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) {
// Get the top condition with highest probability
final conditions = viewModel.currentConditions ?? [];
String suggestedCondition = "Analyzing...";
double probability = 0.0;
if (conditions.isNotEmpty) {
// Sort by probability descending
final sortedConditions = List<TriageCondition>.from(conditions);
sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0));
final topCondition = sortedConditions.first;
suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown";
probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage
}
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
@ -223,14 +419,14 @@ class _TriageScreenState extends State<TriageScreen> {
),
SizedBox(height: 16.h),
CustomRoundedProgressBar(
percentage: currentScore,
percentage: probability.toInt(),
paddingBetween: 5.h,
color: AppColors.primaryRedColor,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17),
height: 8.h,
titleWidget: RichText(
text: TextSpan(
text: "$currentScore% ",
text: "${probability.toStringAsFixed(1)}% ",
style: TextStyle(
color: AppColors.primaryRedColor,
fontWeight: FontWeight.w600,
@ -256,7 +452,7 @@ class _TriageScreenState extends State<TriageScreen> {
child: CustomButton(
text: "Previous".needTranslation,
onPressed: isFirstQuestion ? () {} : _onPreviousPressed,
isDisabled: isFirstQuestion,
isDisabled: isFirstQuestion || viewModel.isTriageDiagnosisLoading,
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
borderColor: Colors.transparent,
textColor: AppColors.primaryRedColor,
@ -266,7 +462,8 @@ class _TriageScreenState extends State<TriageScreen> {
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation,
text: "Next".needTranslation,
isDisabled: viewModel.isTriageDiagnosisLoading,
onPressed: _onNextPressed,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -35,7 +33,6 @@ class AgeSelectionPage extends StatelessWidget {
initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1),
onDateChanged: (date) {
symptomsViewModel.setDateOfBirth(date);
log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}');
},
)
],

@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/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:provider/provider.dart';
class WaterMonitorSettingsScreen extends StatefulWidget {
@ -20,6 +21,14 @@ class WaterMonitorSettingsScreen extends StatefulWidget {
}
class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen> {
late DialogService dialogService;
@override
void initState() {
super.initState();
dialogService = getIt.get<DialogService>();
}
// No need to call initialize() here since it's already called in water_consumption_screen
// The ViewModel is shared via Provider, so data is already loaded
@ -59,7 +68,6 @@ class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen>
required Function(String) onSelected,
bool useUpperCase = false,
}) {
final dialogService = getIt.get<DialogService>();
dialogService.showFamilyBottomSheetWithoutHWithChild(
label: title.needTranslation,
@ -263,9 +271,20 @@ class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen>
onPressed: () async {
final success = await viewModel.saveSettings();
if (!success && viewModel.validationError != null) {
_showSnackBar(context, viewModel.validationError!);
dialogService.showErrorBottomSheet(
message: viewModel.validationError!,
);
} else if (success) {
_showSnackBar(context, "Settings saved successfully");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(
loadingText: "Settings saved successfully".needTranslation,
),
callBackFunc: () {},
isCloseButtonVisible: false,
isDismissible: true,
isFullScreen: false,
);
}
},
borderRadius: 12.r,
@ -340,16 +359,4 @@ class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen>
),
);
}
// Show snackbar for validation errors and success messages
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor,
),
);
}
}

@ -62,18 +62,16 @@ class DialogServiceImp implements DialogService {
message: message,
showCancel: onCancelPressed != null ? true : false,
onOkPressed: () {
print('ok button is pressed');
if (onOkPressed != null) {
print('onOkPressed is not null');
onOkPressed();
}else {
} else {
Navigator.pop(context);
}
},
onCancelPressed: () {
if (onCancelPressed != null) {
onCancelPressed();
}else {
} else {
Navigator.pop(context);
}
},
@ -108,7 +106,8 @@ class DialogServiceImp implements DialogService {
}
@override
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async {
Future<void> showCommonBottomSheetWithoutH(
{String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async {
final context = navigationService.navigatorKey.currentContext;
if (context == null) return;
showCommonBottomSheetWithoutHeight(
@ -162,7 +161,8 @@ class DialogServiceImp implements DialogService {
}
@override
Future<void> showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async {
Future<void> showPhoneNumberPickerSheet(
{String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async {
final context = navigationService.navigatorKey.currentContext;
if (context == null) return;
showCommonBottomSheetWithoutHeight(context,
@ -184,7 +184,8 @@ class DialogServiceImp implements DialogService {
}
}
Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) {
Widget exceptionBottomSheetWidget(
{required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) {
return Column(
children: [
(message).toText16(isBold: false, color: AppColors.textColor),
@ -239,7 +240,8 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin
);
}
Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) {
Widget showPhoneNumberPickerWidget(
{required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) {
return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) {
return Column(
children: [

@ -0,0 +1,57 @@
export interface UserName {
first_name: string;
middle_name: string;
last_name: string;
first_name_ar: string;
middle_name_ar: string;
last_name_ar: string;
}
export interface UserDetails {
FileNo: string;
national_id: string;
email: string;
date_of_birth: string;
date_of_birth_hijri: string | null;
age: number;
name: UserName;
marital_status: number;
marital_status_code: string | null;
nationality: string;
nationality_iso_code: string;
occupation: string | null;
id_type: number;
gender: number;
jwt_token: string | null;
country_dial_code: string;
phone_no: string;
}
export interface TokenDetails {
id: string;
auth_token: string;
expires_in: number;
}
export interface GetUserDetailsResponse {
tokenDetails: TokenDetails;
userDetails: UserDetails;
sessionId: string;
}
// Simplified User type for app usage
export interface User {
id: string;
fileNo: string;
nationalId: string;
email: string;
name: string;
nameAr: string;
phoneNo: string;
dateOfBirth: string;
nationality: string;
gender: number;
authToken: string;
sessionId: string;
}

@ -0,0 +1,18 @@
export function safeJsonParse<T>(data: any): T {
if (typeof data === 'string') {
try {
return JSON.parse(data);
} catch (error) {
console.error('Failed to parse JSON string:', error);
throw new Error('Invalid JSON response from server');
}
}
return data;
}
export function logApiResponse(endpoint: string, response: any) {
if (__DEV__) {
console.log(`[API Response - ${endpoint}]`, JSON.stringify(response, null, 2));
}
}
Loading…
Cancel
Save