feedbacks in symptoms checker (faiz_dev) #331

Merged
Haroon6138 merged 2 commits from faiz_dev_symptoms_checker into master 2 days ago

@ -1444,7 +1444,7 @@
"suggestions": "التوصيات",
"pleaseGoBackAndSelectOrgansFirst": "يرجى العودة واختيار الأعضاء أولاً",
"symptomsSelector": "محدد الأعراض",
"possibleSymptomsRelatedTo": "أعراض مرتبطة بـ",
"possibleSymptomsRelatedTo": "الأعراض المحتملة المتعلقة بأعضاء محددة",
"suggestionAIListDescription": "هذه قائمة بالأعراض المقترحة من قبل الذكاء الاصطناعي، بناءً على المعلومات التي تم جمعها حتى الآن خلال المقابلة",
"whyAmIBeingAskedThis": "لماذا يُطرح علي هذا السؤال؟",
"weAreAskingThisQuestionToBetterUnderstand": "نطرح هذا السؤال لفهم {symptoms} بشكل أفضل وتحسين دقة التقييم.",
@ -1856,6 +1856,7 @@
"watchUsOnYoutube": "شاهدنا على يوتيوب",
"connectOnLinkedin": "تواصل معنا على لينكد إن",
"riskFactorPageTitle": "اعراض تشعر بها",
"doAnyOfTheseApply": "هل ينطبق عليك أي من هذه الأمور؟",
"bloodDonationService": "التبرع \n بالدم",
"bookAppointmentService": "حجز\nموعد",
"pinchToZoom": "اضغط للتكبير",
@ -1925,6 +1926,10 @@
"refundReason6": "تم إلغاء الإجراء السريري أو تغييره من قِبل الطبيب المحيل",
"refundReason7": "تم إلغاء إجراء وحدة الخدمة / المختبر / الأشعة أو تغييره من قِبل الطبيب المحيل",
"selectProcedures": "اختر العمليات",
"beforeUsingSymptomsChecker": "قبل استخدام فاحص الأعراض، يرجى قراءة شروط الخدمة وتذكر:",
"symptomsCheckerNotDiagnosis": "• فاحص الأعراض ليس تشخيصًا. إنه للمعلومات الخاصة بك فقط وليس رأيًا طبيًا مؤهلاً.",
"symptomsCheckerNotEmergency": "• فاحص الأعراض ليس للطوارئ. اتصل برقم الطوارئ المحلي فورًا عند وجود حالة طوارئ صحية.",
"yourDataIsSafe": "• بياناتك آمنة. تضمن تدابير الخصوصية حماية المعلومات التي تشاركها.",
"submitEReferral": "تقديم الطلب",
"redeem": "استبدال",
"points": "نقاط",

@ -1436,7 +1436,7 @@
"suggestions": "Suggestions",
"pleaseGoBackAndSelectOrgansFirst": "Please go back and select organs first",
"symptomsSelector": "Symptoms Selector",
"possibleSymptomsRelatedTo": "Possible symptoms related to",
"possibleSymptomsRelatedTo": "Possible symptoms related to selected organs",
"suggestionAIListDescription": "This is a list of symptoms suggested by our AI, based on the information gathered so far during the interview",
"whyAmIBeingAskedThis": "Why am I being asked this?",
"weAreAskingThisQuestionToBetterUnderstand": "We are asking this question to better understand your {symptoms} and improve the accuracy of the assessment.",
@ -1848,6 +1848,7 @@
"watchUsOnYoutube": "Watch us on youtube",
"connectOnLinkedin": "Connect on LinkedIn",
"riskFactorPageTitle": "What do you feel?",
"doAnyOfTheseApply": "Do any of these apply to you?",
"bloodDonationService": "Blood \nDonation",
"bookAppointmentService": "Book\nAppointment",
"pinchToZoom": "Pinch to zoom",
@ -1917,6 +1918,10 @@
"refundReason6": "Clinical procedure cancelled or changed by requesting doctor",
"refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor",
"selectProcedures": "Select Procedures",
"beforeUsingSymptomsChecker": "Before using the Symptoms Checker, please read the Terms of Service and remember:",
"symptomsCheckerNotDiagnosis": "• Symptoms Checker isn't a diagnosis. It's only for your information and not a qualified medical opinion.",
"symptomsCheckerNotEmergency": "• Symptoms Checker isn't for emergencies. Call your local emergency number right away when there's a health emergency.",
"yourDataIsSafe": "• Your data is safe. Privacy measures ensure the information you share is protected.",
"redeem": "Redeem",
"points": "Points",
"transactions": "Transactions",

@ -105,20 +105,14 @@ class TriageQuestion {
factory TriageQuestion.fromJson(Map<String, dynamic> json) {
// TESTING: Add dummy instructions to every question for testing UI
final dummyInstructions = [
"Clean the thermometer using soap and cold water.",
"For rectal insertion, apply a small amount of petroleum jelly to the end of the thermometer.",
"Insert the silver tip into the anus or mouth.",
"Hold the thermometer in place for 2 minutes, or as long as recommended by the manufacturer.",
];
final dummyExplication = "The most accurate measurement is rectal temperature. You can also measure the temperature under the armpit or use contactless thermometers. However, using the contact thermometer is more accurate. Follow the thermometer manufacturer's instructions. A temperature above 37°C or 98.6°F is classified as a fever.";
return TriageQuestion(
type: json['type'],
text: json['text'],
explication: json['explication'] ?? dummyExplication, // Use dummy if null
instruction: json['instruction'] != null ? List<String>.from(json['instruction']) : dummyInstructions, // Use dummy if null
explication: json['explication'],
// Use dummy if null
instruction: json['instruction'] != null ? List<String>.from(json['instruction']) : [],
// Use dummy if null
items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null,
);
}

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names
abstract class LocaleKeys {
abstract class LocaleKeys {
static const english = 'english';
static const arabic = 'arabic';
static const login = 'login';
@ -1808,6 +1808,7 @@ abstract class LocaleKeys {
static const watchUsOnYoutube = 'watchUsOnYoutube';
static const connectOnLinkedin = 'connectOnLinkedin';
static const riskFactorPageTitle = 'riskFactorPageTitle';
static const doAnyOfTheseApply = 'doAnyOfTheseApply';
static const bloodDonationService = 'bloodDonationService';
static const bookAppointmentService = 'bookAppointmentService';
static const pinchToZoom = 'pinchToZoom';
@ -1883,5 +1884,4 @@ abstract class LocaleKeys {
static const advanceNumber = 'advanceNumber';
static const amountOnly = 'amountOnly';
static const invoiceHistory = 'invoiceHistory';
}

@ -1847,6 +1847,7 @@ abstract class LocaleKeys {
static const watchUsOnYoutube = 'watchUsOnYoutube';
static const connectOnLinkedin = 'connectOnLinkedin';
static const riskFactorPageTitle = 'riskFactorPageTitle';
static const doAnyOfTheseApply = 'doAnyOfTheseApply';
static const bloodDonationService = 'bloodDonationService';
static const bookAppointmentService = 'bookAppointmentService';
static const pinchToZoom = 'pinchToZoom';
@ -1898,6 +1899,10 @@ abstract class LocaleKeys {
static const noProceduresAvailableForRefund = 'noProceduresAvailableForRefund';
static const selectAll = 'selectAll';
static const selectRefundMethod = 'selectRefundMethod';
static const beforeUsingSymptomsChecker = 'beforeUsingSymptomsChecker';
static const symptomsCheckerNotDiagnosis = 'symptomsCheckerNotDiagnosis';
static const symptomsCheckerNotEmergency = 'symptomsCheckerNotEmergency';
static const yourDataIsSafe = 'yourDataIsSafe';
static const hmgWallet = 'hmgWallet';
static const bankTransfer = 'bankTransfer';
static const selectRefundReason = 'selectRefundReason';

@ -71,16 +71,21 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
);
return;
}
// If user is not authenticated, navigate to user consent screen
if (!_appState.isAuthenticated) {
context.navigateWithName(AppRoutes.userConsentScreen);
return;
}
// User is authenticated, proceed with API call
LoaderBottomSheet.showLoader(
loadingText: LocaleKeys.pleaseWait.tr(context: context),
);
// Get fileNo if user is logged in
String? fileNo;
if (_appState.isAuthenticated) {
final user = _appState.getAuthenticatedUser();
fileNo = user?.patientId.toString();
}
// Get fileNo from authenticated user
final user = _appState.getAuthenticatedUser();
String? fileNo = user?.patientId.toString();
await viewModel.getSymptomsUserDetails(
userName: ApiConsts.symptomsCheckerUsername,

@ -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';
@ -31,7 +29,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class PossibleConditionsPage extends StatefulWidget {
const PossibleConditionsPage({super.key});
@ -91,25 +88,81 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
}
Widget _buildLoadingShimmer() {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(16.w),
itemCount: 10,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 80.h,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Triage Level Card Shimmer
// Condition Cards Shimmer
...List.generate(5, (index) {
return Container(
width: double.infinity,
margin: EdgeInsets.only(left: 24.w, right: 24.w, bottom: 16.h),
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(10.r),
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 10.r,
offset: Offset(0, 2.h),
),
],
),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Condition name shimmer
Container(
height: 20.h,
width: 220.w,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(8.r),
),
).toShimmer2(isShow: true, radius: 8.r),
SizedBox(height: 8.h),
Container(
height: 20.h,
width: 200.w,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(8.r),
),
).toShimmer2(isShow: true, radius: 8.r),
SizedBox(height: 8.h),
// Probability percentage shimmer
Container(
height: 16.h,
width: 100.w,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(8.r),
),
).toShimmer2(isShow: true, radius: 8.r),
SizedBox(height: 30.h),
// Action buttons shimmer
Row(
children: [
Expanded(
child: Container(
height: 40.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
),
).toShimmer2(isShow: true, radius: 12.r),
),
],
),
],
),
);
}),
],
);
}
@ -323,7 +376,10 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
trailing: _buildTrailingSection(context),
child: Consumer<SymptomsCheckerViewModel>(
builder: (context, symptomsCheckerViewModel, child) {
if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isTriageDiagnosisLoading) {
// Show shimmer while loading triage level or conditions
if (symptomsCheckerViewModel.isPossibleConditionsLoading ||
symptomsCheckerViewModel.isTriageDiagnosisLoading ||
symptomsCheckerViewModel.isTriageLevelLoading) {
return _buildLoadingShimmer();
}
// Get conditions directly from ViewModel
@ -350,9 +406,7 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
// Show error if no clinic details available
final dialogService = getIt<DialogService>();
dialogService.showErrorBottomSheet(
message: appState.isArabic()
? "لا توجد تفاصيل عيادة متاحة"
: "No clinic details available",
message: appState.isArabic() ? "لا توجد تفاصيل عيادة متاحة" : "No clinic details available",
);
return;
}
@ -451,13 +505,12 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
var id = "";
if (data.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) {
// Check if hospitalList is not empty before accessing first
if (regionalViewModel.selectedHospital?.hospitalList != null &&
regionalViewModel.selectedHospital!.hospitalList.isNotEmpty) {
if (regionalViewModel.selectedHospital?.hospitalList != null && regionalViewModel.selectedHospital!.hospitalList.isNotEmpty) {
id = regionalViewModel.selectedHospital!.hospitalList.first.iD?.toString() ?? "";
}
} else {
// Check if patientDoctorAppointmentList is not empty before accessing first
if (regionalViewModel.selectedHospital?.patientDoctorAppointmentList != null &&
if (regionalViewModel.selectedHospital?.patientDoctorAppointmentList != null &&
regionalViewModel.selectedHospital!.patientDoctorAppointmentList!.isNotEmpty) {
id = regionalViewModel.selectedHospital!.patientDoctorAppointmentList!.first.projectID?.toString() ?? "";
}

@ -1,5 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
@ -121,12 +120,11 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
child: RichText(
text: TextSpan(
style: TextStyle(
height: 1.3,
fontSize: 13.f,
height: 1.3,
fontSize: 13.f,
fontWeight: FontWeight.w600,
color: AppColors.greyInfoTextColor,
fontFamily: isArabic ? 'CairoArabic' : 'Poppins'
),
color: AppColors.greyInfoTextColor,
fontFamily: isArabic ? 'CairoArabic' : 'Poppins'),
children: [
TextSpan(
text: LocaleKeys.aboveYouSeeCommonRiskFactors.tr(context: context),
@ -223,7 +221,7 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.riskFactorPageTitle.tr(context: context),
title: LocaleKeys.doAnyOfTheseApply.tr(context: context),
leadingCallback: () => context.pop(),
child: viewModel.isRiskFactorsLoading
? _buildLoadingShimmer()

@ -200,7 +200,7 @@ class _SuggestionsScreenState extends State<SuggestionsScreen> {
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.symptomsCheckSuggestions.tr() ,
title: LocaleKeys.riskFactorPageTitle.tr() ,
leadingCallback: () => context.pop(),
child: viewModel.isSuggestionsLoading
? _buildLoadingShimmer()

@ -225,7 +225,7 @@ class _SymptomsSelectorPageState extends State<SymptomsSelectorPage> {
children: [
Expanded(
child: Text(
'${LocaleKeys.possibleSymptomsRelatedTo.tr(context: context)} "$organName"',
LocaleKeys.possibleSymptomsRelatedTo.tr(context: context),
style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
),
),

@ -82,7 +82,7 @@ class _TriagePageState extends State<TriagePage> {
// Case 3: Probability >= 70% OR
// Case 4: 30 or more questions answered
// if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) {
if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30 || viewModel.shouldStopTriage) {
if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 4 || viewModel.shouldStopTriage) {
// Navigate to results/possible conditions screen
context.navigateWithName(AppRoutes.possibleConditionsPage);
return;
@ -346,7 +346,7 @@ class _TriagePageState extends State<TriagePage> {
final hasInstructions = instructions.isNotEmpty;
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.howTo.tr(context: context),
title: LocaleKeys.information.tr(context: context),
context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

@ -8,16 +8,14 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:provider/provider.dart';
// Before using the checkup, please read the Terms of Service and remember:
//
// Checkup isn't a diagnosis. It's only for your information and not a qualified medical opinion.
// Checkup isn't for emergencies. Call your local emergency number right away when there's a health emergency.
// Your data is safe. Privacy measures ensure the information you share is protected.
class UserConsentScreen extends StatefulWidget {
const UserConsentScreen({super.key});
@ -43,6 +41,35 @@ class _UserConsentScreenState extends State<UserConsentScreen> {
bool get _canProceed => _isTermsAccepted && _isPrivacyAccepted;
void _onNextPressed() async {
final viewModel = context.read<SymptomsCheckerViewModel>();
final appState = getIt.get<AppState>();
LoaderBottomSheet.showLoader(
loadingText: LocaleKeys.pleaseWait.tr(context: context),
);
// For non-authenticated users, fileNo is null
String? fileNo;
if (appState.isAuthenticated) {
final user = appState.getAuthenticatedUser();
fileNo = user?.patientId.toString();
}
await viewModel.getSymptomsUserDetails(
userName: ApiConsts.symptomsCheckerUsername,
password: ApiConsts.symptomsCheckerPassword,
fileNo: fileNo,
onSuccess: () {
LoaderBottomSheet.hideLoader();
context.navigateWithName(AppRoutes.symptomsSelectorPage);
},
onError: (String error) {
LoaderBottomSheet.hideLoader();
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -55,14 +82,14 @@ class _UserConsentScreenState extends State<UserConsentScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Before using the checkup, please read the Terms of Service and remember:".toText14(),
SizedBox(height: 44.h),
"• Checkup isn't a diagnosis. It's only for your information and not a qualified medical opinion.".toText14(),
LocaleKeys.beforeUsingSymptomsChecker.tr(context: context).toText14(),
SizedBox(height: 30.h),
LocaleKeys.symptomsCheckerNotDiagnosis.tr(context: context).toText14(),
SizedBox(height: 12.h),
"• Checkup isn't for emergencies. Call your local emergency number right away when there's a health emergency.".toText14(),
LocaleKeys.symptomsCheckerNotEmergency.tr(context: context).toText14(),
SizedBox(height: 12.h),
"• Your data is safe. Privacy measures ensure the information you share is protected.".toText14(),
SizedBox(height: 24.h),
LocaleKeys.yourDataIsSafe.tr(context: context).toText14(),
SizedBox(height: 30.h),
GestureDetector(
onTap: _onTermsAndConditionsAgreed,
child: Container(
@ -110,7 +137,7 @@ class _UserConsentScreenState extends State<UserConsentScreen> {
style: TextStyle(
fontSize: 16.f,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: AppColors.primaryRedColor,
decoration: TextDecoration.underline,
decorationColor: AppColors.primaryRedColor,
@ -176,8 +203,9 @@ class _UserConsentScreenState extends State<UserConsentScreen> {
style: TextStyle(
fontSize: 16.f,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: AppColors.primaryRedColor,
decoration: TextDecoration.underline,
decorationColor: AppColors.primaryRedColor,
),
),
@ -213,7 +241,7 @@ class _UserConsentScreenState extends State<UserConsentScreen> {
Expanded(
child: CustomButton(
text: LocaleKeys.next.tr(context: context),
onPressed: _canProceed ? () => context.navigateWithName(AppRoutes.userInfoSelection) : null,
onPressed: _canProceed ? _onNextPressed : null,
isDisabled: !_canProceed,
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,

@ -71,8 +71,8 @@ dependencies:
firebase_analytics: ^11.5.1
firebase_crashlytics: ^4.3.8
jiffy: ^6.4.3
# hijri_gregorian_calendar:
# path: /Users/aamir/StudioProjects/hijri_gregorian_calendar
# hijri_gregorian_calendar:
# path: /Users/aamir/StudioProjects/hijri_gregorian_calendar
sms_otp_auto_verify: ^2.2.0
web: any
flutter_staggered_animations: ^1.1.1
@ -89,7 +89,7 @@ dependencies:
path: package/flutter_swiper_view
flutter_callkit_incoming: ^3.0.0
in_app_update: ^4.2.5
# device_preview: ^1.3.1
# device_preview: ^1.3.1
image_picker_android: ^0.8.13+16
image_picker_platform_interface: ^2.10.0

Loading…
Cancel
Save