diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index e8d3071..bfbccfc 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -287,7 +287,8 @@ class AppDependencies { getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + //commenting this because its already define there was on run time error because of this. + // getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); getIt.registerLazySingleton(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); getIt.registerLazySingleton(() => MyInvoicesViewModel( diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index f75a71b..f632502 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -216,6 +216,14 @@ class _VitalSignDetailsPageState extends State { } Widget _historyCard(BuildContext context, {required List history}) { + // For blood pressure, we need both systolic and diastolic series + List? secondaryHistory; + if (args.metric == VitalSignMetric.bloodPressure) { + secondaryHistory = _buildBloodPressureDiastolicSeries( + context.read().vitalSignList, + ); + } + return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -282,7 +290,7 @@ class _VitalSignDetailsPageState extends State { if (history.isEmpty) Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) else if (_isGraphVisible) - _buildHistoryGraph(history) + _buildHistoryGraph(history, secondaryHistory: secondaryHistory) else _buildHistoryList(context, history), ], @@ -290,31 +298,25 @@ class _VitalSignDetailsPageState extends State { ); } - Widget _buildHistoryGraph(List history) { - final minY = _minY(history); - final maxY = _maxY(history); + Widget _buildHistoryGraph(List history, {List? secondaryHistory}) { + final minY = _minY(history, secondaryHistory: secondaryHistory); + final maxY = _maxY(history, secondaryHistory: secondaryHistory); final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); return CustomGraph( dataPoints: history, + secondaryDataPoints: secondaryHistory, makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, showGridLines: true, showShadow: true, - leftLabelInterval: _leftInterval(history), + leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory), maxY: maxY, minY: minY, maxX: history.length.toDouble() - .75, - horizontalInterval: _leftInterval(history), + horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory), leftLabelFormatter: (value) { - // Show labels at interval points - if (args.high != null && (value - args.high!).abs() < 0.1) { - return _axisLabel('High'); - } - if (args.low != null && (value - args.low!).abs() < 0.1) { - return _axisLabel('Low'); - } - // Show numeric labels at regular intervals + // Show only numeric labels at regular intervals return _axisLabel(value.toStringAsFixed(0)); }, getDrawingHorizontalLine: (value) { @@ -341,6 +343,7 @@ class _VitalSignDetailsPageState extends State { ); }, graphColor: AppColors.bgGreenColor, + secondaryGraphColor: AppColors.blueColor, graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), graphGridColor: scheme.iconFg, bottomLabelFormatter: (value, data) { @@ -350,7 +353,7 @@ class _VitalSignDetailsPageState extends State { if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); return const SizedBox.shrink(); }, - rangeAnnotations: _rangeAnnotations(history), + rangeAnnotations: _rangeAnnotations(history, secondaryHistory: secondaryHistory), minX: (history.length == 1) ? null : -.2, scrollDirection: Axis.horizontal, height: 180.h, @@ -361,6 +364,15 @@ class _VitalSignDetailsPageState extends State { Widget _buildHistoryList(BuildContext context, List history) { final items = history.reversed.toList(); final height = items.length < 3 ? items.length * 64.0 : 180.h; + + // Get diastolic values if this is blood pressure + List? secondaryItems; + if (args.metric == VitalSignMetric.bloodPressure) { + final viewModel = context.read(); + final secondaryHistory = _buildBloodPressureDiastolicSeries(viewModel.vitalSignList); + secondaryItems = secondaryHistory.reversed.toList(); + } + return SizedBox( height: height, child: ListView.separated( @@ -372,13 +384,25 @@ class _VitalSignDetailsPageState extends State { ), itemBuilder: (context, index) { final dp = items[index]; + + // Build the value text based on metric type + String valueText; + if (args.metric == VitalSignMetric.bloodPressure && secondaryItems != null && index < secondaryItems.length) { + // Show systolic/diastolic for blood pressure + final diastolic = secondaryItems[index]; + valueText = '${dp.actualValue}/${diastolic.actualValue} ${dp.unitOfMeasurement ?? ''}'; + } else { + // Show single value for other metrics + valueText = '${dp.actualValue} ${dp.unitOfMeasurement ?? ''}'; + } + return Padding( padding: EdgeInsets.symmetric(vertical: 12.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), - ('${dp.actualValue} ${dp.unitOfMeasurement ?? ''}').toText12( + valueText.toText12( color: AppColors.textColor, fontWeight: FontWeight.w600, ), @@ -390,34 +414,46 @@ class _VitalSignDetailsPageState extends State { ); } - double _minY(List points) { + double _minY(List points, {List? secondaryHistory}) { // IMPORTANT: y-axis uses actual numeric values (from actualValue). final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + + // Include secondary data points if provided (for blood pressure) + if (secondaryHistory != null && secondaryHistory.isNotEmpty) { + values.addAll(secondaryHistory.map((e) => double.tryParse(e.actualValue) ?? 0)); + } + final min = values.reduce((a, b) => a < b ? a : b); final double boundLow = args.low ?? min; return (min < boundLow ? min : boundLow) - 1; } - double _maxY(List points) { + double _maxY(List points, {List? secondaryHistory}) { // IMPORTANT: y-axis uses actual numeric values (from actualValue). final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + + // Include secondary data points if provided (for blood pressure) + if (secondaryHistory != null && secondaryHistory.isNotEmpty) { + values.addAll(secondaryHistory.map((e) => double.tryParse(e.actualValue) ?? 0)); + } + final max = values.reduce((a, b) => a > b ? a : b); final double boundHigh = args.high ?? max; return (max > boundHigh ? max : boundHigh) + 1; } - double _leftInterval(List points) { + double _leftInterval(List points, {List? secondaryHistory}) { // Keep it stable; graph will mostly show just two labels. - final range = (_maxY(points) - _minY(points)).abs(); + final range = (_maxY(points, secondaryHistory: secondaryHistory) - _minY(points, secondaryHistory: secondaryHistory)).abs(); if (range <= 0) return 1; return (range / 4).clamp(1, 20); } - RangeAnnotations? _rangeAnnotations(List points) { + RangeAnnotations? _rangeAnnotations(List points, {List? secondaryHistory}) { if (args.low == null && args.high == null) return null; - final minY = _minY(points); - final maxY = _maxY(points); + final minY = _minY(points, secondaryHistory: secondaryHistory); + final maxY = _maxY(points, secondaryHistory: secondaryHistory); final List ranges = []; @@ -480,7 +516,7 @@ class _VitalSignDetailsPageState extends State { case VitalSignMetric.respiratoryRate: return _toDouble(v.respirationBeatPerMinute); case VitalSignMetric.bloodPressure: - // Graph only systolic for now (simple single-series). + // Graph systolic for primary series return _toDouble(v.bloodPressureHigher); } } @@ -513,6 +549,46 @@ class _VitalSignDetailsPageState extends State { return points; } + /// Build diastolic blood pressure series for dual-line graph + List _buildBloodPressureDiastolicSeries(List vitals) { + final List points = []; + + // Build a chronological series (oldest -> newest), skipping null/zero values. + final sorted = List.from(vitals); + sorted.sort((a, b) { + final ad = a.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + final bd = b.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + return ad.compareTo(bd); + }); + + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + double index = 0; + for (final v in sorted) { + final diastolic = _toDouble(v.bloodPressureLower); + if (diastolic == null) continue; + if (diastolic == 0) continue; + + final dt = v.vitalSignDate ?? DateTime.now(); + final label = '${monthNames[dt.month - 1]}, ${dt.year}'; + + points.add( + DataPoint( + value: index, + label: label, + actualValue: diastolic.toStringAsFixed(0), + time: dt, + displayTime: '${dt.day}/${dt.month}/${dt.year}', + unitOfMeasurement: args.unit, + ), + ); + index += 1; + } + + return points; + } + double? _toDouble(dynamic v) { if (v == null) return null; if (v is num) return v.toDouble(); diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index ad47cd2..f114521 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -130,9 +130,23 @@ class CustomGraph extends StatelessWidget { minX: minX, lineTouchData: LineTouchData( getTouchLineEnd: (_, __) => 0, + handleBuiltInTouches: true, + touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) { + // Let fl_chart handle the touch + }, getTouchedSpotIndicator: (barData, indicators) { - // Only show custom marker for touched spot + // Show custom marker for touched spot with correct color per line return indicators.map((int index) { + // Determine which line is being touched based on barData + Color dotColor = spotColor; + if (secondaryDataPoints != null && barData.spots.length > 0) { + // Check if this is the secondary line by comparing the first spot's color + final gradient = barData.gradient; + if (gradient != null && gradient.colors.isNotEmpty) { + dotColor = gradient.colors.first; + } + } + return TouchedSpotIndicatorData( FlLine(color: Colors.transparent), FlDotData( @@ -140,7 +154,7 @@ class CustomGraph extends StatelessWidget { getDotPainter: (spot, percent, barData, idx) { return FlDotCirclePainter( radius: 8, - color: spotColor, + color: dotColor, strokeWidth: 2, strokeColor: Colors.white, ); @@ -154,17 +168,18 @@ class CustomGraph extends StatelessWidget { getTooltipColor: (_) => Colors.white, getTooltipItems: (touchedSpots) { if (touchedSpots.isEmpty) return []; - // Only show tooltip for the first touched spot, hide others + // Show tooltip for each touched line return touchedSpots.map((spot) { - if (spot == touchedSpots.first) { - final dataPoint = dataPoints[spot.x.toInt()]; + // Determine which dataset this spot belongs to + final isSecondary = secondaryDataPoints != null && spot.barIndex == 1; + final dataPoint = isSecondary + ? secondaryDataPoints![spot.x.toInt()] + : dataPoints[spot.x.toInt()]; - return LineTooltipItem( - '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', - TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), - ); - } - return null; // hides the rest + return LineTooltipItem( + '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', + TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), + ); }).toList(); }, ),