diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 24aa244..cd4065e 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:core'; import 'dart:math'; @@ -39,6 +40,13 @@ class LabViewModel extends ChangeNotifier { String labSpecialResult = ""; List labOrderTests = []; String patientLabResultReportPDFBase64 = ""; + String? flagForHighReferenceRange; + double highRefrenceValue = double.negativeInfinity; + double lowRefenceValue = double.infinity; + + double highTransformedReferenceValue = double.negativeInfinity; + double lowTransformedReferenceValue = double.infinity; + String? flagForLowReferenceRange; PatientLabOrdersResponseModel? currentlySelectedPatientOrder; @@ -67,8 +75,11 @@ class LabViewModel extends ChangeNotifier { List get labSuggestions => _labSuggestionsList; Set uniqueTests = {}; + List uniqueTestsList = []; + List indexedCharacterForUniqueTest = []; double maxY = 0.0; + double minY = double.infinity; double maxX = double.infinity; LabViewModel( @@ -195,6 +206,19 @@ class LabViewModel extends ChangeNotifier { createdOn: item.createdOn, model: item)) }; + var sortedResult = SplayTreeSet.from(uniqueTests, (a, b) => a.description?[0].toUpperCase().compareTo(b.description?[0] ?? "") ?? -1); + uniqueTestsList = uniqueTests.toList(); + uniqueTestsList.sort((a, b) { + return a.description!.toLowerCase().compareTo(b.description!.toLowerCase()); + }); + indexedCharacterForUniqueTest.clear(); + for (var test in uniqueTestsList) { + String label = test.description ?? ""; + if (label.isEmpty) continue; + if (indexedCharacterForUniqueTest.contains(label[0].toLowerCase())) continue; + indexedCharacterForUniqueTest.add(label[0].toLowerCase()); + } + for (var element in uniqueTests) { labOrderTests.add(element.description ?? ""); } @@ -294,7 +318,8 @@ class LabViewModel extends ChangeNotifier { mainLabResults.clear(); filteredGraphValues.clear(); maxY = double.negativeInfinity; - + minY = double.infinity; + maxX = double.infinity; final result = await labRepo.getPatientLabResults( laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")), @@ -313,7 +338,12 @@ class LabViewModel extends ChangeNotifier { var recentThree = sort(sortedResponse); mainLabResults = recentThree; - double counter = 1; + + double highRefrenceValue = double.negativeInfinity; + String? flagForHighReferenceRange; + double lowRefenceValue = double.infinity; + String? flagForLowReferenceRange; + recentThree.reversed.forEach((element) { try { var dateTime = @@ -324,6 +354,17 @@ class LabViewModel extends ChangeNotifier { maxY = resultValue; maxX = maxY; } + if (resultValue < minY) { + minY = resultValue; + } + if (highRefrenceValue < double.parse(element.referenceHigh ?? "0.0")) { + highRefrenceValue = double.parse(element.referenceHigh ?? "0.0"); + flagForHighReferenceRange = element.calculatedResultFlag; + } + if (lowRefenceValue > double.parse(element.referenceLow ?? "0.0")) { + lowRefenceValue = double.parse(element.referenceLow ?? "0.0"); + flagForLowReferenceRange = element.calculatedResultFlag; + } filteredGraphValues.add(DataPoint( value: transformedValue, @@ -335,9 +376,25 @@ class LabViewModel extends ChangeNotifier { referenceValue: element.calculatedResultFlag ?? "", )); - counter++; } catch (e) {} }); + if (flagForLowReferenceRange == null && flagForHighReferenceRange == null) { + highRefrenceValue = maxY; + lowRefenceValue = minY; + } + + this.flagForHighReferenceRange = flagForHighReferenceRange; + this.flagForLowReferenceRange = flagForLowReferenceRange; + highTransformedReferenceValue = double.parse(transformValueInRange(highRefrenceValue, flagForHighReferenceRange ?? "").toStringAsFixed(1)); + lowTransformedReferenceValue = double.parse(transformValueInRange(lowRefenceValue, flagForLowReferenceRange ?? "").toStringAsFixed(1)); + this.highRefrenceValue = double.parse(highRefrenceValue.toStringAsFixed(1)); + this.lowRefenceValue = double.parse(lowRefenceValue.toStringAsFixed(1)); + + if(maxY< highRefrenceValue) { + maxY = highRefrenceValue; + } + maxY += 25; + minY -= 25; LabResult recentResult = recentThree.first; recentResult.uOM = unitOfMeasure; checkIfGraphShouldBeDisplayed(recentResult); diff --git a/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart b/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart index 8bef76a..1c437a4 100644 --- a/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart +++ b/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart @@ -250,4 +250,7 @@ class TestDetails { data['CreatedOn'] = this.createdOn; return data; } + + @override + String toString() { return description??"";} } diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart new file mode 100644 index 0000000..7593ea1 --- /dev/null +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -0,0 +1,235 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart' show AnimationConfiguration, SlideAnimation, FadeInAnimation; +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/debouncer.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +import 'lab_order_by_test.dart'; + +class AlphabeticScroll extends StatefulWidget{ + final List alpahbetsAvailable; + final List details; + final AppState appState; + final LabViewModel labViewModel; + final DateRangeSelectorRangeViewModel rangeViewModel; + + const AlphabeticScroll({super.key, required this.alpahbetsAvailable, required this.details, required this.appState, required this.labViewModel, required this.rangeViewModel}); + + @override + State createState() => _AlphabetScrollPageState(); +} + +class _AlphabetScrollPageState extends State { + final ItemScrollController itemScrollController = ItemScrollController(); + final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); + final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); + final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create(); + final ScrollController _scrollController = ScrollController(); + + Map> data = {}; + Map density = {}; + + Map _offsetMap = {}; + Map _offsetKeys = {}; + int _activeIndex = 0; // <-- Highlighted letter + + @override + void initState() { + super.initState(); + scheduleMicrotask((){ + for(var char in widget.alpahbetsAvailable){ + data[char] = widget.details.where((element)=>element.description?.toLowerCase().startsWith(char.toLowerCase()) == true).toList(); + + } + setState((){}); + + }); + itemPositionsListener.itemPositions.addListener((){ + + final positions = itemPositionsListener.itemPositions.value; + + if (positions.isEmpty) return; + + // Get FIRST visible item (top-most) + final firstVisible = positions + .where((p) => p.itemTrailingEdge > 0) // visible + .reduce((min, p) => + p.itemLeadingEdge < min.itemLeadingEdge ? p : min); + + if(_activeIndex == firstVisible.index) return ; + setState(() { + _activeIndex = firstVisible.index; + }); + + print("Active index = $_activeIndex"); + }); + } + + @override + void dispose() { + itemPositionsListener.itemPositions.removeListener((){ + + }); + super.dispose(); + + } + + void _scrollToLetter(String letter) async { + + // itemScrollController.jumpTo(index:density[letter]?.toInt()??0, ); + itemScrollController.scrollTo( + index: data.keys.toList().indexOf(letter).toInt()??0, + duration: Duration(seconds: 2), + curve: Curves.easeInOutCubic); + // final key = _offsetKeys[letter]; + // if (key == null) return; + // + // BuildContext? ctx = key.currentContext; + // + // // Retry until built (max 100ms) + // int retry = 0; + // while (ctx == null && retry < 5) { + // await Future.delayed(Duration(milliseconds: 20)); + // ctx = key.currentContext; + // retry++; + // } + // + // if (ctx == null) { + // print("❌ $letter still not built"); + // return; + // } + // + // final renderBox = ctx.findRenderObject() as RenderBox; + // final yOffset = renderBox.localToGlobal(Offset.zero).dy; + // + // _scrollController.animateTo( + // _scrollController.offset + yOffset - 80, + // duration: Duration(milliseconds: 400), + // curve: Curves.easeInOut, + // ); + } + + @override + Widget build(BuildContext context) { + + return + + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, // Add this + children: [ + Expanded( + child: Column( + children: [ + SizedBox( + height: (MediaQuery.sizeOf(context).height), + child: _buildList(widget.alpahbetsAvailable) + ), + ], + ), + ), + + SizedBox( + width: 24.w, + height: MediaQuery.sizeOf(context).height-(70.h+ kToolbarHeight+100.h), + child: Column( + mainAxisSize: MainAxisSize.max, // Add this + mainAxisAlignment: MainAxisAlignment.center, // Changed from center to start + crossAxisAlignment: CrossAxisAlignment.center, + children:List.generate(widget.alpahbetsAvailable.length, (i) { + final isActive = (i == _activeIndex); + + return GestureDetector( + onTap: () { + setState(() => _activeIndex = i); + _scrollToLetter(widget.alpahbetsAvailable[i]); + }, + child: TweenAnimationBuilder( + tween: Tween(begin: 1.0, end: isActive ? 1.8 : 1.0), + duration: Duration(milliseconds: 120), + curve: Curves.easeOut, + builder: (_, scale, child) { + return Transform.scale( + scale: scale, + child: Opacity( + opacity: isActive ? 1.0 : 0.5, + child: widget.alpahbetsAvailable[i].toUpperCase().toText14( + color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor + ), + ), + ); + }, + ), + ); + } + ), + ), + ), + ], + ), + ); + // ); + } + + Widget _buildList(List alphabet) { + return ScrollablePositionedList.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemScrollController: itemScrollController, + scrollOffsetController: scrollOffsetController, + itemPositionsListener: itemPositionsListener, + scrollOffsetListener: scrollOffsetListener, + itemCount: data.length, + itemBuilder: (_, index) { + final letter = alphabet[index].toLowerCase(); + print("the letter is $letter"); + final items = data[letter]!; + return Container( + key: _offsetKeys[letter], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + List.generate(items.length,(itemIndex)=> + AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: LabOrderByTest( + + appState: getIt(), + onTap: () { + if (items[itemIndex].model != null) { + widget.rangeViewModel.flush(); + widget.labViewModel.getPatientLabResult(items[itemIndex].model!, items[itemIndex].description!, + (widget.appState.isArabic() ? items[itemIndex].testDescriptionAr! : items[itemIndex].testDescriptionEn!),""); + } + }, + tests: items[itemIndex], + index: itemIndex, + isExpanded: true)), + ), + )) + // ...items.indexed((item) => + // + // + // + // ) + + ), + ); + }, + ); + } +} diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index 2b791ae..bd56df6 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -27,6 +27,7 @@ class LabOrderByTest extends StatelessWidget { @override build(BuildContext context) { return AnimatedContainer( + key:key, duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 81c507f..a868d59 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.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/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!), ""); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.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/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingToolbar( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) // : ListView.builder( // shrinkWrap: true, // physics: NeverScrollableScrollPhysics(), // padding: EdgeInsets.zero, // itemCount: model.isLabOrdersLoading // ? 5 // : model.uniqueTests.toList().isNotEmpty // ? model.uniqueTests.toList().length // : 1, // itemBuilder: (context, index) { // final isExpanded = expandedIndex == index; // return model.isLabOrdersLoading // ? LabResultItemView( // onTap: () {}, // labOrder: null, // index: index, // isLoading: true, // ) // : model.uniqueTests.toList().isNotEmpty // ? AnimationConfiguration.staggeredList( // position: index, // duration: const Duration(milliseconds: 500), // child: SlideAnimation( // verticalOffset: 100.0, // child: FadeInAnimation( // child: LabOrderByTest( // appState: _appState, // onTap: () { // if (model.uniqueTests.toList()[index].model != null) { // rangeViewModel.flush(); // model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, // (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); // } // }, // tests: model.uniqueTests.toList()[index], // index: index, // isExpanded: isExpanded)), // ), // ) // : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); // }, // ) : (model.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) :AlphabeticScroll( alpahbetsAvailable: model.indexedCharacterForUniqueTest, details: model.uniqueTestsList, labViewModel: model, rangeViewModel: rangeViewModel, appState: _appState, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 67eae63..1d54e06 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; @@ -236,29 +237,41 @@ class LabResultDetails extends StatelessWidget { dataPoints: labmodel.filteredGraphValues, makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, + showGridLines: true, leftLabelInterval: getInterval(labmodel), - maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/2, + // maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/2, + maxY: (labmodel.maxY), + minY: labmodel.minY, maxX: labmodel.filteredGraphValues.length.toDouble()-.75, + horizontalInterval: .1, + getDrawingHorizontalLine: (value){ + value = double.parse(value.toStringAsFixed(1)); + if(value == labmodel.highRefrenceValue ||value== labmodel.lowRefenceValue) { + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.6), + strokeWidth: 1, + // dashArray: [5, 5], + ); + } + return FlLine( + color: Colors.transparent, + strokeWidth: 1, + );; + }, leftLabelFormatter: (value) { - return leftLabels(value.toStringAsFixed(2)); - // switch (value.toInt()) { - // case 10: - // return leftLabels("Critical Low".needTranslation); - // case 30: - // return leftLabels("Low".needTranslation); - // case 50: - // return leftLabels("Normal".needTranslation); - // case 70: - // return leftLabels("High".needTranslation); - // case 90: - // return leftLabels( - // "Critical High".needTranslation); - // default: - // return SizedBox.shrink(); + value = double.parse(value.toStringAsFixed(1)); + // return leftLabels(value.toStringAsFixed(2)); + if(value == labmodel.highRefrenceValue) + return leftLabels("High".needTranslation); + + if(value== labmodel.lowRefenceValue) + return leftLabels("Low".needTranslation); + + return SizedBox.shrink(); // } }, - graphColor:graphColor , - graphShadowColor: graphColor.withOpacity(.1), + graphColor:AppColors.blackColor, + graphShadowColor: Colors.transparent, graphGridColor: graphColor.withOpacity(.4), bottomLabelFormatter: (value, data) { if(data.isEmpty) return SizedBox.shrink(); @@ -273,6 +286,9 @@ class LabResultDetails extends StatelessWidget { } return SizedBox.shrink(); }, + rangeAnnotations: RangeAnnotations( + horizontalRangeAnnotations: _buildRangeShades(labmodel) + ), minX:(labmodel.filteredGraphValues.length == 1)?null : -.2, scrollDirection: Axis.horizontal, height: 180.h); @@ -281,6 +297,29 @@ class LabResultDetails extends StatelessWidget { } } + List _buildRangeShades( LabViewModel model,) { + List ranges = []; + + ranges.add(HorizontalRangeAnnotation( + y1:model.minY, + y2: model.lowRefenceValue, + color: AppColors.highAndLow.withOpacity(0.05), + )); + + ranges.add(HorizontalRangeAnnotation( + y1:model.lowRefenceValue, + y2: model.highRefrenceValue, + color: AppColors.bgGreenColor.withOpacity(0.05), + )); + + ranges.add(HorizontalRangeAnnotation( + y1:model.highRefrenceValue, + y2: model.maxY, + color: AppColors.criticalLowAndHigh.withOpacity(0.05), + )); + return ranges; + } + Widget labHistoryList(LabViewModel labmodel) { return SizedBox( height: labmodel.filteredGraphValues.length<3?labmodel.filteredGraphValues.length*64:180.h, @@ -304,14 +343,15 @@ class LabResultDetails extends StatelessWidget { } double? getInterval(LabViewModel labmodel) { - var maxX = labmodel.maxY; - if(maxX<1) return .5; - if(maxX >1 && maxX < 5) return 1; - if(maxX >5 && maxX < 10) return 5; - if(maxX >10 && maxX < 50) return 10; - if(maxX >50 && maxX < 100) return 20; - if(maxX >100 && maxX < 200) return 30; - return 50; + return .1; + // var maxX = labmodel.maxY; + // if(maxX<1) return .5; + // if(maxX >1 && maxX < 5) return 1; + // if(maxX >5 && maxX < 10) return 5; + // if(maxX >10 && maxX < 50) return 10; + // if(maxX >50 && maxX < 100) return 20; + // if(maxX >100 && maxX < 200) return 30; + // return 50; } Widget getLabDescription(BuildContext context) { diff --git a/lib/widgets/appbar/collapsing_toolbar.dart b/lib/widgets/appbar/collapsing_toolbar.dart new file mode 100644 index 0000000..87cf15a --- /dev/null +++ b/lib/widgets/appbar/collapsing_toolbar.dart @@ -0,0 +1,205 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.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/utils/utils.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 '../../core/dependencies.dart'; + +class CollapsingToolbar extends StatefulWidget { + final String title; + Widget child; + VoidCallback? search; + VoidCallback? report; + VoidCallback? logout; + VoidCallback? history; + VoidCallback? instructions; + VoidCallback? requests; + Widget? bottomChild; + Widget? trailing; + bool isClose; + bool isLeading; + + CollapsingToolbar({ + super.key, + required this.title, + required this.child, + this.search, + this.isClose = false, + this.bottomChild, + this.report, + this.logout, + this.history, + this.instructions, + this.requests, + this.isLeading = true, + this.trailing, + }); + + @override + State createState() => _CollapsingToolbarState(); +} + +class _CollapsingToolbarState extends State { + bool isCollapsed = false; + + final ScrollController _controller = ScrollController(); + double expandedHeight = 0; + + double get maxCollapseOffset => expandedHeight - kToolbarHeight; + @override + void initState() { + super.initState(); + + + _controller.addListener(() { + // If scrolling UP beyond collapsed point → force stop + print("the height is $maxCollapseOffset"); + if (_controller.offset > maxCollapseOffset) { + _controller.jumpTo(maxCollapseOffset); + } + }); + } + + @override + Widget build(BuildContext context) { + expandedHeight = MediaQuery.of(context).size.height * 0.11.h; + AppState appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: + // Column( + // children: [ + NestedScrollView( + controller: _controller, + floatHeaderSlivers: true, + physics: isCollapsed?NeverScrollableScrollPhysics():BouncingScrollPhysics(), + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverAppBar( + automaticallyImplyLeading: false, + pinned: true, + expandedHeight: MediaQuery.of(context).size.height * 0.11.h, + stretch: true, + systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), + surfaceTintColor: Colors.transparent, + backgroundColor: AppColors.bgScaffoldColor, + leading: widget.isLeading + ? Transform.flip( + flipX: appState.isArabic(), + child: IconButton( + icon: Utils.buildSvgWithAssets(icon: widget.isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), + padding: EdgeInsets.only(left: 12), + onPressed: () => Navigator.pop(context), + highlightColor: Colors.transparent, + ), + ) + : SizedBox.shrink(), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final double maxHeight = 100.h; + final double minHeight = kToolbarHeight; + double t = (constraints.maxHeight - minHeight) / (maxHeight - minHeight); + t = t - 1; + if (t < 0.7) t = 0.7; + t = t.clamp(0.0, 1.0); + + final double fontSize = lerpDouble(14, 18, t)!; + final double bottomPadding = lerpDouble(0, 0, t)!; + final double leftPadding = lerpDouble(150, 24, t)!; + + return Stack( + children: [ + Align( + alignment: Alignment.lerp( + Alignment.center, + Alignment.bottomLeft, + t, + )!, + child: Padding( + padding: EdgeInsets.only(left: appState.isArabic() ? 0 : leftPadding, right: appState.isArabic() ? leftPadding : 0, bottom: bottomPadding), + child: Row( + spacing: 4.h, + children: [ + Text( + widget.title, + maxLines: 1, + style: TextStyle( + fontSize: (27 - (5 * (2 - t))).f, + fontWeight: FontWeight.lerp( + FontWeight.w300, + FontWeight.w600, + t, + )!, + color: AppColors.blackColor, + letterSpacing: -0.5), + ).expanded, + if (widget.logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(widget.logout!), + if (widget.report != null) actionButton(context, t, title: "Report".needTranslation, icon: AppAssets.report_icon).onPress(widget.report!), + if (widget.history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.history!), + if (widget.instructions != null) actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(widget.instructions!), + if (widget.requests != null) actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.requests!), + if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!).paddingOnly(right: 24), + if (widget.trailing != null) widget.trailing!, + ], + )), + ), + ], + ); + }, + ), + ), + ]; + }, + body: widget.child, + ), + + // ], + // ), + ); + } + + Widget actionButton(BuildContext context, double t, {required String title, required String icon}) { + return AnimatedSize( + duration: Duration(milliseconds: 150), + child: Container( + height: 40.h, + padding: EdgeInsets.all(8.w), + margin: EdgeInsets.only(right: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.secondaryLightRedColor, + borderRadius: 10.r, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 8.h, + children: [ + Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.primaryRedColor), + if (t == 1) + Text( + title, + style: context.dynamicTextStyle( + color: AppColors.primaryRedColor, + letterSpacing: -0.4, + fontSize: (14 - (2 * (1 - t))).f, + fontWeight: FontWeight.lerp( + FontWeight.w300, + FontWeight.w500, + t, + )!, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index 7fc1f2a..b955b32 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -56,6 +56,12 @@ class CustomGraph extends StatelessWidget { final FontWeight? bottomLabelFontWeight; final double? leftLabelInterval; final double? leftLabelReservedSize; + final bool? showGridLines; + final GetDrawingGridLine? getDrawingHorizontalLine; + final double? horizontalInterval; + final double? minY; + final bool showShadow; + final RangeAnnotations? rangeAnnotations; ///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart final Widget Function(double) leftLabelFormatter; @@ -89,14 +95,17 @@ class CustomGraph extends StatelessWidget { this.leftLabelReservedSize, this.makeGraphBasedOnActualValue = false, required this.bottomLabelFormatter, - this.minX, + this.minX, + this.showGridLines = false, + this.getDrawingHorizontalLine, + this.horizontalInterval, + this.minY, + this.showShadow = false, + this.rangeAnnotations }); @override Widget build(BuildContext context) { - // var maxY = 0.0; - double interval = 20; - return Material( color: Colors.white, child: SizedBox( @@ -104,10 +113,8 @@ class CustomGraph extends StatelessWidget { height: height, child: LineChart( LineChartData( - minY: 0, - // maxY: ((maxY?.ceilToDouble() ?? 0.0) + interval).floorToDouble(), + minY: minY??0, maxY: maxY, - // minX: dataPoints.first.labelValue - 1, maxX: maxX, minX: minX , lineTouchData: LineTouchData( @@ -190,12 +197,12 @@ class CustomGraph extends StatelessWidget { ), lineBarsData: _buildColoredLineSegments(dataPoints), gridData: FlGridData( - show: true, + show: showGridLines??true, drawVerticalLine: false, - // horizontalInterval: 40, - checkToShowHorizontalLine: (value) => - value >= 0 && value <= 100, - getDrawingHorizontalLine: (value) { + horizontalInterval:horizontalInterval, + // checkToShowHorizontalLine: (value) => + // value >= 0 && value <= 100, + getDrawingHorizontalLine: getDrawingHorizontalLine??(value) { return FlLine( color: graphGridColor, strokeWidth: 1, @@ -203,15 +210,17 @@ class CustomGraph extends StatelessWidget { ); }, ), + rangeAnnotations: rangeAnnotations ), ), - )); + ), + ); } + List _buildColoredLineSegments(List dataPoints) { final List allSpots = dataPoints.asMap().entries.map((entry) { double value = (makeGraphBasedOnActualValue)?double.tryParse(entry.value.actualValue)??0.0:entry.value.value; - debugPrint("the value is $value"); return FlSpot(entry.key.toDouble(), value); }).toList(); @@ -221,7 +230,7 @@ class CustomGraph extends StatelessWidget { isCurved: true, isStrokeCapRound: true, isStrokeJoinRound: true, - barWidth: 4, + barWidth: 2, gradient: LinearGradient( colors: [graphColor, graphColor], begin: Alignment.centerLeft, @@ -231,7 +240,7 @@ class CustomGraph extends StatelessWidget { show: false, ), belowBarData: BarAreaData( - show: true, + show: showShadow, gradient: LinearGradient( colors: [ graphShadowColor, @@ -246,33 +255,4 @@ class CustomGraph extends StatelessWidget { return data; } - - // Widget buildLabel(String label) { - // return Padding( - // padding: const EdgeInsets.only(right: 8), - // child: Text( - // label, - // style: TextStyle( - // fontSize: leftLabelSize ?? 8.fSize, color: leftLabelColor), - // textAlign: TextAlign.right, - // ), - // ); - // } - - -} - -// final List sampleData = [ -// DataPoint( -// value: 20, -// label: 'Jan 2024', -// ), -// DataPoint( -// value: 36, -// label: 'Feb 2024', -// ), -// DataPoint( -// value: 80, -// label: 'This result', -// ), -// ]; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index cd26847..c4fa97b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -93,6 +93,8 @@ dependencies: url: https://github.com/fleoparra/hms-flutter-plugin.git path: flutter-hms-map + scrollable_positioned_list: ^0.3.8 + dev_dependencies: flutter_test: sdk: flutter