no message

pull/140/head
Sultan khan 8 months ago
parent 0895d94df2
commit f7021f685a

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

@ -284,6 +284,19 @@ class AppAssets {
static const String covid19icon = '$svgBasePath/covid_19.svg'; static const String covid19icon = '$svgBasePath/covid_19.svg';
//vital sign
static const String heartRate = '$svgBasePath/heart_rate.svg';
static const String respRate = '$svgBasePath/resp_rate.svg';
static const String weightVital = '$svgBasePath/weight_2.svg';
static const String bmiVital = '$svgBasePath/bmi_2.svg';
static const String heightVital = '$svgBasePath/height_2.svg';
static const String bloodPressure = '$svgBasePath/blood_pressure.svg';
static const String temperature = '$svgBasePath/temperature.svg';
// PNGS // // PNGS //
static const String hmgLogo = '$pngBasePath/hmg_logo.png'; static const String hmgLogo = '$pngBasePath/hmg_logo.png';
static const String liveCareService = '$pngBasePath/livecare_service.png'; static const String liveCareService = '$pngBasePath/livecare_service.png';
@ -309,7 +322,7 @@ class AppAssets {
static const String fullBodyFront = '$pngBasePath/full_body_front.png'; static const String fullBodyFront = '$pngBasePath/full_body_front.png';
static const String fullBodyBack = '$pngBasePath/full_body_back.png'; static const String fullBodyBack = '$pngBasePath/full_body_back.png';
static const String bmiFullBody = '$pngBasePath/bmi_image_1.png';
} }

@ -935,8 +935,8 @@ class HmgServicesRepoImp implements HmgServicesRepo {
try { try {
List<VitalSignResModel> vitalSignList = []; List<VitalSignResModel> vitalSignList = [];
if (response['PatientVitalSignList'] != null && response['PatientVitalSignList'] is List) { if (response['List_DoctorPatientVitalSign'] != null && response['List_DoctorPatientVitalSign'] is List) {
final vitalSignsList = response['PatientVitalSignList'] as List; final vitalSignsList = response['List_DoctorPatientVitalSign'] as List;
for (var vitalSignJson in vitalSignsList) { for (var vitalSignJson in vitalSignsList) {
if (vitalSignJson is Map<String, dynamic>) { if (vitalSignJson is Map<String, dynamic>) {

@ -51,6 +51,19 @@ class HmgServicesViewModel extends ChangeNotifier {
HospitalsModel? selectedHospital; HospitalsModel? selectedHospital;
List<VitalSignResModel> vitalSignList = []; List<VitalSignResModel> vitalSignList = [];
// Vital Sign PageView Controller
PageController _vitalSignPageController = PageController();
PageController get vitalSignPageController => _vitalSignPageController;
int _vitalSignCurrentPage = 0;
int get vitalSignCurrentPage => _vitalSignCurrentPage;
void setVitalSignCurrentPage(int page) {
_vitalSignCurrentPage = page;
notifyListeners();
}
// HHC specific lists // HHC specific lists
List<GetCMCAllOrdersResponseModel> hhcOrdersList = []; List<GetCMCAllOrdersResponseModel> hhcOrdersList = [];
List<GetCMCServicesResponseModel> hhcServicesList = []; List<GetCMCServicesResponseModel> hhcServicesList = [];
@ -896,4 +909,10 @@ class HmgServicesViewModel extends ChangeNotifier {
}, },
); );
} }
@override
void dispose() {
_vitalSignPageController.dispose();
super.dispose();
}
} }

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
/// UI-only helper model for Vital Sign cards.
///
/// Keeps presentation logic (chip colors, icon colors, simple status rules)
/// in one place so it can be reused across multiple pages.
class VitalSignUiModel {
final Color iconBg;
final Color iconFg;
final Color chipBg;
final Color chipFg;
const VitalSignUiModel({
required this.iconBg,
required this.iconFg,
required this.chipBg,
required this.chipFg,
});
/// Returns a color scheme for a card based on its [status] and [label].
///
/// Rules (mirrors existing behavior in Medical File page):
/// - Height is always blue.
/// - High => red scheme.
/// - Low => yellow scheme.
/// - Otherwise => green scheme (Normal).
static VitalSignUiModel scheme({required String? status, required String label}) {
final s = (status ?? '').toLowerCase();
final l = label.toLowerCase();
// Height should always be blue.
if (l.contains('height')) {
return VitalSignUiModel(
iconBg: AppColors.infoColor.withValues(alpha: 0.12),
iconFg: AppColors.infoColor,
chipBg: AppColors.infoColor.withValues(alpha: 0.12),
chipFg: AppColors.infoColor,
);
}
if (s.contains('high')) {
return const VitalSignUiModel(
iconBg: AppColors.chipSecondaryLightRedColor,
iconFg: AppColors.primaryRedColor,
chipBg: AppColors.chipSecondaryLightRedColor,
chipFg: AppColors.primaryRedColor,
);
}
if (s.contains('low')) {
final Color yellowBg = AppColors.warningColor.withValues(alpha: 0.12);
return VitalSignUiModel(
iconBg: yellowBg,
iconFg: AppColors.warningColor,
chipBg: yellowBg,
chipFg: AppColors.warningColor,
);
}
// Normal (green)
final Color greenBg = AppColors.lightGreenColor;
return VitalSignUiModel(
iconBg: greenBg,
iconFg: AppColors.bgGreenColor,
chipBg: greenBg,
chipFg: AppColors.bgGreenColor,
);
}
/// Simple, user-friendly classification:
/// - Low: systolic < 90 OR diastolic < 60
/// - High: systolic >= 140 OR diastolic >= 90
/// - Normal: otherwise
/// Returns null if values are missing/unparseable.
static String? bloodPressureStatus({dynamic systolic, dynamic diastolic}) {
final int? s = toIntOrNull(systolic);
final int? d = toIntOrNull(diastolic);
if (s == null || d == null) return null;
if (s < 90 || d < 60) return 'Low';
if (s >= 140 || d >= 90) return 'High';
return 'Normal';
}
static int? toIntOrNull(dynamic v) {
if (v == null) return null;
if (v is int) return v;
if (v is double) return v.round();
return int.tryParse(v.toString());
}
static String bmiStatus(dynamic bmi) {
if (bmi == null) return 'N/A';
final double bmiValue = double.tryParse(bmi.toString()) ?? 0;
if (bmiValue < 18.5) return 'Underweight';
if (bmiValue < 25) return 'Normal';
if (bmiValue < 30) return 'Overweight';
return 'High';
}
}

@ -1,8 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.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/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -171,54 +169,50 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown');
final isExpanded = expandedIndex == index; final isExpanded = expandedIndex == index;
return Container( return AnimatedContainer(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()), duration: const Duration(milliseconds: 300),
margin: EdgeInsets.only(bottom: 12.h), curve: Curves.easeInOut,
padding: EdgeInsets.all(16.h), margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 20.h, borderRadius: 20.h,
hasShadow: true, hasShadow: true,
), ),
child: Column( child: InkWell(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
expandedIndex = isExpanded ? null : index; expandedIndex = isExpanded ? null : index;
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys[index]; final key = _groupKeys.putIfAbsent(index, () => GlobalKey());
if (key != null && key.currentContext != null && expandedIndex == index) { if (key.currentContext != null && expandedIndex == index) {
Future.delayed(const Duration(milliseconds: 450), () {
if (key.currentContext != null) {
Scrollable.ensureVisible( Scrollable.ensureVisible(
key.currentContext!, key.currentContext!,
duration: Duration(milliseconds: 350), duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut, curve: Curves.easeInOut,
alignment: 0.1, alignment: 0.0,
); );
} }
}); });
}
});
}, },
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
padding: EdgeInsets.all(16.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
CustomButton( AppCustomChipWidget(labelText: "${group.length} ${'doctors'.needTranslation}"),
text: "${group.length} ${'doctors'.needTranslation}", Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(isExpanded ? Icons.expand_less : Icons.chevron_right, color: AppColors.greyColor),
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@ -231,69 +225,65 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
), ),
), ),
AnimatedSwitcher( AnimatedSwitcher(
duration: Duration(milliseconds: 400), duration: const Duration(milliseconds: 500),
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: child,
),
);
},
child: isExpanded child: isExpanded
? Container( ? Container(
key: ValueKey<int>(index), key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h), padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
children: group.map<Widget>((doctor) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( ...group.map<Widget>((doctor) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Image.network( Image.network(
(doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"), (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
width: 24.h, width: 24.w,
height: 24.h, height: 24.h,
fit: BoxFit.cover, fit: BoxFit.cover,
).circle(100), ).circle(100),
SizedBox(width: 8.h), SizedBox(width: 8.h),
Expanded( Expanded(
child: Column( child: (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
SizedBox(height: 6.h),
],
),
), ),
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
Row( Wrap(
direction: Axis.horizontal,
spacing: 4.h,
runSpacing: 4.h,
children: [ children: [
CustomButton( AppCustomChipWidget(
text: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""), labelText: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
), ),
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 12.h),
Row( Row(
children: [ children: [
Expanded( Expanded(
flex: 6, flex: 2,
child: CustomButton( child: CustomButton(
icon: AppAssets.view_report_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
text: "View Profile".needTranslation.tr(context: context), text: "View Profile".needTranslation.tr(context: context),
onPressed: () async { onPressed: () async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
@ -320,76 +310,32 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
); );
}); });
}, },
backgroundColor: AppColors.bgRedLightColor, backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.primaryRedColor, borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor, textColor: AppColors.primaryRedColor,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
borderRadius: 12, borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0), padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h, height: 40.h,
), ),
), ),
SizedBox(width: 8.h),
Expanded(
flex: 1,
child: Container(
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 12,
),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Transform.flip(
flipX: getIt<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.whiteColor,
fit: BoxFit.contain,
),
),
),
).onPress(() async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: doctor?.clinicID ?? 0,
projectID: doctor?.projectID ?? 0,
doctorID: doctor?.doctorID ?? 0,
));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
),
], ],
), ),
SizedBox(height: 12.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h),
SizedBox(height: 12.h),
], ],
),
),
); );
}).toList(), }).toList(),
],
), ),
) )
: SizedBox.shrink(), : const SizedBox.shrink(),
), ),
], ],
), ),
),
); );
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),

@ -15,6 +15,9 @@ 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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.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/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
@ -48,6 +51,7 @@ import 'package:hmg_patient_app_new/presentation/medical_report/medical_reports_
import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart';
import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart';
import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart';
import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
@ -79,9 +83,14 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
late MedicalFileViewModel medicalFileViewModel; late MedicalFileViewModel medicalFileViewModel;
late BookAppointmentsViewModel bookAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel;
late LabViewModel labViewModel; late LabViewModel labViewModel;
late HmgServicesViewModel hmgServicesViewModel;
int currentIndex = 0; int currentIndex = 0;
// Used to make the PageView height follow the card's intrinsic height
final GlobalKey _vitalSignMeasureKey = GlobalKey();
double? _vitalSignMeasuredHeight;
@override @override
void initState() { void initState() {
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
@ -92,11 +101,29 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
medicalFileViewModel.getPatientSickLeaveList(); medicalFileViewModel.getPatientSickLeaveList();
medicalFileViewModel.onTabChanged(0); medicalFileViewModel.onTabChanged(0);
// Load vital signs
hmgServicesViewModel.getPatientVitalSign();
} }
}); });
super.initState(); super.initState();
} }
void _scheduleVitalSignMeasure() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = _vitalSignMeasureKey.currentContext;
if (ctx == null) return;
final box = ctx.findRenderObject();
if (box is RenderBox) {
final h = box.size.height;
if (h > 0 && h != _vitalSignMeasuredHeight) {
setState(() {
_vitalSignMeasuredHeight = h;
});
}
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
labViewModel = Provider.of<LabViewModel>(context, listen: false); labViewModel = Provider.of<LabViewModel>(context, listen: false);
@ -104,6 +131,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false); myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false); medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false); bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
hmgServicesViewModel = Provider.of<HmgServicesViewModel>(context, listen: false);
NavigationService navigationService = getIt.get<NavigationService>(); NavigationService navigationService = getIt.get<NavigationService>();
return CollapsingListView( return CollapsingListView(
title: "Medical File".needTranslation, title: "Medical File".needTranslation,
@ -250,6 +278,111 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
), ),
).paddingSymmetrical(24.w, 0.0), ).paddingSymmetrical(24.w, 0.0),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Vital Signs Section
Consumer<HmgServicesViewModel>(builder: (context, hmgServicesVM, child) {
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(),
),
);
}),
SizedBox(height: 16.h),
// Make this section dynamic-height (no fixed 160.h)
LayoutBuilder(
builder: (context, constraints) {
if (hmgServicesVM.isVitalSignLoading) {
return _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),
],
),
);
}
// The cards define their own height; measure the first rendered page once
_scheduleVitalSignMeasure();
final double hostHeight = _vitalSignMeasuredHeight ?? (160.h);
return SizedBox(
height: hostHeight,
child: PageView(
controller: hmgServicesVM.vitalSignPageController,
onPageChanged: (index) {
hmgServicesVM.setVitalSignCurrentPage(index);
_scheduleVitalSignMeasure();
},
children: _buildVitalSignPages(
vitalSign: hmgServicesVM.vitalSignList.first,
onTap: () {
Navigator.of(context).push(
CustomPageRoute(
page: VitalSignPage(),
),
);
},
measureKey: _vitalSignMeasureKey,
currentPageIndex: hmgServicesVM.vitalSignCurrentPage,
),
),
);
},
),
if (!hmgServicesVM.isVitalSignLoading && hmgServicesVM.vitalSignList.isNotEmpty) ...[
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
2, // 2 pages (BMI+Height on page 1, Weight+BP on page 2)
(index) => Container(
margin: EdgeInsets.symmetric(horizontal: 3.w),
width: hmgServicesVM.vitalSignCurrentPage == index ? 24.w : 8.w,
height: 8.h,
decoration: BoxDecoration(
color: hmgServicesVM.vitalSignCurrentPage == index
? AppColors.primaryRedColor
: AppColors.dividerColor,
borderRadius: BorderRadius.circular(4.r),
),
),
),
),
],
],
).paddingSymmetrical(24.w, 0.0);
}),
SizedBox(height: 16.h),
TextInputWidget( TextInputWidget(
labelText: LocaleKeys.search.tr(context: context), labelText: LocaleKeys.search.tr(context: context),
hintText: "Type any record".needTranslation, hintText: "Type any record".needTranslation,
@ -268,7 +401,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
// Using CustomExpandableList // Using CustomExpandableList
CustomExpandableList( CustomExpandableList(
expansionMode: ExpansionMode.exactlyOne, expansionMode: ExpansionMode.exactlyOne,
dividerColor: Color(0xFF2B353E1A), dividerColor: Color(0xff2b353e1a),
itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h),
items: [ items: [
ExpandableListItem( ExpandableListItem(
@ -485,7 +618,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
horizontalOffset: 100.0, horizontalOffset: 100.0,
child: FadeInAnimation( child: FadeInAnimation(
child: AnimatedContainer( child: AnimatedContainer(
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: MedicalFileAppointmentCard( child: MedicalFileAppointmentCard(
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index],
@ -517,15 +650,19 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
); );
}); });
} else { } else {
LoaderBottomSheet.hideLoader();
print("Doctor is not available"); print("Doctor is not available");
} }
}); },
onError: (_) {
LoaderBottomSheet.hideLoader();
},
);
}, },
), ),
), ),
), ),
), ));
);
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h),
), ),
@ -643,8 +780,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
); );
}), }),
), ),
), ));
);
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
), ),
@ -826,8 +962,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
}); });
}), }),
), ),
), ));
);
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
), ),
@ -1065,7 +1200,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
backgroundColor: AppColors.whiteColor, backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.medical_reports_icon, svgIcon: AppAssets.medical_reports_icon,
isLargeText: true, isLargeText: true,
iconSize: 36.h, iconSize: 36.w,
).onPress(() { ).onPress(() {
medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
medicalFileViewModel.getPatientMedicalReportList(); medicalFileViewModel.getPatientMedicalReportList();
@ -1185,4 +1320,265 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
return Container(); return Container();
} }
} }
// Build shimmer for vital signs
Widget _buildVitalSignShimmer() {
return Row(
children: [
Expanded(child: _buildSingleShimmerCard()),
SizedBox(width: 12.w),
Expanded(child: _buildSingleShimmerCard()),
],
);
}
Widget _buildSingleShimmerCard() {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Icon shimmer at top
Container(
width: 44.w,
height: 44.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
),
).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),
// Value shimmer (larger)
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,
children: [
Container(
width: 60.w,
height: 20.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
),
).toShimmer(),
Container(
width: 16.w,
height: 16.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.r),
),
).toShimmer(),
],
),
],
),
),
);
}
// Build pages with 2 cards each
List<Widget> _buildVitalSignPages({
required VitalSignResModel vitalSign,
required VoidCallback onTap,
required GlobalKey measureKey,
required int currentPageIndex,
}) {
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,
),
),
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,
),
),
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,
),
),
],
),
];
}
String _getBMIStatus(dynamic bmi) {
return VitalSignUiModel.bmiStatus(bmi);
}
String? _getBloodPressureStatus({dynamic systolic, dynamic diastolic}) {
return VitalSignUiModel.bloodPressureStatus(systolic: systolic, diastolic: diastolic);
}
Widget _buildVitalSignCard({
required String icon,
required String label,
required String value,
required String unit,
required String? status,
required VoidCallback onTap,
}) {
final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label);
return GestureDetector(
onTap: onTap,
child: Container(
// Same styling used originally for vitals in MedicalFilePage
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 16.r,
hasShadow: false,
),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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),
Expanded(
child: label.toText14(
color: AppColors.textColor,
weight: FontWeight.w600,
),
),
],
),
SizedBox(height: 14.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(10.r),
),
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,
),
],
],
),
if (status != null)
AppCustomChipWidget(
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,
),
),
],
),
),
),
);
}
} }

@ -190,30 +190,35 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final group = model.patientRadiologyOrdersViewList[index]; final group = model.patientRadiologyOrdersViewList[index];
final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown');
final isExpanded = expandedIndex == index;
return AnimationConfiguration.staggeredList( return AnimationConfiguration.staggeredList(
position: index, position: index,
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
child: SlideAnimation( child: SlideAnimation(
verticalOffset: 50.0, verticalOffset: 50.0,
child: FadeInAnimation( child: FadeInAnimation(
child: Column( child: AnimatedContainer(
crossAxisAlignment: CrossAxisAlignment.start, duration: const Duration(milliseconds: 300),
children: [ curve: Curves.easeInOut,
// Group header container with key so we can scroll to it margin: EdgeInsets.symmetric(vertical: 8.h),
GestureDetector( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
expandedIndex = expandedIndex == index ? null : index; expandedIndex = isExpanded ? null : index;
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); final key = _groupKeys.putIfAbsent(index, () => GlobalKey());
if (key.currentContext != null && expandedIndex == index) { if (key.currentContext != null && expandedIndex == index) {
// Delay scrolling to wait for expansion animation Future.delayed(const Duration(milliseconds: 450), () {
Future.delayed(Duration(milliseconds: 450), () {
if (key.currentContext != null) { if (key.currentContext != null) {
Scrollable.ensureVisible( Scrollable.ensureVisible(
key.currentContext!, key.currentContext!,
duration: Duration(milliseconds: 350), duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut, curve: Curves.easeInOut,
alignment: 0.0, alignment: 0.0,
); );
@ -222,44 +227,20 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
} }
}); });
}, },
child: Container( child: Column(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()), crossAxisAlignment: CrossAxisAlignment.start,
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.h),
boxShadow: [
BoxShadow(
color: AppColors.blackColor.withValues(alpha: 0.03),
blurRadius: 6,
offset: Offset(0, 2),
)
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Padding(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
padding: EdgeInsets.all(16.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
CustomButton( AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"),
text: "${group.length} ${'results'.needTranslation}", Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(expandedIndex == index ? Icons.expand_less : Icons.expand_more),
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@ -271,153 +252,114 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
], ],
), ),
), ),
],
),
),
),
AnimatedSwitcher( AnimatedSwitcher(
duration: Duration(milliseconds: 400), duration: const Duration(milliseconds: 500),
child: expandedIndex == index switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: child,
),
);
},
child: isExpanded
? Container( ? Container(
key: ValueKey<int>(index), key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h), padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
children: group.map<Widget>((order) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( ...group.map<Widget>((order) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Image.network( Image.network(
order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 24.h, width: 24.w,
height: 24.h, height: 24.h,
fit: BoxFit.cover, fit: BoxFit.cover,
).circle(100), ).circle(100),
SizedBox(width: 8.h), SizedBox(width: 8.h),
Expanded( Expanded(
child: Column( child: (order.doctorName ?? '').toString().toText14(weight: FontWeight.w500),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(order.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
SizedBox(height: 6.h),
],
),
), ),
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
Row(children: [ Wrap(
CustomButton( direction: Axis.horizontal,
text: order.description!, spacing: 4.h,
onPressed: () {}, runSpacing: 4.h,
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
)
]
),
SizedBox(height: 6.h),
Row(
children: [ children: [
CustomButton( if ((order.description ?? '').isNotEmpty)
text: DateUtil.formatDateToDate(order.orderDate ?? order.appointmentDate ?? "", false), AppCustomChipWidget(
onPressed: () {}, labelText: (order.description ?? '').toString(),
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
), ),
SizedBox(width: 8.h), AppCustomChipWidget(
CustomButton( labelText: DateUtil.formatDateToDate(
text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), (order.orderDate ?? order.appointmentDate),
onPressed: () {}, false,
backgroundColor: AppColors.greyColor, ),
borderColor: AppColors.greyColor, ),
textColor: AppColors.blackColor, AppCustomChipWidget(
fontSize: 10, labelText: model.isSortByClinic ? (order.clinicDescription ?? '') : (order.projectName ?? ''),
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
), ),
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 12.h),
Row( Row(
children: [ children: [
Expanded(flex: 2, child: const SizedBox()),
Expanded( Expanded(
flex: 6, flex: 2,
child: SizedBox(), child: CustomButton(
), icon: AppAssets.view_report_icon,
SizedBox(width: 8.h), iconColor: AppColors.primaryRedColor,
Expanded( iconSize: 16.h,
flex: 1, text: "View Results".needTranslation,
child: Container( onPressed: () {
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 12,
),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Transform.flip(
flipX: false,
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.whiteColor,
fit: BoxFit.contain,
),
),
),
).onPress(() {
model.navigationService.push( model.navigationService.push(
CustomPageRoute( CustomPageRoute(
page: RadiologyResultPage(patientRadiologyResponseModel: order), page: RadiologyResultPage(patientRadiologyResponseModel: order),
), ),
); );
}), },
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
), ),
],
), ),
], ],
), ),
), SizedBox(height: 12.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h),
SizedBox(height: 12.h),
],
); );
}).toList(), }).toList(),
],
), ),
) )
: SizedBox.shrink(), : const SizedBox.shrink(),
), ),
], ],
), ),
), ),
), ),
),
),
); );
}, },
); );

@ -1,4 +1,4 @@
import 'dart:async'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -11,7 +11,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vit
import 'package:hmg_patient_app_new/theme/colors.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/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class VitalSignPage extends StatefulWidget { class VitalSignPage extends StatefulWidget {
@ -22,22 +22,10 @@ class VitalSignPage extends StatefulWidget {
} }
class _VitalSignPageState extends State<VitalSignPage> { class _VitalSignPageState extends State<VitalSignPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final HmgServicesViewModel hmgServicesViewModel = context.read<HmgServicesViewModel>();
scheduleMicrotask(() async {
LoaderBottomSheet.showLoader(loadingText: 'Loading Vital Signs...');
await hmgServicesViewModel.getPatientVitalSign(
onSuccess: (_) {
LoaderBottomSheet.hideLoader();
},
onError: (_) {
LoaderBottomSheet.hideLoader();
},
);
});
} }
@override @override
@ -72,48 +60,40 @@ class _VitalSignPageState extends State<VitalSignPage> {
children: [ children: [
// BMI Card // BMI Card
_buildVitalSignCard( _buildVitalSignCard(
icon: AppAssets.activity, icon: AppAssets.bmiVital,
iconColor: AppColors.successColor,
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
label: 'BMI', label: 'BMI',
value: latestVitalSign?.bodyMassIndex?.toString() ?? '--', value: latestVitalSign?.bodyMassIndex?.toString() ?? '--',
unit: '', unit: '',
chipText: _getBMIStatus(latestVitalSign?.bodyMassIndex), status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex),
chipBgColor: AppColors.successColor.withValues(alpha: 0.1), onTap: () {},
chipTextColor: AppColors.successColor,
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Height Card // Height Card
_buildVitalSignCard( _buildVitalSignCard(
icon: AppAssets.height, icon: AppAssets.heightVital,
iconColor: AppColors.infoColor,
iconBgColor: AppColors.infoColor.withValues(alpha: 0.1),
label: 'Height', label: 'Height',
value: latestVitalSign?.heightCm?.toString() ?? '--', value: latestVitalSign?.heightCm?.toString() ?? '--',
unit: 'cm', unit: 'cm',
status: null,
onTap: () {},
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Weight Card // Weight Card
_buildVitalSignCard( _buildVitalSignCard(
icon: AppAssets.weight, icon: AppAssets.weightVital,
iconColor: AppColors.successColor,
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
label: 'Weight', label: 'Weight',
value: latestVitalSign?.weightKg?.toString() ?? '--', value: latestVitalSign?.weightKg?.toString() ?? '--',
unit: 'kg', unit: 'kg',
chipText: 'Normal', status: (latestVitalSign?.weightKg != null) ? 'Normal' : null,
chipBgColor: AppColors.successColor.withValues(alpha: 0.1), onTap: () {},
chipTextColor: AppColors.successColor,
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Blood Pressure Card // Blood Pressure Card
_buildVitalSignCard( _buildVitalSignCard(
icon: AppAssets.activity, icon: AppAssets.bloodPressure,
iconColor: AppColors.warningColor,
iconBgColor: AppColors.warningColor.withValues(alpha: 0.1),
label: 'Blood Pressure', label: 'Blood Pressure',
value: latestVitalSign != null && value: latestVitalSign != null &&
latestVitalSign.bloodPressureHigher != null && latestVitalSign.bloodPressureHigher != null &&
@ -121,17 +101,22 @@ class _VitalSignPageState extends State<VitalSignPage> {
? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}' ? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}'
: '--', : '--',
unit: '', unit: '',
status: VitalSignUiModel.bloodPressureStatus(
systolic: latestVitalSign?.bloodPressureHigher,
diastolic: latestVitalSign?.bloodPressureLower,
),
onTap: () {},
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Temperature Card // Temperature Card
_buildVitalSignCard( _buildVitalSignCard(
icon: AppAssets.activity, icon: AppAssets.temperature,
iconColor: AppColors.errorColor,
iconBgColor: AppColors.errorColor.withValues(alpha: 0.1),
label: 'Temperature', label: 'Temperature',
value: latestVitalSign?.temperatureCelcius?.toString() ?? '--', value: latestVitalSign?.temperatureCelcius?.toString() ?? '--',
unit: '°C', unit: '°C',
status: null,
onTap: () {},
), ),
], ],
), ),
@ -143,50 +128,76 @@ class _VitalSignPageState extends State<VitalSignPage> {
Expanded( Expanded(
child: Column( child: Column(
children: [ children: [
// Body anatomy image // Body anatomy image with Heart Rate card overlaid at bottom
Container( SizedBox(
height: 280.h, height: 480.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( width: double.infinity,
color: AppColors.whiteColor, child: Stack(
borderRadius: 20.h, clipBehavior: Clip.none,
hasShadow: true, children: [
// Image
Positioned.fill(
child: Stack(
fit: StackFit.expand,
children: [
Image.asset(
AppAssets.bmiFullBody,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 420.h,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.whiteColor.withValues(alpha: 0.0),
AppColors.whiteColor.withValues(alpha: 0.97),
AppColors.whiteColor,
],
), ),
child: Center(
child: Image.asset(
AppAssets.fullBodyFront,
height: 260.h,
fit: BoxFit.contain,
), ),
), ),
), ),
SizedBox(height: 16.h), ),
),
],
),
),
// Heart Rate Card // Overlay Heart Rate card
_buildVitalSignCard( Positioned(
left: 0,
right: 0,
bottom: 12.h,
child: _buildVitalSignCard(
icon: AppAssets.heart, icon: AppAssets.heart,
iconColor: AppColors.errorColor,
iconBgColor: AppColors.errorColor.withValues(alpha: 0.1),
label: 'Heart Rate', label: 'Heart Rate',
value: latestVitalSign?.heartRate?.toString() ?? value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--',
latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--',
unit: 'bpm', unit: 'bpm',
chipText: 'Normal', status: 'Normal',
chipBgColor: AppColors.successColor.withValues(alpha: 0.1), onTap: () {},
chipTextColor: AppColors.successColor,
), ),
SizedBox(height: 16.h), ),
],
),
),
SizedBox(height: 12.h),
// Respiratory rate Card // Respiratory rate Card
_buildVitalSignCard( _buildVitalSignCard(
icon: AppAssets.activity, icon: AppAssets.respRate,
iconColor: AppColors.successColor,
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
label: 'Respiratory rate', label: 'Respiratory rate',
value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--',
unit: 'bpm', unit: 'bpm',
chipText: 'Normal', status: 'Normal',
chipBgColor: AppColors.successColor.withValues(alpha: 0.1), onTap: () {},
chipTextColor: AppColors.successColor,
), ),
], ],
), ),
@ -205,104 +216,106 @@ class _VitalSignPageState extends State<VitalSignPage> {
); );
} }
String? _getBMIStatus(dynamic bmi) {
if (bmi == null) return null;
double bmiValue = double.tryParse(bmi.toString()) ?? 0;
if (bmiValue < 18.5) return 'Underweight';
if (bmiValue < 25) return 'Normal';
if (bmiValue < 30) return 'Overweight';
return 'Obese';
}
Widget _buildVitalSignCard({ Widget _buildVitalSignCard({
required String icon, required String icon,
required Color iconColor,
required Color iconBgColor,
required String label, required String label,
required String value, required String value,
required String unit, required String unit,
String? chipText, required String? status,
Color? chipBgColor, required VoidCallback onTap,
Color? chipTextColor,
}) { }) {
return Container( final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label);
return GestureDetector(
onTap: onTap,
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 20.h, borderRadius: 16.r,
hasShadow: true, hasShadow: false,
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(12.h), padding: EdgeInsets.all(16.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
// Icon with background
Container( Container(
padding: EdgeInsets.all(8.h), padding: EdgeInsets.all(10.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: iconBgColor, color: scheme.iconBg,
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
child: Utils.buildSvgWithAssets( child: Utils.buildSvgWithAssets(
icon: icon, icon: icon,
width: 16.w, width: 20.w,
height: 16.h, height: 20.h,
iconColor: iconColor, iconColor: scheme.iconFg,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
SizedBox(width: 8.w), SizedBox(width: 10.w),
Expanded( Expanded(
child: label.toText10( child: label.toText14(
color: AppColors.textColorLight, color: AppColors.textColor,
weight: FontWeight.w500, weight: FontWeight.w600,
), ),
), ),
// Forward arrow
Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 16.w,
height: 16.h,
iconColor: AppColors.textColorLight,
fit: BoxFit.contain,
),
], ],
), ),
SizedBox(height: 12.h), SizedBox(height: 14.h),
Container(
// Value padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(10.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
value.toText18( value.toText17(
isBold: true, isBold: true,
color: AppColors.textColor, color: AppColors.textColor,
), ),
if (unit.isNotEmpty) ...[ if (unit.isNotEmpty) ...[
SizedBox(width: 4.w), SizedBox(width: 3.w),
unit.toText12( unit.toText12(
color: AppColors.textColorLight, color: AppColors.textColor,
fontWeight: FontWeight.w500,
), ),
], ],
], ],
), ),
if (status != null)
// Chip if available
if (chipText != null) ...[
SizedBox(height: 8.h),
AppCustomChipWidget( AppCustomChipWidget(
labelText: chipText, labelText: status,
backgroundColor: chipBgColor, backgroundColor: scheme.chipBg,
textColor: chipTextColor, textColor: scheme.chipFg,
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), )
), 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,
),
),
], ],
), ),
), ),
),
); );
} }
} }

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class VitalSignShimmerWidget extends StatelessWidget {
const VitalSignShimmerWidget({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
// BMI Card Shimmer
Expanded(child: _buildShimmerCard()),
SizedBox(width: 8.w),
// Height Card Shimmer
Expanded(child: _buildShimmerCard()),
SizedBox(width: 8.w),
// Weight Card Shimmer
Expanded(child: _buildShimmerCard()),
SizedBox(width: 8.w),
// Blood Pressure Card Shimmer
Expanded(child: _buildShimmerCard()),
],
);
}
Widget _buildShimmerCard() {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Icon shimmer
Container(
width: 32.w,
height: 32.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
),
).toShimmer(),
SizedBox(height: 8.h),
// Label shimmer
Container(
width: 50.w,
height: 10.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.r),
),
).toShimmer(),
SizedBox(height: 4.h),
// Value shimmer
Container(
width: 40.w,
height: 16.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.r),
),
).toShimmer(),
SizedBox(height: 4.h),
// Chip shimmer
Container(
width: 45.w,
height: 18.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
),
).toShimmer(),
SizedBox(height: 4.h),
// Arrow shimmer
Align(
alignment: AlignmentDirectional.centerEnd,
child: Container(
width: 10.w,
height: 10.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.r),
),
).toShimmer(),
),
],
),
),
);
}
}
Loading…
Cancel
Save