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/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.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/models/resp_models/triage_response_model.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/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.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'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; class TriagePage extends StatefulWidget { const TriagePage({super.key}); @override State createState() => _TriagePageState(); } class _TriagePageState extends State { late SymptomsCheckerViewModel viewModel; late DialogService dialogService; late AppState _appState; @override void initState() { super.initState(); viewModel = context.read(); dialogService = getIt.get(); _appState = getIt.get(); // Start triage process when screen loads WidgetsBinding.instance.addPostFrameCallback((_) { _startTriage(); }); } void _startTriage() { viewModel.startOrContinueTriage( onSuccess: () { _handleTriageResponse(); }, onError: (error) { dialogService.showErrorBottomSheet( message: error, onOkPressed: () => context.pop(), ); }, ); } void _handleTriageResponse() { // Case 1: Emergency evidence detected // if (viewModel.hasEmergencyEvidence) { // _showEmergencyDialog(); // return; // } // Get the highest probability condition final conditions = viewModel.currentConditions ?? []; double highestProbability = 0.0; if (conditions.isNotEmpty) { final sortedConditions = List.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: 30 or more questions answered // if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) { if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 4 || viewModel.shouldStopTriage) { // Navigate to results/possible conditions screen context.navigateWithName(AppRoutes.possibleConditionsPage); return; } // Continue triage - question is loaded, reset selection for new question viewModel.resetTriageChoice(); } void _showEmergencyDialog() { showCommonBottomSheetWithoutHeight( context, 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), LocaleKeys.emergencyTriage.tr(context: context).toText28(color: AppColors.whiteColor, isBold: true), SizedBox(height: 8.h), LocaleKeys.emergencyEvidenceDetected.tr(context: context).toText14(color: AppColors.whiteColor, isBold: true), SizedBox(height: 24.h), CustomButton( text: LocaleKeys.confirm.tr(context: context), onPressed: () { context.pop(); context.navigateWithName(AppRoutes.possibleConditionsPage); }, backgroundColor: AppColors.whiteColor, borderColor: AppColors.whiteColor, textColor: AppColors.primaryRedColor, icon: AppAssets.checkmark_icon, iconColor: AppColors.primaryRedColor, ), SizedBox(height: 8.h), ], ), ), ), isFullScreen: false, isCloseButtonVisible: false, hasBottomPadding: false, backgroundColor: AppColors.primaryRedColor, callBackFunc: () {}, ); } bool get isFirstQuestion => viewModel.getTriageEvidence().isEmpty; void _onOptionSelectedForItem(String itemId, int choiceIndex) { viewModel.selectTriageChoiceForItem(itemId, choiceIndex); } void _onPreviousPressed() { // Try to go back to previous question bool wentBack = viewModel.goBackToPreviousTriageQuestion(); if (!wentBack) { // If already at first question, go back to suggestions screen context.pop(); } } void _onNextPressed() { final currentQuestion = viewModel.currentTriageQuestion; if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) { dialogService.showErrorBottomSheet( message: LocaleKeys.noQuestionItemsAvailable.tr(context: context), ); return; } // Check if all items have been answered if (!viewModel.areAllTriageItemsAnswered) { dialogService.showErrorBottomSheet(message: LocaleKeys.pleaseAnswerAllQuestionsBeforeProceeding.tr(context: context)); return; } // Collect evidence based on question type if (viewModel.isTriageQuestionSingleSelection) { // Type 1: Single selection - only one evidence entry with "Yes" choice final selectedItemId = viewModel.selectedSingleItemId; if (selectedItemId != null) { // Find the item and its "Yes" choice for (var item in currentQuestion.items!) { if (item.id == selectedItemId) { // Find the "Yes" choice (case-insensitive) String? yesChoiceId; if (item.choices != null) { for (var choice in item.choices!) { final label = choice.label?.toLowerCase() ?? ''; if (label == 'yes' || label == 'نعم') { yesChoiceId = choice.id; break; } } } // If "Yes" choice found, add evidence if (yesChoiceId != null && yesChoiceId.isNotEmpty) { viewModel.addTriageEvidence(selectedItemId, yesChoiceId); } break; } } } } else { // Type 2: Multi-item selection - collect evidence from all items for (var item in currentQuestion.items!) { final itemId = item.id ?? ""; if (itemId.isEmpty) continue; 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 evidence: initial symptoms + risk factors + suggestions (with sources) + triage evidence List> initialEvidenceList = viewModel.getAllEvidenceWithSources(); List> triageEvidence = viewModel.getTriageEvidence(); // Call API with updated evidence viewModel.getDiagnosisForTriage( age: viewModel.selectedAge!, gender: viewModel.selectedGender!.toLowerCase() == "male" ? 1 : 2, initialEvidenceList: initialEvidenceList, triageEvidence: triageEvidence, language: viewModel.appState.isArabic() ? 'ar' : 'en', onSuccess: (response) { _handleTriageResponse(); }, onError: (error) { dialogService.showErrorBottomSheet(message: error); }, ); } void _showRationaleBottomSheet(BuildContext context) { // Get all current evidence to send to the API List> evidence = []; // Add all initial evidence (symptoms + risk factors + suggestions) with sources final initialEvidence = viewModel.getAllEvidenceWithSources(); evidence.addAll(initialEvidence); // Add triage evidence answered so far final triageEvidence = viewModel.getTriageEvidence(); for (var triageItem in triageEvidence) { evidence.add({ "id": triageItem["id"] ?? "", "choice_id": triageItem["choice_id"] ?? "present", "source": "initial", }); } // Show loading state LoaderBottomSheet.showLoader(); // Call Rationale API viewModel.fetchRationale( evidence: evidence, onSuccess: (response) { LoaderBottomSheet.hideLoader(); // Get observation params (symptoms being analyzed) final observationParams = response.dataDetails?.observationParams ?? []; // Build symptom names string String symptomNames = ''; if (observationParams.isNotEmpty) { final names = observationParams.map((param) => param.getDisplayName()).where((name) => name.isNotEmpty).toList(); final isArabic = _appState.isArabic(); final andSeparator = isArabic ? ' و ' : ' and '; if (names.length == 1) { symptomNames = names.first; } else if (names.length == 2) { symptomNames = names.join(andSeparator); } else if (names.length > 2) { final lastItem = names.last; final allButLast = names.sublist(0, names.length - 1); symptomNames = '${allButLast.join(', ')}$andSeparator$lastItem'; } } if (symptomNames.isEmpty) { symptomNames = LocaleKeys.analyzingYourSymptoms.tr(context: context); } // Show bottom sheet with rationale _displayRationaleBottomSheet(context, symptomNames); }, onError: (error) { LoaderBottomSheet.hideLoader(); dialogService.showErrorBottomSheet(message: error); }, ); } void _displayRationaleBottomSheet(BuildContext context, String symptomNames) { showCommonBottomSheetWithoutHeight( title: LocaleKeys.whyAmIBeingAskedThis.tr(context: context), context, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.weAreAskingThisQuestionToBetterUnderstand.tr(context: context, namedArgs: {'symptoms': symptomNames}).toText14( color: AppColors.textColor, ), SizedBox(height: 24.h), CustomButton( text: LocaleKeys.ok.tr(context: context), onPressed: () => Navigator.pop(context), backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, ), ], ), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); } void _showInstructionsBottomSheet(BuildContext context, TriageQuestion question) { final instructions = question.instruction ?? []; final hasExplication = question.explication != null && question.explication!.isNotEmpty; final hasInstructions = instructions.isNotEmpty; showCommonBottomSheetWithoutHeight( title: LocaleKeys.information.tr(context: context), context, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Explication (if available) if (hasExplication) ...[ (question.explication!).toText14( color: AppColors.textColor, ), if (hasInstructions) SizedBox(height: 16.h), ], // Instructions list with bullets (if available) if (hasInstructions) ...[ ...instructions.asMap().entries.map((entry) { final instruction = entry.value; return Padding( padding: EdgeInsets.only(bottom: 12.h), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.only(top: 6.h, right: 12.w, left: 12.w), width: 6.w, height: 6.h, decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.primaryRedColor, ), ), Expanded( child: instruction.toText14( color: AppColors.textColor, ), ), ], ), ); }).toList(), ], SizedBox(height: 24.h), CustomButton( text: LocaleKeys.ok.tr(context: context), onPressed: () => Navigator.pop(context), backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, ), ], ), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer( builder: (context, viewModel, child) { // Show normal question UI return Column( children: [ Expanded( child: CollapsingListView( title: LocaleKeys.triage.tr(context: context), leadingCallback: () => _showConfirmationBeforeExit(context), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), _buildQuestionCard(viewModel), ], ), ), ), _buildStickyBottomCard(context, viewModel), ], ); }, ), ); } Widget _buildLoadingShimmer() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), // Create 2-3 shimmer cards ...List.generate(1, (index) { return Padding( padding: EdgeInsets.only(bottom: 16.h), child: _buildShimmerCard(), ); }), ], ); } Widget _buildShimmerCard() { return Container( 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: 16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Shimmer title Container( height: 40.h, decoration: BoxDecoration( color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24.r), ), ).toShimmer2(isShow: true, radius: 24.r), SizedBox(height: 16.h), // Shimmer chips Wrap( runSpacing: 12.h, spacing: 8.w, children: List.generate(4, (index) { return Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24.r), border: Border.all(color: AppColors.bottomNAVBorder, width: 1), ), child: Text( 'Not Applicable Suggestion', style: TextStyle(fontSize: 14.f, color: AppColors.textColor), ), ).toShimmer2(isShow: true, radius: 24.r); }), ), ], ), ); } void _showConfirmationBeforeExit(BuildContext context) { showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: context), context, child: Utils.getWarningWidget( loadingText: LocaleKeys.areYouSureYouWantToExitProgress.tr(context: context), isShowActionButtons: true, onCancelTap: () => Navigator.pop(context), onConfirmTap: () { context.pop(); viewModel.reset(); // Clear all symptoms checker data final navigationService = getIt.get(); // navigationService.replaceAllRoutesAndNavigateToLanding(); navigationService.replaceAllRoutesAndNavigateToLandingAuth(); }, ), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); } Widget _buildQuestionCard(SymptomsCheckerViewModel viewModel) { if (viewModel.isTriageDiagnosisLoading) { return _buildLoadingShimmer(); } if (viewModel.currentTriageQuestion == null) { return Center( child: LocaleKeys.noQuestionAvailable.tr(context: context).toText16(isBold: true), ); } final question = viewModel.currentTriageQuestion; if (question == null || question.items == null || question.items!.isEmpty) { return SizedBox.shrink(); } 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(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), padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Main question text with optional help icon Row( children: [ Expanded( child: (question.text ?? "").toText16(isBold: true, color: AppColors.textColor), ), // Show help icon if instructions are available if (question.hasInstructions) ...[ SizedBox(width: 8.w), GestureDetector( onTap: () => _showInstructionsBottomSheet(context, question), child: Utils.buildSvgWithAssets( icon: AppAssets.alertSquare, height: 24.h, width: 24.h, iconColor: AppColors.textColor, ), ), ], ], ), SizedBox(height: 24.h), // Type 1: Show items as checkboxes only (no choices displayed) if (viewModel.isTriageQuestionSingleSelection) ...[ ...List.generate(question.items!.length, (itemIndex) { final item = question.items![itemIndex]; final itemId = item.id ?? ""; final itemName = item.name ?? ""; final isSelected = viewModel.selectedSingleItemId == itemId; return GestureDetector( onTap: () => _onOptionSelectedForItem(itemId, 0), // Pass 0 as placeholder 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: isSelected ? AppColors.primaryRedColor : Colors.transparent, borderRadius: BorderRadius.circular(5.r), border: Border.all( color: isSelected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w, ), ), child: isSelected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), Expanded(child: itemName.toText13(isBold: true)), ], ), ), ); }), ] else ...[ // Type 2: Show all items with their choices ...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(isBold: true, color: AppColors.textColor), SizedBox(height: 8.h), // Choices for this item ...List.generate(choices.length, (choiceIndex) { // Check selection based on question type bool selected = viewModel.isTriageQuestionSingleSelection ? viewModel.isTriageSingleOptionSelected(itemId, choiceIndex) : 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), ], ], ); }), ], SizedBox(height: 12.h), GestureDetector( onTap: () => _showRationaleBottomSheet(context), child: Row( children: [ Expanded( child: LocaleKeys.whyAmIBeingAskedThis.tr(context: context).toText12( decorationColor: AppColors.primaryRedColor, isUnderLine: true, color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, ), ) ], ), ), ], ), ), ); } Widget _buildOptionItem(String itemId, int choiceIndex, bool selected, String optionText) { return GestureDetector( onTap: () => _onOptionSelectedForItem(itemId, choiceIndex), 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(5.r), border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), ), child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), Expanded(child: optionText.toText13(isBold: true)), ], ), ), ); } Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { // Get the top condition with highest probability final conditions = viewModel.currentConditions ?? []; final isArabic = _appState.isArabic(); String suggestedCondition = LocaleKeys.analyzing.tr(context: context); double probability = 0.0; if (conditions.isNotEmpty) { // Sort by probability descending final sortedConditions = List.from(conditions); sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); final topCondition = sortedConditions.first; suggestedCondition = topCondition.getDisplayName(isArabic: isArabic); 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), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 16.h), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ RichText( text: TextSpan( text: "${LocaleKeys.possibleConditions.tr(context: context)} ", style: TextStyle( color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'), children: [ TextSpan( text: suggestedCondition, style: TextStyle( color: AppColors.textColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'), ), ], ), ), SizedBox(height: 16.h), CustomRoundedProgressBar( percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( text: "${probability.toStringAsFixed(1)}% ", style: TextStyle( fontFamily: isArabic ? 'CairoArabic' : 'Poppins', color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, fontSize: 14.f, ), children: [ TextSpan( text: LocaleKeys.symptomsCheckerFindingScore.tr(context: context), style: TextStyle( color: AppColors.textColor, fontWeight: FontWeight.w600, fontSize: 13.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins', ), ), ], ), ), ), // Show high confidence message SizedBox(height: 12.h), Row( children: [ Expanded( child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, isDisabled: viewModel.isTriageDiagnosisLoading, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, ), ), SizedBox(width: 12.w), Expanded( child: CustomButton( text: LocaleKeys.next.tr(context: context), isDisabled: viewModel.isTriageDiagnosisLoading, onPressed: _onNextPressed, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, fontSize: 16.f, ), ), ], ), ], ), SizedBox(height: 24.h), ], ).paddingSymmetrical(24.w, 0), ); } }