triage design

pull/113/head
faizatflutter 3 months ago
parent acb6d31ff3
commit ac84d268e9

@ -0,0 +1,209 @@
import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart';
class TriageQuestionsData {
static List<TriageQuestionModel> 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<TriageQuestionModel> 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';
}
}
}

@ -0,0 +1,85 @@
class TriageQuestionModel {
final String id;
final String question;
final List<TriageOptionModel> 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<TriageOptionModel>? 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,
);
}
}

@ -48,36 +48,33 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
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<SymptomsCheckerViewModel>(
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<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
SizedBox(height: 8.h),
Expanded(
child: Stack(
children: [
_buildBodyViewer(viewModel),
_buildViewToggleButtons(viewModel),
_buildBottomSheet(viewModel),
],
),
],
);
},
),
),
],
);
},
),
);
}

@ -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<TriageProgressScreen> createState() => _TriageProgressScreenState();
}
class _TriageProgressScreenState extends State<TriageProgressScreen> {
late List<TriageQuestionModel> 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<double> animation) {
final offsetAnimation = Tween<Offset>(
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<int>(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),
);
}
}

@ -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) {

@ -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<String, WidgetBuilder> 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()
};
}

@ -10,7 +10,7 @@ class AppTheme {
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
),
hintColor: Colors.grey[400],

@ -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,
),
)

Loading…
Cancel
Save