From 512628fb8b2c1f1179c14bb5d7eeab377669a278 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Sun, 28 Sep 2025 16:11:59 +0300 Subject: [PATCH] Lab result view along with the listing of items is added --- assets/images/svg/critical_low_result.svg | 3 + assets/images/svg/graph.svg | 5 + assets/images/svg/ic_date_filter.svg | 7 + assets/images/svg/ic_list.svg | 8 + assets/images/svg/low_result.svg | 3 + assets/images/svg/normal_result.svg | 3 + assets/images/svg/range_calender.svg | 8 + .../images/svg/refernce_range_indicator.svg | 7 + lib/core/app_assets.dart | 8 + lib/core/common_models/data_points.dart | 13 + lib/core/dependencies.dart | 1 + .../lab/history/lab_history_viewmodel.dart | 7 + lib/features/lab/lab_range_view_model.dart | 93 +++++ lib/features/lab/lab_repo.dart | 56 ++- lib/features/lab/lab_view_model.dart | 358 +++++++++++++++++- lib/features/lab/models/Range.dart | 6 + .../lab/models/resp_models/lab_result.dart | 116 ++++++ .../patient_lab_orders_response_model.dart | 3 +- lib/main.dart | 10 +- lib/presentation/home/landing_page.dart | 1 + lib/presentation/lab/lab_order_by_test.dart | 86 ++--- lib/presentation/lab/lab_orders_page.dart | 16 +- .../lab/lab_results/lab_result_calender.dart | 336 ++++++++++++++++ .../lab/lab_results/lab_result_details.dart | 291 ++++++++++++++ .../lab/lab_results/lab_result_list_item.dart | 51 +++ lib/theme/colors.dart | 6 + lib/widgets/graph/custom_graph.dart | 241 ++++++------ pubspec.yaml | 5 +- 28 files changed, 1563 insertions(+), 185 deletions(-) create mode 100644 assets/images/svg/critical_low_result.svg create mode 100644 assets/images/svg/graph.svg create mode 100644 assets/images/svg/ic_date_filter.svg create mode 100644 assets/images/svg/ic_list.svg create mode 100644 assets/images/svg/low_result.svg create mode 100644 assets/images/svg/normal_result.svg create mode 100644 assets/images/svg/range_calender.svg create mode 100644 assets/images/svg/refernce_range_indicator.svg create mode 100644 lib/features/lab/history/lab_history_viewmodel.dart create mode 100644 lib/features/lab/lab_range_view_model.dart create mode 100644 lib/features/lab/models/Range.dart create mode 100644 lib/features/lab/models/resp_models/lab_result.dart create mode 100644 lib/presentation/lab/lab_results/lab_result_calender.dart create mode 100644 lib/presentation/lab/lab_results/lab_result_details.dart create mode 100644 lib/presentation/lab/lab_results/lab_result_list_item.dart diff --git a/assets/images/svg/critical_low_result.svg b/assets/images/svg/critical_low_result.svg new file mode 100644 index 0000000..2706852 --- /dev/null +++ b/assets/images/svg/critical_low_result.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/graph.svg b/assets/images/svg/graph.svg new file mode 100644 index 0000000..8c79844 --- /dev/null +++ b/assets/images/svg/graph.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/ic_date_filter.svg b/assets/images/svg/ic_date_filter.svg new file mode 100644 index 0000000..43d28f2 --- /dev/null +++ b/assets/images/svg/ic_date_filter.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/ic_list.svg b/assets/images/svg/ic_list.svg new file mode 100644 index 0000000..e68f20b --- /dev/null +++ b/assets/images/svg/ic_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/low_result.svg b/assets/images/svg/low_result.svg new file mode 100644 index 0000000..52a2ef1 --- /dev/null +++ b/assets/images/svg/low_result.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/normal_result.svg b/assets/images/svg/normal_result.svg new file mode 100644 index 0000000..abe036f --- /dev/null +++ b/assets/images/svg/normal_result.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/range_calender.svg b/assets/images/svg/range_calender.svg new file mode 100644 index 0000000..940c002 --- /dev/null +++ b/assets/images/svg/range_calender.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/refernce_range_indicator.svg b/assets/images/svg/refernce_range_indicator.svg new file mode 100644 index 0000000..a3508b9 --- /dev/null +++ b/assets/images/svg/refernce_range_indicator.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 83fb842..82546d3 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -7,6 +7,7 @@ class AppAssets { static const String arrow_forward = '$svgBasePath/arrow_forward.svg'; static const String externalLink = '$svgBasePath/external_link.svg'; static const String calendar = '$svgBasePath/calendar.svg'; + static const String rangeCalendar = '$svgBasePath/range_calender.svg'; static const String hmc = '$svgBasePath/hmc.svg'; static const String ksa = '$svgBasePath/ksa.svg'; static const String sms = '$svgBasePath/sms.svg'; @@ -133,6 +134,13 @@ class AppAssets { static const String minus = '$svgBasePath/minus.svg'; static const String home_lab_result_icon = '$svgBasePath/home_lab_result_icon.svg'; static const String visa_mastercard_icon = '$svgBasePath/visa_mastercard.svg'; + static const String lab_result_indicator = '$svgBasePath/refernce_range_indicator.svg'; + static const String ic_date_filter = '$svgBasePath/ic_date_filter.svg'; + static const String ic_list = '$svgBasePath/ic_list.svg'; + static const String ic_graph = '$svgBasePath/graph.svg'; + static const String ic_normal_result = '$svgBasePath/normal_result.svg'; + static const String ic_low_result = '$svgBasePath/low_result.svg'; + static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; diff --git a/lib/core/common_models/data_points.dart b/lib/core/common_models/data_points.dart index af7c473..89fa6e1 100644 --- a/lib/core/common_models/data_points.dart +++ b/lib/core/common_models/data_points.dart @@ -6,9 +6,22 @@ class DataPoint { final double value; ///label shown on the bottom of the graph String label; + String refernceValue; + String actualValue; + DateTime time; + String displayTime; DataPoint( {required this.value, required this.label, + required this.refernceValue, + required this.actualValue, + required this.time, + required this.displayTime, }); + + @override + String toString() { + return "the time is $time"; + } } diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 4de7c5f..b42706f 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -100,6 +100,7 @@ class AppDependencies { () => LabViewModel( labRepo: getIt(), errorHandlerService: getIt(), + navigationService: getIt() ), ); diff --git a/lib/features/lab/history/lab_history_viewmodel.dart b/lib/features/lab/history/lab_history_viewmodel.dart new file mode 100644 index 0000000..a377b52 --- /dev/null +++ b/lib/features/lab/history/lab_history_viewmodel.dart @@ -0,0 +1,7 @@ + + +import 'package:flutter/material.dart'; + +class LabHistoryViewModel extends ChangeNotifier{ + bool isGraphShowing = false; +} \ No newline at end of file diff --git a/lib/features/lab/lab_range_view_model.dart b/lib/features/lab/lab_range_view_model.dart new file mode 100644 index 0000000..aa91964 --- /dev/null +++ b/lib/features/lab/lab_range_view_model.dart @@ -0,0 +1,93 @@ +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/lab/models/Range.dart'; + +class LabRangeViewModel extends ChangeNotifier { + List months = [ + 'Jan', + 'Feb', + 'Mar', + 'April', + 'May', + 'Jun', + 'July', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ]; + bool isGraphVisible = true; + Range? _currentlySelectedRange; + + Range? get currentlySelectedRange => _currentlySelectedRange; + + set currentlySelectedRange(Range? value) { + _currentlySelectedRange = value; + notifyListeners(); + + } + + DateTime? _toDate; + + DateTime? get toDate => _toDate; + + set toDate(DateTime? value) { + _toDate = value; + notifyListeners(); + } + + DateTime? _fromDate; + + DateTime? get fromDate => _fromDate; + + set fromDate(DateTime? value) { + _fromDate = value; + notifyListeners(); + + } + + LabRangeViewModel(); + + get getCurrentYear => DateTime.now().year; + + calculateDatesFromRange() { + _toDate = DateTime.now(); + switch (_currentlySelectedRange) { + case Range.WEEKLY: + _fromDate = _toDate!.subtract(Duration(days: 7)); + case Range.LAST_MONTH: + _fromDate = _toDate!.subtract(Duration(days: 30)); + case Range.LAST_6MONTH: + _fromDate = _toDate!.subtract(Duration(days: (30 * 6))); + case Range.THIS_YEAR: + _toDate = DateTime(_toDate!.year, DateTime.december, 31); + _fromDate = DateTime(_toDate!.year, DateTime.january, 01); + default: + } + } + + getDateString(DateTime? date){ + if(date == null) return "-"; + + String year = date.year.toString().substring(2); + return '${date.day} ${months[date.month-1]},$year'; + } + + flush(){ + toDate = null; + fromDate = null; + currentlySelectedRange = null; + isGraphVisible = true; + notifyListeners(); + } + + resetCurrentlySelectedRange(){ + currentlySelectedRange = null; + } + + alterGraphVisibility(){ + isGraphVisible = !isGraphVisible; + notifyListeners(); + } +} diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index f205494..36f9767 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -6,8 +6,11 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'models/resp_models/lab_result.dart' show LabResult; + abstract class LabRepo { Future>>> getPatientLabOrders(); + Future>>> getPatientLabResults(PatientLabOrdersResponseModel laborder, bool isVidaPlus, String procedureName); } class LabRepoImp implements LabRepo { @@ -19,7 +22,6 @@ class LabRepoImp implements LabRepo { @override Future>>> getPatientLabOrders() async { Map mapDevice = {}; - try { GenericApiModel>? apiResponse; Failure? failure; @@ -56,4 +58,56 @@ class LabRepoImp implements LabRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getPatientLabResults( + PatientLabOrdersResponseModel laborder, bool isVidaPlus, String procedureName + ) async { + + Map request = Map(); + request['InvoiceNo_VP'] = isVidaPlus ? laborder!.invoiceNo : "0"; + request['InvoiceNo'] = isVidaPlus ? "0" : laborder!.invoiceNo; + request['OrderNo'] = laborder!.orderNo; + request['isDentalAllowedBackend'] = false; + request['SetupID'] = laborder!.setupID; + request['ProjectID'] = laborder.projectID; + request['ClinicID'] = laborder.clinicID; + request['Procedure'] = procedureName; + request['LanguageID'] = 1; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_Patient_LAB_ORDERS_RESULT, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListPLR']; + if (list == null || list.isEmpty) { + throw Exception("lab list is empty"); + } + + final labOrders = list.map((item) => LabResult.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: labOrders, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 29b90f8..811e5f8 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -1,7 +1,20 @@ +import 'dart:core'; + import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/features/lab/lab_repo.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/patient_lab_orders_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_details.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:logger/logger.dart'; class LabViewModel extends ChangeNotifier { bool isLabOrdersLoading = false; @@ -9,17 +22,42 @@ class LabViewModel extends ChangeNotifier { LabRepo labRepo; ErrorHandlerService errorHandlerService; + NavigationService navigationService; List patientLabOrders = []; List filteredLabOrders = []; List tempLabOrdersList = []; + + List mainLabResults = []; + List mainGraphPoints = []; + List filteredGraphValues = []; + List months = [ + 'Jan', + 'Feb', + 'Mar', + 'April', + 'May', + 'Jun', + 'July', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ]; + late List _labSuggestionsList = []; List get labSuggestions => _labSuggestionsList; Set uniqueTests = {}; - LabViewModel({required this.labRepo, required this.errorHandlerService}); + double maxYForThreeDots = 0.0; + + LabViewModel( + {required this.labRepo, + required this.errorHandlerService, + required this.navigationService}); initLabProvider() { patientLabOrders.clear(); @@ -84,9 +122,321 @@ class LabViewModel extends ChangeNotifier { uniqueTests = { for (var item in patientLabOrders) if (item.testDetails != null) - ...?item.testDetails?.map((test) => - TestDetails(description: test.description.toString(), testCode: test.testCode.toString(), testID: test.testID, createdOn: item.createdOn)) + ...?item.testDetails?.map((test) => TestDetails( + description: test.description.toString(), + testCode: test.testCode.toString(), + testID: test.testID, + createdOn: item.createdOn, + model: item)) }; - uniqueTests.forEach(print); + } + + Future getPatientLabResult( + PatientLabOrdersResponseModel laborder, String procedureName) async { + LoaderBottomSheet.showLoader(); + mainLabResults.clear(); + filteredGraphValues.clear(); + + final result = await labRepo.getPatientLabResults( + laborder, + Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")), + procedureName); + + result.fold( + (failure) async { + LoaderBottomSheet.hideLoader(); + await errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + LoaderBottomSheet.hideLoader(); + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + var sortedResponse = sortByFlagAndValue(apiResponse.data!); + var recentThree = sort(sortedResponse); + mainLabResults = recentThree; + + double counter = 1; + recentThree.reversed.forEach((element) { + try { + var dateTime = + DateUtil.convertStringToDate(element.verifiedOnDateTime!); + if (double.parse(element.resultValue!) > maxYForThreeDots) { + maxYForThreeDots = double.parse(element.resultValue!); + } + filteredGraphValues.add(DataPoint( + value: transformValueInRange(double.parse(element.resultValue!), element.calculatedResultFlag??""), + actualValue:element.resultValue!, + label: formatDateAsMMYY(dateTime), + displayTime: resultDate(dateTime), + time: DateUtil.convertStringToDate(element.verifiedOnDateTime), + refernceValue: element.calculatedResultFlag ?? "", + + )); + counter++; + } catch (e) {} + }); + LabResult recentResult = recentThree.first; + recentResult.verifiedOn = resultDate(DateUtil.convertStringToDate(recentResult.verifiedOnDateTime!)); + navigationService.push(MaterialPageRoute( + builder: (_) => + LabResultDetails(recentLabResult: recentResult))); + notifyListeners(); + } + }, + ); + } + + String resultDate(DateTime date){ + + + return '${date.day} ${months[date.month-1]},${date.year}'; + } + + double transformValueInRange(double inputValue, String flag) { + // Define range boundaries + double rangeStart, rangeEnd; + + switch (flag) { + case'LCL': + case 'CL': + rangeStart = 0.0; + rangeEnd = 19.0; + break; + case 'L': + rangeStart = 20.0; + rangeEnd = 39.0; + break; + case 'N': + rangeStart = 40.0; + rangeEnd = 59.0; + break; + case 'H': + rangeStart = 60.0; + rangeEnd = 79.0; + break; + case 'HCH': + case 'CH': + rangeStart = 80.0; + rangeEnd = 100.0; + break; + default: + throw ArgumentError('Invalid flag: $flag'); + } + + // Clamp input value to 0-100 and map it to the range bounds + final clampedValue = inputValue.clamp(0.0, 100.0); + final normalizedValue = clampedValue / 100.0; // Normalize input to 0-1 + + // Map the normalized value to the target range bounds + final transformedValue = rangeStart + (normalizedValue * (rangeEnd - rangeStart)); + + return transformedValue; + } + void getSelectedDateRange(DateTime? start, DateTime? end) { + if(start == null && end == null) { + print("the dates are null"); + mainLabResults.forEach((element) { + final time = DateUtil.convertStringToDate(element.verifiedOnDateTime!); + try{ + filteredGraphValues.add(DataPoint( + value: transformValueInRange(double.parse(element.resultValue!), + element.calculatedResultFlag ?? ""), + actualValue: element.resultValue!, + label: formatDateAsMMYY(time), + displayTime: resultDate(time), + time: DateUtil.convertStringToDate(element.verifiedOnDateTime), + refernceValue: element.calculatedResultFlag ?? "", + )); + }catch(e){ + + } + }); + + }else { + filteredGraphValues.clear(); + + mainLabResults.forEach((element) { + try { + var dateTime = + DateUtil.convertStringToDate(element.verifiedOnDateTime!); + if (start != null && end == null) { + if (dateTime.isAtSameMomentAs(start)) { + + filteredGraphValues.add(DataPoint( + value: transformValueInRange( + double.parse(element.resultValue!), + element.calculatedResultFlag ?? ""), + actualValue: element.resultValue!, + label: formatDateAsMMYY(dateTime), + displayTime: resultDate(dateTime), + time: + DateUtil.convertStringToDate(element.verifiedOnDateTime), + refernceValue: element.calculatedResultFlag ?? "")); + } + } else if (start != null && end != null) { + if ((dateTime.isAfter(start)) && (dateTime.isBefore(end))) { + + filteredGraphValues.add(DataPoint( + value: transformValueInRange( + double.parse(element.resultValue!), + element.calculatedResultFlag ?? ""), + actualValue: element.resultValue!, + label: formatDateAsMMYY(dateTime), + displayTime: resultDate(dateTime), + time: + DateUtil.convertStringToDate(element.verifiedOnDateTime), + refernceValue: element.calculatedResultFlag ?? "")); + } + } + } catch (e) {} + }); + } + filteredGraphValues = sortFilteredList(filteredGraphValues).reversed.toList(); + + + notifyListeners(); + } + + String formatDateAsMMYY(DateTime date) { + + String year = date.year.toString().substring(2); + return '${months[date.month-1]},$year'; + } + + + List sortByFlagAndValue(List original) { + const priorityOrder = ['LCL', 'CL', 'L', 'N', 'H', 'CH', 'HCH']; + + int getFlagPriority(String? flag) { + if (flag == null) return priorityOrder.length; + final index = priorityOrder.indexOf(flag); + return index == -1 ? priorityOrder.length : index; + } + + double parseResultValue(String? value) { + if (value == null) return double.nan; + return double.tryParse(value) ?? double.nan; + } + + final copy = List.from(original); + copy.sort((a, b) { + final aFlagPriority = getFlagPriority(a.calculatedResultFlag); + final bFlagPriority = getFlagPriority(b.calculatedResultFlag); + + if (aFlagPriority != bFlagPriority) { + return aFlagPriority.compareTo(bFlagPriority); + } + + final aValue = parseResultValue(a.resultValue); + final bValue = parseResultValue(b.resultValue); + + return aValue.compareTo(bValue); + }); + + return copy; + } + + List sort(List original) { + DateTime? parseVerifiedDate(String? raw) { + if (raw == null) return null; + final regex = RegExp(r'\/Date\((\d+)\)\/'); + final match = regex.firstMatch(raw); + if (match != null) { + final millis = int.tryParse(match.group(1)!); + if (millis != null) { + + return DateTime.fromMillisecondsSinceEpoch(millis); + } + } + return null; + } + + final copy = List.from(original); + copy.sort((a, b) { + final aDate = DateUtil.convertStringToDate(a.verifiedOnDateTime); + final bDate = DateUtil.convertStringToDate(b.verifiedOnDateTime); + final now = DateTime.now(); + if (aDate == now && bDate == now) return 0; + if (aDate == now) return 1; + if (bDate == now) return -1; + return bDate.compareTo(aDate); // descending + }); + return copy.toList(); + } + + List sortFilteredList(List original) { + + + final copy = List.from(original); + copy.sort((a, b) { + final aDate =a.time; + final bDate = a.time; + final now = DateTime.now(); + if (aDate == now && bDate == now) return 0; + if (aDate == now) return 1; + if (bDate == now) return -1; + return bDate.compareTo(aDate); // descending + }); + return copy.toList(); + } + + Color getColor(String flag) { + switch (flag) { + case 'LCL': + return AppColors.criticalLowAndHigh; + case 'CL': + return AppColors.criticalLowAndHigh; + case 'L': + return AppColors.highAndLow; + case 'N': + return AppColors.bgGreenColor; + case 'H': + return AppColors.highAndLow; + case 'CH': + return AppColors.criticalLowAndHigh; + case 'HCH': + return AppColors.criticalLowAndHigh; + default: + return Colors.grey; + } + } + + String getFormattedDate(DateTime date){ + return DateFormat('EEEE, dd MMMM. yyyy').format(date); + } + + String getAssetUrlWRTResult(String refernceValue) { + switch (refernceValue) { + case 'CL': + case 'LCL': + return AppAssets.ic_critical_low_result; + case 'L': + return AppAssets.ic_low_result; + case 'N': + return AppAssets.ic_normal_result; + case 'H': + return AppAssets.ic_low_result; + case 'CH': + case 'HCH': + return AppAssets.ic_critical_low_result; + default: + return AppAssets.ic_normal_result; + } + } + + bool getRotationWRTResult(String refernceValue) { + switch (refernceValue) { + case 'CL': + case 'LCL': + case 'L': + case 'N': + return false; + case 'H': + case 'CH': + case 'HCH': + return true; + default: + return true; + } } } diff --git a/lib/features/lab/models/Range.dart b/lib/features/lab/models/Range.dart new file mode 100644 index 0000000..5dd39dc --- /dev/null +++ b/lib/features/lab/models/Range.dart @@ -0,0 +1,6 @@ +enum Range{ + WEEKLY, + LAST_MONTH, + LAST_6MONTH, + THIS_YEAR, +} \ No newline at end of file diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart new file mode 100644 index 0000000..d4e9223 --- /dev/null +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -0,0 +1,116 @@ +class LabResult { + String? description; + dynamic femaleInterpretativeData; + int? gender; + int? lineItemNo; + dynamic maleInterpretativeData; + dynamic notes; + String? packageID; + int? patientID; + String? projectID; + String? referanceRange; + String? resultValue; + String? sampleCollectedOn; + String? sampleReceivedOn; + String? setupID; + dynamic superVerifiedOn; + String? testCode; + String? uOM; + String? verifiedOn; + String? verifiedOnDateTime; + String? changeResult; + String? calculatedResultFlag; + String? criticalHigh; + String? referenceHigh; + String? criticalLow; + String? referenceLow; + + LabResult( + {this.description, + this.femaleInterpretativeData, + this.gender, + this.lineItemNo, + this.maleInterpretativeData, + this.notes, + this.packageID, + this.patientID, + this.projectID, + this.referanceRange, + this.resultValue, + this.sampleCollectedOn, + this.sampleReceivedOn, + this.setupID, + this.superVerifiedOn, + this.testCode, + this.uOM, + this.verifiedOn, + this.calculatedResultFlag, + this.verifiedOnDateTime, + this.criticalHigh, + this.referenceHigh, + this.criticalLow, + this.referenceLow, + }); + + LabResult.fromJson(Map json) { + description = json['Description']; + femaleInterpretativeData = json['FemaleInterpretativeData']; + gender = json['Gender']; + lineItemNo = json['LineItemNo']; + maleInterpretativeData = json['MaleInterpretativeData']; + notes = json['Notes']; + packageID = json['PackageID']; + patientID = json['PatientID']; + projectID = json['ProjectID']; + referanceRange = json['ReferanceRange']; + resultValue = json['ResultValue']; + sampleCollectedOn = json['SampleCollectedOn']; + sampleReceivedOn = json['SampleReceivedOn']; + setupID = json['SetupID']; + superVerifiedOn = json['SuperVerifiedOn']; + testCode = json['TestCode']; + uOM = json['UOM']; + verifiedOn = json['VerifiedOn']; + verifiedOnDateTime = json['VerifiedOnDateTime']; + changeResult = json['ChangeResult']; + calculatedResultFlag = json['CalculatedResultFlag']; + criticalHigh = json['CriticalHigh']; + referenceHigh = json['ReferenceHigh']; + criticalLow = json['CriticalLow']; + referenceLow = json['ReferenceLow']; + } + + Map toJson() { + final Map data = new Map(); + data['Description'] = this.description; + data['FemaleInterpretativeData'] = this.femaleInterpretativeData; + data['Gender'] = this.gender; + data['LineItemNo'] = this.lineItemNo; + data['MaleInterpretativeData'] = this.maleInterpretativeData; + data['Notes'] = this.notes; + data['PackageID'] = this.packageID; + data['PatientID'] = this.patientID; + data['ProjectID'] = this.projectID; + data['ReferanceRange'] = this.referanceRange; + data['ResultValue'] = this.resultValue; + data['SampleCollectedOn'] = this.sampleCollectedOn; + data['SampleReceivedOn'] = this.sampleReceivedOn; + data['SetupID'] = this.setupID; + data['SuperVerifiedOn'] = this.superVerifiedOn; + data['TestCode'] = this.testCode; + data['UOM'] = this.uOM; + data['VerifiedOn'] = this.verifiedOn; + data['VerifiedOnDateTime'] = this.verifiedOnDateTime; + data['ChangeResult'] = this.changeResult; + data['CriticalHigh'] = this.criticalHigh; + data['ReferenceHigh'] = this.referenceHigh; + data['CriticalLow'] = this.criticalLow; + data['ReferenceLow'] = this.referenceLow; + return data; + } + + @override + String toString() { + return 'LabOrderResult(flag: $calculatedResultFlag, value: $resultValue, verifiedOn: $verifiedOnDateTime)'; + } +} 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 265a19e..aadfc76 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 @@ -227,7 +227,8 @@ class TestDetails { String? testCode; String? testID; String? createdOn; - TestDetails({this.description, this.testCode, this.testID, this.createdOn}); + PatientLabOrdersResponseModel? model; + TestDetails({this.description, this.testCode, this.testID, this.createdOn, this.model}); TestDetails.fromJson(Map json) { description = json['Description']; diff --git a/lib/main.dart b/lib/main.dart index bdd1f29..3690c72 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; @@ -78,7 +80,7 @@ void main() async { create: (_) => LabViewModel( labRepo: getIt(), errorHandlerService: getIt(), - ), + navigationService: getIt()), ), ChangeNotifierProvider( create: (_) => RadiologyViewModel( @@ -148,7 +150,11 @@ void main() async { ), ChangeNotifierProvider( create: (_) => AppointmentViaRegionViewmodel( - navigationService: getIt(), appState: getIt())) + navigationService: getIt(), appState: getIt())), + ChangeNotifierProvider( + create: (_) => LabHistoryViewModel()), + ChangeNotifierProvider( + create: (_) => LabRangeViewModel()) ], child: MyApp()), ), ); diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 12bc8f5..65f5c11 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card. import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_calender.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index e5c6929..bb0f391 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -29,56 +29,52 @@ class LabOrderByTest extends StatelessWidget { curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), - child: InkWell( - onTap: () { - if (!isLoading) { - onTap(); - } - }, - child: Container( - key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ...labOrder!.testDetails!.map((detail) { - Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: '${tests!.description}'.toText14(weight: FontWeight.w500), - ), + child: Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ...labOrder!.testDetails!.map((detail) { + Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: '${tests!.description}'.toText14(weight: FontWeight.w500), + ), - SizedBox(height: 12.h), + SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AppCustomChipWidget( - richText: '${"Last Tested:".needTranslation} ${ DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false)}'.toText12(isBold: true), - // chipType: ChipTypeEnum.lightBg, - backgroundColor: AppColors.greyLightColor, - textColor: AppColors.textColor, - // borderRadius: 5, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + richText: '${"Last Tested:".needTranslation} ${ DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false)}'.toText12(isBold: true), + // chipType: ChipTypeEnum.lightBg, + backgroundColor: AppColors.greyLightColor, + textColor: AppColors.textColor, + // borderRadius: 5, - ), - CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewReport.tr(context: context), - onPressed: () {}, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14, - fontWeight: FontWeight.bold, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - ], + ), + CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: LocaleKeys.viewReport.tr(context: context), + onPressed: () { + onTap(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.bold, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, ), ], ), - ))); + ], + ), + )); } } diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 6e910b8..8657534 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -8,6 +8,7 @@ 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/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_range_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/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; @@ -30,6 +31,8 @@ class LabOrdersPage extends StatefulWidget { class _LabOrdersPageState extends State { late LabViewModel labProvider; + late LabRangeViewModel rangeViewModel; + List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; @@ -45,6 +48,8 @@ class _LabOrdersPageState extends State { @override Widget build(BuildContext context) { labProvider = Provider.of(context); + rangeViewModel = Provider.of(context); + return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( @@ -154,9 +159,14 @@ class _LabOrdersPageState extends State { child: FadeInAnimation( child: LabOrderByTest( onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); + if(model.uniqueTests.toList()[index].model != null) { + rangeViewModel.flush(); + model.getPatientLabResult( + model.uniqueTests + .toList()[index] + .model!, model.uniqueTests + .toList()[index].description!); + } }, tests: model.uniqueTests.toList()[index], index: index, diff --git a/lib/presentation/lab/lab_results/lab_result_calender.dart b/lib/presentation/lab/lab_results/lab_result_calender.dart new file mode 100644 index 0000000..b118293 --- /dev/null +++ b/lib/presentation/lab/lab_results/lab_result_calender.dart @@ -0,0 +1,336 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.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/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/lab_range_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/models/Range.dart'; +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/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:syncfusion_flutter_datepicker/datepicker.dart'; + +typedef OnRangeSelected = void Function(DateTime? start, DateTime? end); + +class LabResultCalender extends StatefulWidget { + final OnRangeSelected onRangeSelected; + + const LabResultCalender({super.key, required this.onRangeSelected}); + + @override + State createState() => _LabResultCalenderState(); +} + +class _LabResultCalenderState extends State { + late DateRangePickerController _calendarController; + DateTime? start; + DateTime? end; + late LabRangeViewModel model; + @override + void initState() { + _calendarController = DateRangePickerController(); + scheduleMicrotask(() { + _calendarController.selectedRange = PickerDateRange(model.fromDate,model.toDate); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + model = Provider.of(context); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 0.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (_, model, __) => selectionChip(model), + ).paddingOnly(bottom: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + padding: EdgeInsets.all( + 16.h + ), + child: Column( + children: [ + Row( + children: [ + fromDateComponent(), + Text( + "to".needTranslation, + style: TextStyle( + color: AppColors.calenderTextColor, + fontSize: 14.h, + fontWeight: FontWeight.w500, + letterSpacing: -.2 + ), + ).paddingSymmetrical(24.h,0.h), + toDateComponent(), + ], + ), + Divider( + color: AppColors.spacerLineColor, + thickness: 1, + ).paddingOnly(bottom: 16.h, top: 16.h), + Material( + color: Colors.white, + child: SfDateRangePicker( + controller: _calendarController, + selectionMode: DateRangePickerSelectionMode.range, + showNavigationArrow: true, + headerHeight: 40.h, + backgroundColor: Colors.white, + headerStyle: DateRangePickerHeaderStyle( + backgroundColor: Colors.white, + textAlign: TextAlign.start, + textStyle: TextStyle( + fontSize: 18.fSize, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.primaryRedColor, + fontFamily: "Poppins", + ), + ), + monthViewSettings: DateRangePickerMonthViewSettings( + viewHeaderStyle: DateRangePickerViewHeaderStyle( + backgroundColor: Colors.white, + textStyle: TextStyle( + fontSize: 14.fSize, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.textColor, + ), + ), + showTrailingAndLeadingDates: false, + dayFormat: "EEE", + ), + selectionShape: DateRangePickerSelectionShape.rectangle, + selectionRadius: 12.h, + selectionColor: AppColors.transparent, + startRangeSelectionColor: AppColors.primaryRedColor, + endRangeSelectionColor: AppColors.primaryRedColor, + rangeSelectionColor: + AppColors.primaryRedColor.withOpacity(0.1), + todayHighlightColor: Colors.transparent, + monthCellStyle: DateRangePickerMonthCellStyle( + textStyle: TextStyle( + fontSize: 12.fSize, + color: AppColors.textColor, + ), + todayTextStyle: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.bold, + ), + ), + onSelectionChanged: + (DateRangePickerSelectionChangedArgs args) { + if (args.value is PickerDateRange) { + final PickerDateRange range = args.value; + start = range.startDate; + end = range.endDate; + model.fromDate = start; + model.toDate = end; + model.resetCurrentlySelectedRange(); + } + }, + ), + ), + ], + ), + ), + Row( + children: [ + Consumer( + builder: (_, model, __) => Visibility( + visible: (model.fromDate != null || model.toDate != null), + child: Expanded( + child: Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + onPressed: () { + _calendarController.selectedRange = null; + _calendarController.selectedDate = null; + model.flush(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.cancel, + iconColor: AppColors.primaryRedColor, + height: 56.h, + ), + ), + SizedBox(width: 16.h,) + ], + ), + ), + ), + ), + Expanded( + child: CustomButton( + text: LocaleKeys.search.tr(), + onPressed: () { + Navigator.of(context).pop(); + widget.onRangeSelected(model.fromDate, model.toDate); + }, + backgroundColor: AppColors.lightGreenButtonColor, + borderColor: Colors.transparent, + textColor: AppColors.textGreenColor, + icon: AppAssets.reminder_bell, + iconColor: AppColors.textGreenColor, + + height: 56.h, + ), + ), + ], + ).paddingOnly(top: 24.h), + ], + ), + ); + } + + fromDateComponent() { + return Consumer( + builder: (_, model, __) { + return displayDate("Start Date".needTranslation, + model.getDateString(model.fromDate), model.fromDate == null); + }, + ); + } + + toDateComponent() { + return Consumer( + builder: (_, model, __) { + return displayDate("End Date".needTranslation, + model.getDateString(model.toDate), model.toDate == null); + }, + ); + } + + displayDate(String label, String? date, bool isNotSelected) => Expanded( + child: Row( + spacing: 12.h, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.rangeCalendar, + iconColor: isNotSelected ? AppColors.borderOnlyColor: AppColors.blackColor , + height: 24, + width: 24), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: AppColors.inputLabelTextColor, + fontSize: 12.h, + fontWeight: FontWeight.w500, + ), + ), + Text( + date!, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.h, + fontWeight: FontWeight.w500, + ), + ) + ], + ) + ], + ), + ); + + selectionChip(LabRangeViewModel model) { + return Row( + spacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: "This Week".needTranslation, + backgroundColor: model.currentlySelectedRange == Range.WEEKLY + ? AppColors.primaryRedColor.withOpacity(0.1) + : AppColors.whiteColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: model.currentlySelectedRange == Range.WEEKLY + ? AppColors.primaryRedBorderColor + : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + ).onPress((){ + _calendarController.selectedRange = null; + model.currentlySelectedRange = Range.WEEKLY; + model.calculateDatesFromRange(); + }), + AppCustomChipWidget( + labelText: "Last Month".needTranslation, + backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH + ? AppColors.primaryRedColor.withOpacity(0.1) + : AppColors.whiteColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: model.currentlySelectedRange == Range.LAST_MONTH + ? AppColors.primaryRedBorderColor + : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + ).onPress((){ + _calendarController.selectedRange = null; + model.currentlySelectedRange = Range.LAST_MONTH; + model.calculateDatesFromRange(); + }), + AppCustomChipWidget( + labelText: "Last 6 Months".needTranslation, + backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH + ? AppColors.primaryRedColor.withOpacity(0.1) + : AppColors.whiteColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: model.currentlySelectedRange == Range.LAST_6MONTH + ? AppColors.primaryRedBorderColor + : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + ).onPress((){ + _calendarController.selectedRange = null; + model.currentlySelectedRange = Range.LAST_6MONTH; + model.calculateDatesFromRange(); + }), + AppCustomChipWidget( + labelText: "Year ${model.getCurrentYear}", + backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR + ? AppColors.primaryRedColor.withOpacity(0.1) + : AppColors.whiteColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: model.currentlySelectedRange == Range.THIS_YEAR + ? AppColors.primaryRedBorderColor + : AppColors.chipBorderColorOpacity20, + width: 1, + ), + borderRadius: BorderRadius.circular(10)), + ).onPress((){ + _calendarController.selectedRange = null; + model.currentlySelectedRange = Range.THIS_YEAR; + model.calculateDatesFromRange(); + }), + ], + ); + } +} diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart new file mode 100644 index 0000000..2cac8fc --- /dev/null +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -0,0 +1,291 @@ +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.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/history/lab_history_viewmodel.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart' show LabRangeViewModel; +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/presentation/lab/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_calender.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_list_item.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:provider/provider.dart' show Consumer, Provider; + +import '../../../widgets/common_bottom_sheet.dart' + show showCommonBottomSheetWithoutHeight; +import '../../book_appointment/widgets/appointment_calendar.dart' + show AppointmentCalendar; + +class LabResultDetails extends StatelessWidget { + final LabResult recentLabResult; + + // final List graphPoint; + late LabViewModel model; + + LabResultDetails({super.key, required this.recentLabResult}); + + @override + Widget build(BuildContext context) { + model = Provider.of(context, listen: false); + return CollapsingListView( + title: 'Lab Result Details'.needTranslation, + child: SingleChildScrollView( + child: Column( + spacing: 16.h, + children: [LabNameAndStatus, LabGraph(context)], + ).paddingAll(24.h), + ), + ); + } + + Widget get LabNameAndStatus => Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + recentLabResult.testCode ?? "", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + letterSpacing: -2), + ), + Text( + "Result of ${recentLabResult.verifiedOn ?? ""}".needTranslation, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ), + ], + ), + //todo change the text color according to the provided test values + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Row( + spacing: 4.h, + + children: [ + Flexible( + child: Text( + recentLabResult.resultValue ?? "", + style: TextStyle( + fontSize: 24.fSize, + fontWeight: FontWeight.w600, + color: model.getColor( + recentLabResult.calculatedResultFlag ?? "", + ), + letterSpacing: -2, + ), + overflow: TextOverflow.ellipsis, // prevent overflow + maxLines: 1, + softWrap: false, + ), + ), + Visibility( + visible: recentLabResult.referanceRange != null, + child: Text( + "(Reference range ${recentLabResult.referanceRange})".needTranslation, + style: TextStyle( + fontSize: 12.fSize, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + ), + ), + ], + ), + ), + Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: model.getColor( + recentLabResult.calculatedResultFlag ?? "", + ), + ), + ], + ) + + ], + )); + + Widget LabGraph(BuildContext context) => Consumer( + builder: (_, model, ___) => Consumer( + builder: (_, labmodel, ___) => Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + height: 260.h, + padding: EdgeInsets.all(16.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + //title and filter icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + model.isGraphVisible?"History FlowChart".needTranslation: "History".needTranslation, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + Row( + spacing: 16.h, + children: [ + //todo handle when the graph icon is being displayed + Utils.buildSvgWithAssets( + icon: model.isGraphVisible?AppAssets.ic_list:AppAssets.ic_graph, + width: 24, + height: 24) + .onPress(() { + model.alterGraphVisibility(); + }), + Utils.buildSvgWithAssets( + icon: AppAssets.ic_date_filter, + width: 24, + height: 24) + .onPress(() { + showCommonBottomSheetWithoutHeight( + title: "Set The Date Range".needTranslation, + context, + child: LabResultCalender( + onRangeSelected: (start, end) { + + // if (start != null) { + labmodel.getSelectedDateRange(start, end); + // } + }, + ), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); + }), + ], + ) + ], + ).paddingOnly(bottom: model.isGraphVisible? 16.h :24.h), + historyBody(model, labmodel) + ], + )), + )); + + Widget leftLabels(String value) { + return Text( + value, + style: TextStyle( + fontWeight: FontWeight.w300, + fontFamily: 'Poppins', + fontSize: 8.fSize, + color: AppColors.textColor, + ), + ); + } + + Widget buildBottomLabel(String label) { + return Padding( + padding: const EdgeInsets.only(top:8.0), + child: Text( + label, + style: TextStyle( + fontSize: 8.fSize, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.labelTextColor), + ), + ); + } + + Widget historyBody(LabRangeViewModel model, LabViewModel labmodel) { + if(model.isGraphVisible){ + return CustomGraph( + dataPoints: labmodel.filteredGraphValues, + maxY: 100, + leftLabelFormatter: (value) { + switch (value.toInt()) { + case 20: + return leftLabels("Critical Low".needTranslation); + case 40: + return leftLabels("Low".needTranslation); + case 60: + return leftLabels("Normal".needTranslation); + case 80: + return leftLabels("High".needTranslation); + case 100: + return leftLabels( + "Critical High".needTranslation); + default: + return SizedBox.shrink(); + } + }, + bottomLabelFormatter: (value, data) { + if(data.isEmpty) return SizedBox.shrink(); + if (value == 0) { + return buildBottomLabel(data[value.toInt()].label); + } + if (value == data.length - 1) { + return buildBottomLabel(data[value.toInt()].label); + } + if (value == ((data.length - 1) / 2)) { + return buildBottomLabel(data[value.toInt()].label); + } + return SizedBox.shrink(); + }, + scrollDirection: Axis.horizontal, + height: 180.h); + }else { + return labHistoryList(model, labmodel); + } + } + + Widget labHistoryList(LabRangeViewModel model, LabViewModel labmodel) { + return SizedBox( + height: 180.h, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: labmodel.filteredGraphValues.length,itemBuilder: (context, index){ + var data = labmodel.filteredGraphValues.reversed.toList()[index]; + return Column( + children: [ + LabHistoryItem( + dayNameAndDate: labmodel.getFormattedDate(data.time), + result: data.actualValue, + assetUrl: labmodel.getAssetUrlWRTResult(data.refernceValue), + shouldRotateIcon: labmodel.getRotationWRTResult(data.refernceValue), + ), + if(index != labmodel.filteredGraphValues.length-1) + Divider(color: AppColors.spacerLineColor,thickness: 1.h,) + ], + ); + }), + ); + } +} diff --git a/lib/presentation/lab/lab_results/lab_result_list_item.dart b/lib/presentation/lab/lab_results/lab_result_list_item.dart new file mode 100644 index 0000000..00b5ff2 --- /dev/null +++ b/lib/presentation/lab/lab_results/lab_result_list_item.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart' ; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class LabHistoryItem extends StatelessWidget{ + + final String dayNameAndDate; + final String result; + final String assetUrl; + final bool shouldRotateIcon; + + const LabHistoryItem({super.key, required this.dayNameAndDate, required this.result, required this.assetUrl, this.shouldRotateIcon = false}); + + @override + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + dayNameAndDate, + style: TextStyle( + fontSize: 14.fSize, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.labelTextColor + ), + ), + Text( + result, + style: TextStyle( + fontSize: 18.fSize, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: AppColors.textColor + ), + ) + ], + ), + Transform.flip( + flipY: shouldRotateIcon, + child: Utils.buildSvgWithAssets(icon: assetUrl,height: 18, width: 18) + ), + ], + ); + +} \ No newline at end of file diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 06079dc..4015249 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -69,4 +69,10 @@ static const Color quickLoginColor = Color(0xFF666666); static const Color tooltipTextColor = Color(0xFF414D55); static const Color graphGridColor = Color(0x4D18C273); +static const Color criticalLowAndHigh = Color(0xFFED1C2B); +static const Color highAndLow = Color(0xFFFFAF15); +static const Color labelTextColor = Color(0xFF838383); +static const Color calenderTextColor = Color(0xFFD0D0D0); +static const Color lightGreenButtonColor = Color(0x2618C273); + } diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index d66d281..ee246ad 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -43,6 +43,7 @@ class CustomGraph extends StatelessWidget { final double height; final double? maxY; final double? maxX; + final double? minX; final Color spotColor; final Color graphColor; final Color graphShadowColor; @@ -52,7 +53,9 @@ class CustomGraph extends StatelessWidget { final FontWeight? bottomLabelFontWeight; ///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 value) leftLabelFormatter; + final Widget Function(double) leftLabelFormatter; + final Widget Function(double , List) bottomLabelFormatter; + final Axis scrollDirection; final bool showBottomTitleDates; @@ -76,6 +79,8 @@ class CustomGraph extends StatelessWidget { this.bottomLabelColor = AppColors.textColor, this.bottomLabelFontWeight = FontWeight.w500, this.bottomLabelSize, + required this.bottomLabelFormatter, + this.minX, }); @override @@ -94,117 +99,108 @@ class CustomGraph extends StatelessWidget { child: SizedBox( width: width, height: height, - child: Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 8), - child: LineChart( - LineChartData( - minY: 0, - maxY: - ((maxY?.ceilToDouble() ?? 0.0) + interval).floorToDouble(), - // minX: dataPoints.first.labelValue - 1, - maxX: maxX, - minX: -0.2, - lineTouchData: LineTouchData( - getTouchLineEnd: (_, __) => 0, - getTouchedSpotIndicator: (barData, indicators) { - // Only show custom marker for touched spot - return indicators.map((int index) { - return TouchedSpotIndicatorData( - FlLine(color: Colors.transparent), - FlDotData( - show: true, - getDotPainter: (spot, percent, barData, idx) { - return FlDotCirclePainter( - radius: 8, - color: spotColor, - strokeWidth: 2, - strokeColor: Colors.white, - ); - }, - ), - ); + child: LineChart( + LineChartData( + minY: 0, + maxY: + ((maxY?.ceilToDouble() ?? 0.0) + interval).floorToDouble(), + // minX: dataPoints.first.labelValue - 1, + maxX: maxX, + minX: minX ??-0.2, + lineTouchData: LineTouchData( + getTouchLineEnd: (_, __) => 0, + getTouchedSpotIndicator: (barData, indicators) { + // Only show custom marker for touched spot + return indicators.map((int index) { + return TouchedSpotIndicatorData( + FlLine(color: Colors.transparent), + FlDotData( + show: true, + getDotPainter: (spot, percent, barData, idx) { + return FlDotCirclePainter( + radius: 8, + color: spotColor, + strokeWidth: 2, + strokeColor: Colors.white, + ); + }, + ), + ); + }).toList(); + }, + enabled: true, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (_) => Colors.white, + getTooltipItems: (touchedSpots) { + if (touchedSpots.isEmpty) return []; + // Only show tooltip for the first touched spot, hide others + return touchedSpots.map((spot) { + if (spot == touchedSpots.first) { + final dataPoint = dataPoints[spot.x.toInt()]; + + return LineTooltipItem( + // '${dataPoint.label} ${spot.y.toStringAsFixed(2)}', + '${dataPoint.actualValue} ${dataPoint.displayTime}', + TextStyle( + color: Colors.black, + fontSize: 12.fSize, + fontWeight: FontWeight.w500), + ); + } + return null; // hides the rest }).toList(); }, - enabled: true, - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (_) => Colors.white, - getTooltipItems: (touchedSpots) { - if (touchedSpots.isEmpty) return []; - // Only show tooltip for the first touched spot, hide others - return touchedSpots.map((spot) { - if (spot == touchedSpots.first) { - final dataPoint = dataPoints[spot.x.toInt()]; - - return LineTooltipItem( - // '${dataPoint.label} ${spot.y.toStringAsFixed(2)}', - '${dataPoint.value} ', - TextStyle( - color: Colors.black, - fontSize: 12.fSize, - fontWeight: FontWeight.w500), - ); - } - return null; // hides the rest - }).toList(); - }, - ), ), - titlesData: FlTitlesData( - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 77, - interval: .1, // Let fl_chart handle it - getTitlesWidget: (value, _) { - return leftLabelFormatter(value); - }, - ), - ), - bottomTitles: AxisTitles( - axisNameSize: 60, - sideTitles: SideTitles( - showTitles: showBottomTitleDates, - reservedSize: 50, - getTitlesWidget: (value, _) { - if ((value.toDouble() >= 0) && - (value.toDouble() < (maxX ?? dataPoints.length))) { - var label = dataPoints[value.toInt()].label; - - return buildBottomLabel(label); - } - return const SizedBox.shrink(); - }, - interval: 1, // ensures 1:1 mapping with spots - ), + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 77, + interval: .1, // Let fl_chart handle it + getTitlesWidget: (value, _) { + return leftLabelFormatter(value); + }, ), - topTitles: AxisTitles(), - rightTitles: AxisTitles(), ), - borderData: FlBorderData( - show: true, - border: const Border( - bottom: BorderSide.none, - left: BorderSide(color: Colors.grey, width: .5), - right: BorderSide.none, - top: BorderSide.none, + bottomTitles: AxisTitles( + axisNameSize: 20, + sideTitles: SideTitles( + showTitles: showBottomTitleDates, + reservedSize: 20, + getTitlesWidget: (value, _) { + return bottomLabelFormatter(value, dataPoints, ); + }, + interval: 1, // ensures 1:1 mapping with spots ), ), - lineBarsData: _buildColoredLineSegments(dataPoints), - gridData: FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: 20, - checkToShowHorizontalLine: (value) => - value >= 0 && value <= 100, - getDrawingHorizontalLine: (value) { - return FlLine( - color: AppColors.graphGridColor, - strokeWidth: 1, - dashArray: [5, 5], - ); - }, + topTitles: AxisTitles(), + rightTitles: AxisTitles(), + ), + borderData: FlBorderData( + show: true, + border: const Border( + bottom: BorderSide.none, + left: BorderSide(color: Colors.grey, width: .5), + right: BorderSide.none, + top: BorderSide.none, ), ), + lineBarsData: _buildColoredLineSegments(dataPoints), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 20, + checkToShowHorizontalLine: (value) => + value >= 0 && value <= 100, + getDrawingHorizontalLine: (value) { + return FlLine( + color: AppColors.graphGridColor, + strokeWidth: 1, + dashArray: [5, 5], + ); + }, + ), ), ), )); @@ -259,29 +255,20 @@ class CustomGraph extends StatelessWidget { // ); // } - Widget buildBottomLabel(String label) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - label, - style: TextStyle( - fontSize: bottomLabelSize ?? 8.fSize, color: bottomLabelColor), - ), - ); - } + } -final List sampleData = [ - DataPoint( - value: 20, - label: 'Jan 2024', - ), - DataPoint( - value: 36, - label: 'Feb 2024', - ), - DataPoint( - value: 80, - label: 'This result', - ), -]; +// final List sampleData = [ +// DataPoint( +// value: 20, +// label: 'Jan 2024', +// ), +// DataPoint( +// value: 36, +// label: 'Feb 2024', +// ), +// DataPoint( +// value: 80, +// label: 'This result', +// ), +// ]; diff --git a/pubspec.yaml b/pubspec.yaml index 0466933..adf737b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -84,6 +84,7 @@ dependencies: location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 + intl: ^0.20.2 dev_dependencies: flutter_test: @@ -119,8 +120,8 @@ flutter: weight: 500 - asset: assets/fonts/poppins/Poppins-Regular.ttf weight: 400 - # - asset: assets/fonts/poppins/Poppins-Light.ttf - # weight: 300 + - asset: assets/fonts/poppins/Poppins-Light.ttf + weight: 300 # - asset: assets/fonts/poppins/Poppins-ExtraLight.ttf # weight: 200 # - asset: assets/fonts/poppins/Poppins-Thin.ttf