diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 96908b62..ed554e97 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1966,5 +1966,6 @@ "doctorSchedule": "جدول الطبيب", "weeklyShift": "الفترات الأسبوعية", "dayOff": "إجازة", - "notAvailable": "غير متاح" + "notAvailable": "غير متاح", + "hide": "إخفاء" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 0a34c8bf..ae344b01 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1955,7 +1955,8 @@ "doctorSchedule": "Doctor Schedule", "weeklyShift": "Weekly Shift", "dayOff": "Off", - "notAvailable": "Not Available" + "notAvailable": "Not Available", + "hide": "Hide" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 6a44cb0e..20be1f41 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4769748; + body['TokenID'] = "@dm!n"; + body['PatientID'] = 1018977; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index 82468ada..94679047 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -29,6 +29,7 @@ abstract class LabRepo { Future>> getPatientAiOverViewLabOrders({required int langId, required dynamic payload}); Future>> getPatientAiOverViewLabOrder(var payloa); + Map> groupLabResultsByPackageName(List labResults); } class LabRepoImp implements LabRepo { @@ -37,6 +38,55 @@ class LabRepoImp implements LabRepo { LabRepoImp({required this.loggerService, required this.apiClient}); + /// Groups a list of LabResult objects by their PackageName + /// Returns a Map where the key is the PackageName and the value is a List of LabResult objects + /// Both the keys and the lists are sorted alphabetically + /// + /// Example usage: + /// ```dart + /// List labResults = [/* your lab results */]; + /// Map> groupedResults = labRepo.groupLabResultsByPackageName(labResults); + /// + /// // Access results by package name + /// groupedResults.forEach((packageName, results) { + /// print('Package: $packageName has ${results.length} results'); + /// }); + /// ``` + Map> groupLabResultsByPackageName(List labResults) { + Map> groupedResults = {}; + + // Group results by package name + for (var labResult in labResults) { + String packageName = labResult.PackageName ?? 'Unknown Package'; + + if (groupedResults.containsKey(packageName)) { + // Package name exists, add to existing list + groupedResults[packageName]!.add(labResult); + } else { + // Package name doesn't exist, create new key with new list + groupedResults[packageName] = [labResult]; + } + } + + // Sort each list alphabetically by description + groupedResults.forEach((key, list) { + list.sort((a, b) { + String descA = (a.description ?? '').toLowerCase(); + String descB = (b.description ?? '').toLowerCase(); + return descA.compareTo(descB); + }); + }); + + // Sort the map keys alphabetically and return a new sorted map + var sortedKeys = groupedResults.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + Map> sortedGroupedResults = {}; + for (var key in sortedKeys) { + sortedGroupedResults[key] = groupedResults[key]!; + } + + return sortedGroupedResults; + } + @override Future>>> getPatientLabOrders() async { Map mapDevice = {}; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 09caf578..1c25ef9d 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -57,7 +57,11 @@ class LabViewModel extends ChangeNotifier { PatientLabOrdersResponseModel? currentlySelectedPatientOrder; - List mainLabResultsByHospitals = []; + // Flat list for UI compatibility + List mainLabResultsByHospitalsList = []; + + // Grouped map by package name for efficient lookups + Map> mainLabResultsByPackageName = {}; List mainLabResults = []; List mainGraphPoints = []; @@ -383,7 +387,8 @@ class LabViewModel extends ChangeNotifier { Future getPatientLabResultByHospital(PatientLabOrdersResponseModel laborder) async { isLabResultByHospitalLoading = true; notifyListeners(); - mainLabResultsByHospitals.clear(); + mainLabResultsByHospitalsList.clear(); + mainLabResultsByPackageName.clear(); final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); @@ -397,7 +402,9 @@ class LabViewModel extends ChangeNotifier { isLabResultByHospitalLoading = false; if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - mainLabResultsByHospitals = apiResponse.data ?? []; + mainLabResultsByHospitalsList = apiResponse.data ?? []; + // Group the results by package name for efficient lookups + mainLabResultsByPackageName = labRepo.groupLabResultsByPackageName(mainLabResultsByHospitalsList); notifyListeners(); } }, @@ -842,6 +849,25 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + // /// Finds a matching LabResult from the grouped map by package name + // /// Searches through all packages to find a result matching the test code or description + // LabResult? _findMatchingLabResult(TestDetails testDetail) { + // // Search through all packages in the grouped map + // for (var packageResults in mainLabResultsByPackageName.values) { + // try { + // var matchingResult = packageResults.firstWhere( + // (labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, + // ); + // return matchingResult; + // } catch (e) { + // // Continue searching in other packages + // continue; + // } + // } + // // Return null if no match found + // return LabResult(); + // } + Future getAiOverviewLabOrders({required int langId, required PatientLabOrdersResponseModel labOrder, required String loadingText}) async { isLabAIAnalysisNeedsToBeShown = true; notifyListeners(); @@ -858,7 +884,7 @@ class LabViewModel extends ChangeNotifier { if (labOrder.testDetails != null && labOrder.testDetails!.isNotEmpty) { for (var testDetail in labOrder.testDetails!) { LabResult? matchingResult = - mainLabResultsByHospitals.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult()); + mainLabResultsByHospitalsList.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult()); testResults.add({ "test_name": testDetail.description ?? "", "result_value": matchingResult.resultValue ?? "", diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart index 2808dce9..78be004a 100644 --- a/lib/features/lab/models/resp_models/lab_result.dart +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -28,6 +28,7 @@ class LabResult { String? packageShortDescription; int? testId; String? testShortDescription; + String? PackageName; LabResult( {this.description, @@ -56,6 +57,7 @@ class LabResult { this.referenceLow, this.testId, this.testShortDescription, + this.PackageName }); LabResult.fromJson(Map json) { @@ -88,6 +90,7 @@ class LabResult { resultTypeID = json['ResultTypeID']; testId = json['TestID']; testShortDescription = json['TestShortDescription']; + PackageName = json['PackageName']; } Map toJson() { @@ -118,6 +121,7 @@ class LabResult { data['ReferenceLow'] = this.referenceLow; data['TestID'] = this.testId; data['TestShortDescription'] = this.testShortDescription; + data['PackageName'] = this.PackageName; return data; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 9d48db25..80cea8b4 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1947,5 +1947,6 @@ abstract class LocaleKeys { static const doctorSchedule = 'doctorSchedule'; static const weeklyShift = 'weeklyShift'; static const dayOff = 'dayOff'; + static const hide = 'hide'; } diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 8aedd8a5..1d47ea2c 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -1,45 +1,271 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.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/widget_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/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: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 createState() => _LabResultListState(); +} -class LabResultList extends StatelessWidget { +class _LabResultListState extends State { late LabViewModel model; + int expandedIndex = -1; + Map descriptionExpandedMap = {}; @override Widget build(BuildContext context) { model = Provider.of(context); - return Selector>( - selector: (_, model) => model.mainLabResultsByHospitals, - builder: (__, list, ___) { - if (list.isEmpty && context.read().labSpecialResult.isEmpty) { + return Selector>>( + selector: (_, model) => model.mainLabResultsByPackageName, + builder: (__, groupedResults, ___) { + if (groupedResults.isEmpty && context.read().labSpecialResult.isEmpty) { // return Utils.getNoDataWidget(context, // noDataText: LocaleKeys.noLabResults.tr(context: context)); return Container(); } else { + // Convert map entries to a list for indexed access + final packageEntries = groupedResults.entries.toList(); + return ListView.builder( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, - shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { - var labItem = list[index]; - return LabOrderResultItem(onTap: () { - context.read().flush(); - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription ?? "", labItem.uOM ?? ""); - }, - tests: labItem, - index: index, - iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), - severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N")); - }); + shrinkWrap: true, + itemCount: packageEntries.length, + 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: 10.r, hasShadow: false), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Package name as title/header + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.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( + 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().flush(); + model.getPatientLabResult( + model.currentlySelectedPatientOrder!, + labItem.description ?? "", + labItem.testShortDescription ?? "", + labItem.uOM ?? "" + ); + }, + tests: labItem, + index: index, + iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), + severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N") + ), + if(labResults.length>1) + ...[SizedBox( + height: 8.h, + ), + 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.textColor, // or Colors.black87 + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + 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), + ], + ), + ), + ); }, ); } -} \ No newline at end of file +} diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index ad2ad77f..e3f07e54 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -4,6 +4,7 @@ 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/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.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'; @@ -12,6 +13,7 @@ import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.d import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'dart:ui' as ui; @@ -24,7 +26,16 @@ class LabOrderResultItem extends StatelessWidget { final bool isExpanded; final Color iconColor; - const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false, required this.iconColor, required this.severityText}); + const LabOrderResultItem({ + super.key, + required this.onTap, + this.tests, + required this.index, + this.isLoading = false, + this.isExpanded = false, + required this.iconColor, + required this.severityText + }); @override build(BuildContext context) { @@ -32,7 +43,7 @@ class LabOrderResultItem extends StatelessWidget { duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false), child: Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), @@ -84,7 +95,20 @@ class LabOrderResultItem extends StatelessWidget { ), softWrap: true, ), - ) + ), + Visibility( + visible: tests?.notes != null && tests?.notes?.isNotEmpty == true, + child: Text( + tests?.notes??"", + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', + color: AppColors.greyTextColor, + ), + softWrap: true, + ), + ), ], ), ), @@ -104,7 +128,9 @@ class LabOrderResultItem extends StatelessWidget { severityText.tr().toText10(isBold: true, color: AppColors.greyTextColor), Utils.buildSvgWithAssets(icon: AppAssets.lab_result_indicator, width: 21, height: 23, iconColor: iconColor), ], - ), + ).onPress((){ + _buildSeverityDetailsBottomsheet(severity: tests?.calculatedResultFlag ?? "N", context: context); + }), ), ), Flexible( @@ -136,4 +162,77 @@ class LabOrderResultItem extends StatelessWidget { ), )); } + + _buildSeverityDetailsBottomsheet({required String severity,required BuildContext context}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.colorScience.tr(context: context), + context, + child: Container( + padding: EdgeInsets.all(16.w), + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildInfoTile(severityValue: severity, context: context), + ], + ), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + Widget _buildInfoTile({required String severityValue, required BuildContext context}) { + + String title = LocaleKeys.emergency.tr(context: context); + String description = LocaleKeys.emergencyDescription.tr(context: context); + Color dotColor = AppColors.chipColorEmergency; + switch (severityValue) { + case 'CL': + case 'LCL': + title = LocaleKeys.emergency.tr(context: context); + description = LocaleKeys.emergencyDescription.tr(context: context); + dotColor = AppColors.chipColorEmergency; + case 'L': + title = LocaleKeys.seekMedicalAdvice.tr(context: context); + description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context); + dotColor = AppColors.chipColorSeekMedicalAdvice; + case 'N': + title = LocaleKeys.monitor.tr(context: context); + description = LocaleKeys.monitorDescription.tr(context: context); + dotColor = AppColors.chipColorMonitor; + case 'H': + title = LocaleKeys.seekMedicalAdvice.tr(context: context); + description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context); + dotColor = AppColors.chipColorSeekMedicalAdvice; + case 'CH': + case 'HCH': + title = LocaleKeys.emergency.tr(context: context); + description = LocaleKeys.emergencyDescription.tr(context: context); + dotColor = AppColors.chipColorEmergency; + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 14.h, + width: 14.w, + decoration: BoxDecoration(shape: BoxShape.circle, color: dotColor), + ).paddingOnly(top: 3.h), + SizedBox(width: 8.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14(isBold: true, color: AppColors.textColor), + SizedBox(height: 4.h), + description.toText12(isBold: true, color: AppColors.greyTextColor), + ], + ), + ), + ], + ); + } }