lab result added along with the available characters scroll bar
#111
Merged
Haroon6138
merged 5 commits from lab-result-changes into master 2 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