Merge branch 'master' into haroon_dev

pull/156/head
haroon amjad 3 days ago
commit fc750174fe

@ -307,55 +307,64 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2),
Row(
children: [
LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
),
],
).paddingSymmetrical(0.w, 0.h).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: VitalSignPage(),
),
);
}),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2),
Row(
children: [
LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h),
],
),
],
).paddingSymmetrical(0.w, 0.h).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: VitalSignPage(),
),
);
}),
),
SizedBox(height: 16.h),
// Make this section dynamic-height (no fixed 160.h)
LayoutBuilder(
builder: (context, constraints) {
if (hmgServicesVM.isVitalSignLoading) {
return _buildVitalSignShimmer();
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _buildVitalSignShimmer(),
);
}
if (hmgServicesVM.vitalSignList.isEmpty) {
return Container(
padding: EdgeInsets.all(16.w),
width: MediaQuery.of(context).size.width,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.r,
hasShadow: false,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h),
SizedBox(height: 12.h),
"No vital signs recorded yet".needTranslation.toText12(isCenter: true),
],
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Container(
padding: EdgeInsets.all(16.w),
width: MediaQuery.of(context).size.width,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.r,
hasShadow: false,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h),
SizedBox(height: 12.h),
"No vital signs recorded yet".needTranslation.toText12(isCenter: true),
],
),
),
);
}
// The cards define their own height; measure the first rendered page once
_scheduleVitalSignMeasure();
final double hostHeight = _vitalSignMeasuredHeight ?? (160.h);
final double hostHeight = _vitalSignMeasuredHeight ?? (135.h);
return SizedBox(
height: hostHeight,
@ -400,7 +409,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
),
],
],
).paddingSymmetrical(24.w, 0.0);
);
}),
SizedBox(height: 16.h),
@ -1413,23 +1422,23 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
).toShimmer(),
SizedBox(height: 16.h),
// Label shimmer
Container(
width: 70.w,
height: 12.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.r),
),
).toShimmer(),
SizedBox(height: 8.h),
// Container(
// width: 70.w,
// height: 12.h,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(4.r),
// ),
// ).toShimmer(),
// SizedBox(height: 8.h),
// Value shimmer (larger)
Container(
width: 60.w,
height: 32.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.r),
),
).toShimmer(),
SizedBox(height: 12.h),
// Container(
// width: 60.w,
// height: 32.h,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(4.r),
// ),
// ).toShimmer(),
// SizedBox(height: 12.h),
// Bottom row with chip and arrow
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -1465,62 +1474,66 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
}) {
return [
// Page 1: BMI + Height
Row(
children: [
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.bmiVital,
label: "BMI",
value: vitalSign.bodyMassIndex?.toString() ?? '--',
unit: '',
status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null,
onTap: onTap,
Padding(
padding: EdgeInsets.only(left: 24.w),
child: Row(
children: [
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.bmiVital,
label: "BMI",
value: vitalSign.bodyMassIndex?.toString() ?? '--',
unit: '',
status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null,
onTap: onTap,
),
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.heightVital,
label: "Height",
value: vitalSign.heightCm?.toString() ?? '--',
unit: 'cm',
status: null,
onTap: onTap,
SizedBox(width: 12.w),
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.heightVital,
label: "Height",
value: vitalSign.heightCm?.toString() ?? '--',
unit: 'cm',
status: null,
onTap: onTap,
),
),
),
],
],
),
),
// Page 2: Weight + Blood Pressure
Row(
children: [
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.weightVital,
label: "Weight",
value: vitalSign.weightKg?.toString() ?? '--',
unit: 'kg',
status: vitalSign.weightKg != null ? "Normal" : null,
onTap: onTap,
Padding(padding: EdgeInsets.symmetric(horizontal: 12.w),child: Row(
children: [
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.weightVital,
label: "Weight",
value: vitalSign.weightKg?.toString() ?? '--',
unit: 'kg',
status: vitalSign.weightKg != null ? "Normal" : null,
onTap: onTap,
),
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.bloodPressure,
label: "Blood Pressure",
value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null
? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}"
: '--',
unit: '',
status: _getBloodPressureStatus(
systolic: vitalSign.bloodPressureHigher,
diastolic: vitalSign.bloodPressureLower,
SizedBox(width: 12.w),
Expanded(
child: _buildVitalSignCard(
icon: AppAssets.bloodPressure,
label: "Blood Pressure",
value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null
? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}"
: '--',
unit: '',
status: _getBloodPressureStatus(
systolic: vitalSign.bloodPressureHigher,
diastolic: vitalSign.bloodPressureLower,
),
onTap: onTap,
),
onTap: onTap,
),
),
],
),
],
)),
];
}
@ -1545,7 +1558,6 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
return GestureDetector(
onTap: onTap,
child: Container(
// Same styling used originally for vitals in MedicalFilePage
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 16.r,
@ -1559,15 +1571,15 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
Row(
children: [
Container(
padding: EdgeInsets.all(10.h),
padding: EdgeInsets.all(8.h),
decoration: BoxDecoration(
color: scheme.iconBg,
borderRadius: BorderRadius.circular(12.r),
),
child: Utils.buildSvgWithAssets(
icon: icon,
width: 20.w,
height: 20.h,
width: 22.w,
height: 22.h,
iconColor: scheme.iconFg,
fit: BoxFit.contain,
),
@ -1583,52 +1595,54 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
),
SizedBox(height: 14.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(10.r),
borderRadius: 10.r,
hasShadow: false,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
value.toText17(
isBold: true,
color: AppColors.textColor,
),
if (unit.isNotEmpty) ...[
SizedBox(width: 3.w),
unit.toText12(
color: AppColors.textColor,
fontWeight: FontWeight.w500,
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: value.toText17(
isBold: true,
color: AppColors.textColor,
),
),
if (unit.isNotEmpty) ...[
SizedBox(width: 3.w),
unit.toText12(
color: AppColors.textColor,
fontWeight: FontWeight.w500,
),
],
],
],
),
),
if (status != null)
if (status != null) ...[
SizedBox(width: 4.w),
AppCustomChipWidget(
labelText: status,
backgroundColor: scheme.chipBg,
textColor: scheme.chipFg,
)
),
]
else
const SizedBox.shrink(),
AppCustomChipWidget(
labelText: "",
backgroundColor: AppColors.bgScaffoldColor,
textColor:null,
)
],
),
),
SizedBox(height: 8.h),
Align(
alignment: AlignmentDirectional.centerEnd,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 18.w,
height: 18.h,
iconColor: AppColors.textColorLight,
fit: BoxFit.contain,
),
),
],
),
),

@ -0,0 +1,652 @@
import 'package:fl_chart/fl_chart.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/hmg_services/hmg_services_view_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart';
import 'package:provider/provider.dart';
/// Which vital sign is being shown in the details screen.
enum VitalSignMetric {
bmi,
height,
weight,
bloodPressure,
temperature,
heartRate,
respiratoryRate,
}
class VitalSignDetailsArgs {
final VitalSignMetric metric;
final String title;
final String icon;
final String unit;
/// Optional bounds used for graph shading and labels.
final double? low;
final double? high;
const VitalSignDetailsArgs({
required this.metric,
required this.title,
required this.icon,
required this.unit,
this.low,
this.high,
});
}
class VitalSignDetailsPage extends StatefulWidget {
final VitalSignDetailsArgs args;
const VitalSignDetailsPage({super.key, required this.args});
@override
State<VitalSignDetailsPage> createState() => _VitalSignDetailsPageState();
}
class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
bool _isGraphVisible = true;
VitalSignDetailsArgs get args => widget.args;
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: 'Vital Sign Details'.needTranslation,
child: Consumer<HmgServicesViewModel>(
builder: (context, viewModel, child) {
final latest = viewModel.vitalSignList.isNotEmpty ? viewModel.vitalSignList.first : null;
final history = _buildSeries(viewModel.vitalSignList, args);
final latestValueText = _latestValueText(latest);
final status = _statusForLatest(latest);
final scheme = VitalSignUiModel.scheme(status: status, label: args.title);
return SingleChildScrollView(
child: Column(
spacing: 16.h,
children: [
_headerCard(
context,
title: args.title,
icon: args.icon,
valueText: latestValueText,
status: status,
scheme: scheme,
latestDate: latest?.vitalSignDate,
),
_whatIsThisResultCard(context),
_historyCard(context, history: history),
],
).paddingAll(24.h),
);
},
),
);
}
Widget _headerCard(
BuildContext context, {
required String title,
required String icon,
required String valueText,
required String? status,
required VitalSignUiModel scheme,
required DateTime? latestDate,
}) {
return 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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
title.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -1),
],
),
SizedBox(height: 8.h),
(latestDate != null
? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation)
: ('Result of --'.needTranslation))
.toText11(weight: FontWeight.w500, color: AppColors.greyTextColor),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: valueText.toText28(
isBold: true,
color: scheme.iconFg,
letterSpacing: -2,
),
),
SizedBox(width: 4.h),
if (status != null)
Column(
spacing: 6.h,
children: [
status.toText10(weight: FontWeight.w500, color: AppColors.greyTextColor),
Utils.buildSvgWithAssets(
icon: AppAssets.lab_result_indicator,
width: 21,
height: 23,
iconColor: scheme.iconFg,
),
],
),
],
),
),
],
),
if (args.low != null || args.high != null)
Text(
_referenceText(context),
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w500,
fontFamily: 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
),
],
),
);
}
String _referenceText(BuildContext context) {
if (args.low != null && args.high != null) {
return 'Reference range: ${args.low} ${args.high} ${args.unit}'.needTranslation;
}
if (args.low != null) {
return 'Reference range: ≥ ${args.low} ${args.unit}'.needTranslation;
}
if (args.high != null) {
return 'Reference range: ≤ ${args.high} ${args.unit}'.needTranslation;
}
return '';
}
Widget _whatIsThisResultCard(BuildContext context) {
return 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: [
'What is this result?'.needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor),
_descriptionText(context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
],
),
);
}
Widget _historyCard(BuildContext context, {required List<DataPoint> history}) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
height: _isGraphVisible
? 260.h
: (history.length < 3)
? (history.length * 64) + 80.h
: 260.h,
padding: EdgeInsets.all(15.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isGraphVisible ? 'History flowchart'.needTranslation : 'History'.needTranslation,
style: TextStyle(
fontSize: 16,
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
color: AppColors.textColor,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 24.h,
height: 24.h,
alignment: Alignment.center,
child: InkWell(
onTap: () {
setState(() {
_isGraphVisible = !_isGraphVisible;
});
},
child: Utils.buildSvgWithAssets(
icon: _isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph,
width: 24.h,
height: 24.h,
),
),
),
// SizedBox(width: 16.h),
// Container(
// width: 24.h,
// height: 24.h,
// alignment: Alignment.center,
// child: Utils.buildSvgWithAssets(
// icon: AppAssets.ic_date_filter,
// width: 24.h,
// height: 24.h,
// ),
// ),
],
),
],
).paddingOnly(bottom: _isGraphVisible ? 16.h : 24.h),
if (history.isEmpty)
Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true)
else if (_isGraphVisible)
_buildHistoryGraph(history)
else
_buildHistoryList(context, history),
],
),
);
}
Widget _buildHistoryGraph(List<DataPoint> history) {
final minY = _minY(history);
final maxY = _maxY(history);
final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title);
return CustomGraph(
dataPoints: history,
makeGraphBasedOnActualValue: true,
leftLabelReservedSize: 40,
showGridLines: true,
showShadow: true,
leftLabelInterval: _leftInterval(history),
maxY: maxY,
minY: minY,
maxX: history.length.toDouble() - .75,
horizontalInterval: _leftInterval(history),
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
return _axisLabel(value.toStringAsFixed(0));
},
getDrawingHorizontalLine: (value) {
// Draw reference lines for high/low bounds
if (args.high != null && (value - args.high!).abs() < 0.1) {
return FlLine(
color: AppColors.bgGreenColor.withOpacity(0.2),
strokeWidth: 1,
dashArray: [5, 5],
);
}
if (args.low != null && (value - args.low!).abs() < 0.1) {
return FlLine(
color: AppColors.bgGreenColor.withOpacity(0.2),
strokeWidth: 1,
dashArray: [5, 5],
);
}
// Draw grid lines at intervals
return FlLine(
color: AppColors.bgGreenColor.withOpacity(0.2),
strokeWidth: 1,
dashArray: [5, 5],
);
},
graphColor: AppColors.bgGreenColor,
graphShadowColor: AppColors.lightGreenColor.withOpacity(.4),
graphGridColor: scheme.iconFg,
bottomLabelFormatter: (value, data) {
if (data.isEmpty) return const SizedBox.shrink();
if (value == 0) return _bottomLabel(data[value.toInt()].label);
if (value == data.length - 1) return _bottomLabel(data[value.toInt()].label, isLast: true);
if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label);
return const SizedBox.shrink();
},
rangeAnnotations: _rangeAnnotations(history),
minX: (history.length == 1) ? null : -.2,
scrollDirection: Axis.horizontal,
height: 180.h,
);
}
Widget _buildHistoryList(BuildContext context, List<DataPoint> history) {
final items = history.reversed.toList();
final height = items.length < 3 ? items.length * 64.0 : 180.h;
return SizedBox(
height: height,
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: items.length,
separatorBuilder: (_, __) => Divider(
color: AppColors.borderOnlyColor.withValues(alpha: 0.1),
height: 1,
),
itemBuilder: (context, index) {
final dp = items[index];
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(
color: AppColors.textColor,
fontWeight: FontWeight.w600,
),
],
),
);
},
),
);
}
double _minY(List<DataPoint> points) {
// IMPORTANT: y-axis uses actual numeric values (from actualValue).
final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList();
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) {
// IMPORTANT: y-axis uses actual numeric values (from actualValue).
final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList();
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) {
// Keep it stable; graph will mostly show just two labels.
final range = (_maxY(points) - _minY(points)).abs();
if (range <= 0) return 1;
return (range / 4).clamp(1, 20);
}
RangeAnnotations? _rangeAnnotations(List<DataPoint> points) {
if (args.low == null && args.high == null) return null;
final minY = _minY(points);
final maxY = _maxY(points);
final List<HorizontalRangeAnnotation> ranges = [];
if (args.low != null) {
ranges.add(
HorizontalRangeAnnotation(
y1: minY,
y2: args.low!,
color: AppColors.highAndLow.withOpacity(0.05),
),
);
}
if (args.low != null && args.high != null) {
ranges.add(
HorizontalRangeAnnotation(
y1: args.low!,
y2: args.high!,
color: AppColors.bgGreenColor.withOpacity(0.05),
),
);
}
if (args.high != null) {
ranges.add(
HorizontalRangeAnnotation(
y1: args.high!,
y2: maxY,
color: AppColors.criticalLowAndHigh.withOpacity(0.05),
),
);
}
return RangeAnnotations(horizontalRangeAnnotations: ranges);
}
List<DataPoint> _buildSeries(List<VitalSignResModel> vitals, VitalSignDetailsArgs args) {
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);
});
double? metricValue(VitalSignResModel v) {
switch (args.metric) {
case VitalSignMetric.bmi:
return _toDouble(v.bodyMassIndex);
case VitalSignMetric.height:
return _toDouble(v.heightCm);
case VitalSignMetric.weight:
return _toDouble(v.weightKg);
case VitalSignMetric.temperature:
return _toDouble(v.temperatureCelcius);
case VitalSignMetric.heartRate:
return _toDouble(v.heartRate ?? v.pulseBeatPerMinute);
case VitalSignMetric.respiratoryRate:
return _toDouble(v.respirationBeatPerMinute);
case VitalSignMetric.bloodPressure:
// Graph only systolic for now (simple single-series).
return _toDouble(v.bloodPressureHigher);
}
}
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
double index = 0;
for (final v in sorted) {
final mv = metricValue(v);
if (mv == null) continue;
if (mv == 0) continue;
final dt = v.vitalSignDate ?? DateTime.now();
final label = '${monthNames[dt.month - 1]}, ${dt.year}';
points.add(
DataPoint(
value: index,
label: label,
actualValue: mv.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();
return double.tryParse(v.toString());
}
String _latestValueText(VitalSignResModel? latest) {
if (latest == null) return '--';
switch (args.metric) {
case VitalSignMetric.bmi:
final v = _toDouble(latest.bodyMassIndex);
return v == null ? '--' : v.toStringAsFixed(0);
case VitalSignMetric.height:
final v = _toDouble(latest.heightCm);
return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}';
case VitalSignMetric.weight:
final v = _toDouble(latest.weightKg);
return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}';
case VitalSignMetric.temperature:
final v = _toDouble(latest.temperatureCelcius);
return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}';
case VitalSignMetric.heartRate:
final v = _toDouble(latest.heartRate ?? latest.pulseBeatPerMinute);
return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}';
case VitalSignMetric.respiratoryRate:
final v = _toDouble(latest.respirationBeatPerMinute);
return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}';
case VitalSignMetric.bloodPressure:
final s = _toDouble(latest.bloodPressureHigher);
final d = _toDouble(latest.bloodPressureLower);
if (s == null || d == null) return '--';
return '${s.toStringAsFixed(0)}/${d.toStringAsFixed(0)}';
}
}
String? _statusForLatest(VitalSignResModel? latest) {
if (latest == null) return null;
switch (args.metric) {
case VitalSignMetric.bmi:
return VitalSignUiModel.bmiStatus(latest.bodyMassIndex);
case VitalSignMetric.bloodPressure:
return VitalSignUiModel.bloodPressureStatus(systolic: latest.bloodPressureHigher, diastolic: latest.bloodPressureLower);
case VitalSignMetric.height:
return null;
case VitalSignMetric.weight:
return latest.weightKg != null ? 'Normal' : null;
case VitalSignMetric.temperature:
return null;
case VitalSignMetric.heartRate:
return (latest.heartRate ?? latest.pulseBeatPerMinute) != null ? 'Normal' : null;
case VitalSignMetric.respiratoryRate:
return latest.respirationBeatPerMinute != null ? 'Normal' : null;
}
}
String _descriptionText(BuildContext context) {
switch (args.metric) {
case VitalSignMetric.bmi:
return 'BMI is a measurement based on height and weight that estimates body fat.'.needTranslation;
case VitalSignMetric.height:
return 'Height is measured in centimeters and is used to calculate BMI and dosage recommendations.'.needTranslation;
case VitalSignMetric.weight:
return 'Weight helps track overall health, nutrition, and changes over time.'.needTranslation;
case VitalSignMetric.bloodPressure:
return 'Blood pressure reflects the force of blood against artery walls. It is shown as systolic/diastolic.'.needTranslation;
case VitalSignMetric.temperature:
return 'Body temperature reflects how hot your body is and may change with infection or inflammation.'.needTranslation;
case VitalSignMetric.heartRate:
return 'Heart rate refers to the number of heart beats per minute.'.needTranslation;
case VitalSignMetric.respiratoryRate:
return 'Respiratory rate is the number of breaths taken per minute.'.needTranslation;
}
}
String _nextStepsText(BuildContext context) {
switch (args.metric) {
case VitalSignMetric.bmi:
return 'Maintain a balanced diet and regular activity. If your BMI is high or low, consider consulting your doctor.'.needTranslation;
case VitalSignMetric.height:
return 'No action is needed unless your measurement looks incorrect. Update it during your next visit.'.needTranslation;
case VitalSignMetric.weight:
return 'Monitor weight changes. Sudden gain or loss may require medical advice.'.needTranslation;
case VitalSignMetric.bloodPressure:
return 'Keep tracking your blood pressure. High or low readings should be discussed with your doctor.'.needTranslation;
case VitalSignMetric.temperature:
return 'If you have a persistent fever or symptoms, contact your healthcare provider.'.needTranslation;
case VitalSignMetric.heartRate:
return 'Track your heart rate trends. If you feel dizziness or chest pain, seek medical care.'.needTranslation;
case VitalSignMetric.respiratoryRate:
return 'If you notice shortness of breath or abnormal breathing, seek medical advice.'.needTranslation;
}
}
Widget _nextStepsCard(BuildContext context) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
'What should I do next?'.needTranslation.toText16(weight: FontWeight.w600),
SizedBox(height: 8.h),
_nextStepsText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10),
],
),
);
}
Widget _axisLabel(String value) {
return Text(
value,
style: TextStyle(
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontSize: 8.f,
color: AppColors.textColor,
),
);
}
Widget _bottomLabel(String label, {bool isLast = false}) {
return Padding(
padding: EdgeInsets.only(
top: 8.0,
right: isLast ? 16.h : 0,
),
child: label.toText8(fontWeight: FontWeight.w500),
);
}
}

@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart';
import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_details_page.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class VitalSignPage extends StatefulWidget {
@ -22,6 +24,13 @@ class VitalSignPage extends StatefulWidget {
}
class _VitalSignPageState extends State<VitalSignPage> {
void _openDetails(VitalSignDetailsArgs args) {
Navigator.of(context).push(
CustomPageRoute(
page: VitalSignDetailsPage(args: args),
),
);
}
@override
void initState() {
@ -65,7 +74,14 @@ class _VitalSignPageState extends State<VitalSignPage> {
value: latestVitalSign?.bodyMassIndex?.toString() ?? '--',
unit: '',
status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex),
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.bmi,
title: 'BMI',
icon: AppAssets.bmiVital,
unit: '',
),
),
),
SizedBox(height: 16.h),
@ -76,7 +92,14 @@ class _VitalSignPageState extends State<VitalSignPage> {
value: latestVitalSign?.heightCm?.toString() ?? '--',
unit: 'cm',
status: null,
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.height,
title: 'Height',
icon: AppAssets.heightVital,
unit: 'cm',
),
),
),
SizedBox(height: 16.h),
@ -87,7 +110,14 @@ class _VitalSignPageState extends State<VitalSignPage> {
value: latestVitalSign?.weightKg?.toString() ?? '--',
unit: 'kg',
status: (latestVitalSign?.weightKg != null) ? 'Normal' : null,
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.weight,
title: 'Weight',
icon: AppAssets.weightVital,
unit: 'kg',
),
),
),
SizedBox(height: 16.h),
@ -105,7 +135,16 @@ class _VitalSignPageState extends State<VitalSignPage> {
systolic: latestVitalSign?.bloodPressureHigher,
diastolic: latestVitalSign?.bloodPressureLower,
),
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.bloodPressure,
title: 'Blood Pressure',
icon: AppAssets.bloodPressure,
unit: 'mmHg',
low: 90,
high: 140,
),
),
),
SizedBox(height: 16.h),
@ -116,7 +155,16 @@ class _VitalSignPageState extends State<VitalSignPage> {
value: latestVitalSign?.temperatureCelcius?.toString() ?? '--',
unit: '°C',
status: null,
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.temperature,
title: 'Temperature',
icon: AppAssets.temperature,
unit: '°C',
low: 36.1,
high: 37.2,
),
),
),
],
),
@ -130,7 +178,7 @@ class _VitalSignPageState extends State<VitalSignPage> {
children: [
// Body anatomy image with Heart Rate card overlaid at bottom
SizedBox(
height: 480.h,
height: 420.h,
width: double.infinity,
child: Stack(
clipBehavior: Clip.none,
@ -148,7 +196,7 @@ class _VitalSignPageState extends State<VitalSignPage> {
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 420.h,
height: 460.h,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(
@ -182,13 +230,22 @@ class _VitalSignPageState extends State<VitalSignPage> {
value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--',
unit: 'bpm',
status: 'Normal',
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.heartRate,
title: 'Heart Rate',
icon: AppAssets.heart,
unit: 'bpm',
low: 60,
high: 100,
),
),
),
),
],
),
),
SizedBox(height: 12.h),
SizedBox(height: 12.h),
// Respiratory rate Card
_buildVitalSignCard(
@ -197,7 +254,16 @@ class _VitalSignPageState extends State<VitalSignPage> {
value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--',
unit: 'bpm',
status: 'Normal',
onTap: () {},
onTap: () => _openDetails(
const VitalSignDetailsArgs(
metric: VitalSignMetric.respiratoryRate,
title: 'Respiratory rate',
icon: AppAssets.respRate,
unit: 'bpm',
low: 12,
high: 20,
),
),
),
],
),
@ -242,15 +308,15 @@ class _VitalSignPageState extends State<VitalSignPage> {
Row(
children: [
Container(
padding: EdgeInsets.all(10.h),
padding: EdgeInsets.all(8.h),
decoration: BoxDecoration(
color: scheme.iconBg,
borderRadius: BorderRadius.circular(12.r),
),
child: Utils.buildSvgWithAssets(
icon: icon,
width: 20.w,
height: 20.h,
width: 22.w,
height: 22.h,
iconColor: scheme.iconFg,
fit: BoxFit.contain,
),
@ -266,10 +332,15 @@ class _VitalSignPageState extends State<VitalSignPage> {
),
SizedBox(height: 14.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h),
// decoration: BoxDecoration(
// color: AppColors.bgScaffoldColor,
// borderRadius: BorderRadius.circular(10.r),
// ),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(10.r),
borderRadius: 10.r,
hasShadow: false,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -295,23 +366,24 @@ class _VitalSignPageState extends State<VitalSignPage> {
labelText: status,
backgroundColor: scheme.chipBg,
textColor: scheme.chipFg,
)
else
const SizedBox.shrink(),
],
),
),
SizedBox(height: 8.h),
Align(
alignment: AlignmentDirectional.centerEnd,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 18.w,
height: 18.h,
iconColor: AppColors.textColorLight,
fit: BoxFit.contain,
),
),
// SizedBox(height: 8.h),
// Align(
// alignment: AlignmentDirectional.centerEnd,
// child: Utils.buildSvgWithAssets(
// icon: AppAssets.arrow_forward,
// width: 18.w,
// height: 18.h,
// iconColor: AppColors.textColorLight,
// fit: BoxFit.contain,
// ),
// ),
],
),
),

Loading…
Cancel
Save