lab result added along with the available characters scroll bar
#111
Merged
Haroon6138
merged 5 commits from lab-result-changes into master 4 months ago
@ -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<String> alpahbetsAvailable;
|
||||||
|
final List<TestDetails> 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<AlphabeticScroll> createState() => _AlphabetScrollPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlphabetScrollPageState extends State<AlphabeticScroll> {
|
||||||
|
final ItemScrollController itemScrollController = ItemScrollController();
|
||||||
|
final ScrollOffsetController scrollOffsetController = ScrollOffsetController();
|
||||||
|
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
Map<String, List<TestDetails>> data = {};
|
||||||
|
Map<String, num> density = {};
|
||||||
|
|
||||||
|
Map<String, double> _offsetMap = {};
|
||||||
|
Map<String, GlobalKey> _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<double>(
|
||||||
|
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<String> 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<AppState>(),
|
||||||
|
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) =>
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@ -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<CollapsingToolbar> createState() => _CollapsingToolbarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollapsingToolbarState extends State<CollapsingToolbar> {
|
||||||
|
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<AppState>();
|
||||||
|
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,
|
||||||
|
)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue