From cc255e9c9ab93df54e5d0ab2e3d4b33710935f7c Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Wed, 7 Jan 2026 15:57:32 +0300 Subject: [PATCH 1/3] vital sign detail page --- .../vital_sign/vital_sign_details_page.dart | 620 ++++++++++++++++++ .../vital_sign/vital_sign_page.dart | 80 ++- 2 files changed, 693 insertions(+), 7 deletions(-) create mode 100644 lib/presentation/vital_sign/vital_sign_details_page.dart diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart new file mode 100644 index 0000000..fbbea64 --- /dev/null +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -0,0 +1,620 @@ +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 createState() => _VitalSignDetailsPageState(); +} + +class _VitalSignDetailsPageState extends State { + bool _isGraphVisible = true; + + VitalSignDetailsArgs get args => widget.args; + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: 'Vital Sign Details'.needTranslation, + child: Consumer( + 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( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _headerCard( + context, + title: args.title, + icon: args.icon, + valueText: latestValueText, + status: status, + scheme: scheme, + latestDate: latest?.vitalSignDate, + ), + SizedBox(height: 16.h), + + _whatIsThisResultCard(context), + SizedBox(height: 16.h), + + _historyCard(context, history: history), + SizedBox(height: 16.h), + + _nextStepsCard(context), + SizedBox(height: 32.h), + ], + ).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, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + title.toText18(isBold: true, weight: FontWeight.w600), + ], + ), + if (status != null) + Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), + decoration: BoxDecoration( + color: scheme.chipBg, + borderRadius: BorderRadius.circular(100.r), + ), + child: status.toText11( + color: scheme.chipFg, + weight: FontWeight.w500, + ), + ), + ], + ), + SizedBox(height: 10.h), + ( + latestDate != null + ? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation) + : ('Result of --'.needTranslation) + ).toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), + SizedBox(height: 12.h), + + valueText.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -2), + + if (args.low != null || args.high != null) ...[ + SizedBox(height: 8.h), + Text( + _referenceText(context), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ) + ] + ], + ), + ); + } + + 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, + children: [ + 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600), + SizedBox(height: 8.h), + _descriptionText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10), + SizedBox(height: 12.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bulb, width: 16.w, height: 16.h, iconColor: AppColors.greyTextColor), + SizedBox(width: 6.w), + Expanded( + child: 'This information is for monitoring and not a diagnosis.'.needTranslation + .toText11(color: AppColors.greyTextColor, weight: FontWeight.w500, maxLine: 3), + ), + ], + ) + ], + ), + ); + } + + Widget _historyCard(BuildContext context, {required List history}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 'History flowchart'.needTranslation.toText16(weight: FontWeight.w600), + Row( + children: [ + // toggle graph/list similar to lab result details + Utils.buildSvgWithAssets( + icon: _isGraphVisible ? AppAssets.graphIcon : AppAssets.listIcon, + width: 18.w, + height: 18.h, + iconColor: AppColors.greyTextColor, + ).onPress(() { + setState(() { + _isGraphVisible = !_isGraphVisible; + }); + }), + SizedBox(width: 10.w), + Utils.buildSvgWithAssets(icon: AppAssets.calendarGrey, width: 18.w, height: 18.h, iconColor: AppColors.greyTextColor), + ], + ), + ], + ), + SizedBox(height: 12.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 history) { + final minY = _minY(history); + final maxY = _maxY(history); + return CustomGraph( + dataPoints: history, + makeGraphBasedOnActualValue: true, + leftLabelReservedSize: 40, + showGridLines: true, + leftLabelInterval: _leftInterval(history), + maxY: maxY, + minY: minY, + maxX: history.length.toDouble() - .75, + horizontalInterval: .1, + leftLabelFormatter: (value) { + // Match the lab screen behavior: only show High/Low labels. + final v = double.parse(value.toStringAsFixed(1)); + if (args.high != null && v == args.high) { + return _axisLabel('High'.needTranslation); + } + if (args.low != null && v == args.low) { + return _axisLabel('Low'.needTranslation); + } + return const SizedBox.shrink(); + }, + getDrawingHorizontalLine: (value) { + value = double.parse(value.toStringAsFixed(1)); + if ((args.high != null && value == args.high) || (args.low != null && value == args.low)) { + return FlLine( + color: AppColors.bgGreenColor.withValues(alpha: 0.6), + strokeWidth: 1, + ); + } + return const FlLine(color: Colors.transparent, strokeWidth: 1); + }, + graphColor: AppColors.blackColor, + graphShadowColor: Colors.transparent, + graphGridColor: AppColors.graphGridColor.withValues(alpha: .4), + 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); + 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 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 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 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 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 points) { + if (args.low == null && args.high == null) return null; + + final minY = _minY(points); + final maxY = _maxY(points); + + final List ranges = []; + + if (args.low != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: minY, + y2: args.low!, + color: AppColors.highAndLow.withValues(alpha: 0.05), + ), + ); + } + + if (args.low != null && args.high != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: args.low!, + y2: args.high!, + color: AppColors.bgGreenColor.withValues(alpha: 0.05), + ), + ); + } + + if (args.high != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: args.high!, + y2: maxY, + color: AppColors.criticalLowAndHigh.withValues(alpha: 0.05), + ), + ); + } + + return RangeAnnotations(horizontalRangeAnnotations: ranges); + } + + List _buildSeries(List vitals, VitalSignDetailsArgs args) { + 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); + }); + + 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); + } + } + + 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 = '${dt.day}/${dt.month}'; + + 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) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: 8.f, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.labelTextColor, + ), + ), + ); + } +} diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index bd25641..d8b6d7e 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -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 { + void _openDetails(VitalSignDetailsArgs args) { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignDetailsPage(args: args), + ), + ); + } @override void initState() { @@ -65,7 +74,14 @@ class _VitalSignPageState extends State { 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 { 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 { 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 { 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 { 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, + ), + ), ), ], ), @@ -182,7 +230,16 @@ class _VitalSignPageState extends State { 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, + ), + ), ), ), ], @@ -197,7 +254,16 @@ class _VitalSignPageState extends State { 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, + ), + ), ), ], ), From ac0d72b3ff8b94d8e72dc6daeb33c5c74f356079 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 12 Jan 2026 11:41:35 +0300 Subject: [PATCH 2/3] vital sign finalized. --- .../medical_file/medical_file_page.dart | 287 +++++++++--------- .../vital_sign/vital_sign_details_page.dart | 274 +++++++++-------- .../vital_sign/vital_sign_page.dart | 46 +-- 3 files changed, 329 insertions(+), 278 deletions(-) diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 6dde958..3d8d115 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -305,55 +305,64 @@ class _MedicalFilePageState extends State { 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 { ), ], ], - ).paddingSymmetrical(24.w, 0.0); + ); }), SizedBox(height: 16.h), @@ -1394,23 +1403,23 @@ class _MedicalFilePageState extends State { ).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, @@ -1446,62 +1455,66 @@ class _MedicalFilePageState extends State { }) { 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, ), - ), - ], - ), + ], + )), + ]; } @@ -1526,7 +1539,6 @@ class _MedicalFilePageState extends State { return GestureDetector( onTap: onTap, child: Container( - // Same styling used originally for vitals in MedicalFilePage decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 16.r, @@ -1540,15 +1552,15 @@ class _MedicalFilePageState extends State { 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, ), @@ -1563,55 +1575,56 @@ class _MedicalFilePageState extends State { ], ), 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, - ), - ), ], ), ), diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index fbbea64..f75a71b 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -74,7 +74,7 @@ class _VitalSignDetailsPageState extends State { return SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, children: [ _headerCard( context, @@ -85,16 +85,8 @@ class _VitalSignDetailsPageState extends State { scheme: scheme, latestDate: latest?.vitalSignDate, ), - SizedBox(height: 16.h), - _whatIsThisResultCard(context), - SizedBox(height: 16.h), - _historyCard(context, history: history), - SizedBox(height: 16.h), - - _nextStepsCard(context), - SizedBox(height: 32.h), ], ).paddingAll(24.h), ); @@ -121,65 +113,71 @@ class _VitalSignDetailsPageState extends State { padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - padding: EdgeInsets.all(10.h), - decoration: BoxDecoration( - color: scheme.iconBg, - borderRadius: BorderRadius.circular(12.r), - ), - child: Utils.buildSvgWithAssets( - icon: icon, - width: 20.w, - height: 20.h, - iconColor: scheme.iconFg, - fit: BoxFit.contain, - ), - ), - SizedBox(width: 10.w), - title.toText18(isBold: true, weight: FontWeight.w600), + title.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -1), + + ], ), - if (status != null) - Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), - decoration: BoxDecoration( - color: scheme.chipBg, - borderRadius: BorderRadius.circular(100.r), - ), - child: status.toText11( - color: scheme.chipFg, - weight: FontWeight.w500, - ), + 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, + ), + ], + ), + ], ), + ), ], ), - SizedBox(height: 10.h), - ( - latestDate != null - ? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation) - : ('Result of --'.needTranslation) - ).toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), - SizedBox(height: 12.h), - - valueText.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -2), - - if (args.low != null || args.high != null) ...[ - SizedBox(height: 8.h), + 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, + ), ], ), ); @@ -208,21 +206,10 @@ class _VitalSignDetailsPageState extends State { padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, children: [ - 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600), - SizedBox(height: 8.h), - _descriptionText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10), - SizedBox(height: 12.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.bulb, width: 16.w, height: 16.h, iconColor: AppColors.greyTextColor), - SizedBox(width: 6.w), - Expanded( - child: 'This information is for monitoring and not a diagnosis.'.needTranslation - .toText11(color: AppColors.greyTextColor, weight: FontWeight.w500, maxLine: 3), - ), - ], - ) + 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor), + _descriptionText(context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), ], ), ); @@ -235,34 +222,63 @@ class _VitalSignDetailsPageState extends State { borderRadius: 24.h, hasShadow: true, ), - padding: EdgeInsets.all(16.h), + height: _isGraphVisible + ? 260.h + : (history.length < 3) + ? (history.length * 64) + 80.h + : 260.h, + padding: EdgeInsets.all(15.h), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - 'History flowchart'.needTranslation.toText16(weight: FontWeight.w600), + Text( + _isGraphVisible ? 'History flowchart'.needTranslation : 'History'.needTranslation, + style: TextStyle( + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), Row( + mainAxisSize: MainAxisSize.min, children: [ - // toggle graph/list similar to lab result details - Utils.buildSvgWithAssets( - icon: _isGraphVisible ? AppAssets.graphIcon : AppAssets.listIcon, - width: 18.w, - height: 18.h, - iconColor: AppColors.greyTextColor, - ).onPress(() { - setState(() { - _isGraphVisible = !_isGraphVisible; - }); - }), - SizedBox(width: 10.w), - Utils.buildSvgWithAssets(icon: AppAssets.calendarGrey, width: 18.w, height: 18.h, iconColor: AppColors.greyTextColor), + 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, + // ), + // ), ], ), ], - ), - SizedBox(height: 12.h), + ).paddingOnly(bottom: _isGraphVisible ? 16.h : 24.h), + if (history.isEmpty) Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) else if (_isGraphVisible) @@ -277,47 +293,63 @@ class _VitalSignDetailsPageState extends State { Widget _buildHistoryGraph(List 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: .1, + horizontalInterval: _leftInterval(history), leftLabelFormatter: (value) { - // Match the lab screen behavior: only show High/Low labels. - final v = double.parse(value.toStringAsFixed(1)); - if (args.high != null && v == args.high) { - return _axisLabel('High'.needTranslation); + // Show labels at interval points + if (args.high != null && (value - args.high!).abs() < 0.1) { + return _axisLabel('High'); } - if (args.low != null && v == args.low) { - return _axisLabel('Low'.needTranslation); + if (args.low != null && (value - args.low!).abs() < 0.1) { + return _axisLabel('Low'); } - return const SizedBox.shrink(); + // Show numeric labels at regular intervals + return _axisLabel(value.toStringAsFixed(0)); }, getDrawingHorizontalLine: (value) { - value = double.parse(value.toStringAsFixed(1)); - if ((args.high != null && value == args.high) || (args.low != null && value == args.low)) { + // Draw reference lines for high/low bounds + if (args.high != null && (value - args.high!).abs() < 0.1) { return FlLine( - color: AppColors.bgGreenColor.withValues(alpha: 0.6), + color: AppColors.bgGreenColor.withOpacity(0.2), strokeWidth: 1, + dashArray: [5, 5], ); } - return const FlLine(color: Colors.transparent, strokeWidth: 1); - }, - graphColor: AppColors.blackColor, - graphShadowColor: Colors.transparent, - graphGridColor: AppColors.graphGridColor.withValues(alpha: .4), - 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); - if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); - return const SizedBox.shrink(); + 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, @@ -325,6 +357,7 @@ 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; @@ -393,7 +426,7 @@ class _VitalSignDetailsPageState extends State { HorizontalRangeAnnotation( y1: minY, y2: args.low!, - color: AppColors.highAndLow.withValues(alpha: 0.05), + color: AppColors.highAndLow.withOpacity(0.05), ), ); } @@ -403,7 +436,7 @@ class _VitalSignDetailsPageState extends State { HorizontalRangeAnnotation( y1: args.low!, y2: args.high!, - color: AppColors.bgGreenColor.withValues(alpha: 0.05), + color: AppColors.bgGreenColor.withOpacity(0.05), ), ); } @@ -413,7 +446,7 @@ class _VitalSignDetailsPageState extends State { HorizontalRangeAnnotation( y1: args.high!, y2: maxY, - color: AppColors.criticalLowAndHigh.withValues(alpha: 0.05), + color: AppColors.criticalLowAndHigh.withOpacity(0.05), ), ); } @@ -447,11 +480,14 @@ class _VitalSignDetailsPageState extends State { case VitalSignMetric.respiratoryRate: return _toDouble(v.respirationBeatPerMinute); case VitalSignMetric.bloodPressure: - // Graph only systolic for now (simple single-series). + // 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); @@ -459,7 +495,7 @@ class _VitalSignDetailsPageState extends State { if (mv == 0) continue; final dt = v.vitalSignDate ?? DateTime.now(); - final label = '${dt.day}/${dt.month}'; + final label = '${monthNames[dt.month - 1]}, ${dt.year}'; points.add( DataPoint( @@ -603,18 +639,14 @@ class _VitalSignDetailsPageState extends State { ); } - Widget _bottomLabel(String label) { + Widget _bottomLabel(String label, {bool isLast = false}) { return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - label, - style: TextStyle( - fontSize: 8.f, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - color: AppColors.labelTextColor, - ), + padding: EdgeInsets.only( + top: 8.0, + right: isLast ? 16.h : 0, ), + child: label.toText8(fontWeight: FontWeight.w500), ); } + } diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index d8b6d7e..95ff3e5 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -178,7 +178,7 @@ class _VitalSignPageState extends State { 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, @@ -196,7 +196,7 @@ class _VitalSignPageState extends State { Align( alignment: Alignment.bottomCenter, child: SizedBox( - height: 420.h, + height: 480.h, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), child: Container( @@ -245,7 +245,7 @@ class _VitalSignPageState extends State { ], ), ), - SizedBox(height: 12.h), + SizedBox(height: 12.h), // Respiratory rate Card _buildVitalSignCard( @@ -308,15 +308,15 @@ class _VitalSignPageState extends State { 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, ), @@ -332,10 +332,15 @@ class _VitalSignPageState extends State { ), 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, @@ -361,23 +366,24 @@ class _VitalSignPageState extends State { 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, + // ), + // ), ], ), ), From 0514716910eab59caa0f5fcba914f9a608d76333 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 12 Jan 2026 11:48:05 +0300 Subject: [PATCH 3/3] no message --- lib/presentation/vital_sign/vital_sign_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index 95ff3e5..fbf9fc6 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -196,7 +196,7 @@ class _VitalSignPageState extends State { Align( alignment: Alignment.bottomCenter, child: SizedBox( - height: 480.h, + height: 460.h, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), child: Container(