Merge pull request 'dev_sultan' (#154) from dev_sultan into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/154
pull/159/head
Haroon6138 2 days ago
commit d1b1bbf777

@ -287,7 +287,8 @@ class AppDependencies {
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<MyInvoicesViewModel>(() => 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>(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<MonthlyReportViewModel>(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt()));
getIt.registerLazySingleton<MyInvoicesViewModel>(() => MyInvoicesViewModel(

@ -216,6 +216,14 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
}
Widget _historyCard(BuildContext context, {required List<DataPoint> history}) {
// For blood pressure, we need both systolic and diastolic series
List<DataPoint>? secondaryHistory;
if (args.metric == VitalSignMetric.bloodPressure) {
secondaryHistory = _buildBloodPressureDiastolicSeries(
context.read<HmgServicesViewModel>().vitalSignList,
);
}
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
@ -282,7 +290,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
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<VitalSignDetailsPage> {
);
}
Widget _buildHistoryGraph(List<DataPoint> history) {
final minY = _minY(history);
final maxY = _maxY(history);
Widget _buildHistoryGraph(List<DataPoint> history, {List<DataPoint>? 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<VitalSignDetailsPage> {
);
},
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<VitalSignDetailsPage> {
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<VitalSignDetailsPage> {
Widget _buildHistoryList(BuildContext context, List<DataPoint> 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<DataPoint>? secondaryItems;
if (args.metric == VitalSignMetric.bloodPressure) {
final viewModel = context.read<HmgServicesViewModel>();
final secondaryHistory = _buildBloodPressureDiastolicSeries(viewModel.vitalSignList);
secondaryItems = secondaryHistory.reversed.toList();
}
return SizedBox(
height: height,
child: ListView.separated(
@ -372,13 +384,25 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
),
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<VitalSignDetailsPage> {
);
}
double _minY(List<DataPoint> points) {
double _minY(List<DataPoint> points, {List<DataPoint>? 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<DataPoint> points) {
double _maxY(List<DataPoint> points, {List<DataPoint>? 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<DataPoint> points) {
double _leftInterval(List<DataPoint> points, {List<DataPoint>? 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<DataPoint> points) {
RangeAnnotations? _rangeAnnotations(List<DataPoint> points, {List<DataPoint>? 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<HorizontalRangeAnnotation> ranges = [];
@ -480,7 +516,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
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<VitalSignDetailsPage> {
return points;
}
/// Build diastolic blood pressure series for dual-line graph
List<DataPoint> _buildBloodPressureDiastolicSeries(List<VitalSignResModel> vitals) {
final List<DataPoint> points = [];
// Build a chronological series (oldest -> newest), skipping null/zero values.
final sorted = List<VitalSignResModel>.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();

@ -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();
},
),

Loading…
Cancel
Save