diff --git a/lib/features/symptoms_checker/data/triage_questions_data.dart b/lib/features/symptoms_checker/data/triage_questions_data.dart new file mode 100644 index 0000000..ffaef92 --- /dev/null +++ b/lib/features/symptoms_checker/data/triage_questions_data.dart @@ -0,0 +1,209 @@ +import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; + +class TriageQuestionsData { + static List getSampleTriageQuestions() { + return [ + // Question 1: Pain Location + TriageQuestionModel( + id: 'q1', + question: 'Where do you feel the chest pain, and where does the pain spread or move to?', + options: [ + TriageOptionModel( + id: 'q1_opt1', + text: 'It is all over', + severityScore: 3, + ), + TriageOptionModel( + id: 'q1_opt2', + text: 'It is behind the breast bone', + severityScore: 5, + ), + TriageOptionModel( + id: 'q1_opt3', + text: 'Moves or spread to the upper limb; for example the shoulder, arm, or fingers', + severityScore: 7, + ), + TriageOptionModel( + id: 'q1_opt4', + text: 'Moves or spread to the upper back; between the shoulder blades', + severityScore: 6, + ), + TriageOptionModel( + id: 'q1_opt5', + text: 'Moves or spread to the neck or jaw', + severityScore: 8, + ), + ], + ), + + // Question 2: Pain Intensity + TriageQuestionModel( + id: 'q2', + question: 'How would you describe the intensity of your chest pain?', + options: [ + TriageOptionModel( + id: 'q2_opt1', + text: 'Mild - Barely noticeable, does not interfere with daily activities', + severityScore: 2, + ), + TriageOptionModel( + id: 'q2_opt2', + text: 'Moderate - Noticeable but manageable, some interference with activities', + severityScore: 4, + ), + TriageOptionModel( + id: 'q2_opt3', + text: 'Severe - Significantly interferes with activities, difficult to ignore', + severityScore: 7, + ), + TriageOptionModel( + id: 'q2_opt4', + text: 'Very severe - Unbearable, cannot perform any activities', + severityScore: 9, + ), + ], + ), + + // Question 3: Pain Duration + TriageQuestionModel( + id: 'q3', + question: 'How long have you been experiencing this chest pain?', + options: [ + TriageOptionModel( + id: 'q3_opt1', + text: 'Less than 5 minutes', + severityScore: 3, + ), + TriageOptionModel( + id: 'q3_opt2', + text: 'Between 5 to 15 minutes', + severityScore: 5, + ), + TriageOptionModel( + id: 'q3_opt3', + text: 'Between 15 to 30 minutes', + severityScore: 7, + ), + TriageOptionModel( + id: 'q3_opt4', + text: 'More than 30 minutes', + severityScore: 8, + ), + TriageOptionModel( + id: 'q3_opt5', + text: 'Comes and goes (intermittent)', + severityScore: 4, + ), + ], + ), + + // Question 4: Associated Symptoms + TriageQuestionModel( + id: 'q4', + question: 'Are you experiencing any of these symptoms along with chest pain?', + options: [ + TriageOptionModel( + id: 'q4_opt1', + text: 'Shortness of breath or difficulty breathing', + severityScore: 8, + ), + TriageOptionModel( + id: 'q4_opt2', + text: 'Sweating, nausea, or vomiting', + severityScore: 7, + ), + TriageOptionModel( + id: 'q4_opt3', + text: 'Dizziness or lightheadedness', + severityScore: 7, + ), + TriageOptionModel( + id: 'q4_opt4', + text: 'Rapid or irregular heartbeat', + severityScore: 6, + ), + TriageOptionModel( + id: 'q4_opt5', + text: 'None of the above', + severityScore: 2, + ), + ], + ), + + // Question 5: Triggering Factors + TriageQuestionModel( + id: 'q5', + question: 'What triggers or worsens your chest pain?', + options: [ + TriageOptionModel( + id: 'q5_opt1', + text: 'Physical activity or exertion', + severityScore: 6, + ), + TriageOptionModel( + id: 'q5_opt2', + text: 'Emotional stress or anxiety', + severityScore: 4, + ), + TriageOptionModel( + id: 'q5_opt3', + text: 'Deep breathing or coughing', + severityScore: 3, + ), + TriageOptionModel( + id: 'q5_opt4', + text: 'Eating or lying down', + severityScore: 3, + ), + TriageOptionModel( + id: 'q5_opt5', + text: 'Nothing specific, pain is constant', + severityScore: 7, + ), + ], + ), + ]; + } + + /// Calculate total severity score from answered questions + static int calculateTotalScore(List questions) { + int totalScore = 0; + int answeredCount = 0; + + for (var question in questions) { + if (question.isConfirmed && question.confirmedOption != null) { + totalScore += question.confirmedOption!.severityScore ?? 0; + answeredCount++; + } + } + + // Return average score or 0 if no questions answered + return answeredCount > 0 ? (totalScore / answeredCount * 10).round() : 0; + } + + /// Get risk level based on score + static String getRiskLevel(int score) { + if (score >= 70) { + return 'High Risk - Seek immediate medical attention'; + } else if (score >= 50) { + return 'Moderate Risk - Consult a doctor soon'; + } else if (score >= 30) { + return 'Low to Moderate Risk - Monitor symptoms'; + } else { + return 'Low Risk - Self-care may be sufficient'; + } + } + + /// Get suggested condition based on score + static String getSuggestedCondition(int score) { + if (score >= 70) { + return 'Acute Coronary Syndrome'; + } else if (score >= 50) { + return 'Angina or Cardiac concern'; + } else if (score >= 30) { + return 'Non-cardiac chest pain'; + } else { + return 'Musculoskeletal chest pain'; + } + } +} diff --git a/lib/features/symptoms_checker/models/triage_question_model.dart b/lib/features/symptoms_checker/models/triage_question_model.dart new file mode 100644 index 0000000..fb8ab20 --- /dev/null +++ b/lib/features/symptoms_checker/models/triage_question_model.dart @@ -0,0 +1,85 @@ +class TriageQuestionModel { + final String id; + final String question; + final List options; + int? selectedOptionIndex; + int? confirmedOptionIndex; // Confirmed answer when user presses Next + + TriageQuestionModel({ + required this.id, + required this.question, + required this.options, + this.selectedOptionIndex, + this.confirmedOptionIndex, + }); + + bool get isAnswered => selectedOptionIndex != null; + + bool get isConfirmed => confirmedOptionIndex != null; + + void selectOption(int index) { + selectedOptionIndex = index; + } + + void confirmSelection() { + confirmedOptionIndex = selectedOptionIndex; + } + + void clearSelection() { + selectedOptionIndex = null; + } + + TriageOptionModel? get selectedOption { + if (selectedOptionIndex != null && selectedOptionIndex! < options.length) { + return options[selectedOptionIndex!]; + } + return null; + } + + TriageOptionModel? get confirmedOption { + if (confirmedOptionIndex != null && confirmedOptionIndex! < options.length) { + return options[confirmedOptionIndex!]; + } + return null; + } + + TriageQuestionModel copyWith({ + String? id, + String? question, + List? options, + int? selectedOptionIndex, + int? confirmedOptionIndex, + }) { + return TriageQuestionModel( + id: id ?? this.id, + question: question ?? this.question, + options: options ?? this.options, + selectedOptionIndex: selectedOptionIndex ?? this.selectedOptionIndex, + confirmedOptionIndex: confirmedOptionIndex ?? this.confirmedOptionIndex, + ); + } +} + +class TriageOptionModel { + final String id; + final String text; + final int? severityScore; // Optional: for calculating risk scores + + TriageOptionModel({ + required this.id, + required this.text, + this.severityScore, + }); + + TriageOptionModel copyWith({ + String? id, + String? text, + int? severityScore, + }) { + return TriageOptionModel( + id: id ?? this.id, + text: text ?? this.text, + severityScore: severityScore ?? this.severityScore, + ); + } +} diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index d945b5b..738adff 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -48,36 +48,33 @@ class _OrganSelectorPageState extends State { backgroundColor: AppColors.successColor, ), ); - context.navigateWithName(AppRoutes.possibleConditionsScreen); + context.navigateWithName(AppRoutes.triageProgressScreen); } @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => SymptomsCheckerViewModel(), - child: Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - appBar: _buildAppBar(), - body: Consumer( - builder: (context, viewModel, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(), - SizedBox(height: 8.h), - Expanded( - child: Stack( - children: [ - _buildBodyViewer(viewModel), - _buildViewToggleButtons(viewModel), - _buildBottomSheet(viewModel), - ], - ), + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + appBar: _buildAppBar(), + body: Consumer( + builder: (context, viewModel, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(), + SizedBox(height: 8.h), + Expanded( + child: Stack( + children: [ + _buildBodyViewer(viewModel), + _buildViewToggleButtons(viewModel), + _buildBottomSheet(viewModel), + ], ), - ], - ); - }, - ), + ), + ], + ); + }, ), ); } diff --git a/lib/presentation/symptoms_checker/triage_progress_screen.dart b/lib/presentation/symptoms_checker/triage_progress_screen.dart new file mode 100644 index 0000000..a3d0c1a --- /dev/null +++ b/lib/presentation/symptoms_checker/triage_progress_screen.dart @@ -0,0 +1,292 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; + +class TriageProgressScreen extends StatefulWidget { + const TriageProgressScreen({super.key}); + + @override + State createState() => _TriageProgressScreenState(); +} + +class _TriageProgressScreenState extends State { + late List triageQuestions; + int currentQuestionIndex = 0; + + @override + void initState() { + super.initState(); + triageQuestions = TriageQuestionsData.getSampleTriageQuestions(); + } + + TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex]; + + bool get isFirstQuestion => currentQuestionIndex == 0; + + bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1; + + void _onOptionSelected(int optionIndex) { + setState(() { + currentQuestion.selectOption(optionIndex); + }); + } + + void _onPreviousPressed() { + if (!isFirstQuestion) { + setState(() { + currentQuestionIndex--; + }); + } + } + + void _onNextPressed() { + if (currentQuestion.isAnswered) { + currentQuestion.confirmSelection(); + if (isLastQuestion) { + context.navigateWithName(AppRoutes.possibleConditionsScreen); + } else { + setState(() { + currentQuestionIndex++; + }); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select an option before proceeding'.needTranslation), + backgroundColor: AppColors.errorColor, + ), + ); + } + } + + _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to restart the organ selection?".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () => onConfirm(), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Triage".needTranslation, + onLeadingTapped: () => _buildConfirmationBottomSheet( + context: context, + onConfirm: () => { + context.pop(), + context.pop(), + }), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildQuestionCard(), + ], + ), + ), + ), + _buildStickyBottomCard(context), + ], + ), + ); + } + + Widget _buildQuestionCard() { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + final offsetAnimation = Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + )); + + return SlideTransition( + position: offsetAnimation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(currentQuestionIndex), + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentQuestion.question, + style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.w500, color: AppColors.textColor), + ), + SizedBox(height: 24.h), + ...List.generate(currentQuestion.options.length, (index) { + bool selected = currentQuestion.selectedOptionIndex == index; + return _buildOptionItem(index, selected, currentQuestion.options[index].text); + }), + ], + ), + ), + ); + } + + Widget _buildOptionItem(int index, bool selected, String optionText) { + return GestureDetector( + onTap: () => _onOptionSelected(index), + 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: selected ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 2.w), + ), + child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + optionText, + style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStickyBottomCard(BuildContext context) { + final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions); + final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore); + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 16.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: "Possible symptom: ".needTranslation, + style: TextStyle( + color: AppColors.greyTextColor, + fontWeight: FontWeight.w600, + fontSize: 14.f, + ), + children: [ + TextSpan( + text: suggestedCondition, + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + fontSize: 14.f, + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + CustomRoundedProgressBar( + percentage: currentScore, + paddingBetween: 5.h, + color: AppColors.primaryRedColor, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), + height: 8.h, + titleWidget: RichText( + text: TextSpan( + text: "$currentScore% ", + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w600, + fontSize: 14.f, + ), + children: [ + TextSpan( + text: "- Symptoms checker finding score".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + fontSize: 13.f, + ), + ), + ], + ), + ), + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: isFirstQuestion ? () {} : _onPreviousPressed, + isDisabled: isFirstQuestion, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation, + onPressed: _onNextPressed, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + ], + ), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0), + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart b/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart index 5f55395..d09c244 100644 --- a/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart +++ b/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart @@ -6,12 +6,14 @@ class CustomRoundedProgressBar extends StatelessWidget { final Color color; final Color backgroundColor; final double? height; + final double? paddingBetween; final Widget? titleWidget; const CustomRoundedProgressBar({ super.key, this.titleWidget, required this.percentage, + this.paddingBetween, required this.color, required this.backgroundColor, this.height, @@ -21,9 +23,13 @@ class CustomRoundedProgressBar extends StatelessWidget { Widget build(BuildContext context) { final h = height ?? 8.h; return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (titleWidget != null) ...[ titleWidget!, + if (paddingBetween != null) ...[ + SizedBox(height: paddingBetween), + ] ], LayoutBuilder( builder: (context, constraints) { diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 08686d3..0e6ec4d 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -9,6 +9,7 @@ import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_progress_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; class AppRoutes { @@ -25,6 +26,7 @@ class AppRoutes { // Symptoms Checker static const String organSelectorPage = '/organSelectorPage'; static const String possibleConditionsScreen = '/possibleConditionsScreen'; + static const String triageProgressScreen = '/triageProgressScreen'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -37,6 +39,7 @@ class AppRoutes { comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), homeHealthCarePage: (context) => HhcProceduresPage(), organSelectorPage: (context) => OrganSelectorPage(), - possibleConditionsScreen: (context) => PossibleConditionsScreen() + possibleConditionsScreen: (context) => PossibleConditionsScreen(), + triageProgressScreen: (context) => TriageProgressScreen() }; } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index f185479..c09d537 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -10,7 +10,7 @@ class AppTheme { pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder() }, ), hintColor: Colors.grey[400], diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index e124f02..919e787 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -6,6 +6,7 @@ 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/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/theme/colors.dart'; @@ -25,6 +26,7 @@ class CollapsingListView extends StatelessWidget { Widget? trailing; bool isClose; bool isLeading; + VoidCallback? onLeadingTapped; CollapsingListView({ super.key, @@ -40,6 +42,7 @@ class CollapsingListView extends StatelessWidget { this.requests, this.isLeading = true, this.trailing, + this.onLeadingTapped, }); @override @@ -65,7 +68,13 @@ class CollapsingListView extends StatelessWidget { child: IconButton( icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), padding: EdgeInsets.only(left: 12), - onPressed: () => Navigator.pop(context), + onPressed: () { + if (onLeadingTapped != null) { + onLeadingTapped!(); + } else { + context.pop(); + } + }, highlightColor: Colors.transparent, ), )