@ -1,44 +1,274 @@
import ' dart:ui ' as ui ;
import ' package:easy_localization/easy_localization.dart ' ;
import ' package:easy_localization/easy_localization.dart ' ;
import ' package:flutter/material.dart ' ;
import ' package:flutter/material.dart ' ;
import ' package:hmg_patient_app_new/core/ utils/utils .dart' ;
import ' package:hmg_patient_app_new/core/ app_state .dart' ;
import ' package:hmg_patient_app_new/extensions/string_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/lab/lab_view_model.dart ' ;
import ' package:hmg_patient_app_new/features/lab/lab_view_model.dart ' ;
import ' package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart ' ;
import ' package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart ' ;
import ' package:hmg_patient_app_new/generated/locale_keys.g.dart ' ;
import ' package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart ' ;
import ' package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart ' ;
import ' package:provider/provider.dart ' show Selector , Provider , ReadContext ;
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:provider/provider.dart ' ;
import ' ../../../core/dependencies.dart ' show getIt ;
import ' ../../../core/utils/size_utils.dart ' show ResponsiveExtension ;
import ' ../../../generated/locale_keys.g.dart ' show LocaleKeys ;
import ' ../../../widgets/date_range_selector/viewmodel/date_range_view_model.dart ' ;
class LabResultList extends StatefulWidget {
@ override
State < LabResultList > createState ( ) = > _LabResultListState ( ) ;
}
class LabResultList extends StatelessWidget {
class _LabResultListState extends State < LabResultList > {
late LabViewModel model ;
late LabViewModel model ;
int expandedIndex = - 1 ;
Map < int , bool > descriptionExpandedMap = { } ;
@ override
@ override
Widget build ( BuildContext context ) {
Widget build ( BuildContext context ) {
model = Provider . of < LabViewModel > ( context ) ;
model = Provider . of < LabViewModel > ( context ) ;
return Selector < LabViewModel , List< LabResult > > (
return Selector < LabViewModel , Map< String , List< LabResult > > > (
selector: ( _ , model ) = > model . mainLabResultsBy Hospitals ,
selector: ( _ , model ) = > model . mainLabResultsBy PackageName ,
builder: ( __ , list , ___ ) {
builder: ( __ , groupedResults , ___ ) {
if ( list . isEmpty & & context . read < LabViewModel > ( ) . labSpecialResult . isEmpty ) {
if ( groupedResults . isEmpty & & context . read < LabViewModel > ( ) . labSpecialResult . isEmpty ) {
/ / return Utils . getNoDataWidget ( context ,
/ / return Utils . getNoDataWidget ( context ,
/ / noDataText: LocaleKeys . noLabResults . tr ( context: context ) ) ;
/ / noDataText: LocaleKeys . noLabResults . tr ( context: context ) ) ;
return Container ( ) ;
return Container ( ) ;
} else {
} else {
/ / Convert map entries to a list for indexed access
final packageEntries = groupedResults . entries . toList ( ) ;
return ListView . builder (
return ListView . builder (
physics: NeverScrollableScrollPhysics ( ) ,
physics: NeverScrollableScrollPhysics ( ) ,
padding: EdgeInsets . zero ,
padding: EdgeInsets . zero ,
shrinkWrap: true , itemCount: list . length , itemBuilder: ( ____ , index ) {
shrinkWrap: true ,
var labItem = list [ index ] ;
itemCount: packageEntries . length ,
return LabOrderResultItem ( onTap: ( ) {
itemBuilder: ( context , packageIndex ) {
final packageName = packageEntries [ packageIndex ] . key ;
final labResults = packageEntries [ packageIndex ] . value ;
final isExpanded = expandedIndex = = packageIndex ;
return AnimatedContainer (
duration: Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
margin: EdgeInsets . symmetric ( vertical: 8. h ) ,
decoration: RoundedRectangleBorder ( ) . toSmoothCornerDecoration ( color: AppColors . whiteColor , borderRadius: 24. r , hasShadow: true ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
/ / Package name as title / header
Padding (
padding: EdgeInsets . symmetric ( horizontal: 16. w , vertical: 16.0 . h ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
spacing: 8. h ,
children: [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Expanded (
child: packageName . toText18 (
isBold: true
) ,
) ,
AnimatedRotation (
turns: isExpanded ? 0.5 : 0 ,
duration: Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
child: Icon ( Icons . expand_more ) ,
) ,
] ,
) ,
if ( labResults . first . packageShortDescription ! = null )
_ExpandableDescriptionText (
description: labResults . first . packageShortDescription ! ,
isExpanded: descriptionExpandedMap [ packageIndex ] ? ? false ,
onToggle: ( ) {
setState ( ( ) {
descriptionExpandedMap [ packageIndex ] = ! ( descriptionExpandedMap [ packageIndex ] ? ? false ) ;
} ) ;
} ,
)
else
SizedBox . shrink ( )
] ,
) , ) ,
/ / Lab results under this package with animation
AnimatedSize (
duration: Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
child: isExpanded
? Column (
children: labResults . asMap ( ) . entries . map ( ( entry ) {
final index = entry . key ;
final labItem = entry . value ;
return TweenAnimationBuilder < double > (
tween: Tween ( begin: 0.0 , end: 1.0 ) ,
duration: Duration ( milliseconds: 300 + ( index * 50 ) ) ,
curve: Curves . easeOut ,
builder: ( context , value , child ) {
return Opacity (
opacity: value ,
child: Transform . translate (
offset: Offset ( 0 , 20 * ( 1 - value ) ) ,
child: child ,
) ,
) ;
} ,
child: Column (
children: [
LabOrderResultItem (
onTap: ( ) {
context . read < DateRangeSelectorRangeViewModel > ( ) . flush ( ) ;
context . read < DateRangeSelectorRangeViewModel > ( ) . flush ( ) ;
model . getPatientLabResult ( model . currentlySelectedPatientOrder ! , labItem . description ? ? " " , labItem . testShortDescription ? ? " " , labItem . uOM ? ? " " ) ;
model . getPatientLabResult (
model . currentlySelectedPatientOrder ! ,
labItem . description ? ? " " ,
labItem . testShortDescription ? ? " " ,
labItem . uOM ? ? " "
) ;
} ,
} ,
tests: labItem ,
tests: labItem ,
index: index ,
index: index ,
iconColor: model . getColor ( labItem . calculatedResultFlag ? ? " N " ) ,
iconColor: model . getColor ( labItem . calculatedResultFlag ? ? " N " ) ,
severityText: model . getSeverityText ( labItem . calculatedResultFlag ? ? " N " ) ) ;
severityText: model . getSeverityText ( labItem . calculatedResultFlag ? ? " N " )
) ,
if ( labResults . length > 1 )
. . . [ SizedBox (
height: 8. h ,
) ,
Padding (
padding: EdgeInsets . symmetric ( horizontal: 16. h ) ,
child: Divider (
color: AppColors . dividerColor ,
thickness: 1. h ,
) ,
) ]
] ,
) ,
) ;
} ) . toList ( ) ,
)
: SizedBox . shrink ( ) ,
) ,
] ,
) ,
) . onPress ( ( ) {
setState ( ( ) {
expandedIndex = isExpanded ? - 1 : packageIndex ;
} ) ;
} ) ;
} ) ;
}
}
) ;
}
} ,
) ;
}
}
class _ExpandableDescriptionText extends StatelessWidget {
final String description ;
final bool isExpanded ;
final VoidCallback onToggle ;
const _ExpandableDescriptionText ( {
required this . description ,
required this . isExpanded ,
required this . onToggle ,
} ) ;
static TextStyle get textStyle = > TextStyle (
fontSize: 14. f ,
color: AppColors . textColorLight ,
/ / or Colors . black87
/ / fontFamily: ' Poppins ' ,
fontWeight: FontWeight . w600 ,
fontStyle: FontStyle . normal ,
height: 1.4 ,
letterSpacing: 0.0 ,
decoration: TextDecoration . none ,
) ;
@ override
Widget build ( BuildContext context ) {
final readMoreText = isExpanded
? ' ${ LocaleKeys . hide . tr ( ) } '
: ' ${ LocaleKeys . readMore . tr ( context: context ) } ' ;
final readMoreStyle = TextStyle (
fontSize: 12. f ,
color: AppColors . primaryRedColor ,
/ / fontFamily: ' Poppins ' ,
fontWeight: FontWeight . w500 ,
) ;
return LayoutBuilder (
builder: ( context , constraints ) {
/ / Check if description alone overflows 2 lines
final descriptionPainter = TextPainter (
text: TextSpan ( text: description , style: textStyle ) ,
maxLines: 2 ,
textDirection: Directionality . of ( context ) ,
) . . layout ( maxWidth: constraints . maxWidth ) ;
final isOverflowing = descriptionPainter . didExceedMaxLines ;
/ / No overflow , or already expanded - > show full text
if ( ! isOverflowing | | isExpanded ) {
return GestureDetector (
onTap: isOverflowing ? onToggle : null ,
child: Text . rich (
TextSpan (
style: textStyle ,
children: [
TextSpan ( text: description , style: textStyle ) ,
if ( isOverflowing )
TextSpan ( text: readMoreText , style: readMoreStyle ) ,
] ,
) ,
) ,
) ;
}
/ / Binary search for max chars that fit in 2 lines with " Read more " appended
int low = 0 , high = description . length ;
while ( low < high ) {
final mid = ( low + high + 1 ) ~ / 2 ;
final testPainter = TextPainter (
text: TextSpan (
style: textStyle ,
children: [
TextSpan ( text: ' ${ description . substring ( 0 , mid ) } ... ' ) ,
TextSpan ( text: readMoreText , style: readMoreStyle ) ,
] ,
) ,
maxLines: 2 ,
textDirection: Directionality . of ( context ) ,
) . . layout ( maxWidth: constraints . maxWidth ) ;
if ( testPainter . didExceedMaxLines ) {
high = mid - 1 ;
} else {
low = mid ;
}
}
final truncated = description . substring ( 0 , low ) . trimRight ( ) ;
return GestureDetector (
onTap: onToggle ,
child: Text . rich (
TextSpan (
style: textStyle ,
children: [
TextSpan ( text: ' $ truncated ... ' ) ,
TextSpan ( text: readMoreText , style: readMoreStyle ) ,
] ,
) ,
) ,
) ;
} ,
} ,
) ;
) ;
}
}