From 49d091547a6d6e366b6a11406293610b8f01dfee Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 4 Jan 2026 22:30:43 +0300 Subject: [PATCH] Till Triage Completed --- contexts/AuthContext.tsx | 31 ++ lib/core/api_consts.dart | 6 +- lib/core/utils/utils.dart | 22 -- .../symptoms_user_details_response_model.dart | 188 +++++++++ .../resp_models/triage_response_model.dart | 209 ++++++++++ .../symptoms_checker_repo.dart | 213 +++++++++-- .../symptoms_checker_view_model.dart | 197 ++++++++++ .../organ_selector_screen.dart | 29 +- .../possible_conditions_screen.dart | 14 +- .../symptoms_checker/risk_factors_screen.dart | 12 +- .../symptoms_checker/suggestions_screen.dart | 12 +- .../symptoms_selector_screen.dart | 12 +- .../symptoms_checker/triage_screen.dart | 359 ++++++++++++++---- .../pages/age_selection_page.dart | 3 - .../water_monitor_settings_screen.dart | 37 +- lib/services/dialog_service.dart | 18 +- services/api.ts | 1 + types/user.ts | 57 +++ utils/apiHelpers.ts | 18 + 19 files changed, 1252 insertions(+), 186 deletions(-) create mode 100644 contexts/AuthContext.tsx create mode 100644 lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart create mode 100644 lib/features/symptoms_checker/models/resp_models/triage_response_model.dart create mode 100644 services/api.ts create mode 100644 types/user.ts create mode 100644 utils/apiHelpers.ts diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx new file mode 100644 index 0000000..48571a8 --- /dev/null +++ b/contexts/AuthContext.tsx @@ -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... +}; + diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 0febdbe..65ac1ef 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -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 diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 0302818..a30763f 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -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 uniqueBy(List list, K Function(T) keySelector) { final seenKeys = {}; return list.where((item) => seenKeys.add(keySelector(item))).toList(); diff --git a/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart new file mode 100644 index 0000000..c0466c4 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart @@ -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 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 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 json) { + return TokenDetails( + id: json['id'], + authToken: json['auth_token'], + expiresIn: json['expires_in'], + ); + } + + Map 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 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 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 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 toJson() { + return { + 'first_name': firstName, + 'middle_name': middleName, + 'last_name': lastName, + 'first_name_ar': firstNameAr, + 'middle_name_ar': middleNameAr, + 'last_name_ar': lastNameAr, + }; + } +} diff --git a/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart new file mode 100644 index 0000000..f0d7e90 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart @@ -0,0 +1,209 @@ +class TriageDataDetails { + final TriageQuestion? question; + final List? conditions; + final bool? hasEmergencyEvidence; + final bool? shouldStop; + final String? interviewToken; + final String? message; + final List? errorList; + final int? id; + final String? language; + final String? generalId; + final String? createDate; + final String? lastEditDate; + final String? createdBy; + final String? lastEditBy; + final bool? active; + final int? sortOrder; + final int? userType; + final String? userId; + + 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 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.from(json['ErrorList']) : null, + id: json['Id'], + language: json['language'], + generalId: json['generalId'], + createDate: json['CreateDate'], + lastEditDate: json['LastEditDate'], + createdBy: json['CreatedBy'], + lastEditBy: json['LastEditBy'], + active: json['Active'], + sortOrder: json['SortOrder'], + userType: json['userType'], + userId: json['userId'], + ); + } + + Map toJson() { + return { + '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? items; + + TriageQuestion({ + this.type, + this.text, + this.items, + }); + + factory TriageQuestion.fromJson(Map 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 toJson() { + return { + 'type': type, + 'text': text, + 'items': items?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageQuestionItem { + final String? id; + final String? name; + final List? choices; + + TriageQuestionItem({ + this.id, + this.name, + this.choices, + }); + + factory TriageQuestionItem.fromJson(Map 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 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 json) { + return TriageChoice( + id: json['id'], + label: json['label'], + ); + } + + Map 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 json) { + return TriageCondition( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + probability: json['probability']?.toDouble(), + conditionDetails: json['condition_details'], + ); + } + + Map 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)}%'; + } +} diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 954d414..2d9a99b 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -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>> getUserDetails({ + required String userName, + required String password, + }); + Future>> getBodySymptomsByName({ required List organNames, + required String userSessionToken, + required int gender, }); Future>> getRiskFactors({ @@ -20,6 +28,9 @@ abstract class SymptomsCheckerRepo { required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required int gender, + required String sessionId, }); Future>> getSuggestions({ @@ -27,6 +38,19 @@ abstract class SymptomsCheckerRepo { required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required String sessionId, + required int gender, + }); + + Future>> getDiagnosisForTriage({ + required int age, + required String sex, + required List 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>> getBodySymptomsByName({required List organNames}) async { - log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}"); - log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}"); + Future>> getUserDetails({ + required String userName, + required String password, + }) async { + Map body = {"userName": userName, "password": password}; + + try { + GenericApiModel? 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 responseData = response is String ? jsonDecode(response) : response; - Map headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'}; + SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = SymptomsUserDetailsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + 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>> getBodySymptomsByName({ + required List organNames, + required String userSessionToken, + required int gender, + }) async { + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + Map body = { + 'bodyPartName': organNames, + 'gender': gender, + }; try { GenericApiModel? 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( @@ -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 evidenceIds, required String language, + required String userSessionToken, + required int gender, + required String sessionId, }) async { final Map body = { "age": { @@ -107,6 +186,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { "sex": sex, "evidence": evidenceIds.map((id) => {"id": id}).toList(), "language": language, + "generalId": sessionId, + }; + + Map 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 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>> getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, + }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + "suggest_method": "diagnosis", + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? 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 responseData = response is String ? jsonDecode(response) : response; + + final updatedResponseData = responseData['dataDetails']; + + TriageDataDetails riskFactorsResponse = TriageDataDetails.fromJson(updatedResponseData); + + apiResponse = GenericApiModel( + 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 evidenceIds, required String language, + required String userSessionToken, + required String sessionId, + required int gender, }) async { final Map body = { "age": { @@ -171,6 +329,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { "sex": sex, "evidence": evidenceIds.map((id) => {"id": id}).toList(), "language": language, + "generalId": sessionId, + }; + + Map 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 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())); } } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index da439c6..b41bab2 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -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 _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? get currentConditions => triageDataDetails?.conditions; + + int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex; + /// Get organs for current view List 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 getRiskFactors({ required int age, required String sex, + required String sessionId, required List 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 getAllEvidenceIds() { + List 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 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 getDiagnosisForTriage({ + required int age, + required String sex, + required List 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 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 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 getBodySymptomsByName({ required List 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( diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index c23b956..4e0ec10 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -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 { late final AppState _appState; + late final DialogService dialogService; @override void initState() { super.initState(); _appState = getIt.get(); + dialogService = getIt(); } - 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 diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 2c99515..188d07b 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -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 conditions) { + Widget _buildPredictionsList(BuildContext context, List conditions) { if (conditions.isEmpty) { return Center( child: Padding( @@ -60,6 +61,8 @@ class PossibleConditionsScreen extends StatelessWidget { ); } + final dialogService = getIt(); + 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); }, ), ), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index d4ff2cb..aef7ce7 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -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 { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Fetch risk factors based on selected symptoms WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -38,11 +43,8 @@ class _RiskFactorsScreenState extends State { 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, ); } } diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index f2aa71e..b5b4388 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -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 { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -40,11 +45,8 @@ class _SuggestionsScreenState extends State { // 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, ); } } diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 522c5f8..3950974 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -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 { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -38,11 +43,8 @@ class _SymptomsSelectorScreenState extends State { // 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, ); } } diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index ff0482e..8262c67 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -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 { - late List triageQuestions; - int currentQuestionIndex = 0; + List 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(); + dialogService = getIt.get(); - 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 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( + 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 animation) { final offsetAnimation = Tween( 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 { ); }, child: Container( - key: ValueKey(currentQuestionIndex), + key: ValueKey(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 { 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 { ); } - 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.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 { ), 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 { 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 { 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, diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart index d73f387..8366545 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -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}'); }, ) ], diff --git a/lib/presentation/water_monitor/water_monitor_settings_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_screen.dart index 1b783ca..3470344 100644 --- a/lib/presentation/water_monitor/water_monitor_settings_screen.dart +++ b/lib/presentation/water_monitor/water_monitor_settings_screen.dart @@ -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 { + late DialogService dialogService; + + @override + void initState() { + super.initState(); + dialogService = getIt.get(); + } + // 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 required Function(String) onSelected, bool useUpperCase = false, }) { - final dialogService = getIt.get(); dialogService.showFamilyBottomSheetWithoutHWithChild( label: title.needTranslation, @@ -263,9 +271,20 @@ class _WaterMonitorSettingsScreenState extends State 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 ), ); } - - // 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, - ), - ); - } } diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 497a009..3c009f3 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -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 showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + Future 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 showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { + Future 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: [ diff --git a/services/api.ts b/services/api.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/services/api.ts @@ -0,0 +1 @@ + diff --git a/types/user.ts b/types/user.ts new file mode 100644 index 0000000..a2158ef --- /dev/null +++ b/types/user.ts @@ -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; +} + diff --git a/utils/apiHelpers.ts b/utils/apiHelpers.ts new file mode 100644 index 0000000..3fae819 --- /dev/null +++ b/utils/apiHelpers.ts @@ -0,0 +1,18 @@ +export function safeJsonParse(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)); + } +} +