From cf26e716459f480cca34ff9b6d3333f34a1359e6 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Thu, 11 Dec 2025 17:00:00 +0300 Subject: [PATCH] Complete USerInfo Selectoiun --- .../symptoms_checker_view_model.dart | 103 ++++---- .../symptoms_checker/user_info_selection.dart | 227 +++++++++++----- .../pages/age_selection_page.dart | 21 +- .../pages/height_selection_page.dart | 177 +++++++------ .../pages/weight_selection_page.dart | 220 ++++++---------- .../user_info_flow_manager.dart | 246 +++++++++++------- .../widgets/custom_date_picker.dart | 235 +++++++++++++++++ .../widgets/height_scale.dart | 169 ++++++++++++ .../widgets/triangle_indicator.dart | 85 ++++++ .../widgets/user_info_progress_bar.dart | 36 --- .../widgets/user_info_selection_scaffold.dart | 67 ----- .../widgets/user_info_sticky_bottom_card.dart | 68 ----- .../widgets/weight_scale.dart | 180 +++++++++++++ lib/widgets/appbar/collapsing_list_view.dart | 20 +- 14 files changed, 1221 insertions(+), 633 deletions(-) create mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart create mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart create mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart delete mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart delete mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart delete mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart create mode 100644 lib/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index 34a1754..1b43fc2 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -39,10 +39,11 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // User Info Flow State int _userInfoCurrentPage = 0; String? _selectedGender; + DateTime? _dateOfBirth; int? _selectedAge; - double? _selectedHeight; + double _selectedHeight = 170; bool _isHeightCm = true; - double? _selectedWeight; + double _selectedWeight = 60; bool _isWeightKg = true; // Getters @@ -51,11 +52,19 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // User Info Getters int get userInfoCurrentPage => _userInfoCurrentPage; + String? get selectedGender => _selectedGender; + + DateTime? get dateOfBirth => _dateOfBirth; + int? get selectedAge => _selectedAge; + double? get selectedHeight => _selectedHeight; + bool get isHeightCm => _isHeightCm; + double? get selectedWeight => _selectedWeight; + bool get isWeightKg => _isWeightKg; BodyView get currentView => _currentView; @@ -84,23 +93,22 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Get count of selected organs int get selectedOrgansCount => _selectedOrganIds.length; - List get organSymptomsResults { + List get organSymptomsResults { if (bodySymptomResponse?.dataDetails?.result == null) { return []; } return bodySymptomResponse!.dataDetails!.result ?? []; } - int get totalSelectedSymptomsCount { + int get totalSelectedSymptomsCount { return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); } - bool get hasSelectedSymptoms { + bool get hasSelectedSymptoms { return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty); } - - void toggleView() { + void toggleView() { _currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front; notifyListeners(); } @@ -110,7 +118,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } - void toggleOrganSelection(String organId) { + void toggleOrganSelection(String organId) { if (_selectedOrganIds.contains(organId)) { _selectedOrganIds.remove(organId); } else { @@ -123,10 +131,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } - void _showTooltip(String organId) { - _tooltipTimer?.cancel(); + void _showTooltip(String organId) { + _tooltipTimer?.cancel(); - _tooltipOrganId = organId; + _tooltipOrganId = organId; notifyListeners(); // Hide tooltip after 2 seconds @@ -143,39 +151,39 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } - void removeOrgan(String organId) { + void removeOrgan(String organId) { _selectedOrganIds.remove(organId); notifyListeners(); } - void clearAllSelections() { + void clearAllSelections() { _selectedOrganIds.clear(); notifyListeners(); } - void toggleBottomSheet() { + void toggleBottomSheet() { _isBottomSheetExpanded = !_isBottomSheetExpanded; notifyListeners(); } - void setBottomSheetExpanded(bool isExpanded) { + void setBottomSheetExpanded(bool isExpanded) { _isBottomSheetExpanded = isExpanded; notifyListeners(); } - bool validateSelection() { + bool validateSelection() { return _selectedOrganIds.isNotEmpty; } - List getSelectedOrganIds() { + List getSelectedOrganIds() { return _selectedOrganIds.toList(); } - List getSelectedOrganNames() { + List getSelectedOrganNames() { return selectedOrgans.map((organ) => organ.description).toList(); } - Future initializeSymptomGroups({ + Future initializeSymptomGroups({ Function()? onSuccess, Function(String)? onError, }) async { @@ -186,12 +194,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier { return; } - List organNames = selectedOrgans.map((organ) => organ.name).toList(); + List organNames = selectedOrgans.map((organ) => organ.name).toList(); - await getBodySymptomsByName( + await getBodySymptomsByName( organNames: organNames, onSuccess: (response) { - if (onSuccess != null) { + if (onSuccess != null) { onSuccess(); } }, @@ -203,7 +211,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); } - void toggleSymptomSelection(String organId, String symptomId) { + void toggleSymptomSelection(String organId, String symptomId) { if (!_selectedSymptomsByOrgan.containsKey(organId)) { _selectedSymptomsByOrgan[organId] = {}; } @@ -216,11 +224,11 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } - bool isSymptomSelected(String organId, String symptomId) { + bool isSymptomSelected(String organId, String symptomId) { return _selectedSymptomsByOrgan[organId]?.contains(symptomId) ?? false; } - List getAllSelectedSymptoms() { + List getAllSelectedSymptoms() { List allSymptoms = []; if (bodySymptomResponse?.dataDetails?.result == null) { @@ -228,7 +236,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } for (var organResult in bodySymptomResponse!.dataDetails!.result!) { - String? matchingOrganId; + String? matchingOrganId; for (var organ in selectedOrgans) { if (organ.name == organResult.name) { matchingOrganId = organ.id; @@ -252,12 +260,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier { return allSymptoms; } - void clearAllSymptomSelections() { + void clearAllSymptomSelections() { _selectedSymptomsByOrgan.clear(); notifyListeners(); } - void reset() { + void reset() { _currentView = BodyView.front; _selectedOrganIds.clear(); _selectedSymptomsByOrgan.clear(); @@ -268,10 +276,11 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Reset user info flow _userInfoCurrentPage = 0; _selectedGender = null; + _dateOfBirth = null; _selectedAge = null; - _selectedHeight = null; + _selectedHeight = 170; _isHeightCm = true; - _selectedWeight = null; + _selectedWeight = 60; _isWeightKg = true; notifyListeners(); } @@ -312,6 +321,19 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } + /// Set date of birth + void setDateOfBirth(DateTime dateOfBirth) { + _dateOfBirth = dateOfBirth; + // Calculate age from date of birth + final now = DateTime.now(); + int age = now.year - dateOfBirth.year; + if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { + age--; + } + _selectedAge = age; + notifyListeners(); + } + /// Set selected height void setHeight(double height, bool isCm) { _selectedHeight = height; @@ -329,10 +351,13 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Check if user info page is last bool get isUserInfoLastPage => _userInfoCurrentPage == 3; + bool get isUserInfoFirstPage => _userInfoCurrentPage == 0; + /// Validate and submit user info Map getUserInfoData() { return { 'gender': _selectedGender, + 'dateOfBirth': _dateOfBirth?.toIso8601String(), 'age': _selectedAge, 'height': _selectedHeight, 'heightUnit': _isHeightCm ? 'cm' : 'ft', @@ -341,19 +366,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { }; } - /// Reset user info flow - void resetUserInfo() { - _userInfoCurrentPage = 0; - _selectedGender = null; - _selectedAge = null; - _selectedHeight = null; - _isHeightCm = true; - _selectedWeight = null; - _isWeightKg = true; - notifyListeners(); - } - - Future getBodySymptomsByName({ + Future getBodySymptomsByName({ required List organNames, Function(BodySymptomResponseModel)? onSuccess, Function(String)? onError, @@ -366,7 +379,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); result.fold( - (failure) async { + (failure) async { isBodySymptomsLoading = false; notifyListeners(); await errorHandlerService.handleError(failure: failure); @@ -374,7 +387,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { onError(failure.toString()); } }, - (apiResponse) { + (apiResponse) { isBodySymptomsLoading = false; if (apiResponse.messageStatus == 1 && apiResponse.data != null) { bodySymptomResponse = apiResponse.data; diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index 2fb6cc9..9a66a65 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -7,13 +7,63 @@ 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/symptoms_checker_view_model.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:provider/provider.dart'; -class UserInfoSelectionScreen extends StatelessWidget { +class UserInfoSelectionScreen extends StatefulWidget { const UserInfoSelectionScreen({super.key}); + @override + State createState() => _UserInfoSelectionScreenState(); +} + +class _UserInfoSelectionScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeUserInfo(); + }); + } + + /// Initialize user info from appState if user is logged in + void _initializeUserInfo() { + final appState = getIt.get(); + final viewModel = context.read(); + + if (appState.isAuthenticated) { + final user = appState.getAuthenticatedUser(); + + if (user == null) return; + + // Populate gender (gender is int: 1=Male, 2=Female) + if (user.gender != null) { + String genderStr = user.gender == 1 + ? "Male" + : user.gender == 2 + ? "Female" + : "Other"; + viewModel.setGender(genderStr); + } + + if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { + try { + DateTime dob = DateTime.parse(user.dateofBirth!); + viewModel.setDateOfBirth(dob); + } catch (e) { + // If date parsing fails, ignore and let user fill manually + } + } + + // Note: AuthenticatedUser doesn't have height/weight fields + // User will need to fill these manually + } + // If not authenticated or fields are empty, user will fill them manually + } + _buildEditInfoTile({ required String leadingIcon, required String title, @@ -69,79 +119,111 @@ class UserInfoSelectionScreen extends StatelessWidget { } else { name = "Guest"; } + return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: "Symptoms Checker".needTranslation, - isLeading: true, - child: SingleChildScrollView( - child: Column( - children: [ - Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), - child: Column( - children: [ - "Hi $name, Is your information up to date?".needTranslation.toText18( - weight: FontWeight.w600, - color: AppColors.textColor, + body: Consumer( + builder: (context, viewModel, child) { + // Check if any field is empty + bool hasEmptyFields = viewModel.selectedGender == null || + viewModel.selectedAge == null || + viewModel.selectedHeight == null || + viewModel.selectedWeight == null; + + // Get display values + String genderText = viewModel.selectedGender ?? "Not set"; + // Show age calculated from DOB, not the DOB itself + String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set"; + String heightText = + viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set"; + String weightText = + viewModel.selectedWeight != null ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" : "Not set"; + + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Symptoms Checker".needTranslation, + isLeading: true, + child: SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + children: [ + "Hello $name, Is your information up to date?".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + SizedBox(height: 24.h), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.genderIcon, + title: "Gender".needTranslation, + subTitle: genderText, + onTap: () { + viewModel.setUserInfoPage(0); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, ), - SizedBox(height: 24.h), - _buildEditInfoTile( - context: context, - leadingIcon: AppAssets.genderIcon, - title: "Gender".needTranslation, - subTitle: "Male".needTranslation, - onTap: () {}, - trailingIcon: AppAssets.edit_icon, - ), - _getDivider(), - _buildEditInfoTile( - context: context, - leadingIcon: AppAssets.calendar, - title: "Age".needTranslation, - subTitle: "32 Years", - iconColor: AppColors.greyTextColor, - onTap: () {}, - trailingIcon: AppAssets.edit_icon, - ), - _getDivider(), - _buildEditInfoTile( - context: context, - leadingIcon: AppAssets.rulerIcon, - title: "Height".needTranslation, - subTitle: "17 8cm", - onTap: () {}, - trailingIcon: AppAssets.edit_icon, - ), - _getDivider(), - _buildEditInfoTile( - context: context, - leadingIcon: AppAssets.weightScale, - title: "Weight".needTranslation, - subTitle: "88 kg", - onTap: () {}, - trailingIcon: AppAssets.edit_icon, + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.calendarGrey, + title: "Age".needTranslation, + subTitle: ageText, + iconColor: AppColors.greyTextColor, + onTap: () { + viewModel.setUserInfoPage(1); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.rulerIcon, + title: "Height".needTranslation, + subTitle: heightText, + onTap: () { + viewModel.setUserInfoPage(2); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.weightScale, + title: "Weight".needTranslation, + subTitle: weightText, + onTap: () { + viewModel.setUserInfoPage(3); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + trailingIcon: AppAssets.edit_icon, + ), + ], ), - ], - ), - ), - ], - ).paddingAll(24.w), + ), + ], + ).paddingAll(24.w), + ), + ), ), - ), - ), - _buildBottomCard(context), - ], + _buildBottomCard(context, hasEmptyFields), + ], + ); + }, ), ); } - Widget _buildBottomCard(BuildContext context) { + Widget _buildBottomCard(BuildContext context, bool hasEmptyFields) { return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), child: SafeArea( @@ -157,7 +239,10 @@ class UserInfoSelectionScreen extends StatelessWidget { text: "No, Edit all".needTranslation, icon: AppAssets.edit_icon, iconColor: AppColors.primaryRedColor, - onPressed: () => context.navigateWithName(AppRoutes.userInfoFlowManager), + onPressed: () { + context.read().setUserInfoPage(0); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, @@ -170,10 +255,12 @@ class UserInfoSelectionScreen extends StatelessWidget { text: "Yes, It is".needTranslation, icon: AppAssets.tickIcon, iconColor: AppColors.whiteColor, - onPressed: () => () {}, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + onPressed: hasEmptyFields + ? () {} // Empty function for disabled state + : () => context.navigateWithName(AppRoutes.organSelectorPage), + backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, fontSize: 16.f, ), ), diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart index 6433b93..d73f387 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -1,7 +1,11 @@ -import 'package:flutter/material.dart'; +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_export.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/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:provider/provider.dart'; @@ -19,14 +23,21 @@ class AgeSelectionPage extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( - padding: EdgeInsets.all(24.w), child: Consumer( builder: (BuildContext context, symptomsViewModel, Widget? child) { return Column( children: [ - "What is your gender?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor), - SizedBox(height: 70.h), - "< Age Widget Here >".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.greyTextColor), + "What is your Date of Birth?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor).paddingAll(24.w), + SizedBox(height: 30.h), + ThreeColumnDatePicker( + enableHaptic: true, + enableSound: true, + initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1), + onDateChanged: (date) { + symptomsViewModel.setDateOfBirth(date); + log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}'); + }, + ) ], ); }, diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart index e10aca3..0744e81 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart @@ -1,36 +1,19 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.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/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; /// Height selection page content -class HeightSelectionPage extends StatefulWidget { - final double height; - final bool isCm; - final Function(double, bool) onHeightChanged; - - const HeightSelectionPage({ - super.key, - required this.height, - required this.isCm, - required this.onHeightChanged, - }); +class HeightSelectionPage extends StatelessWidget { + const HeightSelectionPage({super.key}); - @override - State createState() => _HeightSelectionPageState(); -} - -class _HeightSelectionPageState extends State { - late double heightValue; - - @override - void initState() { - super.initState(); - heightValue = widget.height; - } - - Widget _unitSelector() { + Widget _unitSelector(SymptomsCheckerViewModel viewModel) { return Container( height: 54.h, padding: EdgeInsets.all(4.h), @@ -43,13 +26,12 @@ class _HeightSelectionPageState extends State { final tabWidth = (constraints.maxWidth - 8.w) / 2; return Stack( children: [ - // Animated sliding indicator AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, width: tabWidth, height: constraints.maxHeight, - margin: EdgeInsets.only(left: widget.isCm ? 0 : tabWidth + 8.w), + margin: EdgeInsets.only(left: viewModel.isHeightCm ? 0 : tabWidth + 8.w), decoration: BoxDecoration( color: AppColors.bottomNAVBorder, borderRadius: BorderRadius.circular(7.r), @@ -61,10 +43,9 @@ class _HeightSelectionPageState extends State { Expanded( child: GestureDetector( onTap: () { - if (!widget.isCm) { - // Convert from FT to CM (1 ft = 30.48 cm) - final convertedHeight = heightValue * 30.48; - widget.onHeightChanged(convertedHeight, true); + if (!viewModel.isHeightCm) { + final convertedHeight = viewModel.selectedHeight! * 30.48; + viewModel.setHeight(convertedHeight, true); } }, child: Container( @@ -75,7 +56,7 @@ class _HeightSelectionPageState extends State { style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14.f, - color: widget.isCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + color: viewModel.isHeightCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), ), ), ), @@ -84,10 +65,9 @@ class _HeightSelectionPageState extends State { Expanded( child: GestureDetector( onTap: () { - if (widget.isCm) { - // Convert from CM to FT (1 cm = 0.0328084 ft) - final convertedHeight = heightValue / 30.48; - widget.onHeightChanged(convertedHeight, false); + if (viewModel.isHeightCm) { + final convertedHeight = viewModel.selectedHeight! / 30.48; + viewModel.setHeight(convertedHeight, false); } }, child: Container( @@ -98,7 +78,7 @@ class _HeightSelectionPageState extends State { style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14.f, - color: !widget.isCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + color: !viewModel.isHeightCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), ), ), ), @@ -115,61 +95,80 @@ class _HeightSelectionPageState extends State { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - Center( - child: Text( - 'How tall are you?'.needTranslation, - style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), - ), - ), - SizedBox(height: 24.h), - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: _unitSelector(), - ), - SizedBox(height: 90.h), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.center, + return Consumer( + builder: (context, viewModel, child) { + // Define min/max values based on unit + final minValue = viewModel.isHeightCm ? 50.0 : 1.6; // 50cm or 1.6ft (approx. 1'7") + final maxValue = viewModel.isHeightCm ? 280.0 : 9.2; // 280cm or 9.2ft (approx. 9'2") + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - heightValue.round().toString(), - style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1), + SizedBox(height: 24.h), + Center( + child: Text( + 'How tall are you?'.needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + SizedBox(height: 24.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _unitSelector(viewModel), + ), + SizedBox(height: 20.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(flex: 1), + Expanded( + flex: 3, + child: Consumer( + builder: (context, viewModel, child) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: + viewModel.isHeightCm ? viewModel.selectedHeight?.round().toString() : viewModel.selectedHeight?.toStringAsFixed(1), + style: TextStyle( + fontSize: 90.f, + color: AppColors.textColor, + height: 1, + ), + ), + TextSpan( + text: viewModel.isHeightCm ? 'cm' : 'ft', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24.f, + ), + ), + ], + ), + ).paddingOnly(bottom: 100.h, left: 20.w); + }, + ), + ), + Expanded( + child: HeightScale( + enableHaptic: true, + enableSound: true, + minValue: minValue, + maxValue: maxValue, + initialHeight: viewModel.selectedHeight ?? 100, + isCm: viewModel.isHeightCm, + onHeightChanged: (newHeight) { + log("height: $newHeight"); + viewModel.setHeight(newHeight, viewModel.isHeightCm); + }, + ), + ), + ], ), - SizedBox(width: 8.w), - Text(widget.isCm ? 'cm' : 'ft', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f)).paddingOnly(bottom: 10.h, left: 8.w), ], - ), - ], + ); + }, ); } } - -class _RulerPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paintTick = Paint()..color = const Color(0xFF222222); - final paintSmall = Paint()..color = const Color(0xFF222222).withValues(alpha: 0.6); - - final width = size.width; - final start = 120; - final end = 210; - final steps = end - start; - for (int i = 0; i <= steps; i++) { - final x = (i / steps) * width; - if (i % 10 == 0) { - canvas.drawLine(Offset(x, size.height * 0.1), Offset(x, size.height * 0.6), paintTick); - } else if (i % 5 == 0) { - canvas.drawLine(Offset(x, size.height * 0.2), Offset(x, size.height * 0.5), paintSmall); - } else { - canvas.drawLine(Offset(x, size.height * 0.35), Offset(x, size.height * 0.5), paintSmall); - } - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart index 0319f4b..1d38a91 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart @@ -1,36 +1,22 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.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/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; /// Weight selection page content -class WeightSelectionPage extends StatefulWidget { - final double weight; - final bool isKg; +class WeightSelectionPage extends StatelessWidget { final Function(double, bool) onWeightChanged; - const WeightSelectionPage({ - super.key, - required this.weight, - required this.isKg, - required this.onWeightChanged, - }); - - @override - State createState() => _WeightSelectionPageState(); -} - -class _WeightSelectionPageState extends State { - late double weightValue; + const WeightSelectionPage({super.key, required this.onWeightChanged}); - @override - void initState() { - super.initState(); - weightValue = widget.weight; - } - - Widget _unitSelector() { + Widget _unitSelector(SymptomsCheckerViewModel viewModel) { + bool isKg = viewModel.isWeightKg; return Container( height: 54.h, padding: EdgeInsets.all(4.h), @@ -49,7 +35,7 @@ class _WeightSelectionPageState extends State { curve: Curves.easeInOut, width: tabWidth, height: constraints.maxHeight, - margin: EdgeInsets.only(left: widget.isKg ? 0 : tabWidth + 8.w), + margin: EdgeInsets.only(left: isKg ? 0 : tabWidth + 8.w), decoration: BoxDecoration( color: AppColors.bottomNAVBorder, borderRadius: BorderRadius.circular(7.r), @@ -61,10 +47,10 @@ class _WeightSelectionPageState extends State { Expanded( child: GestureDetector( onTap: () { - if (!widget.isKg) { + if (!isKg) { // Convert from LBS to KG (1 lb = 0.453592 kg) - final convertedWeight = weightValue / 2.20462; - widget.onWeightChanged(convertedWeight, true); + final convertedWeight = viewModel.selectedWeight! / 2.20462; + Future.microtask(() => onWeightChanged(convertedWeight, true)); } }, child: Container( @@ -75,7 +61,7 @@ class _WeightSelectionPageState extends State { style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14.f, - color: widget.isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + color: isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), ), ), ), @@ -84,10 +70,10 @@ class _WeightSelectionPageState extends State { Expanded( child: GestureDetector( onTap: () { - if (widget.isKg) { + if (isKg) { // Convert from KG to LBS (1 kg = 2.20462 lbs) - final convertedWeight = weightValue * 2.20462; - widget.onWeightChanged(convertedWeight, false); + final convertedWeight = viewModel.selectedWeight! * 2.20462; + Future.microtask(() => onWeightChanged(convertedWeight, false)); } }, child: Container( @@ -98,7 +84,7 @@ class _WeightSelectionPageState extends State { style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14.f, - color: !widget.isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + color: !isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), ), ), ), @@ -113,124 +99,64 @@ class _WeightSelectionPageState extends State { ); } - Widget _weightSlider() { - return Column( - children: [ - SizedBox(height: 18.h), - SizedBox(height: 12.h), - SizedBox( - height: 80.h, - child: Row( - children: [ - Expanded(child: Container()), - Expanded( - flex: 6, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: CustomPaint( - painter: _WeightRulerPainter(), - ), - ), - SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 6.h, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), - overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), - activeTrackColor: AppColors.primaryRedColor, - inactiveTrackColor: AppColors.lightRedButtonColor, - ), - child: Slider( - min: 30, - max: 150, - value: weightValue, - onChanged: (v) { - setState(() => weightValue = v); - widget.onWeightChanged(v, widget.isKg); - }, - ), - ), - Positioned( - right: 0, - child: Container( - width: 18.w, - height: 18.h, - decoration: BoxDecoration( - color: AppColors.primaryRedColor, - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(4.r), - ), - ), - ), - ], - ), - ), - Expanded(child: Container()), - ], - ), - ), - ], - ); - } - @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - Center( - child: Text( - 'What is your weight?'.needTranslation, - style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + return Consumer(builder: (context, viewModel, child) { + bool isKg = viewModel.isWeightKg; + // Define min/max values based on unit + final minValue = isKg ? 10.0 : 22.0; // 10kg or 22lbs + final maxValue = isKg ? 200.0 : 440.0; // 200kg or 440lbs + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Center( + child: Text( + 'What is your weight?'.needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + SizedBox(height: 24.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _unitSelector(viewModel), ), - ), - SizedBox(height: 20.h), - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: _unitSelector(), - ), - SizedBox(height: 90.h), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - weightValue.round().toString(), - style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1), + SizedBox(height: 60.h), + // Weight display centered + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + viewModel.selectedWeight!.round().toString(), + style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1), + ), + SizedBox(width: 8.w), + Text( + isKg ? 'kg' : 'lbs', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f), + ).paddingOnly(bottom: 10.h), + ], ), - SizedBox(width: 8.w), - Text(widget.isKg ? 'kg' : 'lbs', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f)).paddingOnly(bottom: 10.h, left: 8.w), - ], - ), - ], - ); - } -} - -class _WeightRulerPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paintTick = Paint()..color = const Color(0xFF222222); - final paintSmall = Paint()..color = const Color(0xFF222222).withValues(alpha: 0.6); - - final width = size.width; - final start = 30; - final end = 150; - final steps = end - start; - for (int i = 0; i <= steps; i++) { - final x = (i / steps) * width; - if (i % 10 == 0) { - canvas.drawLine(Offset(x, size.height * 0.1), Offset(x, size.height * 0.6), paintTick); - } else if (i % 5 == 0) { - canvas.drawLine(Offset(x, size.height * 0.2), Offset(x, size.height * 0.5), paintSmall); - } else { - canvas.drawLine(Offset(x, size.height * 0.35), Offset(x, size.height * 0.5), paintSmall); - } - } + ), + SizedBox(height: 40.h), + // Horizontal weight picker + WeightScale( + enableHaptic: true, + enableSound: true, + minValue: minValue, + maxValue: maxValue, + initialWeight: viewModel.selectedWeight!, + isKg: isKg, + onWeightChanged: (newWeight) { + log("weight: $newWeight"); + Future.microtask(() => onWeightChanged(newWeight, isKg)); + }, + ), + ], + ); + }); } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } diff --git a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart index 43d485a..725d78e 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart @@ -39,7 +39,6 @@ class _UserInfoFlowManagerState extends State { void initState() { super.initState(); _viewModel = context.read(); - // _viewModel.resetUserInfo(); } @override @@ -57,7 +56,6 @@ class _UserInfoFlowManagerState extends State { curve: Curves.easeInOut, ); } else { - // Submit and navigate to next screen _submitUserInfo(); } } @@ -91,119 +89,165 @@ class _UserInfoFlowManagerState extends State { context.pop(); } - Widget _buildProgressBar(int currentPage) { - return Row( - children: List.generate(4, (index) { - final isActive = index <= currentPage; - return Expanded( - child: Container( - height: 4.h, - margin: EdgeInsets.symmetric(horizontal: 6.w), - decoration: BoxDecoration( - color: isActive ? AppColors.primaryRedColor : AppColors.greyLightColor, - borderRadius: BorderRadius.circular(8.r), - ), - ), + Widget _buildProgressBar() { + return Consumer( + builder: (BuildContext context, viewModel, child) { + return Row( + children: List.generate(4, (index) { + final isCompleted = index < viewModel.userInfoCurrentPage; + final isCurrentStep = index == viewModel.userInfoCurrentPage; + + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 6.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Stack( + children: [ + // Background (grey) + Container( + height: 4.h, + decoration: BoxDecoration( + color: AppColors.greyLightColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + // Animated red fill from left to right + TweenAnimationBuilder( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + tween: Tween( + begin: 0.0, + end: isCompleted + ? 1.0 + : isCurrentStep + ? 1.0 + : 0.0, + ), + builder: (context, value, child) { + return FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: value, + child: Container( + height: 4.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + borderRadius: BorderRadius.circular(8.r), + boxShadow: isCurrentStep && value > 0 + ? [ + BoxShadow( + color: AppColors.primaryRedColor.withValues(alpha: 0.4), + blurRadius: 8.r, + spreadRadius: 1.r, + ), + ] + : null, + ), + ), + ); + }, + ), + ], + ), + ), + ), + ); + }), ); - }), + }, ); } - Widget _buildStickyBottomCard(bool isLastPage) { - return Container( - decoration: BoxDecoration( - color: AppColors.whiteColor, - borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), - ), - padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), - child: SafeArea( - top: false, - child: Row( - children: [ - Expanded( - child: CustomButton( - text: "Previous".needTranslation, - onPressed: _onPrevious, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), - borderColor: Colors.transparent, - textColor: AppColors.primaryRedColor, - fontSize: 16.f, - ), - ), - SizedBox(width: 12.w), - Expanded( - child: CustomButton( - text: isLastPage ? "Submit".needTranslation : "Next".needTranslation, - onPressed: _onNext, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - fontSize: 16.f, + Widget _buildStickyBottomCard() { + return Consumer(builder: (BuildContext context, viewModel, child) { + bool isLastPage = viewModel.isUserInfoLastPage; + bool isFirstPage = viewModel.isUserInfoFirstPage; + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), + ), + padding: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h), + child: SafeArea( + top: false, + child: Row( + children: [ + if (!isFirstPage) ...[ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: _onPrevious, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + ], + Expanded( + child: CustomButton( + text: isLastPage ? "Submit".needTranslation : "Next".needTranslation, + onPressed: _onNext, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), ), - ), - ], + ], + ), ), - ), - ); + ); + }); } @override Widget build(BuildContext context) { - return Consumer( - builder: (context, viewModel, child) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: _pageTitles[viewModel.userInfoCurrentPage].needTranslation, - isLeading: true, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - _buildProgressBar(viewModel.userInfoCurrentPage), - SizedBox(height: 24.h), - SizedBox( - height: 600.h, - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), // Disable swipe - onPageChanged: (index) { - viewModel.setUserInfoPage(index); - }, - children: [ - GenderSelectionPage( - selectedGender: viewModel.selectedGender, - onGenderSelected: viewModel.setGender, - ), - AgeSelectionPage( - selectedAge: viewModel.selectedAge, - onAgeSelected: viewModel.setAge, - ), - HeightSelectionPage( - height: viewModel.selectedHeight ?? 178, - isCm: viewModel.isHeightCm, - onHeightChanged: viewModel.setHeight, - ), - WeightSelectionPage( - weight: viewModel.selectedWeight ?? 70, - isKg: viewModel.isWeightKg, - onWeightChanged: viewModel.setWeight, - ), - ], + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + physics: NeverScrollableScrollPhysics(), + title: _pageTitles[_viewModel.userInfoCurrentPage].needTranslation, + isLeading: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + _buildProgressBar(), + SizedBox(height: 24.h), + SizedBox( + height: 600.h, + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), // Disable swipe + onPageChanged: (index) { + _viewModel.setUserInfoPage(index); + }, + children: [ + GenderSelectionPage( + selectedGender: _viewModel.selectedGender, + onGenderSelected: _viewModel.setGender, ), - ), - ], + AgeSelectionPage( + selectedAge: _viewModel.selectedAge, + onAgeSelected: _viewModel.setAge, + ), + HeightSelectionPage(), + WeightSelectionPage(onWeightChanged: _viewModel.setWeight), + ], + ), ), - ), + ], ), - _buildStickyBottomCard(viewModel.isUserInfoLastPage), - ], + ), ), - ); - }, + _buildStickyBottomCard(), + ], + ), ); } } diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart new file mode 100644 index 0000000..98051e0 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart @@ -0,0 +1,235 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class ThreeColumnDatePicker extends StatefulWidget { + final DateTime initialDate; + final ValueChanged? onDateChanged; + + // Feedback config + final bool enableHaptic; + final bool enableSound; + final Duration feedbackDebounce; + + const ThreeColumnDatePicker({ + super.key, + required this.initialDate, + this.onDateChanged, + this.enableHaptic = true, + this.enableSound = true, + this.feedbackDebounce = const Duration(milliseconds: 80), + }); + + @override + State createState() => _ThreeColumnDatePickerState(); +} + +class _ThreeColumnDatePickerState extends State { + static const int yearRange = 100; + static const double _defaultItemExtent = 48.0; // will be scaled with .h + + late final List _days; + late final List _months; + late final List _years; + + late FixedExtentScrollController _dayController; + late FixedExtentScrollController _monthController; + late FixedExtentScrollController _yearController; + + int _selectedDay = 0; + int _selectedMonth = 0; + int _selectedYearIndex = 0; + + // Debounce timer used for playing feedback only after small pause + Timer? _feedbackTimer; + + double get _itemExtent => _defaultItemExtent.h; + + @override + void initState() { + super.initState(); + + _days = List.generate(31, (i) => (i + 1).toString().padLeft(2, '0')); + _months = const ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + final currentYear = DateTime.now().year; + _years = List.generate(yearRange, (i) => currentYear - i); + + _selectedDay = (widget.initialDate.day - 1).clamp(0, _days.length - 1); + _selectedMonth = (widget.initialDate.month - 1).clamp(0, _months.length - 1); + _selectedYearIndex = _years.indexOf(widget.initialDate.year); + if (_selectedYearIndex == -1) _selectedYearIndex = 0; + + _dayController = FixedExtentScrollController(initialItem: _selectedDay); + _monthController = FixedExtentScrollController(initialItem: _selectedMonth); + _yearController = FixedExtentScrollController(initialItem: _selectedYearIndex); + } + + @override + void dispose() { + _feedbackTimer?.cancel(); + _dayController.dispose(); + _monthController.dispose(); + _yearController.dispose(); + super.dispose(); + } + + void _emitDate() { + final day = int.parse(_days[_selectedDay]); + final month = _selectedMonth + 1; + final year = _years[_selectedYearIndex]; + final date = DateTime(year, month, day); + widget.onDateChanged?.call(date); + } + + // Schedule haptic + sound feedback with debounce (prevents spamming during fling) + void _scheduleFeedback() { + if (!(widget.enableHaptic || widget.enableSound)) return; + + _feedbackTimer?.cancel(); + _feedbackTimer = Timer(widget.feedbackDebounce, () { + // Haptic + if (widget.enableHaptic) { + // selection click is lightweight and appropriate for wheel ticks + HapticFeedback.selectionClick(); + } + // Sound + if (widget.enableSound) { + // simple system click - note: may be muted by device settings + SystemSound.play(SystemSoundType.click); + } + }); + } + + Widget _wheel({ + required FixedExtentScrollController controller, + required int itemCount, + required Widget Function(int index, bool selected) itemBuilder, + required ValueChanged onSelectedItemChanged, + required int currentlySelectedIndex, + }) { + return Expanded( + child: SizedBox( + height: _itemExtent * 5, // show ~5 rows + child: ListWheelScrollView.useDelegate( + controller: controller, + itemExtent: _itemExtent, + physics: const BouncingScrollPhysics(), + diameterRatio: 2.2, + squeeze: 1.2, + perspective: 0.004, + // overAndUnderCenterOpacity: 0.6, + onSelectedItemChanged: (i) { + // update selected index, emit date and schedule feedback + onSelectedItemChanged(i); + _scheduleFeedback(); + }, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + if (index < 0 || index >= itemCount) return null; + final bool selected = index == currentlySelectedIndex; + return Center(child: itemBuilder(index, selected)); + }, + childCount: itemCount, + ), + ), + ), + ); + } + + Widget _styledText(String text, bool selected) { + return Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: selected ? 22.f : 20.f, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? AppColors.textColor : AppColors.greyTextColor.withValues(alpha: 0.9), + height: 1.0, + letterSpacing: selected ? -0.02 * 30 : -0.02 * 18, + ), + ); + } + + @override + Widget build(BuildContext context) { + final pickerHeight = _itemExtent * 5; + final pointerSize = 20.w; + final pointerTop = (pickerHeight / 2) - (pointerSize / 2); + + return LayoutBuilder(builder: (context, constraints) { + return SizedBox( + height: pickerHeight, + child: Stack( + children: [ + Row( + children: [ + // Day wheel + _wheel( + controller: _dayController, + itemCount: _days.length, + currentlySelectedIndex: _selectedDay, + onSelectedItemChanged: (i) { + setState(() => _selectedDay = i); + _emitDate(); + }, + itemBuilder: (index, selected) => _styledText(_days[index], selected), + ), + + // Month wheel + _wheel( + controller: _monthController, + itemCount: _months.length, + currentlySelectedIndex: _selectedMonth, + onSelectedItemChanged: (i) { + setState(() => _selectedMonth = i); + _emitDate(); + }, + itemBuilder: (index, selected) => _styledText(_months[index], selected), + ), + + // Year wheel + _wheel( + controller: _yearController, + itemCount: _years.length, + currentlySelectedIndex: _selectedYearIndex, + onSelectedItemChanged: (i) { + setState(() => _selectedYearIndex = i); + _emitDate(); + }, + itemBuilder: (index, selected) => _styledText(_years[index].toString(), selected), + ), + ], + ), + + // subtle center overlay (optional — keeps layout consistent & highlights center row) + Positioned.fill( + child: IgnorePointer( + child: Center( + child: SizedBox( + height: _itemExtent, + ), + ), + ), + ), + + // left red triangular pointer aligned to center row + Positioned( + left: 0.w, + top: pointerTop, + child: TriangleIndicator( + pointerSize: pointerSize, + // your TriangleIndicator supports direction param; use left as in the original + // if your TriangleIndicator doesn't accept direction, remove the param + direction: TriangleDirection.left, + ), + ), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart new file mode 100644 index 0000000..29baf64 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/height_scale.dart @@ -0,0 +1,169 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HeightScale extends StatefulWidget { + final double minValue; + final double maxValue; + final double initialHeight; + final bool isCm; + final ValueChanged? onHeightChanged; + + // Feedback config + final bool enableHaptic; + final bool enableSound; + final Duration feedbackDebounce; + + const HeightScale({ + super.key, + required this.minValue, + required this.maxValue, + required this.initialHeight, + required this.isCm, + this.onHeightChanged, + this.enableHaptic = true, + this.enableSound = true, + this.feedbackDebounce = const Duration(milliseconds: 80), + }); + + @override + State createState() => _HeightScaleState(); +} + +class _HeightScaleState extends State { + late FixedExtentScrollController _scrollController; + + // Debounce timer used for playing feedback only after small pause + Timer? _feedbackTimer; + + // Get increment based on unit (CM = 1.0, FT = 0.1) + double get _increment => widget.isCm ? 1.0 : 0.1; + + @override + void initState() { + super.initState(); + int initialIndex = ((widget.initialHeight - widget.minValue) / _increment).round(); + _scrollController = FixedExtentScrollController(initialItem: initialIndex); + } + + // Schedule haptic + sound feedback with debounce (prevents spamming during fling) + void _scheduleFeedback() { + if (!(widget.enableHaptic || widget.enableSound)) return; + + _feedbackTimer?.cancel(); + _feedbackTimer = Timer(widget.feedbackDebounce, () { + // Haptic + if (widget.enableHaptic) { + // selection click is lightweight and appropriate for wheel ticks + HapticFeedback.selectionClick(); + } + // Sound + if (widget.enableSound) { + // simple system click - note: may be muted by device settings + SystemSound.play(SystemSoundType.click); + } + }); + } + + @override + void dispose() { + _feedbackTimer?.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final pointerSize = 20.w; + final pickerHeight = 300.h; + final pointerTop = (pickerHeight / 2) - (pointerSize / 2); + + return SizedBox( + height: pickerHeight, + child: Stack( + children: [ + // Scrollable wheel picker + ListWheelScrollView.useDelegate( + controller: _scrollController, + itemExtent: 10.h, + diameterRatio: 2.0, + squeeze: 1.2, + perspective: 0.001, + physics: const BouncingScrollPhysics(), + onSelectedItemChanged: (index) { + final selectedValue = widget.minValue + (index * _increment); + widget.onHeightChanged?.call(selectedValue); + _scheduleFeedback(); + }, + childDelegate: ListWheelChildBuilderDelegate( + childCount: ((widget.maxValue - widget.minValue) / _increment).round() + 1, + builder: (context, index) { + final height = widget.minValue + (index * _increment); + + // For CM: main mark every 10, mid mark every 5 + // For FT: main mark every 1.0 (10 ticks), mid mark every 0.5 (5 ticks) + final isMainMark = widget.isCm ? height % 10 == 0 : (height * 10).round() % 10 == 0; + final isMidMark = widget.isCm ? height % 5 == 0 : (height * 10).round() % 5 == 0; + + return SizedBox( + width: 100.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Number label for main marks + if (isMainMark) + SizedBox( + width: 30.w, + child: Text( + widget.isCm ? height.round().toString() : height.toStringAsFixed(1), + style: TextStyle( + fontSize: 11.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + height: 1, + ), + textAlign: TextAlign.right, + ), + ) + else + SizedBox(width: 30.w), + SizedBox(width: 4.w), + // Ruler mark + Container( + width: isMainMark + ? 40.w + : isMidMark + ? 30.w + : 25.w, + height: isMainMark || isMidMark ? 2.5.h : 1.5.h, + decoration: BoxDecoration( + color: isMainMark + ? AppColors.textColor + : isMidMark + ? AppColors.textColorLight + : AppColors.textColorLight.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2.r), + ), + ), + ], + ), + ); + }, + ), + ), + // Triangle indicator pointing to selected value + Positioned( + right: 0, + top: pointerTop, + child: TriangleIndicator(pointerSize: pointerSize), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart new file mode 100644 index 0000000..429279d --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +enum TriangleDirection { left, right, up, down } + +class TriangleIndicator extends StatelessWidget { + final double pointerSize; + final Color? color; + final TriangleDirection direction; + + const TriangleIndicator({ + super.key, + required this.pointerSize, + this.color, + this.direction = TriangleDirection.right, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(pointerSize, pointerSize), + painter: _TrianglePainter( + color: color ?? AppColors.primaryRedColor, + direction: direction, + ), + ); + } +} + +class _TrianglePainter extends CustomPainter { + final Color color; + final TriangleDirection direction; + + _TrianglePainter({required this.color, required this.direction}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final path = Path(); + final w = size.width; + final h = size.height; + + switch (direction) { + case TriangleDirection.right: + // apex on the right, base on the left + path.moveTo(0, h / 2); + path.lineTo(w, 0); + path.lineTo(w, h); + path.close(); + break; + case TriangleDirection.left: + // apex on the left, base on the right + path.moveTo(w, h / 2); + path.lineTo(0, 0); + path.lineTo(0, h); + path.close(); + break; + case TriangleDirection.up: + // apex on top, base on bottom + path.moveTo(w / 2, 0); + path.lineTo(0, h); + path.lineTo(w, h); + path.close(); + break; + case TriangleDirection.down: + // apex on bottom, base on top + path.moveTo(w / 2, h); + path.lineTo(0, 0); + path.lineTo(w, 0); + path.close(); + break; + } + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _TrianglePainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.direction != direction; + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart deleted file mode 100644 index 6e6d762..0000000 --- a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; - -/// Progress bar widget showing the current step in user info selection flow -/// Total steps: 4 (Gender -> Age -> Height -> Weight) -class UserInfoProgressBar extends StatelessWidget { - final int currentStep; - final int totalSteps; - - const UserInfoProgressBar({ - super.key, - required this.currentStep, - this.totalSteps = 4, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: List.generate(totalSteps, (index) { - final isActive = index < currentStep; - return Expanded( - child: Container( - height: 4.h, - margin: EdgeInsets.symmetric(horizontal: 6.w), - decoration: BoxDecoration( - color: isActive ? AppColors.primaryRedColor : AppColors.greyLightColor, - borderRadius: BorderRadius.circular(8.r), - ), - ), - ); - }), - ); - } -} - diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart deleted file mode 100644 index 3390c31..0000000 --- a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart'; -import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; - -/// Base scaffold for user info selection flow pages -/// Provides consistent layout with progress bar and sticky bottom card -class UserInfoSelectionScaffold extends StatelessWidget { - final String title; - final int currentStep; - final Widget child; - final VoidCallback? onPrevious; - final VoidCallback? onNext; - final bool showPrevious; - final String? nextButtonText; - final bool isScrollable; - - const UserInfoSelectionScaffold({ - super.key, - required this.title, - required this.currentStep, - required this.child, - this.onPrevious, - this.onNext, - this.showPrevious = true, - this.nextButtonText, - this.isScrollable = true, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: title, - isLeading: true, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - UserInfoProgressBar(currentStep: currentStep), - SizedBox(height: 24.h), - isScrollable - ? SingleChildScrollView( - child: child, - ) - : child, - ], - ), - ), - ), - UserInfoStickyBottomCard( - onPrevious: onPrevious, - onNext: onNext, - showPrevious: showPrevious, - nextButtonText: nextButtonText, - ), - ], - ), - ); - } -} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart deleted file mode 100644 index 10fd934..0000000 --- a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_export.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'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; - -/// Sticky bottom card with Previous/Next navigation buttons -class UserInfoStickyBottomCard extends StatelessWidget { - final VoidCallback? onPrevious; - final VoidCallback? onNext; - final bool showPrevious; - final String? nextButtonText; - - const UserInfoStickyBottomCard({ - super.key, - this.onPrevious, - this.onNext, - this.showPrevious = true, - this.nextButtonText, - }); - - @override - Widget build(BuildContext context) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 16.h), - Row( - children: [ - if (showPrevious) ...[ - Expanded( - child: CustomButton( - text: "Previous".needTranslation, - onPressed: onPrevious ?? () => context.pop(), - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), - borderColor: Colors.transparent, - textColor: AppColors.primaryRedColor, - fontSize: 16.f, - ), - ), - SizedBox(width: 12.w), - ], - Expanded( - child: CustomButton( - text: nextButtonText ?? "Next".needTranslation, - onPressed: onNext ?? () {}, - 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/user_info_selection/widgets/weight_scale.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart new file mode 100644 index 0000000..8526a52 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/weight_scale.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/triangle_indicator.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class WeightScale extends StatefulWidget { + final double minValue; + final double maxValue; + final double initialWeight; + final bool isKg; + final ValueChanged? onWeightChanged; + + // Feedback config + final bool enableHaptic; + final bool enableSound; + final Duration feedbackDebounce; + + const WeightScale({ + super.key, + required this.minValue, + required this.maxValue, + required this.initialWeight, + required this.isKg, + this.onWeightChanged, + this.enableHaptic = true, + this.enableSound = true, + this.feedbackDebounce = const Duration(milliseconds: 80), + }); + + @override + State createState() => _WeightScaleState(); +} + +class _WeightScaleState extends State { + late ScrollController _scrollController; + + // Debounce timer used for playing feedback only after small pause + Timer? _feedbackTimer; + + int? _lastReportedIndex; + final double _itemWidth = 8.0; // Width per weight unit + + @override + void initState() { + super.initState(); + int initialIndex = (widget.initialWeight - widget.minValue).round(); + final initialOffset = initialIndex * _itemWidth; + _scrollController = ScrollController(initialScrollOffset: initialOffset); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (!_scrollController.hasClients) return; + + final offset = _scrollController.offset; + final index = (offset / _itemWidth).round(); + final maxIndex = (widget.maxValue - widget.minValue).round(); + + if (index != _lastReportedIndex && index >= 0 && index <= maxIndex) { + _lastReportedIndex = index; + final selectedValue = widget.minValue + index; + widget.onWeightChanged?.call(selectedValue); + _scheduleFeedback(); + } + } + + // Schedule haptic + sound feedback with debounce (prevents spamming during fling) + void _scheduleFeedback() { + if (!(widget.enableHaptic || widget.enableSound)) return; + + _feedbackTimer?.cancel(); + _feedbackTimer = Timer(widget.feedbackDebounce, () { + // Haptic + if (widget.enableHaptic) { + // selection click is lightweight and appropriate for wheel ticks + HapticFeedback.selectionClick(); + } + // Sound + if (widget.enableSound) { + // simple system click - note: may be muted by device settings + SystemSound.play(SystemSoundType.click); + } + }); + } + + @override + void dispose() { + _feedbackTimer?.cancel(); + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final pointerSize = 20.h; + final pickerHeight = 100.h; + final itemCount = (widget.maxValue - widget.minValue).round() + 1; + + return SizedBox( + height: pickerHeight, + child: Stack( + alignment: Alignment.center, + children: [ + // Horizontal scrollable ruler with gradient fade + ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width / 2), + itemCount: itemCount, + itemBuilder: (context, index) { + final weight = (widget.minValue + index).round(); + final isMainMark = weight % 10 == 0; + final isMidMark = weight % 5 == 0; + + return SizedBox( + width: _itemWidth, + child: Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + // Ruler mark (vertical line) + Positioned( + bottom: 0, + child: Container( + width: isMainMark || isMidMark ? 2.5.w : 1.5.w, + height: isMainMark + ? 40.h + : isMidMark + ? 30.h + : 25.h, + decoration: BoxDecoration( + color: isMainMark + ? AppColors.textColor + : isMidMark + ? AppColors.textColorLight + : AppColors.textColorLight.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2.r), + ), + ), + ), + // Number label for main marks + if (isMainMark) + Positioned( + bottom: 45.h, + child: Text( + weight.toString(), + style: TextStyle( + fontSize: 11.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + height: 1, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.visible, + ), + ), + ], + ), + ); + }, + ), + // Triangle indicator pointing to selected value + Positioned( + bottom: 0, + child: TriangleIndicator( + pointerSize: pointerSize, + direction: TriangleDirection.up, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index ad8e743..7329de9 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -27,6 +27,7 @@ class CollapsingListView extends StatelessWidget { bool isClose; bool isLeading; VoidCallback? leadingCallback; + ScrollPhysics? physics; CollapsingListView({ super.key, @@ -43,6 +44,7 @@ class CollapsingListView extends StatelessWidget { this.isLeading = true, this.trailing, this.leadingCallback, + this.physics, }); @override @@ -53,6 +55,7 @@ class CollapsingListView extends StatelessWidget { body: Column( children: [ CustomScrollView( + physics: physics, slivers: [ SliverAppBar( automaticallyImplyLeading: false, @@ -119,11 +122,18 @@ class CollapsingListView extends StatelessWidget { color: AppColors.blackColor, letterSpacing: -0.5), ).expanded, - if (logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!), - if (report != null) actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(report!), - if (history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(history!), - if (instructions != null) actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(instructions!), - if (requests != null) actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon).onPress(requests!), + if (logout != null) + actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!), + if (report != null) + actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(report!), + if (history != null) + actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon) + .onPress(history!), + if (instructions != null) + actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(instructions!), + if (requests != null) + actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon) + .onPress(requests!), if (search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(search!).paddingOnly(right: 24), if (trailing != null) trailing!, ],