Triage Completed. Explain API left

faiz_dev
faizatflutter 1 week ago
parent 49d091547a
commit 8c91b6830a

@ -1,31 +0,0 @@
// ...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...
};

@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient {
final int statusCode = response.statusCode; final int statusCode = response.statusCode;
log("uri: ${Uri.parse(url.trim())}"); log("uri: ${Uri.parse(url.trim())}");
log("body: ${json.encode(body)}"); log("body: ${json.encode(body)}");
log("response.body: ${response.body}"); // log("response.body: ${response.body}");
if (statusCode < 200 || statusCode >= 400) { if (statusCode < 200 || statusCode >= 400) {
onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data"));
logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode);

@ -12,18 +12,21 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models
import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class SymptomsCheckerRepo { abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<SymptomsUserDetailsResponseModel>>> getUserDetails({ Future<Either<Failure, GenericApiModel<SymptomsUserDetailsResponseModel>>>
getUserDetails({
required String userName, required String userName,
required String password, required String password,
}); });
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({ Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>>
getBodySymptomsByName({
required List<String> organNames, required List<String> organNames,
required String userSessionToken, required String userSessionToken,
required int gender, required int gender,
}); });
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getRiskFactors({ Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>>
getRiskFactors({
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
@ -33,7 +36,8 @@ abstract class SymptomsCheckerRepo {
required String sessionId, required String sessionId,
}); });
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({ Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>>
getSuggestions({
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
@ -43,10 +47,12 @@ abstract class SymptomsCheckerRepo {
required int gender, required int gender,
}); });
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({ Future<Either<Failure, GenericApiModel<TriageDataDetails>>>
getDiagnosisForTriage({
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
List<Map<String, String>>? triageEvidence,
required String language, required String language,
required String userSessionToken, required String userSessionToken,
required int gender, required int gender,
@ -58,10 +64,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
final ApiClient apiClient; final ApiClient apiClient;
final LoggerService loggerService; final LoggerService loggerService;
SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService}); SymptomsCheckerRepoImp(
{required this.apiClient, required this.loggerService});
@override @override
Future<Either<Failure, GenericApiModel<SymptomsUserDetailsResponseModel>>> getUserDetails({ Future<Either<Failure, GenericApiModel<SymptomsUserDetailsResponseModel>>>
getUserDetails({
required String userName, required String userName,
required String password, required String password,
}) async { }) async {
@ -84,9 +92,11 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
// Parse response if it's a string // Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response; final Map<String, dynamic> responseData =
response is String ? jsonDecode(response) : response;
SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = SymptomsUserDetailsResponseModel.fromJson(responseData); SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel =
SymptomsUserDetailsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<SymptomsUserDetailsResponseModel>( apiResponse = GenericApiModel<SymptomsUserDetailsResponseModel>(
messageStatus: messageStatus ?? 1, messageStatus: messageStatus ?? 1,
@ -113,7 +123,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} }
@override @override
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({ Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>>
getBodySymptomsByName({
required List<String> organNames, required List<String> organNames,
required String userSessionToken, required String userSessionToken,
required int gender, required int gender,
@ -143,7 +154,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
}, },
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response); BodySymptomResponseModel bodySymptomResponse =
BodySymptomResponseModel.fromJson(response);
apiResponse = GenericApiModel<BodySymptomResponseModel>( apiResponse = GenericApiModel<BodySymptomResponseModel>(
messageStatus: messageStatus ?? 1, messageStatus: messageStatus ?? 1,
@ -152,7 +164,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
data: bodySymptomResponse, data: bodySymptomResponse,
); );
} catch (e, stackTrace) { } catch (e, stackTrace) {
loggerService.logError("Error parsing GetBodySymptomsByName response: $e"); loggerService
.logError("Error parsing GetBodySymptomsByName response: $e");
loggerService.logError("StackTrace: $stackTrace"); loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString()); failure = DataParsingFailure(e.toString());
} }
@ -170,7 +183,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} }
@override @override
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getRiskFactors({ Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>>
getRiskFactors({
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
@ -211,9 +225,11 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
// Parse response if it's a string // Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response; final Map<String, dynamic> responseData =
response is String ? jsonDecode(response) : response;
RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); RiskAndSuggestionsResponseModel riskFactorsResponse =
RiskAndSuggestionsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<RiskAndSuggestionsResponseModel>( apiResponse = GenericApiModel<RiskAndSuggestionsResponseModel>(
messageStatus: messageStatus ?? 1, messageStatus: messageStatus ?? 1,
@ -239,22 +255,37 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} }
} }
@override Future<Either<Failure, GenericApiModel<TriageDataDetails>>>
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({ getDiagnosisForTriage({
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
List<Map<String, String>>?
triageEvidence, // Additional triage-specific evidence
required String language, required String language,
required String userSessionToken, required String userSessionToken,
required int gender, required int gender,
required String sessionId, required String sessionId,
}) async { }) async {
// Build evidence list: combine initial symptoms with triage evidence
List<Map<String, dynamic>> evidenceList = [];
// Add initial evidence as simple IDs
for (var id in evidenceIds) {
evidenceList.add({"id": id});
}
// Add triage evidence with proper format (id, choice_id, source)
if (triageEvidence != null && triageEvidence.isNotEmpty) {
evidenceList.addAll(triageEvidence);
}
final Map<String, dynamic> body = { final Map<String, dynamic> body = {
"age": { "age": {
"value": age, "value": age,
}, },
"sex": sex, "sex": sex,
"evidence": evidenceIds.map((id) => {"id": id}).toList(), "evidence": evidenceList,
"language": language, "language": language,
"suggest_method": "diagnosis", "suggest_method": "diagnosis",
"generalId": sessionId, "generalId": sessionId,
@ -282,11 +313,13 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
// Parse response if it's a string // Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response; final Map<String, dynamic> responseData =
response is String ? jsonDecode(response) : response;
final updatedResponseData = responseData['dataDetails']; final updatedResponseData = responseData['dataDetails'];
TriageDataDetails riskFactorsResponse = TriageDataDetails.fromJson(updatedResponseData); TriageDataDetails riskFactorsResponse =
TriageDataDetails.fromJson(updatedResponseData);
apiResponse = GenericApiModel<TriageDataDetails>( apiResponse = GenericApiModel<TriageDataDetails>(
messageStatus: messageStatus ?? 1, messageStatus: messageStatus ?? 1,
@ -295,7 +328,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
data: riskFactorsResponse, data: riskFactorsResponse,
); );
} catch (e, stackTrace) { } catch (e, stackTrace) {
loggerService.logError("Error parsing getDiagnosisForTriage response: $e"); loggerService
.logError("Error parsing getDiagnosisForTriage response: $e");
loggerService.logError("StackTrace: $stackTrace"); loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString()); failure = DataParsingFailure(e.toString());
} }
@ -313,7 +347,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
} }
@override @override
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({ Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>>
getSuggestions({
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
@ -354,9 +389,11 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
// Parse response if it's a string // Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response; final Map<String, dynamic> responseData =
response is String ? jsonDecode(response) : response;
RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); RiskAndSuggestionsResponseModel riskFactorsResponse =
RiskAndSuggestionsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<RiskAndSuggestionsResponseModel>( apiResponse = GenericApiModel<RiskAndSuggestionsResponseModel>(
messageStatus: messageStatus ?? 1, messageStatus: messageStatus ?? 1,

@ -47,7 +47,13 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
TriageDataDetails? triageDataDetails; TriageDataDetails? triageDataDetails;
// Triage state // Triage state
int? _selectedTriageChoiceIndex; int?
_selectedTriageChoiceIndex; // Deprecated - keeping for backward compatibility
final Map<String, int> _selectedTriageChoicesByItemId =
{}; // Map of itemId -> choiceIndex for multi-item questions
final List<Map<String, String>> _triageEvidenceList =
[]; // Store triage evidence with proper format
int _triageQuestionCount = 0; // Track number of triage questions answered
// Selected risk factors tracking // Selected risk factors tracking
final Set<String> _selectedRiskFactorIds = {}; final Set<String> _selectedRiskFactorIds = {};
@ -60,7 +66,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
// User Info Flow State // User Info Flow State
int _userInfoCurrentPage = 0; int _userInfoCurrentPage = 0;
bool _isSinglePageEditMode = false; // Track if editing single page or full flow bool _isSinglePageEditMode =
false; // Track if editing single page or full flow
String? _selectedGender; String? _selectedGender;
DateTime? _dateOfBirth; DateTime? _dateOfBirth;
int? _selectedAge; int? _selectedAge;
@ -100,14 +107,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
String? get tooltipOrganId => _tooltipOrganId; String? get tooltipOrganId => _tooltipOrganId;
String get currentSessionAuthToken => symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? ""; String get currentSessionAuthToken =>
symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? "";
String get currentSessionId => symptomsUserDetailsResponseModel?.sessionId ?? ""; String get currentSessionId =>
symptomsUserDetailsResponseModel?.sessionId ?? "";
// Triage-related getters // Triage-related getters
bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false; bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false;
bool get hasEmergencyEvidence => triageDataDetails?.hasEmergencyEvidence ?? false; bool get hasEmergencyEvidence =>
triageDataDetails?.hasEmergencyEvidence ?? false;
String? get currentInterviewToken => triageDataDetails?.interviewToken; String? get currentInterviewToken => triageDataDetails?.interviewToken;
@ -117,8 +127,34 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex; int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex;
/// Get the number of triage questions answered
int get triageQuestionCount => _triageQuestionCount;
/// Get choice index for a specific item
int? getTriageChoiceForItem(String itemId) {
return _selectedTriageChoicesByItemId[itemId];
}
/// Check if all items in current question have been answered
bool get areAllTriageItemsAnswered {
if (currentTriageQuestion?.items == null ||
currentTriageQuestion!.items!.isEmpty) {
return false;
}
// Check if we have an answer for each item
for (var item in currentTriageQuestion!.items!) {
if (item.id != null &&
!_selectedTriageChoicesByItemId.containsKey(item.id)) {
return false;
}
}
return true;
}
/// Get organs for current view /// Get organs for current view
List<OrganModel> get currentOrgans => OrganData.getOrgansForView(_currentView); List<OrganModel> get currentOrgans =>
OrganData.getOrgansForView(_currentView);
/// Get all selected organs from both views /// Get all selected organs from both views
List<OrganModel> get selectedOrgans { List<OrganModel> get selectedOrgans {
@ -126,7 +162,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
...OrganData.frontViewOrgans, ...OrganData.frontViewOrgans,
...OrganData.backViewOrgans, ...OrganData.backViewOrgans,
]; ];
return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList(); return allOrgans
.where((organ) => _selectedOrganIds.contains(organ.id))
.toList();
} }
/// Check if any organs are selected /// Check if any organs are selected
@ -143,11 +181,13 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
} }
int get totalSelectedSymptomsCount { int get totalSelectedSymptomsCount {
return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); return _selectedSymptomsByOrgan.values
.fold(0, (sum, symptomIds) => sum + symptomIds.length);
} }
bool get hasSelectedSymptoms { bool get hasSelectedSymptoms {
return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty); return _selectedSymptomsByOrgan.values
.any((symptomIds) => symptomIds.isNotEmpty);
} }
/// Get risk factors list /// Get risk factors list
@ -173,7 +213,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
} }
void toggleView() { void toggleView() {
_currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front; _currentView =
_currentView == BodyView.front ? BodyView.back : BodyView.front;
notifyListeners(); notifyListeners();
} }
@ -276,7 +317,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return; return;
} }
List<String> organNames = selectedOrgans.map((organ) => organ.name).toList(); List<String> organNames =
selectedOrgans.map((organ) => organ.name).toList();
await getBodySymptomsByName( await getBodySymptomsByName(
organNames: organNames, organNames: organNames,
@ -326,7 +368,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
} }
} }
if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { if (matchingOrganId != null &&
_selectedSymptomsByOrgan.containsKey(matchingOrganId)) {
final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!; final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!;
if (organResult.bodySymptoms != null) { if (organResult.bodySymptoms != null) {
@ -377,7 +420,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Get all selected risk factors /// Get all selected risk factors
List<RiskAndSuggestionsItemModel> getAllSelectedRiskFactors() { List<RiskAndSuggestionsItemModel> getAllSelectedRiskFactors() {
return riskFactorsList.where((factor) => factor.id != null && _selectedRiskFactorIds.contains(factor.id)).toList(); return riskFactorsList
.where((factor) =>
factor.id != null && _selectedRiskFactorIds.contains(factor.id))
.toList();
} }
/// Clear all risk factor selections /// Clear all risk factor selections
@ -410,7 +456,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
} }
// Extract symptom IDs // Extract symptom IDs
List<String> evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); List<String> evidenceIds =
selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList();
await getRiskFactors( await getRiskFactors(
age: _selectedAge!, age: _selectedAge!,
@ -468,8 +515,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
if (apiResponse.messageStatus == 1 && apiResponse.data != null) { if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
riskFactorsResponse = apiResponse.data; riskFactorsResponse = apiResponse.data;
if (riskFactorsResponse != null && riskFactorsResponse!.dataDetails != null) { if (riskFactorsResponse != null &&
RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( riskFactorsResponse!.dataDetails != null) {
RiskAndSuggestionsItemModel riskFactorItem =
RiskAndSuggestionsItemModel(
id: "not_applicable", id: "not_applicable",
commonName: "Not Applicable", commonName: "Not Applicable",
name: "Not Applicable", name: "Not Applicable",
@ -523,7 +572,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Get all selected risk factors /// Get all selected risk factors
List<RiskAndSuggestionsItemModel> getAllSelectedSuggestions() { List<RiskAndSuggestionsItemModel> getAllSelectedSuggestions() {
return suggestionsList.where((factor) => factor.id != null && _selectedSuggestionsIds.contains(factor.id)).toList(); return suggestionsList
.where((factor) =>
factor.id != null && _selectedSuggestionsIds.contains(factor.id))
.toList();
} }
/// Clear all risk factor selections /// Clear all risk factor selections
@ -538,15 +590,20 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
// Add selected symptoms // Add selected symptoms
final selectedSymptoms = getAllSelectedSymptoms(); final selectedSymptoms = getAllSelectedSymptoms();
evidenceIds.addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!)); evidenceIds
.addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!));
// Add selected risk factors (excluding "not_applicable") // Add selected risk factors (excluding "not_applicable")
final selectedRiskFactors = getAllSelectedRiskFactors(); final selectedRiskFactors = getAllSelectedRiskFactors();
evidenceIds.addAll(selectedRiskFactors.where((rf) => rf.id != null && rf.id != "not_applicable").map((rf) => rf.id!)); evidenceIds.addAll(selectedRiskFactors
.where((rf) => rf.id != null && rf.id != "not_applicable")
.map((rf) => rf.id!));
// Add selected suggestions (excluding "not_applicable") // Add selected suggestions (excluding "not_applicable")
final selectedSuggestions = getAllSelectedSuggestions(); final selectedSuggestions = getAllSelectedSuggestions();
evidenceIds.addAll(selectedSuggestions.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!)); evidenceIds.addAll(selectedSuggestions
.where((s) => s.id != null && s.id != "not_applicable")
.map((s) => s.id!));
return evidenceIds; return evidenceIds;
} }
@ -575,13 +632,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
} }
// Extract symptom IDs // Extract symptom IDs
List<String> evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); List<String> evidenceIds =
selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList();
// Get all selected symptoms // Get all selected symptoms
final selectedRisks = getAllSelectedRiskFactors(); final selectedRisks = getAllSelectedRiskFactors();
if (selectedRisks.isNotEmpty) { if (selectedRisks.isNotEmpty) {
List<String> evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList(); List<String> evidenceRisksIds = selectedRisks
.where((s) => s.id != null && s.id != "not_applicable")
.map((s) => s.id!)
.toList();
evidenceIds.addAll(evidenceRisksIds); evidenceIds.addAll(evidenceRisksIds);
} }
@ -639,8 +700,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
if (apiResponse.messageStatus == 1 && apiResponse.data != null) { if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
suggestionsResponse = apiResponse.data; suggestionsResponse = apiResponse.data;
if (suggestionsResponse != null && suggestionsResponse!.dataDetails != null) { if (suggestionsResponse != null &&
RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( suggestionsResponse!.dataDetails != null) {
RiskAndSuggestionsItemModel riskFactorItem =
RiskAndSuggestionsItemModel(
id: "not_applicable", id: "not_applicable",
commonName: "Not Applicable", commonName: "Not Applicable",
name: "Not Applicable", name: "Not Applicable",
@ -669,6 +732,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
required int age, required int age,
required String sex, required String sex,
required List<String> evidenceIds, required List<String> evidenceIds,
List<Map<String, String>>? triageEvidence,
required String language, required String language,
Function(TriageDataDetails)? onSuccess, Function(TriageDataDetails)? onSuccess,
Function(String)? onError, Function(String)? onError,
@ -680,6 +744,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
age: age, age: age,
sex: sex, sex: sex,
evidenceIds: evidenceIds, evidenceIds: evidenceIds,
triageEvidence: triageEvidence,
language: language, language: language,
sessionId: currentSessionId, sessionId: currentSessionId,
userSessionToken: currentSessionAuthToken, userSessionToken: currentSessionAuthToken,
@ -755,15 +820,44 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
); );
} }
/// Select a triage choice option /// Select a triage choice option (for backward compatibility with single-item questions)
void selectTriageChoice(int choiceIndex) { void selectTriageChoice(int choiceIndex) {
_selectedTriageChoiceIndex = choiceIndex; _selectedTriageChoiceIndex = choiceIndex;
notifyListeners(); notifyListeners();
} }
/// Reset triage choice selection /// Select a choice for a specific item (for multi-item questions)
void selectTriageChoiceForItem(String itemId, int choiceIndex) {
_selectedTriageChoicesByItemId[itemId] = choiceIndex;
notifyListeners();
}
/// Reset triage choice selection and increment question count
void resetTriageChoice() { void resetTriageChoice() {
_selectedTriageChoiceIndex = null; _selectedTriageChoiceIndex = null;
_selectedTriageChoicesByItemId.clear();
_triageQuestionCount++; // Increment question count
notifyListeners();
}
/// Add triage evidence in the proper format
void addTriageEvidence(String itemId, String choiceId) {
_triageEvidenceList.add({
"id": itemId,
"choice_id": choiceId,
"source": "triage",
});
notifyListeners();
}
/// Get all triage evidence
List<Map<String, String>> getTriageEvidence() {
return List.from(_triageEvidenceList);
}
/// Clear triage evidence
void clearTriageEvidence() {
_triageEvidenceList.clear();
notifyListeners(); notifyListeners();
} }
@ -773,6 +867,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_selectedSymptomsByOrgan.clear(); _selectedSymptomsByOrgan.clear();
_selectedRiskFactorIds.clear(); _selectedRiskFactorIds.clear();
_selectedSuggestionsIds.clear(); _selectedSuggestionsIds.clear();
_triageEvidenceList.clear();
_selectedTriageChoicesByItemId.clear();
_triageQuestionCount = 0; // Reset question count
bodySymptomResponse = null; bodySymptomResponse = null;
riskFactorsResponse = null; riskFactorsResponse = null;
suggestionsResponse = null; suggestionsResponse = null;
@ -844,7 +941,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
// Calculate age from date of birth // Calculate age from date of birth
final now = DateTime.now(); final now = DateTime.now();
int age = now.year - dateOfBirth.year; int age = now.year - dateOfBirth.year;
if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { if (now.month < dateOfBirth.month ||
(now.month == dateOfBirth.month && now.day < dateOfBirth.day)) {
age--; age--;
} }
_selectedAge = age; _selectedAge = age;
@ -891,7 +989,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
}) async { }) async {
isBodySymptomsLoading = true; isBodySymptomsLoading = true;
notifyListeners(); notifyListeners();
final result = await symptomsCheckerRepo.getUserDetails(userName: userName, password: password); final result = await symptomsCheckerRepo.getUserDetails(
userName: userName, password: password);
result.fold( result.fold(
(failure) async { (failure) async {

@ -2,6 +2,7 @@ import 'dart:developer';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/app_export.dart';
import 'package:hmg_patient_app_new/core/dependencies.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/core/utils/utils.dart';
@ -17,6 +18,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:lottie/lottie.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class TriageScreen extends StatefulWidget { class TriageScreen extends StatefulWidget {
@ -27,7 +29,6 @@ class TriageScreen extends StatefulWidget {
} }
class _TriageScreenState extends State<TriageScreen> { class _TriageScreenState extends State<TriageScreen> {
List<String> answeredEvidenceIds = []; // Track user's answers
late SymptomsCheckerViewModel viewModel; late SymptomsCheckerViewModel viewModel;
late DialogService dialogService; late DialogService dialogService;
@ -58,43 +59,96 @@ class _TriageScreenState extends State<TriageScreen> {
} }
void _handleTriageResponse() { void _handleTriageResponse() {
// Case 1: Emergency evidence detected
if (viewModel.hasEmergencyEvidence) { if (viewModel.hasEmergencyEvidence) {
_showEmergencyDialog(); _showEmergencyDialog();
return; return;
} }
if (viewModel.shouldStopTriage) { // Get the highest probability condition
final conditions = viewModel.currentConditions ?? [];
double highestProbability = 0.0;
if (conditions.isNotEmpty) {
final sortedConditions = List<TriageCondition>.from(conditions);
sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0));
highestProbability = (sortedConditions.first.probability ?? 0.0) * 100;
}
// Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered
if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 7) {
// Navigate to results/possible conditions screen // Navigate to results/possible conditions screen
context.navigateWithName(AppRoutes.possibleConditionsScreen); context.navigateWithName(AppRoutes.possibleConditionsScreen);
return; return;
} }
// Question is loaded, reset selection for new question // Continue triage - question is loaded, reset selection for new question
viewModel.resetTriageChoice(); viewModel.resetTriageChoice();
} }
void _showEmergencyDialog() { void _showEmergencyDialog() {
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
title: "Emergency".needTranslation, child: Container(
child: Utils.getWarningWidget( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
loadingText: "Emergency evidence detected. Please seek immediate medical attention.".needTranslation, color: AppColors.primaryRedColor,
isShowActionButtons: true, borderRadius: 24.h,
onCancelTap: () => Navigator.pop(context), ),
onConfirmTap: () { child: Padding(
Navigator.pop(context); padding: EdgeInsets.all(24.h),
context.pop(); child: Column(
}, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText14(),
Utils.buildSvgWithAssets(
icon: AppAssets.cancel_circle_icon,
iconColor: AppColors.whiteColor,
width: 24.h,
height: 24.h,
fit: BoxFit.contain,
).onPress(() {
Navigator.of(context).pop();
}),
],
),
Lottie.asset(AppAnimations.ambulanceAlert,
repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain),
SizedBox(height: 8.h),
"Emergency".needTranslation.toText28(color: AppColors.whiteColor, isBold: true),
SizedBox(height: 8.h),
"Emergency evidence detected. Please seek medical attention."
.needTranslation
.toText14(color: AppColors.whiteColor, weight: FontWeight.w500),
SizedBox(height: 24.h),
CustomButton(
text: LocaleKeys.confirm.tr(context: context),
onPressed: () async => Navigator.of(context).pop(),
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.whiteColor,
textColor: AppColors.primaryRedColor,
icon: AppAssets.checkmark_icon,
iconColor: AppColors.primaryRedColor,
),
SizedBox(height: 8.h),
],
),
),
), ),
isFullScreen: false, isFullScreen: false,
isCloseButtonVisible: true, isCloseButtonVisible: false,
hasBottomPadding: false,
backgroundColor: AppColors.primaryRedColor,
callBackFunc: () {},
); );
} }
bool get isFirstQuestion => answeredEvidenceIds.isEmpty; bool get isFirstQuestion => viewModel.getTriageEvidence().isEmpty;
void _onOptionSelected(int choiceIndex) { void _onOptionSelectedForItem(String itemId, int choiceIndex) {
viewModel.selectTriageChoice(choiceIndex); viewModel.selectTriageChoiceForItem(itemId, choiceIndex);
} }
void _onPreviousPressed() { void _onPreviousPressed() {
@ -102,13 +156,6 @@ class _TriageScreenState extends State<TriageScreen> {
} }
void _onNextPressed() { 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; final currentQuestion = viewModel.currentTriageQuestion;
if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) { if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) {
dialogService.showErrorBottomSheet( dialogService.showErrorBottomSheet(
@ -117,32 +164,43 @@ class _TriageScreenState extends State<TriageScreen> {
return; return;
} }
final questionItem = currentQuestion.items!.first; // Check if all items have been answered
if (questionItem.choices == null || viewModel.selectedTriageChoiceIndex! >= questionItem.choices!.length) { if (!viewModel.areAllTriageItemsAnswered) {
dialogService.showErrorBottomSheet( dialogService.showErrorBottomSheet(message: 'Please answer all questions before proceeding'.needTranslation);
message: 'Invalid choice selection'.needTranslation,
);
return; return;
} }
final selectedChoice = questionItem.choices![viewModel.selectedTriageChoiceIndex!]; // Collect all evidence from all items
for (var item in currentQuestion.items!) {
final itemId = item.id ?? "";
if (itemId.isEmpty) continue;
final evidenceId = selectedChoice.label ?? ""; final selectedChoiceIndex = viewModel.getTriageChoiceForItem(itemId);
if (evidenceId.isNotEmpty) { if (selectedChoiceIndex == null) continue;
answeredEvidenceIds.add(evidenceId);
if (item.choices != null && selectedChoiceIndex < item.choices!.length) {
final selectedChoice = item.choices![selectedChoiceIndex];
final choiceId = selectedChoice.id ?? "";
if (choiceId.isNotEmpty) {
viewModel.addTriageEvidence(itemId, choiceId);
}
}
} }
// Get all previous evidence IDs // Get all evidence: initial symptoms + risk factors + suggestions + triage evidence
List<String> allEvidenceIds = viewModel.getAllEvidenceIds(); List<String> initialEvidenceIds = viewModel.getAllEvidenceIds();
allEvidenceIds.addAll(answeredEvidenceIds); List<Map<String, String>> triageEvidence = viewModel.getTriageEvidence();
log("allEvidences: ${allEvidenceIds.toString()}"); log("initialEvidenceIds: ${initialEvidenceIds.toString()}");
log("triageEvidence: ${triageEvidence.toString()}");
// Call API with updated evidence // Call API with updated evidence
viewModel.getDiagnosisForTriage( viewModel.getDiagnosisForTriage(
age: viewModel.selectedAge!, age: viewModel.selectedAge!,
sex: viewModel.selectedGender!.toLowerCase(), sex: viewModel.selectedGender!.toLowerCase(),
evidenceIds: allEvidenceIds, evidenceIds: initialEvidenceIds,
triageEvidence: triageEvidence,
language: viewModel.appState.isArabic() ? 'ar' : 'en', language: viewModel.appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) { onSuccess: (response) {
_handleTriageResponse(); _handleTriageResponse();
@ -241,29 +299,6 @@ class _TriageScreenState extends State<TriageScreen> {
); );
} }
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) { void _showConfirmationBeforeExit(BuildContext context) {
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context), title: LocaleKeys.notice.tr(context: context),
@ -299,9 +334,6 @@ class _TriageScreenState extends State<TriageScreen> {
return SizedBox.shrink(); return SizedBox.shrink();
} }
final questionItem = question.items!.first;
final choices = questionItem.choices ?? [];
return AnimatedSwitcher( return AnimatedSwitcher(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) { transitionBuilder: (Widget child, Animation<double> animation) {
@ -324,7 +356,7 @@ class _TriageScreenState extends State<TriageScreen> {
); );
}, },
child: Container( child: Container(
key: ValueKey<String>(questionItem.id ?? answeredEvidenceIds.length.toString()), key: ValueKey<String>(question.items!.first.id ?? viewModel.getTriageEvidence().length.toString()),
width: double.infinity, width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 24.w), margin: EdgeInsets.symmetric(horizontal: 24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
@ -332,11 +364,36 @@ class _TriageScreenState extends State<TriageScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
(question.text ?? "").toText16(weight: FontWeight.w500), // Main question text
(question.text ?? "").toText16(weight: FontWeight.w600, color: AppColors.textColor),
SizedBox(height: 24.h), SizedBox(height: 24.h),
...List.generate(choices.length, (index) {
bool selected = viewModel.selectedTriageChoiceIndex == index; // Show all items with dividers
return _buildOptionItem(index, selected, choices[index].label ?? ""); ...List.generate(question.items!.length, (itemIndex) {
final item = question.items![itemIndex];
final itemId = item.id ?? "";
final choices = item.choices ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Item name (sub-question)
(item.name ?? "").toText14(weight: FontWeight.w600, color: AppColors.textColor),
SizedBox(height: 8.h),
// Choices for this item
...List.generate(choices.length, (choiceIndex) {
bool selected = viewModel.getTriageChoiceForItem(itemId) == choiceIndex;
return _buildOptionItem(itemId, choiceIndex, selected, choices[choiceIndex].label ?? "");
}),
// Add divider between items (but not after the last one)
if (itemIndex < question.items!.length - 1) ...[
SizedBox(height: 8.h),
Divider(color: AppColors.bottomNAVBorder, thickness: 1),
SizedBox(height: 10.h),
],
],
);
}), }),
], ],
), ),
@ -344,9 +401,9 @@ class _TriageScreenState extends State<TriageScreen> {
); );
} }
Widget _buildOptionItem(int index, bool selected, String optionText) { Widget _buildOptionItem(String itemId, int choiceIndex, bool selected, String optionText) {
return GestureDetector( return GestureDetector(
onTap: () => _onOptionSelected(index), onTap: () => _onOptionSelectedForItem(itemId, choiceIndex),
child: Container( child: Container(
margin: EdgeInsets.only(bottom: 12.h), margin: EdgeInsets.only(bottom: 12.h),
child: Row( child: Row(
@ -365,7 +422,7 @@ class _TriageScreenState extends State<TriageScreen> {
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
Expanded(child: optionText.toText14(weight: FontWeight.w500)), Expanded(child: optionText.toText13(weight: FontWeight.w500)),
], ],
), ),
), ),
@ -387,6 +444,7 @@ class _TriageScreenState extends State<TriageScreen> {
suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown"; suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown";
probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage
} }
// final bool isHighConfidence = probability >= 70.0;
return Container( return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
@ -445,6 +503,8 @@ class _TriageScreenState extends State<TriageScreen> {
), ),
), ),
), ),
// Show high confidence message
SizedBox(height: 12.h), SizedBox(height: 12.h),
Row( Row(
children: [ children: [

@ -1,57 +0,0 @@
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;
}

@ -1,18 +0,0 @@
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