From 03ce3b617d4265261924b7396750197232d09a43 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Tue, 28 Apr 2026 17:06:24 +0300 Subject: [PATCH] Completed the SymptomsChecker Changes --- lib/core/api_consts.dart | 1 + lib/core/utils/utils.dart | 65 ++--- .../schedule_appointment_request_model.dart | 48 ++++ .../schedule_appointment_response_model.dart | 32 +++ .../symptoms_checker_repo.dart | 65 ++++- .../symptoms_checker_view_model.dart | 164 +++++++++++- .../appointment_details_page.dart | 5 +- .../authentication/saved_login_screen.dart | 242 +++++++++--------- .../immediate_livecare_payment_details.dart | 72 ++++-- .../review_appointment_page.dart | 34 +++ .../home/widgets/habib_wallet_card.dart | 42 +-- .../possible_conditions_screen.dart | 20 +- .../symptoms_selector_screen.dart | 66 ++++- .../symptoms_checker/triage_screen.dart | 83 ++++-- 14 files changed, 712 insertions(+), 227 deletions(-) create mode 100644 lib/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart create mode 100644 lib/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index db4d6a8c..c01b298a 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -212,6 +212,7 @@ class ApiConsts { static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis'; static final String explain = '$symptomsCheckerApi/ExplainDiagnosisResult'; static final String getClinicFromCondition = '$symptomsCheckerApi/GetClinicsByCondition?condition='; + static final String scheduleAppointment = '$symptomsCheckerApi/ScheduleAppointment'; //E-REFERRAL SERVICES static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes"; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 615641b5..1cf8094d 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -61,7 +61,8 @@ class Utils { "ProjectOutSA": false, "UsingInDoctorApp": false, "IsHMC": false - },{ + }, + { "Desciption": "Jeddah Fayhaa Hospital", "DesciptionN": "مستشفى جدة الفيحاء", "ID": 3, // Campus ID @@ -153,10 +154,10 @@ class Utils { static String getDayMonthYearDateFormatted(DateTime? dateTime) { if (dateTime == null) return ""; return - // appState.isArabic() - // ? "${dateTime.day.toString()} ${getMonthArabic(dateTime.month)}, ${dateTime.year.toString()}" - // : - "${dateTime.day.toString()} ${getMonth(dateTime.month)}, ${dateTime.year.toString()}"; + // appState.isArabic() + // ? "${dateTime.day.toString()} ${getMonthArabic(dateTime.month)}, ${dateTime.year.toString()}" + // : + "${dateTime.day.toString()} ${getMonth(dateTime.month)}, ${dateTime.year.toString()}"; } /// get month by @@ -539,26 +540,26 @@ class Utils { ), ], ) - : showOkButton? - Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.ok.tr(), - onPressed: () async { - if (onConfirmTap != null) { - onConfirmTap(); - } - }, - backgroundColor: AppColors.bgGreenColor, - borderColor: AppColors.bgGreenColor, - textColor: Colors.white, - // icon: AppAssets.confirm, - ), - ), - ], - ) - :SizedBox.shrink(), + : showOkButton + ? Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.ok.tr(), + onPressed: () async { + if (onConfirmTap != null) { + onConfirmTap(); + } + }, + backgroundColor: AppColors.bgGreenColor, + borderColor: AppColors.bgGreenColor, + textColor: Colors.white, + // icon: AppAssets.confirm, + ), + ), + ], + ) + : SizedBox.shrink(), ], ).center; } @@ -833,12 +834,16 @@ class Utils { final iconH = height ?? 24.h; final iconW = width ?? 24.w; return Container( - width: iconW, height: iconH, + width: iconW, + height: iconH, decoration: BoxDecoration( border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : 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() { return Row( + spacing: 6.w, mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 5.w, children: [ Image.asset(AppAssets.mada, width: 35.h, height: 35.h), Image.asset( @@ -1025,7 +1029,6 @@ class Utils { isHMC: hospital.isHMC); } - static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) { if (item == null) return null; return HospitalsModel( diff --git a/lib/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart b/lib/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart new file mode 100644 index 00000000..7d8f9a25 --- /dev/null +++ b/lib/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart @@ -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 toJson() { + return { + 'generalId': generalId, + 'fileNo': fileNo, + 'appointmentNo': appointmentNo, + 'doctorId': doctorId, + 'appointmentDate': appointmentDate, + 'mobileNumber': mobileNumber, + 'projectId': projectId, + 'clinicId': clinicId, + }; + } + + factory ScheduleAppointmentRequestModel.fromJson(Map 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, + ); + } +} + diff --git a/lib/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart b/lib/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart new file mode 100644 index 00000000..8511e18a --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart @@ -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 json) { + return ScheduleAppointmentResponseModel( + success: json['success'], + message: json['message'], + appointmentId: json['appointmentId'], + data: json['data'], + ); + } + + Map toJson() { + return { + 'success': success, + 'message': message, + 'appointmentId': appointmentId, + 'data': data, + }; + } +} + diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index f8282418..824a01c4 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -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/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/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/schedule_appointment_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -59,6 +61,11 @@ abstract class SymptomsCheckerRepo { required String language, required String userSessionToken, }); + + Future>> saveAppointmentDetailsForSymptomsChecker({ + required ScheduleAppointmentRequestModel request, + required String userSessionToken, + }); } class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { @@ -419,7 +426,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { 'Content-Type': 'application/json', 'Authorization': 'Bearer $userSessionToken', }; - Map body = {}; try { GenericApiModel>? apiResponse; @@ -466,4 +472,61 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> saveAppointmentDetailsForSymptomsChecker({ + required ScheduleAppointmentRequestModel request, + required String userSessionToken, + }) async { + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + final body = request.toJson(); + + try { + GenericApiModel? 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 responseData = response is String ? jsonDecode(response) : response; + + ScheduleAppointmentResponseModel scheduleAppointmentResponse = ScheduleAppointmentResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + 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())); + } + } } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index 4aba984d..b6154f35 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -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/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/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/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/schedule_appointment_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.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 isSuggestionsLoading = 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 SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel; @@ -89,6 +95,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Selected symptoms tracking (organId -> Set of symptom IDs) final Map> _selectedSymptomsByOrgan = {}; + // Symptom search/filter state + String _symptomSearchQuery = ''; + List _filteredOrganSymptomsResults = []; + // User Info Flow State int _userInfoCurrentPage = 0; 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) 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) { - return _selectedSingleItemId == itemId && _selectedSingleChoiceIndex == choiceIndex; + return _selectedSingleItemId == itemId; } /// Get selected item ID for type=1 questions @@ -191,6 +201,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Get selected choice index for type=1 questions 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 bool get areAllTriageItemsAnswered { if (currentTriageQuestion?.items == null || currentTriageQuestion!.items!.isEmpty) { @@ -234,6 +250,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier { return bodySymptomResponse!.dataDetails!.result ?? []; } + /// Get filtered organ symptoms results based on search query + List 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 get allSymptoms { + List symptoms = []; + for (var organResult in organSymptomsResults) { + if (organResult.bodySymptoms != null) { + symptoms.addAll(organResult.bodySymptoms!); + } + } + return symptoms; + } + int get totalSelectedSymptomsCount { return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); } @@ -454,6 +492,51 @@ class SymptomsCheckerViewModel extends ChangeNotifier { 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 /// Toggle risk factor selection @@ -873,16 +956,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Select a choice for a specific item (for multi-item questions) 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 same option clicked again, deselect it - if (_selectedSingleItemId == itemId && _selectedSingleChoiceIndex == choiceIndex) { + // If same item clicked again, deselect it + if (_selectedSingleItemId == itemId) { _selectedSingleItemId = null; _selectedSingleChoiceIndex = null; } else { - // Select new option, clear previous selection + // Select new item, clear previous selection _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 { // Type 2: Multi-item selection mode - each item can have one selected option @@ -932,15 +1016,20 @@ class SymptomsCheckerViewModel extends ChangeNotifier { _selectedTriageChoicesByItemId.clear(); _triageQuestionCount = 0; // Reset question count _currentZoomScale = 1.0; // Reset zoom scale + _symptomSearchQuery = ''; // Reset search query + _filteredOrganSymptomsResults.clear(); // Clear filtered results bodySymptomResponse = null; riskFactorsResponse = null; suggestionsResponse = null; triageDataDetails = null; isTriageDiagnosisLoading = false; _selectedTriageChoiceIndex = null; + _selectedSingleItemId = null; + _selectedSingleChoiceIndex = null; _isBottomSheetExpanded = false; _tooltipTimer?.cancel(); _tooltipOrganId = null; + isBookingFromSymptomsChecker = false; // Reset booking flag // Reset user info flow _userInfoCurrentPage = 0; _isSinglePageEditMode = false; @@ -1179,6 +1268,67 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); } + /// Schedule appointment for symptoms checker + Future 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 void dispose() { _tooltipTimer?.cancel(); diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 7b63d87f..76790af7 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -920,10 +920,7 @@ class _AppointmentDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: 200.h, - child: Utils.getPaymentMethods(), - ), + Utils.getPaymentMethods(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 6d326939..10a69711 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.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/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; -import 'dart:ui' as ui; class SavedLogin extends StatefulWidget { const SavedLogin({super.key}); @@ -33,6 +34,7 @@ class _SavedLogin extends State { late AuthenticationViewModel authVm; late AppState appState; bool? isOther; + @override void initState() { authVm = context.read(); @@ -90,11 +92,11 @@ class _SavedLogin extends State { : SizedBox(), SizedBox(height: 24.h), Container( - padding: EdgeInsets.all(16.h), - 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), - ]), + 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), + ]), child: Column( children: [ // Last login info - show WhatsApp only if isOther AND loginType is SMS @@ -105,7 +107,9 @@ class _SavedLogin extends State { textDirection: ui.TextDirection.ltr, child: appState.getSelectDeviceByImeiRespModelElement != 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) : SizedBox(), @@ -115,10 +119,14 @@ class _SavedLogin extends State { ? Container( margin: EdgeInsets.all(16.h), 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, 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(), // Main login button - for isOther with SMS, show WhatsApp, otherwise keep original login type CustomButton( @@ -126,7 +134,6 @@ class _SavedLogin extends State { ? "${LocaleKeys.loginBy.tr()} ${LoginTypeEnum.whatsapp.displayName}" : "${LocaleKeys.loginBy.tr()} ${loginType.displayName}", onPressed: () { - if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { authVm.loginWithFingerPrintFace(() {}); } else { @@ -147,7 +154,8 @@ class _SavedLogin extends State { height: 40.h, padding: EdgeInsets.symmetric(vertical: 10.h), 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,120 +167,124 @@ class _SavedLogin extends State { padding: EdgeInsets.symmetric(horizontal: 16.w), child: Text( 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), // OTP login button loginType.toInt != 1 - ? Column( - children: [ - loginType.toInt != 1 - ? CustomButton( - text: LocaleKeys.loginByOTP.tr(), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: false, - useSafeArea: true, - backgroundColor: Colors.transparent, - enableDrag: false, - // Prevent dragging to avoid focus conflicts - builder: (bottomSheetContext) => - StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { - return Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom), - child: SingleChildScrollView( - child: GenericBottomSheet( - countryCode: "966", - initialPhoneNumber: "", - textController: TextEditingController(), - isFromSavedLogin: true, - isEnableCountryDropdown: true, - onCountryChange: (value) {}, - onChange: (String? value) {}, - buttons: [ - Padding( - padding: EdgeInsets.only(bottom: 10.h), - child: CustomButton( - text: LocaleKeys.sendOTPSMS.tr(), + ? Column( + children: [ + loginType.toInt != 1 + ? CustomButton( + text: LocaleKeys.loginByOTP.tr(), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + useSafeArea: true, + backgroundColor: Colors.transparent, + enableDrag: false, + // Prevent dragging to avoid focus conflicts + builder: (bottomSheetContext) => + StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom), + child: SingleChildScrollView( + child: GenericBottomSheet( + countryCode: "966", + initialPhoneNumber: "", + textController: TextEditingController(), + isFromSavedLogin: true, + isEnableCountryDropdown: true, + onCountryChange: (value) {}, + onChange: (String? value) {}, + buttons: [ + Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPSMS.tr(), + onPressed: () { + Navigator.of(context).pop(); + loginType = LoginTypeEnum.sms; + authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + icon: AppAssets.sms), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.h), + child: (LocaleKeys.oR.tr()).toText16(color: AppColors.textColor)), + ], + ), + Padding( + padding: EdgeInsets.only(bottom: 10.h, top: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPWHATSAPP.tr(), onPressed: () { Navigator.of(context).pop(); - loginType = LoginTypeEnum.sms; - authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); + loginType = LoginTypeEnum.whatsapp; + authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: Colors.white, - icon: AppAssets.sms), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 8.h), - child: (LocaleKeys.oR.tr()).toText16(color: AppColors.textColor)), - ], - ), - Padding( - padding: EdgeInsets.only(bottom: 10.h, top: 10.h), - child: CustomButton( - text: LocaleKeys.sendOTPWHATSAPP.tr(), - onPressed: () { - Navigator.of(context).pop(); - loginType = LoginTypeEnum.whatsapp; - authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); - }, - backgroundColor: AppColors.transparent, - borderColor: AppColors.textColor, - textColor: AppColors.textColor, - icon: AppAssets.whatsapp, - iconColor: null, - applyThemeColor: false, + backgroundColor: AppColors.transparent, + borderColor: AppColors.textColor, + textColor: AppColors.textColor, + icon: AppAssets.whatsapp, + iconColor: null, + applyThemeColor: false, + ), ), - ), - ], + ], + ), ), - ), - ); - }), - ); - }, - backgroundColor: AppColors.whiteColor, - borderColor: AppColors.borderOnlyColor, - textColor: AppColors.textColor, - borderWidth: 2, - padding: EdgeInsets.fromLTRB(0, 14.h, 0, 14.h), - icon: AppAssets.sms, - iconColor: AppColors.textColor, - ) - : Container(), - SizedBox( - height: 20.h, - ), - ], - ) - : CustomButton( - text: "${LocaleKeys.loginBy.tr()} ${LoginTypeEnum.whatsapp.displayName}", - icon: AppAssets.whatsapp, - iconColor: null, - onPressed: () { - if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { - authVm.loginWithFingerPrintFace(() {}); - } else { - loginType = LoginTypeEnum.whatsapp; - authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); - } - }, - backgroundColor: AppColors.whiteColor, - borderColor: AppColors.textColor, - textColor: AppColors.textColor, - borderWidth: 2.w, - padding: EdgeInsets.fromLTRB(0, 14.h, 0, 14.h), - applyThemeColor: false, - ), + ); + }), + ); + }, + height: isFoldable ? 50.h : 40.h, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.borderOnlyColor, + textColor: AppColors.textColor, + borderWidth: 2, + padding: EdgeInsets.fromLTRB(0, 14.h, 0, 14.h), + icon: AppAssets.sms, + iconColor: AppColors.textColor, + ) + : Container(), + SizedBox( + height: 20.h, + ), + ], + ) + : CustomButton( + text: "${LocaleKeys.loginBy.tr()} ${LoginTypeEnum.whatsapp.displayName}", + icon: AppAssets.whatsapp, + iconColor: null, + onPressed: () { + if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { + authVm.loginWithFingerPrintFace(() {}); + } else { + loginType = LoginTypeEnum.whatsapp; + authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); + } + }, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.textColor, + textColor: AppColors.textColor, + borderWidth: 2.w, + padding: EdgeInsets.fromLTRB(0, 14.h, 0, 14.h), + applyThemeColor: false, + ), ], const Spacer(flex: 2), @@ -294,7 +306,7 @@ class _SavedLogin extends State { CustomPageRoute( page: LandingNavigation(), ), - (r) => false); + (r) => false); // Navigator.of(context).pushAndRemoveUntil( // MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()) // ); diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index 528f262f..2dcdb42a 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -75,7 +75,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, 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), Wrap( direction: Axis.horizontal, @@ -83,13 +84,16 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( - richText: Row( - children: [ - "${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isEnglishOnly: true), - 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)}"), + richText: Row( + children: [ + "${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isEnglishOnly: true), + 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)}"), ], ), ], @@ -115,7 +119,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ AppCustomChipWidget( labelText: - "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceName) ?? ""}"), + "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceName) ?? ""}"), SizedBox(height: 16.h), 1.divider, SizedBox(height: 16.h), @@ -124,7 +128,12 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ Row( 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), getLiveCareType(context, immediateLiveCareVM.liveCareSelectedCallType).toText16(isBold: true), ], @@ -136,7 +145,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { ), ), ).onPress(() { - showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(immediateLiveCareViewModel: immediateLiveCareVM), callBackFunc: () async { + showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(immediateLiveCareViewModel: immediateLiveCareVM), + callBackFunc: () async { debugPrint("Selected Call Type: ${immediateLiveCareVM.liveCareSelectedCallType}"); }, title: LocaleKeys.selectLiveCareCallType.tr(context: context), isCloseButtonVisible: true, isFullScreen: false); }); @@ -169,11 +179,15 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, 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( text: LocaleKeys.updateInsurance.tr(context: context), onPressed: () { - Navigator.of(context).push( + Navigator.of(context) + .push( CustomPageRoute( page: InsuranceHomePage(), ), @@ -214,7 +228,10 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ 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" || (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال"), ], @@ -224,7 +241,10 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ LocaleKeys.vat15.tr(context: context).toText14(isBold: true, color: AppColors.greyTextColor), 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" || (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), ], @@ -233,13 +253,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox(width: 200.h, child: Utils.getPaymentMethods()), - Utils.getPaymentAmountWithSymbol((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total ?? "0.0").toText24(isBold: true, isEnglishOnly: true), AppColors.blackColor, 17, + Utils.getPaymentMethods(), + Utils.getPaymentAmountWithSymbol( + (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total ?? "0.0").toText24(isBold: true, isEnglishOnly: true), + AppColors.blackColor, + 17, isSaudiCurrency: ((immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "sar").toLowerCase() == "sar" || (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), ], ).paddingSymmetrical(24.h, 0.h), - (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") + (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || + immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") // (true) ? CustomButton( text: LocaleKeys.confirmLiveCare.tr(context: context), @@ -248,7 +272,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { if (val) { 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(); LoaderBottomSheet.hideLoader(); if (immediateLiveCareVM.patientHasPendingLiveCareRequest) { @@ -296,7 +321,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { borderColor: AppColors.successColor, textColor: AppColors.whiteColor, fontSize: 16, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w600, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, @@ -339,7 +364,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { borderColor: AppColors.infoColor, textColor: AppColors.whiteColor, fontSize: 16, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w600, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, @@ -425,8 +450,9 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { final newlyPermanent = missing.where((p) => (newStatuses[p]?.isPermanentlyDenied ?? false) || (newStatuses[p]?.isRestricted ?? false)).toList(); if (newlyPermanent.isNotEmpty) { final names = newlyPermanent.map((p) => LiveCarePermissionService.instance.friendlyName(p)).join(' and '); - final message = - 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.'; + final message = 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.'; await LiveCarePermissionService.instance.showOpenSettingsDialog( context, title: "Permissions Required", diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 86702dbb..a8954548 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -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/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/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; @@ -37,6 +38,7 @@ class _ReviewAppointmentPageState extends State { late BookAppointmentsViewModel bookAppointmentsViewModel; late AuthenticationViewModel authVM; late MyAppointmentsViewModel myAppointmentsViewModel; + late SymptomsCheckerViewModel symptomsCheckerViewModel; @override Widget build(BuildContext context) { @@ -44,6 +46,7 @@ class _ReviewAppointmentPageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); authVM = Provider.of(context, listen: false); appState = getIt.get(); + symptomsCheckerViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.scaffoldBgColor, body: Column( @@ -363,6 +366,37 @@ class _ReviewAppointmentPageState extends State { isCloseButtonVisible: true, ); }, 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(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () { diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 88c2f2b1..5e4b4dfe 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -47,7 +47,9 @@ class HabibWalletCard extends StatelessWidget { child: Stack(children: [ Positioned( 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: EdgeInsets.all(16.h), @@ -57,25 +59,25 @@ class HabibWalletCard extends StatelessWidget { // Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ - LocaleKeys.habibWallet.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), - // Container( - // height: 40.h, - // width: 40.h, - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.textColor, - // borderRadius: 8.h, - // ), - // child: Padding( - // padding: EdgeInsets.all(8.h), - // child: Utils.buildSvgWithAssets( - // icon: AppAssets.show_icon, - // width: 12.h, - // height: 12.h, - // fit: BoxFit.contain, - // ), - // ), - // ), - // ], + LocaleKeys.habibWallet.tr(context: context).toText16(isBold: true, letterSpacing: -0.2), + // Container( + // height: 40.h, + // width: 40.h, + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + // color: AppColors.textColor, + // borderRadius: 8.h, + // ), + // child: Padding( + // padding: EdgeInsets.all(8.h), + // child: Utils.buildSvgWithAssets( + // icon: AppAssets.show_icon, + // width: 12.h, + // height: 12.h, + // fit: BoxFit.contain, + // ), + // ), + // ), + // ], // ), SizedBox(height: 4.h), Column( diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 01f0dc39..6cb1ccd1 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -120,7 +120,7 @@ class PossibleConditionsPage extends StatelessWidget { categoryName: (condition.conditionDetails!.category!.name) ?? "Other", onSuccess: (value) { LoaderBottomSheet.hideLoader(); - print(symptomsCheckerViewModel.clinicDetailsList.first.clinicID); + debugPrint(symptomsCheckerViewModel.clinicDetailsList.first.clinicID.toString()); initiateBookAppointmentFlow(context); }, onError: (err) { @@ -282,6 +282,9 @@ class PossibleConditionsPage extends StatelessWidget { } initiateBookAppointmentFlow(BuildContext context) { + // Set flag to indicate booking is from symptoms checker + symptomsCheckerViewModel.setBookingFromSymptomsChecker(true); + // bookAppointmentsViewModel.getLocation(); bookAppointmentsViewModel.setSelectedClinic(GetClinicsListResponseModel( clinicID: symptomsCheckerViewModel.clinicDetailsList.first.clinicID, @@ -307,6 +310,10 @@ class PossibleConditionsPage extends StatelessWidget { ), ).onPress(() { 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.setBottomSheetType(type); // AppointmentViaRegionViewmodel? viewmodel = null; - showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data, context)), isDismissible: false, - child: Consumer(builder: (context, data, __) { + showCommonBottomSheetWithoutHeight(context, + title: "", + titleWidget: Consumer(builder: (_, data, __) => getTitle(data, context)), + isDismissible: false, child: Consumer(builder: (context, data, __) { return getRegionalSelectionWidget(data, context); - }), callBackFunc: () {}); + }), callBackFunc: () { + // Reset flag when bottom sheet is closed/cancelled + symptomsCheckerViewModel.setBookingFromSymptomsChecker(false); + }); } Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data, BuildContext context) { diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index d7fbf63c..9f9ac86e 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.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_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -28,12 +29,20 @@ class SymptomsSelectorPage extends StatefulWidget { class _SymptomsSelectorPageState extends State { late DialogService dialogService; late AppState _appState; + final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); dialogService = getIt(); _appState = getIt(); + + // Listen to search input changes + _searchController.addListener(() { + final viewModel = context.read(); + viewModel.filterSymptoms(_searchController.text, isArabic: _appState.isArabic()); + }); + // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -41,6 +50,12 @@ class _SymptomsSelectorPageState extends State { }); } + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedSymptoms) { // Navigate to triage screen @@ -100,7 +115,56 @@ class _SymptomsSelectorPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ 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 String? organId; String? organName; diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index a94222b6..442b9691 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -177,22 +175,29 @@ class _TriagePageState extends State { // Collect evidence based on question type 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 selectedChoiceIndex = viewModel.selectedSingleChoiceIndex; - if (selectedItemId != null && selectedChoiceIndex != null) { - // Find the item and choice + if (selectedItemId != null) { + // Find the item and its "Yes" choice for (var item in currentQuestion.items!) { if (item.id == selectedItemId) { - if (item.choices != null && selectedChoiceIndex < item.choices!.length) { - final selectedChoice = item.choices![selectedChoiceIndex]; - final choiceId = selectedChoice.id ?? ""; - - if (choiceId.isNotEmpty) { - viewModel.addTriageEvidence(selectedItemId, choiceId); + // Find the "Yes" choice (case-insensitive) + String? yesChoiceId; + if (item.choices != null) { + for (var choice in item.choices!) { + final label = choice.label?.toLowerCase() ?? ''; + if (label == 'yes' || label == 'نعم') { + yesChoiceId = choice.id; + break; + } } } + + // If "Yes" choice found, add evidence + if (yesChoiceId != null && yesChoiceId.isNotEmpty) { + viewModel.addTriageEvidence(selectedItemId, yesChoiceId); + } break; } } @@ -221,9 +226,6 @@ class _TriagePageState extends State { List initialEvidenceIds = viewModel.getAllEvidenceIds(); List> triageEvidence = viewModel.getTriageEvidence(); - log("initialEvidenceIds: ${initialEvidenceIds.toString()}"); - log("triageEvidence: ${triageEvidence.toString()}"); - // Call API with updated evidence viewModel.getDiagnosisForTriage( age: viewModel.selectedAge!, @@ -400,8 +402,46 @@ class _TriagePageState extends State { (question.text ?? "").toText16(isBold: true, color: AppColors.textColor), SizedBox(height: 24.h), - // Show all items with dividers - ...List.generate(question.items!.length, (itemIndex) { + // 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) { final item = question.items![itemIndex]; final itemId = item.id ?? ""; final choices = item.choices ?? []; @@ -427,9 +467,10 @@ class _TriagePageState extends State { Divider(color: AppColors.bottomNAVBorder, thickness: 1), SizedBox(height: 10.h), ], - ], - ); - }), + ], + ); + }), + ], ], ), ), @@ -564,7 +605,7 @@ class _TriagePageState extends State { ), ), ], - ), + ), ], ), SizedBox(height: 24.h),