Merge pull request 'Completed the SymptomsChecker Changes (faiz_dev)' (#304) from faiz_dev into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/304
master
Haroon6138 14 hours ago
commit b71a73c49a

@ -212,6 +212,7 @@ class ApiConsts {
static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis'; static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis';
static final String explain = '$symptomsCheckerApi/ExplainDiagnosisResult'; static final String explain = '$symptomsCheckerApi/ExplainDiagnosisResult';
static final String getClinicFromCondition = '$symptomsCheckerApi/GetClinicsByCondition?condition='; static final String getClinicFromCondition = '$symptomsCheckerApi/GetClinicsByCondition?condition=';
static final String scheduleAppointment = '$symptomsCheckerApi/ScheduleAppointment';
//E-REFERRAL SERVICES //E-REFERRAL SERVICES
static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes"; static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes";

@ -61,7 +61,8 @@ class Utils {
"ProjectOutSA": false, "ProjectOutSA": false,
"UsingInDoctorApp": false, "UsingInDoctorApp": false,
"IsHMC": false "IsHMC": false
},{ },
{
"Desciption": "Jeddah Fayhaa Hospital", "Desciption": "Jeddah Fayhaa Hospital",
"DesciptionN": "مستشفى جدة الفيحاء", "DesciptionN": "مستشفى جدة الفيحاء",
"ID": 3, // Campus ID "ID": 3, // Campus ID
@ -539,8 +540,8 @@ class Utils {
), ),
], ],
) )
: showOkButton? : showOkButton
Row( ? Row(
children: [ children: [
Expanded( Expanded(
child: CustomButton( child: CustomButton(
@ -558,7 +559,7 @@ class Utils {
), ),
], ],
) )
:SizedBox.shrink(), : SizedBox.shrink(),
], ],
).center; ).center;
} }
@ -833,12 +834,16 @@ class Utils {
final iconH = height ?? 24.h; final iconH = height ?? 24.h;
final iconW = width ?? 24.w; final iconW = width ?? 24.w;
return Container( return Container(
width: iconW, height: iconH, width: iconW,
height: iconH,
decoration: BoxDecoration( decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null, border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: borderRadius != null ? BorderRadius.circular(borderRadius ?? 12.r) : null, borderRadius: borderRadius != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
image: DecorationImage(image: AssetImage(icon,), fit: fit) image: DecorationImage(
image: AssetImage(
icon,
), ),
fit: fit)),
); );
} }
@ -869,9 +874,8 @@ class Utils {
static Widget getPaymentMethods() { static Widget getPaymentMethods() {
return Row( return Row(
spacing: 6.w,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 5.w,
children: [ children: [
Image.asset(AppAssets.mada, width: 35.h, height: 35.h), Image.asset(AppAssets.mada, width: 35.h, height: 35.h),
Image.asset( Image.asset(
@ -1025,7 +1029,6 @@ class Utils {
isHMC: hospital.isHMC); isHMC: hospital.isHMC);
} }
static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) { static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) {
if (item == null) return null; if (item == null) return null;
return HospitalsModel( return HospitalsModel(

@ -0,0 +1,48 @@
class ScheduleAppointmentRequestModel {
final String generalId;
final String fileNo;
final String appointmentNo;
final String doctorId;
final String appointmentDate;
final String mobileNumber;
final int projectId;
final int clinicId;
ScheduleAppointmentRequestModel({
required this.generalId,
required this.fileNo,
required this.appointmentNo,
required this.doctorId,
required this.appointmentDate,
required this.mobileNumber,
required this.projectId,
required this.clinicId,
});
Map<String, dynamic> toJson() {
return {
'generalId': generalId,
'fileNo': fileNo,
'appointmentNo': appointmentNo,
'doctorId': doctorId,
'appointmentDate': appointmentDate,
'mobileNumber': mobileNumber,
'projectId': projectId,
'clinicId': clinicId,
};
}
factory ScheduleAppointmentRequestModel.fromJson(Map<String, dynamic> json) {
return ScheduleAppointmentRequestModel(
generalId: json['generalId'] ?? '',
fileNo: json['fileNo'] ?? '',
appointmentNo: json['appointmentNo'] ?? '',
doctorId: json['doctorId'] ?? '',
appointmentDate: json['appointmentDate'] ?? '',
mobileNumber: json['mobileNumber'] ?? '',
projectId: json['projectId'] ?? 0,
clinicId: json['clinicId'] ?? 0,
);
}
}

@ -0,0 +1,32 @@
class ScheduleAppointmentResponseModel {
final bool? success;
final String? message;
final String? appointmentId;
final dynamic data;
ScheduleAppointmentResponseModel({
this.success,
this.message,
this.appointmentId,
this.data,
});
factory ScheduleAppointmentResponseModel.fromJson(Map<String, dynamic> json) {
return ScheduleAppointmentResponseModel(
success: json['success'],
message: json['message'],
appointmentId: json['appointmentId'],
data: json['data'],
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'appointmentId': appointmentId,
'data': data,
};
}
}

@ -6,9 +6,11 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; 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/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_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/risk_and_suggestions_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/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/models/resp_models/triage_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -59,6 +61,11 @@ abstract class SymptomsCheckerRepo {
required String language, required String language,
required String userSessionToken, required String userSessionToken,
}); });
Future<Either<Failure, GenericApiModel<ScheduleAppointmentResponseModel>>> saveAppointmentDetailsForSymptomsChecker({
required ScheduleAppointmentRequestModel request,
required String userSessionToken,
});
} }
class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
@ -419,7 +426,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken', 'Authorization': 'Bearer $userSessionToken',
}; };
Map<String, dynamic> body = {};
try { try {
GenericApiModel<List<GetClinicDetailsResponseModel>>? apiResponse; GenericApiModel<List<GetClinicDetailsResponseModel>>? apiResponse;
@ -466,4 +472,61 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
return Left(UnknownFailure(e.toString())); return Left(UnknownFailure(e.toString()));
} }
} }
@override
Future<Either<Failure, GenericApiModel<ScheduleAppointmentResponseModel>>> saveAppointmentDetailsForSymptomsChecker({
required ScheduleAppointmentRequestModel request,
required String userSessionToken,
}) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
final body = request.toJson();
try {
GenericApiModel<ScheduleAppointmentResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.scheduleAppointment,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("ScheduleAppointment API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
// Parse response if it's a string
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
ScheduleAppointmentResponseModel scheduleAppointmentResponse = ScheduleAppointmentResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<ScheduleAppointmentResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: scheduleAppointmentResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing ScheduleAppointment 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 scheduleAppointment: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
} }

@ -5,9 +5,11 @@ import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping_data.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping_data.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_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/risk_and_suggestions_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/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/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/features/symptoms_checker/symptoms_checker_repo.dart';
@ -62,6 +64,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
bool isRiskFactorsLoading = false; bool isRiskFactorsLoading = false;
bool isSuggestionsLoading = false; bool isSuggestionsLoading = false;
bool isTriageDiagnosisLoading = false; bool isTriageDiagnosisLoading = false;
bool isScheduleAppointmentLoading = false;
// Flag to track if appointment is being booked via symptoms checker flow
bool isBookingFromSymptomsChecker = false;
// API data storage - using API models directly // API data storage - using API models directly
SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel; SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel;
@ -89,6 +95,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
// Selected symptoms tracking (organId -> Set of symptom IDs) // Selected symptoms tracking (organId -> Set of symptom IDs)
final Map<String, Set<String>> _selectedSymptomsByOrgan = {}; final Map<String, Set<String>> _selectedSymptomsByOrgan = {};
// Symptom search/filter state
String _symptomSearchQuery = '';
List<OrganSymptomResult> _filteredOrganSymptomsResults = [];
// 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
@ -180,9 +190,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Check if current question type is multi-item selection (type=2) /// Check if current question type is multi-item selection (type=2)
bool get isTriageQuestionMultiItem => currentTriageQuestion?.type == 2; bool get isTriageQuestionMultiItem => currentTriageQuestion?.type == 2;
/// For type=1 questions: check if a specific item-choice combination is selected /// For type=1 questions: check if a specific item is selected (ignore choiceIndex)
bool isTriageSingleOptionSelected(String itemId, int choiceIndex) { bool isTriageSingleOptionSelected(String itemId, int choiceIndex) {
return _selectedSingleItemId == itemId && _selectedSingleChoiceIndex == choiceIndex; return _selectedSingleItemId == itemId;
} }
/// Get selected item ID for type=1 questions /// Get selected item ID for type=1 questions
@ -191,6 +201,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Get selected choice index for type=1 questions /// Get selected choice index for type=1 questions
int? get selectedSingleChoiceIndex => _selectedSingleChoiceIndex; int? get selectedSingleChoiceIndex => _selectedSingleChoiceIndex;
/// Set flag for booking from symptoms checker
void setBookingFromSymptomsChecker(bool value) {
isBookingFromSymptomsChecker = value;
notifyListeners();
}
/// Check if all items in current question have been answered /// Check if all items in current question have been answered
bool get areAllTriageItemsAnswered { bool get areAllTriageItemsAnswered {
if (currentTriageQuestion?.items == null || currentTriageQuestion!.items!.isEmpty) { if (currentTriageQuestion?.items == null || currentTriageQuestion!.items!.isEmpty) {
@ -234,6 +250,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return bodySymptomResponse!.dataDetails!.result ?? []; return bodySymptomResponse!.dataDetails!.result ?? [];
} }
/// Get filtered organ symptoms results based on search query
List<OrganSymptomResult> get filteredOrganSymptomsResults {
if (_symptomSearchQuery.isEmpty) {
return organSymptomsResults;
}
return _filteredOrganSymptomsResults;
}
/// Get current search query
String get symptomSearchQuery => _symptomSearchQuery;
/// Get all symptoms from all organs (for search suggestions)
List<BodySymptom> get allSymptoms {
List<BodySymptom> symptoms = [];
for (var organResult in organSymptomsResults) {
if (organResult.bodySymptoms != null) {
symptoms.addAll(organResult.bodySymptoms!);
}
}
return symptoms;
}
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);
} }
@ -454,6 +492,51 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Filter symptoms based on search query
void filterSymptoms(String query, {bool isArabic = false}) {
_symptomSearchQuery = query;
if (query.isEmpty) {
_filteredOrganSymptomsResults.clear();
notifyListeners();
return;
}
final lowercaseQuery = query.toLowerCase();
_filteredOrganSymptomsResults = [];
for (var organResult in organSymptomsResults) {
if (organResult.bodySymptoms == null || organResult.bodySymptoms!.isEmpty) {
continue;
}
// Filter symptoms that match the query
final filteredSymptoms = organResult.bodySymptoms!.where((symptom) {
final displayName = symptom.getDisplayName(isArabic).toLowerCase();
return displayName.contains(lowercaseQuery);
}).toList();
// Only add organ result if it has matching symptoms
if (filteredSymptoms.isNotEmpty) {
_filteredOrganSymptomsResults.add(
OrganSymptomResult(
name: organResult.name,
bodySymptoms: filteredSymptoms,
),
);
}
}
notifyListeners();
}
/// Clear symptom search filter
void clearSymptomFilter() {
_symptomSearchQuery = '';
_filteredOrganSymptomsResults.clear();
notifyListeners();
}
// Risk Factors Methods // Risk Factors Methods
/// Toggle risk factor selection /// Toggle risk factor selection
@ -873,16 +956,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Select a choice for a specific item (for multi-item questions) /// Select a choice for a specific item (for multi-item questions)
void selectTriageChoiceForItem(String itemId, int choiceIndex) { void selectTriageChoiceForItem(String itemId, int choiceIndex) {
// Type 1: Single selection mode - only one option can be selected across all items // Type 1: Single selection mode - only one ITEM can be selected (choice is always "Yes")
if (isTriageQuestionSingleSelection) { if (isTriageQuestionSingleSelection) {
// If same option clicked again, deselect it // If same item clicked again, deselect it
if (_selectedSingleItemId == itemId && _selectedSingleChoiceIndex == choiceIndex) { if (_selectedSingleItemId == itemId) {
_selectedSingleItemId = null; _selectedSingleItemId = null;
_selectedSingleChoiceIndex = null; _selectedSingleChoiceIndex = null;
} else { } else {
// Select new option, clear previous selection // Select new item, clear previous selection
_selectedSingleItemId = itemId; _selectedSingleItemId = itemId;
_selectedSingleChoiceIndex = choiceIndex; // For type=1, we don't use choiceIndex from UI, we'll find "Yes" choice in the API call
_selectedSingleChoiceIndex = 0; // Placeholder, actual Yes choice will be found later
} }
} else { } else {
// Type 2: Multi-item selection mode - each item can have one selected option // Type 2: Multi-item selection mode - each item can have one selected option
@ -932,15 +1016,20 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_selectedTriageChoicesByItemId.clear(); _selectedTriageChoicesByItemId.clear();
_triageQuestionCount = 0; // Reset question count _triageQuestionCount = 0; // Reset question count
_currentZoomScale = 1.0; // Reset zoom scale _currentZoomScale = 1.0; // Reset zoom scale
_symptomSearchQuery = ''; // Reset search query
_filteredOrganSymptomsResults.clear(); // Clear filtered results
bodySymptomResponse = null; bodySymptomResponse = null;
riskFactorsResponse = null; riskFactorsResponse = null;
suggestionsResponse = null; suggestionsResponse = null;
triageDataDetails = null; triageDataDetails = null;
isTriageDiagnosisLoading = false; isTriageDiagnosisLoading = false;
_selectedTriageChoiceIndex = null; _selectedTriageChoiceIndex = null;
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
_isBottomSheetExpanded = false; _isBottomSheetExpanded = false;
_tooltipTimer?.cancel(); _tooltipTimer?.cancel();
_tooltipOrganId = null; _tooltipOrganId = null;
isBookingFromSymptomsChecker = false; // Reset booking flag
// Reset user info flow // Reset user info flow
_userInfoCurrentPage = 0; _userInfoCurrentPage = 0;
_isSinglePageEditMode = false; _isSinglePageEditMode = false;
@ -1179,6 +1268,67 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
); );
} }
/// Schedule appointment for symptoms checker
Future<void> saveAppointmentDetailsForSymptomsChecker({
required String fileNo,
required String appointmentNo,
required String doctorId,
required String appointmentDate,
required String mobileNumber,
required int projectId,
required int clinicId,
Function(ScheduleAppointmentResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isScheduleAppointmentLoading = true;
notifyListeners();
// Import the request model at the top of the file
final request = ScheduleAppointmentRequestModel(
generalId: currentSessionId,
fileNo: fileNo,
appointmentNo: appointmentNo,
doctorId: doctorId,
appointmentDate: appointmentDate,
mobileNumber: mobileNumber,
projectId: projectId,
clinicId: clinicId,
);
final result = await symptomsCheckerRepo.saveAppointmentDetailsForSymptomsChecker(
request: request,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isScheduleAppointmentLoading = false;
isBookingFromSymptomsChecker = false; // Reset flag on error
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isScheduleAppointmentLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
isBookingFromSymptomsChecker = false; // Reset flag on success
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
isBookingFromSymptomsChecker = false; // Reset flag on error
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to schedule appointment');
}
}
},
);
}
@override @override
void dispose() { void dispose() {
_tooltipTimer?.cancel(); _tooltipTimer?.cancel();

@ -920,10 +920,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox( Utils.getPaymentMethods(),
width: 200.h,
child: Utils.getPaymentMethods(),
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

@ -1,3 +1,5 @@
import 'dart:ui' as ui;
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_assets.dart';
@ -19,7 +21,6 @@ import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dar
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/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'dart:ui' as ui;
class SavedLogin extends StatefulWidget { class SavedLogin extends StatefulWidget {
const SavedLogin({super.key}); const SavedLogin({super.key});
@ -33,6 +34,7 @@ class _SavedLogin extends State<SavedLogin> {
late AuthenticationViewModel authVm; late AuthenticationViewModel authVm;
late AppState appState; late AppState appState;
bool? isOther; bool? isOther;
@override @override
void initState() { void initState() {
authVm = context.read<AuthenticationViewModel>(); authVm = context.read<AuthenticationViewModel>();
@ -90,9 +92,9 @@ class _SavedLogin extends State<SavedLogin> {
: SizedBox(), : SizedBox(),
SizedBox(height: 24.h), SizedBox(height: 24.h),
Container( Container(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false, isCustomShadow: [ decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false, isCustomShadow: [
BoxShadow(color: Color(0x0D000000), blurRadius: 16.h, offset: Offset(0, 0), spreadRadius: 5.h), BoxShadow(color: Color(0x0D000000), blurRadius: 16.h, offset: Offset(0, 0), spreadRadius: 5.h),
]), ]),
child: Column( child: Column(
@ -105,7 +107,9 @@ class _SavedLogin extends State<SavedLogin> {
textDirection: ui.TextDirection.ltr, textDirection: ui.TextDirection.ltr,
child: appState.getSelectDeviceByImeiRespModelElement != null child: appState.getSelectDeviceByImeiRespModelElement != null
? (appState.getSelectDeviceByImeiRespModelElement!.createdOn != null ? (appState.getSelectDeviceByImeiRespModelElement!.createdOn != null
? DateUtil.getFormattedDate(DateUtil.convertStringToDate(appState.getSelectDeviceByImeiRespModelElement!.createdOn!), "d MMMM, y 'at' HH:mm") ? DateUtil.getFormattedDate(
DateUtil.convertStringToDate(appState.getSelectDeviceByImeiRespModelElement!.createdOn!),
"d MMMM, y 'at' HH:mm")
: '--') : '--')
.toText16(isBold: true, color: AppColors.textColor, isEnglishOnly: true) .toText16(isBold: true, color: AppColors.textColor, isEnglishOnly: true)
: SizedBox(), : SizedBox(),
@ -115,10 +119,14 @@ class _SavedLogin extends State<SavedLogin> {
? Container( ? Container(
margin: EdgeInsets.all(16.h), margin: EdgeInsets.all(16.h),
child: Utils.buildSvgWithAssets( child: Utils.buildSvgWithAssets(
icon: (isOther == true && loginType == LoginTypeEnum.sms) ? AppAssets.whatsapp : getTypeIcons(appState.getSelectDeviceByImeiRespModelElement!.logInType!), icon: (isOther == true && loginType == LoginTypeEnum.sms)
? AppAssets.whatsapp
: getTypeIcons(appState.getSelectDeviceByImeiRespModelElement!.logInType!),
height: 54.h, height: 54.h,
width: 54.w, width: 54.w,
iconColor: (isOther == true && loginType == LoginTypeEnum.sms) || loginType.toInt == 4 ? null : AppColors.primaryRedColor)) iconColor: (isOther == true && loginType == LoginTypeEnum.sms) || loginType.toInt == 4
? null
: AppColors.primaryRedColor))
: SizedBox(), : SizedBox(),
// Main login button - for isOther with SMS, show WhatsApp, otherwise keep original login type // Main login button - for isOther with SMS, show WhatsApp, otherwise keep original login type
CustomButton( CustomButton(
@ -126,7 +134,6 @@ class _SavedLogin extends State<SavedLogin> {
? "${LocaleKeys.loginBy.tr()} ${LoginTypeEnum.whatsapp.displayName}" ? "${LocaleKeys.loginBy.tr()} ${LoginTypeEnum.whatsapp.displayName}"
: "${LocaleKeys.loginBy.tr()} ${loginType.displayName}", : "${LocaleKeys.loginBy.tr()} ${loginType.displayName}",
onPressed: () { onPressed: () {
if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) {
authVm.loginWithFingerPrintFace(() {}); authVm.loginWithFingerPrintFace(() {});
} else { } else {
@ -147,7 +154,8 @@ class _SavedLogin extends State<SavedLogin> {
height: 40.h, height: 40.h,
padding: EdgeInsets.symmetric(vertical: 10.h), padding: EdgeInsets.symmetric(vertical: 10.h),
icon: (isOther == true && loginType == LoginTypeEnum.sms) ? AppAssets.whatsapp : getTypeIcons(loginType.toInt), icon: (isOther == true && loginType == LoginTypeEnum.sms) ? AppAssets.whatsapp : getTypeIcons(loginType.toInt),
iconColor: (isOther == true && loginType == LoginTypeEnum.sms) || loginType == LoginTypeEnum.whatsapp ? null : Colors.white, iconColor:
(isOther == true && loginType == LoginTypeEnum.sms) || loginType == LoginTypeEnum.whatsapp ? null : Colors.white,
), ),
], ],
), ),
@ -159,7 +167,10 @@ class _SavedLogin extends State<SavedLogin> {
padding: EdgeInsets.symmetric(horizontal: 16.w), padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text( child: Text(
LocaleKeys.oR.tr(), LocaleKeys.oR.tr(),
style: context.dynamicTextStyle(fontSize: 16.f, fontWeight: FontWeight.w600,), style: context.dynamicTextStyle(
fontSize: 16.f,
fontWeight: FontWeight.w600,
),
), ),
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
@ -240,6 +251,7 @@ class _SavedLogin extends State<SavedLogin> {
}), }),
); );
}, },
height: isFoldable ? 50.h : 40.h,
backgroundColor: AppColors.whiteColor, backgroundColor: AppColors.whiteColor,
borderColor: AppColors.borderOnlyColor, borderColor: AppColors.borderOnlyColor,
textColor: AppColors.textColor, textColor: AppColors.textColor,

@ -75,7 +75,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true, isEnglishOnly: true), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"
.toText16(isBold: true, isEnglishOnly: true),
SizedBox(height: 8.h), SizedBox(height: 8.h),
Wrap( Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
@ -88,8 +89,11 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
"${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isEnglishOnly: true), "${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isEnglishOnly: true),
LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor), LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor),
], ],
),), ),
AppCustomChipWidget(labelText: "${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"), ),
AppCustomChipWidget(
labelText:
"${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"),
], ],
), ),
], ],
@ -124,7 +128,12 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
Utils.buildSvgWithAssets(icon: getLiveCareTypeIcon(immediateLiveCareVM.liveCareSelectedCallType), width: 32.h, height: 32.h, fit: BoxFit.contain, applyThemeColor: false), Utils.buildSvgWithAssets(
icon: getLiveCareTypeIcon(immediateLiveCareVM.liveCareSelectedCallType),
width: 32.h,
height: 32.h,
fit: BoxFit.contain,
applyThemeColor: false),
SizedBox(width: 8.h), SizedBox(width: 8.h),
getLiveCareType(context, immediateLiveCareVM.liveCareSelectedCallType).toText16(isBold: true), getLiveCareType(context, immediateLiveCareVM.liveCareSelectedCallType).toText16(isBold: true),
], ],
@ -136,7 +145,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
), ),
), ),
).onPress(() { ).onPress(() {
showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(immediateLiveCareViewModel: immediateLiveCareVM), callBackFunc: () async { showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(immediateLiveCareViewModel: immediateLiveCareVM),
callBackFunc: () async {
debugPrint("Selected Call Type: ${immediateLiveCareVM.liveCareSelectedCallType}"); debugPrint("Selected Call Type: ${immediateLiveCareVM.liveCareSelectedCallType}");
}, title: LocaleKeys.selectLiveCareCallType.tr(context: context), isCloseButtonVisible: true, isFullScreen: false); }, title: LocaleKeys.selectLiveCareCallType.tr(context: context), isCloseButtonVisible: true, isFullScreen: false);
}); });
@ -169,11 +179,15 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true).paddingSymmetrical(24.h, 0.h), LocaleKeys.insuranceExpiredOrInactive
.tr(context: context)
.toText14(color: AppColors.primaryRedColor, isBold: true)
.paddingSymmetrical(24.h, 0.h),
CustomButton( CustomButton(
text: LocaleKeys.updateInsurance.tr(context: context), text: LocaleKeys.updateInsurance.tr(context: context),
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context)
.push(
CustomPageRoute( CustomPageRoute(
page: InsuranceHomePage(), page: InsuranceHomePage(),
), ),
@ -214,7 +228,10 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.amountBeforeTax.tr(context: context).toText14(isBold: true), LocaleKeys.amountBeforeTax.tr(context: context).toText14(isBold: true),
Utils.getPaymentAmountWithSymbol((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.amount ?? "").toText16(isBold: true, isEnglishOnly: true), AppColors.blackColor, 13, Utils.getPaymentAmountWithSymbol(
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.amount ?? "").toText16(isBold: true, isEnglishOnly: true),
AppColors.blackColor,
13,
isSaudiCurrency: (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" || isSaudiCurrency: (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" ||
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال"), (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال"),
], ],
@ -224,7 +241,10 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
children: [ children: [
LocaleKeys.vat15.tr(context: context).toText14(isBold: true, color: AppColors.greyTextColor), LocaleKeys.vat15.tr(context: context).toText14(isBold: true, color: AppColors.greyTextColor),
Utils.getPaymentAmountWithSymbol( Utils.getPaymentAmountWithSymbol(
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.tax ?? "0.0").toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true), AppColors.greyTextColor, 13, (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.tax ?? "0.0")
.toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true),
AppColors.greyTextColor,
13,
isSaudiCurrency: ((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" || isSaudiCurrency: ((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" ||
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")),
], ],
@ -233,13 +253,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox(width: 200.h, child: Utils.getPaymentMethods()), Utils.getPaymentMethods(),
Utils.getPaymentAmountWithSymbol((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total ?? "0.0").toText24(isBold: true, isEnglishOnly: true), AppColors.blackColor, 17, Utils.getPaymentAmountWithSymbol(
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total ?? "0.0").toText24(isBold: true, isEnglishOnly: true),
AppColors.blackColor,
17,
isSaudiCurrency: ((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" || isSaudiCurrency: ((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" ||
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")),
], ],
).paddingSymmetrical(24.h, 0.h), ).paddingSymmetrical(24.h, 0.h),
(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" ||
immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0")
// (true) // (true)
? CustomButton( ? CustomButton(
text: LocaleKeys.confirmLiveCare.tr(context: context), text: LocaleKeys.confirmLiveCare.tr(context: context),
@ -248,7 +272,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
if (val) { if (val) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.confirmingLiveCareRequest.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.confirmingLiveCareRequest.tr(context: context));
await immediateLiveCareVM.addNewCallRequestForImmediateLiveCare("${appState.getAuthenticatedUser()!.patientId}${DateTime.now().millisecondsSinceEpoch}"); await immediateLiveCareVM.addNewCallRequestForImmediateLiveCare(
"${appState.getAuthenticatedUser()!.patientId}${DateTime.now().millisecondsSinceEpoch}");
await immediateLiveCareVM.getPatientLiveCareHistory(); await immediateLiveCareVM.getPatientLiveCareHistory();
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
if (immediateLiveCareVM.patientHasPendingLiveCareRequest) { if (immediateLiveCareVM.patientHasPendingLiveCareRequest) {
@ -425,8 +450,9 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
final newlyPermanent = missing.where((p) => (newStatuses[p]?.isPermanentlyDenied ?? false) || (newStatuses[p]?.isRestricted ?? false)).toList(); final newlyPermanent = missing.where((p) => (newStatuses[p]?.isPermanentlyDenied ?? false) || (newStatuses[p]?.isRestricted ?? false)).toList();
if (newlyPermanent.isNotEmpty) { if (newlyPermanent.isNotEmpty) {
final names = newlyPermanent.map((p) => LiveCarePermissionService.instance.friendlyName(p)).join(' and '); final names = newlyPermanent.map((p) => LiveCarePermissionService.instance.friendlyName(p)).join(' and ');
final message = final message = newlyPermanent.length == 1
newlyPermanent.length == 1 ? '$names permission is permanently denied. Open app settings to allow it.' : '$names permissions are permanently denied. Open app settings to allow them.'; ? '$names permission is permanently denied. Open app settings to allow it.'
: '$names permissions are permanently denied. Open app settings to allow them.';
await LiveCarePermissionService.instance.showOpenSettingsDialog( await LiveCarePermissionService.instance.showOpenSettingsDialog(
context, context,
title: "Permissions Required", title: "Permissions Required",

@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_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/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
@ -37,6 +38,7 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
late BookAppointmentsViewModel bookAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel;
late AuthenticationViewModel authVM; late AuthenticationViewModel authVM;
late MyAppointmentsViewModel myAppointmentsViewModel; late MyAppointmentsViewModel myAppointmentsViewModel;
late SymptomsCheckerViewModel symptomsCheckerViewModel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -44,6 +46,7 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false); myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
authVM = Provider.of<AuthenticationViewModel>(context, listen: false); authVM = Provider.of<AuthenticationViewModel>(context, listen: false);
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
symptomsCheckerViewModel = Provider.of<SymptomsCheckerViewModel>(context, listen: false);
return Scaffold( return Scaffold(
backgroundColor: AppColors.scaffoldBgColor, backgroundColor: AppColors.scaffoldBgColor,
body: Column( body: Column(
@ -363,6 +366,37 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
isCloseButtonVisible: true, isCloseButtonVisible: true,
); );
}, onSuccess: (apiResp) async { }, onSuccess: (apiResp) async {
// Check if booking is from symptoms checker and call the API
if (symptomsCheckerViewModel.isBookingFromSymptomsChecker) {
final appointmentNo = apiResp.data['AppointmentNo']?.toString() ?? '';
final doctorId = bookAppointmentsViewModel.selectedDoctor.doctorID?.toString() ?? '';
// Combine date and time for ISO string format
final appointmentDate = '${bookAppointmentsViewModel.selectedAppointmentDate} ${bookAppointmentsViewModel.selectedAppointmentTime}';
final mobileNumber = appState.getAuthenticatedUser()?.mobileNumber ?? '';
final fileNo = appState.getAuthenticatedUser()?.patientId?.toString() ?? '';
final projectId = bookAppointmentsViewModel.selectedDoctor.projectID ?? 0;
final clinicId = bookAppointmentsViewModel.selectedDoctor.clinicID ?? 0;
await symptomsCheckerViewModel.saveAppointmentDetailsForSymptomsChecker(
fileNo: fileNo,
appointmentNo: appointmentNo,
doctorId: doctorId,
appointmentDate: appointmentDate,
mobileNumber: mobileNumber,
projectId: projectId,
clinicId: clinicId,
onSuccess: (response) {
// Success - continue with normal flow
debugPrint("onSuccess called for saveAppointmentDetailsForSymptomsChecker: ${response.data}");
},
onError: (error) {
// Log error but don't block the user flow
debugPrint("Error saving symptoms checker appointment: $error");
},
);
}
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
await Future.delayed(Duration(milliseconds: 50)).then((value) async { await Future.delayed(Duration(milliseconds: 50)).then((value) async {
showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () { showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () {

@ -47,7 +47,9 @@ class HabibWalletCard extends StatelessWidget {
child: Stack(children: [ child: Stack(children: [
Positioned( Positioned(
right: 0, right: 0,
child: ClipRRect(borderRadius: BorderRadius.circular(24.0), child: Utils.buildSvgWithAssets(icon: AppAssets.habib_background_icon, width: 150.h, height: 150.h, applyThemeColor: false)), child: ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: Utils.buildSvgWithAssets(icon: AppAssets.habib_background_icon, width: 150.h, height: 150.h, applyThemeColor: false)),
), ),
Padding( Padding(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),

@ -120,7 +120,7 @@ class PossibleConditionsPage extends StatelessWidget {
categoryName: (condition.conditionDetails!.category!.name) ?? "Other", categoryName: (condition.conditionDetails!.category!.name) ?? "Other",
onSuccess: (value) { onSuccess: (value) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
print(symptomsCheckerViewModel.clinicDetailsList.first.clinicID); debugPrint(symptomsCheckerViewModel.clinicDetailsList.first.clinicID.toString());
initiateBookAppointmentFlow(context); initiateBookAppointmentFlow(context);
}, },
onError: (err) { onError: (err) {
@ -282,6 +282,9 @@ class PossibleConditionsPage extends StatelessWidget {
} }
initiateBookAppointmentFlow(BuildContext context) { initiateBookAppointmentFlow(BuildContext context) {
// Set flag to indicate booking is from symptoms checker
symptomsCheckerViewModel.setBookingFromSymptomsChecker(true);
// bookAppointmentsViewModel.getLocation(); // bookAppointmentsViewModel.getLocation();
bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel( bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel(
clinicID: symptomsCheckerViewModel.clinicDetailsList.first.clinicID, clinicID: symptomsCheckerViewModel.clinicDetailsList.first.clinicID,
@ -307,6 +310,10 @@ class PossibleConditionsPage extends StatelessWidget {
), ),
).onPress(() { ).onPress(() {
data.handleBackPress(); data.handleBackPress();
// Reset flag if user goes back during symptoms checker booking
if (symptomsCheckerViewModel.isBookingFromSymptomsChecker) {
symptomsCheckerViewModel.setBookingFromSymptomsChecker(false);
}
}); });
} }
} }
@ -317,10 +324,15 @@ class PossibleConditionsPage extends StatelessWidget {
regionalViewModel.flush(); regionalViewModel.flush();
regionalViewModel.setBottomSheetType(type); regionalViewModel.setBottomSheetType(type);
// AppointmentViaRegionViewmodel? viewmodel = null; // AppointmentViaRegionViewmodel? viewmodel = null;
showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer<AppointmentViaRegionViewmodel>(builder: (_, data, __) => getTitle(data, context)), isDismissible: false, showCommonBottomSheetWithoutHeight(context,
child: Consumer<AppointmentViaRegionViewmodel>(builder: (context, data, __) { title: "",
titleWidget: Consumer<AppointmentViaRegionViewmodel>(builder: (_, data, __) => getTitle(data, context)),
isDismissible: false, child: Consumer<AppointmentViaRegionViewmodel>(builder: (context, data, __) {
return getRegionalSelectionWidget(data, context); return getRegionalSelectionWidget(data, context);
}), callBackFunc: () {}); }), callBackFunc: () {
// Reset flag when bottom sheet is closed/cancelled
symptomsCheckerViewModel.setBookingFromSymptomsChecker(false);
});
} }
Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data, BuildContext context) { Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data, BuildContext context) {

@ -1,5 +1,6 @@
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/app_state.dart'; import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart';
@ -28,12 +29,20 @@ class SymptomsSelectorPage extends StatefulWidget {
class _SymptomsSelectorPageState extends State<SymptomsSelectorPage> { class _SymptomsSelectorPageState extends State<SymptomsSelectorPage> {
late DialogService dialogService; late DialogService dialogService;
late AppState _appState; late AppState _appState;
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
dialogService = getIt<DialogService>(); dialogService = getIt<DialogService>();
_appState = getIt<AppState>(); _appState = getIt<AppState>();
// Listen to search input changes
_searchController.addListener(() {
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.filterSymptoms(_searchController.text, isArabic: _appState.isArabic());
});
// Initialize symptom groups based on selected organs // Initialize symptom groups based on selected organs
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<SymptomsCheckerViewModel>(); final viewModel = context.read<SymptomsCheckerViewModel>();
@ -41,6 +50,12 @@ class _SymptomsSelectorPageState extends State<SymptomsSelectorPage> {
}); });
} }
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) { void _onNextPressed(SymptomsCheckerViewModel viewModel) {
if (viewModel.hasSelectedSymptoms) { if (viewModel.hasSelectedSymptoms) {
// Navigate to triage screen // Navigate to triage screen
@ -100,7 +115,56 @@ class _SymptomsSelectorPageState extends State<SymptomsSelectorPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 16.h), SizedBox(height: 16.h),
...viewModel.organSymptomsResults.map((organResult) { // Inline search field
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.r,
),
child: TextField(
controller: _searchController,
style: TextStyle(
fontSize: 14.f,
color: AppColors.textColor,
fontFamily: _appState.isArabic() ? 'CairoArabic' : 'Poppins',
),
decoration: InputDecoration(
hintText: LocaleKeys.search.tr(context: context),
hintStyle: TextStyle(
color: AppColors.greyTextColor,
fontSize: 14.f,
),
prefixIcon: Icon(
Icons.search,
color: AppColors.greyTextColor,
size: 20.h,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.clear,
color: AppColors.greyTextColor,
size: 20.h,
),
onPressed: () {
_searchController.clear();
viewModel.clearSymptomFilter();
},
)
: null,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
),
),
),
),
),
SizedBox(height: 16.h),
...viewModel.filteredOrganSymptomsResults.map((organResult) {
// Find matching organ ID from selected organs // Find matching organ ID from selected organs
String? organId; String? organId;
String? organName; String? organName;

@ -1,5 +1,3 @@
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_assets.dart';
@ -177,22 +175,29 @@ class _TriagePageState extends State<TriagePage> {
// Collect evidence based on question type // Collect evidence based on question type
if (viewModel.isTriageQuestionSingleSelection) { if (viewModel.isTriageQuestionSingleSelection) {
// Type 1: Single selection - only one evidence entry // Type 1: Single selection - only one evidence entry with "Yes" choice
final selectedItemId = viewModel.selectedSingleItemId; final selectedItemId = viewModel.selectedSingleItemId;
final selectedChoiceIndex = viewModel.selectedSingleChoiceIndex;
if (selectedItemId != null && selectedChoiceIndex != null) { if (selectedItemId != null) {
// Find the item and choice // Find the item and its "Yes" choice
for (var item in currentQuestion.items!) { for (var item in currentQuestion.items!) {
if (item.id == selectedItemId) { if (item.id == selectedItemId) {
if (item.choices != null && selectedChoiceIndex < item.choices!.length) { // Find the "Yes" choice (case-insensitive)
final selectedChoice = item.choices![selectedChoiceIndex]; String? yesChoiceId;
final choiceId = selectedChoice.id ?? ""; if (item.choices != null) {
for (var choice in item.choices!) {
if (choiceId.isNotEmpty) { final label = choice.label?.toLowerCase() ?? '';
viewModel.addTriageEvidence(selectedItemId, choiceId); if (label == 'yes' || label == 'نعم') {
yesChoiceId = choice.id;
break;
}
} }
} }
// If "Yes" choice found, add evidence
if (yesChoiceId != null && yesChoiceId.isNotEmpty) {
viewModel.addTriageEvidence(selectedItemId, yesChoiceId);
}
break; break;
} }
} }
@ -221,9 +226,6 @@ class _TriagePageState extends State<TriagePage> {
List<String> initialEvidenceIds = viewModel.getAllEvidenceIds(); List<String> initialEvidenceIds = viewModel.getAllEvidenceIds();
List<Map<String, String>> triageEvidence = viewModel.getTriageEvidence(); List<Map<String, String>> triageEvidence = viewModel.getTriageEvidence();
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!,
@ -400,7 +402,45 @@ class _TriagePageState extends State<TriagePage> {
(question.text ?? "").toText16(isBold: true, color: AppColors.textColor), (question.text ?? "").toText16(isBold: true, color: AppColors.textColor),
SizedBox(height: 24.h), SizedBox(height: 24.h),
// Show all items with dividers // Type 1: Show items as checkboxes only (no choices displayed)
if (viewModel.isTriageQuestionSingleSelection) ...[
...List.generate(question.items!.length, (itemIndex) {
final item = question.items![itemIndex];
final itemId = item.id ?? "";
final itemName = item.name ?? "";
final isSelected = viewModel.selectedSingleItemId == itemId;
return GestureDetector(
onTap: () => _onOptionSelectedForItem(itemId, 0), // Pass 0 as placeholder
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(5.r),
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor,
width: 1.w,
),
),
child: isSelected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
),
SizedBox(width: 12.w),
Expanded(child: itemName.toText13(isBold: true)),
],
),
),
);
}),
] else ...[
// Type 2: Show all items with their choices
...List.generate(question.items!.length, (itemIndex) { ...List.generate(question.items!.length, (itemIndex) {
final item = question.items![itemIndex]; final item = question.items![itemIndex];
final itemId = item.id ?? ""; final itemId = item.id ?? "";
@ -431,6 +471,7 @@ class _TriagePageState extends State<TriagePage> {
); );
}), }),
], ],
],
), ),
), ),
); );

Loading…
Cancel
Save