triage design
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue