|
|
|
|
@ -2,6 +2,7 @@ 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';
|
|
|
|
|
import 'package:hmg_patient_app_new/core/app_export.dart';
|
|
|
|
|
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
|
|
|
|
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
|
|
|
|
@ -17,6 +18,7 @@ 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';
|
|
|
|
|
import 'package:lottie/lottie.dart';
|
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
|
|
|
|
|
|
class TriageScreen extends StatefulWidget {
|
|
|
|
|
@ -27,7 +29,6 @@ class TriageScreen extends StatefulWidget {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
List<String> answeredEvidenceIds = []; // Track user's answers
|
|
|
|
|
late SymptomsCheckerViewModel viewModel;
|
|
|
|
|
late DialogService dialogService;
|
|
|
|
|
|
|
|
|
|
@ -58,43 +59,96 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleTriageResponse() {
|
|
|
|
|
// Case 1: Emergency evidence detected
|
|
|
|
|
if (viewModel.hasEmergencyEvidence) {
|
|
|
|
|
_showEmergencyDialog();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewModel.shouldStopTriage) {
|
|
|
|
|
// Get the highest probability condition
|
|
|
|
|
final conditions = viewModel.currentConditions ?? [];
|
|
|
|
|
double highestProbability = 0.0;
|
|
|
|
|
|
|
|
|
|
if (conditions.isNotEmpty) {
|
|
|
|
|
final sortedConditions = List<TriageCondition>.from(conditions);
|
|
|
|
|
sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0));
|
|
|
|
|
highestProbability = (sortedConditions.first.probability ?? 0.0) * 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered
|
|
|
|
|
if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 7) {
|
|
|
|
|
// Navigate to results/possible conditions screen
|
|
|
|
|
context.navigateWithName(AppRoutes.possibleConditionsScreen);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Question is loaded, reset selection for new question
|
|
|
|
|
// Continue triage - question is loaded, reset selection for new question
|
|
|
|
|
viewModel.resetTriageChoice();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _showEmergencyDialog() {
|
|
|
|
|
showCommonBottomSheetWithoutHeight(
|
|
|
|
|
context,
|
|
|
|
|
title: "Emergency".needTranslation,
|
|
|
|
|
child: Utils.getWarningWidget(
|
|
|
|
|
loadingText: "Emergency evidence detected. Please seek immediate medical attention.".needTranslation,
|
|
|
|
|
isShowActionButtons: true,
|
|
|
|
|
onCancelTap: () => Navigator.pop(context),
|
|
|
|
|
onConfirmTap: () {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
context.pop();
|
|
|
|
|
},
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
|
|
|
|
color: AppColors.primaryRedColor,
|
|
|
|
|
borderRadius: 24.h,
|
|
|
|
|
),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: EdgeInsets.all(24.h),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
"".toText14(),
|
|
|
|
|
Utils.buildSvgWithAssets(
|
|
|
|
|
icon: AppAssets.cancel_circle_icon,
|
|
|
|
|
iconColor: AppColors.whiteColor,
|
|
|
|
|
width: 24.h,
|
|
|
|
|
height: 24.h,
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
).onPress(() {
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
Lottie.asset(AppAnimations.ambulanceAlert,
|
|
|
|
|
repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain),
|
|
|
|
|
SizedBox(height: 8.h),
|
|
|
|
|
"Emergency".needTranslation.toText28(color: AppColors.whiteColor, isBold: true),
|
|
|
|
|
SizedBox(height: 8.h),
|
|
|
|
|
"Emergency evidence detected. Please seek medical attention."
|
|
|
|
|
.needTranslation
|
|
|
|
|
.toText14(color: AppColors.whiteColor, weight: FontWeight.w500),
|
|
|
|
|
SizedBox(height: 24.h),
|
|
|
|
|
CustomButton(
|
|
|
|
|
text: LocaleKeys.confirm.tr(context: context),
|
|
|
|
|
onPressed: () async => Navigator.of(context).pop(),
|
|
|
|
|
backgroundColor: AppColors.whiteColor,
|
|
|
|
|
borderColor: AppColors.whiteColor,
|
|
|
|
|
textColor: AppColors.primaryRedColor,
|
|
|
|
|
icon: AppAssets.checkmark_icon,
|
|
|
|
|
iconColor: AppColors.primaryRedColor,
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: 8.h),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
isFullScreen: false,
|
|
|
|
|
isCloseButtonVisible: true,
|
|
|
|
|
isCloseButtonVisible: false,
|
|
|
|
|
hasBottomPadding: false,
|
|
|
|
|
backgroundColor: AppColors.primaryRedColor,
|
|
|
|
|
callBackFunc: () {},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool get isFirstQuestion => answeredEvidenceIds.isEmpty;
|
|
|
|
|
bool get isFirstQuestion => viewModel.getTriageEvidence().isEmpty;
|
|
|
|
|
|
|
|
|
|
void _onOptionSelected(int choiceIndex) {
|
|
|
|
|
viewModel.selectTriageChoice(choiceIndex);
|
|
|
|
|
void _onOptionSelectedForItem(String itemId, int choiceIndex) {
|
|
|
|
|
viewModel.selectTriageChoiceForItem(itemId, choiceIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onPreviousPressed() {
|
|
|
|
|
@ -102,13 +156,6 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onNextPressed() {
|
|
|
|
|
// Check if user has selected an option
|
|
|
|
|
if (viewModel.selectedTriageChoiceIndex == null) {
|
|
|
|
|
dialogService.showErrorBottomSheet(message: 'Please select an option before proceeding'.needTranslation);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the selected choice from the current question
|
|
|
|
|
final currentQuestion = viewModel.currentTriageQuestion;
|
|
|
|
|
if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) {
|
|
|
|
|
dialogService.showErrorBottomSheet(
|
|
|
|
|
@ -117,32 +164,43 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final questionItem = currentQuestion.items!.first;
|
|
|
|
|
if (questionItem.choices == null || viewModel.selectedTriageChoiceIndex! >= questionItem.choices!.length) {
|
|
|
|
|
dialogService.showErrorBottomSheet(
|
|
|
|
|
message: 'Invalid choice selection'.needTranslation,
|
|
|
|
|
);
|
|
|
|
|
// Check if all items have been answered
|
|
|
|
|
if (!viewModel.areAllTriageItemsAnswered) {
|
|
|
|
|
dialogService.showErrorBottomSheet(message: 'Please answer all questions before proceeding'.needTranslation);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final selectedChoice = questionItem.choices![viewModel.selectedTriageChoiceIndex!];
|
|
|
|
|
// Collect all evidence from all items
|
|
|
|
|
for (var item in currentQuestion.items!) {
|
|
|
|
|
final itemId = item.id ?? "";
|
|
|
|
|
if (itemId.isEmpty) continue;
|
|
|
|
|
|
|
|
|
|
final evidenceId = selectedChoice.label ?? "";
|
|
|
|
|
if (evidenceId.isNotEmpty) {
|
|
|
|
|
answeredEvidenceIds.add(evidenceId);
|
|
|
|
|
final selectedChoiceIndex = viewModel.getTriageChoiceForItem(itemId);
|
|
|
|
|
if (selectedChoiceIndex == null) continue;
|
|
|
|
|
|
|
|
|
|
if (item.choices != null && selectedChoiceIndex < item.choices!.length) {
|
|
|
|
|
final selectedChoice = item.choices![selectedChoiceIndex];
|
|
|
|
|
final choiceId = selectedChoice.id ?? "";
|
|
|
|
|
|
|
|
|
|
if (choiceId.isNotEmpty) {
|
|
|
|
|
viewModel.addTriageEvidence(itemId, choiceId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all previous evidence IDs
|
|
|
|
|
List<String> allEvidenceIds = viewModel.getAllEvidenceIds();
|
|
|
|
|
allEvidenceIds.addAll(answeredEvidenceIds);
|
|
|
|
|
// Get all evidence: initial symptoms + risk factors + suggestions + triage evidence
|
|
|
|
|
List<String> initialEvidenceIds = viewModel.getAllEvidenceIds();
|
|
|
|
|
List<Map<String, String>> triageEvidence = viewModel.getTriageEvidence();
|
|
|
|
|
|
|
|
|
|
log("allEvidences: ${allEvidenceIds.toString()}");
|
|
|
|
|
log("initialEvidenceIds: ${initialEvidenceIds.toString()}");
|
|
|
|
|
log("triageEvidence: ${triageEvidence.toString()}");
|
|
|
|
|
|
|
|
|
|
// Call API with updated evidence
|
|
|
|
|
viewModel.getDiagnosisForTriage(
|
|
|
|
|
age: viewModel.selectedAge!,
|
|
|
|
|
sex: viewModel.selectedGender!.toLowerCase(),
|
|
|
|
|
evidenceIds: allEvidenceIds,
|
|
|
|
|
evidenceIds: initialEvidenceIds,
|
|
|
|
|
triageEvidence: triageEvidence,
|
|
|
|
|
language: viewModel.appState.isArabic() ? 'ar' : 'en',
|
|
|
|
|
onSuccess: (response) {
|
|
|
|
|
_handleTriageResponse();
|
|
|
|
|
@ -241,29 +299,6 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildErrorState() {
|
|
|
|
|
return CollapsingListView(
|
|
|
|
|
title: "Triage".needTranslation,
|
|
|
|
|
leadingCallback: () => context.pop(),
|
|
|
|
|
child: Center(
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.error_outline, size: 64.f, color: AppColors.errorColor),
|
|
|
|
|
SizedBox(height: 16.h),
|
|
|
|
|
"No question available".needTranslation.toText16(weight: FontWeight.w500),
|
|
|
|
|
SizedBox(height: 24.h),
|
|
|
|
|
CustomButton(
|
|
|
|
|
text: "Go Back".needTranslation,
|
|
|
|
|
onPressed: () => context.pop(),
|
|
|
|
|
backgroundColor: AppColors.primaryRedColor,
|
|
|
|
|
).paddingSymmetrical(48.w, 0),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _showConfirmationBeforeExit(BuildContext context) {
|
|
|
|
|
showCommonBottomSheetWithoutHeight(
|
|
|
|
|
title: LocaleKeys.notice.tr(context: context),
|
|
|
|
|
@ -299,9 +334,6 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
return SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final questionItem = question.items!.first;
|
|
|
|
|
final choices = questionItem.choices ?? [];
|
|
|
|
|
|
|
|
|
|
return AnimatedSwitcher(
|
|
|
|
|
duration: const Duration(milliseconds: 400),
|
|
|
|
|
transitionBuilder: (Widget child, Animation<double> animation) {
|
|
|
|
|
@ -324,7 +356,7 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
child: Container(
|
|
|
|
|
key: ValueKey<String>(questionItem.id ?? answeredEvidenceIds.length.toString()),
|
|
|
|
|
key: ValueKey<String>(question.items!.first.id ?? viewModel.getTriageEvidence().length.toString()),
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
margin: EdgeInsets.symmetric(horizontal: 24.w),
|
|
|
|
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
|
|
|
|
@ -332,11 +364,36 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
(question.text ?? "").toText16(weight: FontWeight.w500),
|
|
|
|
|
// Main question text
|
|
|
|
|
(question.text ?? "").toText16(weight: FontWeight.w600, color: AppColors.textColor),
|
|
|
|
|
SizedBox(height: 24.h),
|
|
|
|
|
...List.generate(choices.length, (index) {
|
|
|
|
|
bool selected = viewModel.selectedTriageChoiceIndex == index;
|
|
|
|
|
return _buildOptionItem(index, selected, choices[index].label ?? "");
|
|
|
|
|
|
|
|
|
|
// Show all items with dividers
|
|
|
|
|
...List.generate(question.items!.length, (itemIndex) {
|
|
|
|
|
final item = question.items![itemIndex];
|
|
|
|
|
final itemId = item.id ?? "";
|
|
|
|
|
final choices = item.choices ?? [];
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Item name (sub-question)
|
|
|
|
|
(item.name ?? "").toText14(weight: FontWeight.w600, color: AppColors.textColor),
|
|
|
|
|
SizedBox(height: 8.h),
|
|
|
|
|
// Choices for this item
|
|
|
|
|
...List.generate(choices.length, (choiceIndex) {
|
|
|
|
|
bool selected = viewModel.getTriageChoiceForItem(itemId) == choiceIndex;
|
|
|
|
|
return _buildOptionItem(itemId, choiceIndex, selected, choices[choiceIndex].label ?? "");
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// Add divider between items (but not after the last one)
|
|
|
|
|
if (itemIndex < question.items!.length - 1) ...[
|
|
|
|
|
SizedBox(height: 8.h),
|
|
|
|
|
Divider(color: AppColors.bottomNAVBorder, thickness: 1),
|
|
|
|
|
SizedBox(height: 10.h),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
@ -344,9 +401,9 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildOptionItem(int index, bool selected, String optionText) {
|
|
|
|
|
Widget _buildOptionItem(String itemId, int choiceIndex, bool selected, String optionText) {
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () => _onOptionSelected(index),
|
|
|
|
|
onTap: () => _onOptionSelectedForItem(itemId, choiceIndex),
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: EdgeInsets.only(bottom: 12.h),
|
|
|
|
|
child: Row(
|
|
|
|
|
@ -365,7 +422,7 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
|
|
|
|
|
),
|
|
|
|
|
SizedBox(width: 12.w),
|
|
|
|
|
Expanded(child: optionText.toText14(weight: FontWeight.w500)),
|
|
|
|
|
Expanded(child: optionText.toText13(weight: FontWeight.w500)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
@ -387,6 +444,7 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown";
|
|
|
|
|
probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage
|
|
|
|
|
}
|
|
|
|
|
// final bool isHighConfidence = probability >= 70.0;
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
|
|
|
|
@ -445,6 +503,8 @@ class _TriageScreenState extends State<TriageScreen> {
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// Show high confidence message
|
|
|
|
|
|
|
|
|
|
SizedBox(height: 12.h),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
|