no message

pull/140/head
Sultan khan 2 weeks 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';
//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 //
static const String hmgLogo = '$pngBasePath/hmg_logo.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 fullBodyBack = '$pngBasePath/full_body_back.png';
static const String bmiFullBody = '$pngBasePath/bmi_image_1.png';
}

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

@ -51,6 +51,19 @@ class HmgServicesViewModel extends ChangeNotifier {
HospitalsModel? selectedHospital;
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
List<GetCMCAllOrdersResponseModel> hhcOrdersList = [];
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:flutter/material.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/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -43,8 +41,8 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
return CollapsingListView(
title: LocaleKeys.myDoctor.tr(context: context),
child: Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
title: LocaleKeys.myDoctor.tr(context: context),
child: Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
// build grouped lists from the flat list
final clinicMap = <String, List<dynamic>>{};
final hospitalMap = <String, List<dynamic>>{};
@ -171,129 +169,121 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown');
final isExpanded = expandedIndex == index;
return Container(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.h),
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys[index];
if (key != null && key.currentContext != null && expandedIndex == index) {
child: InkWell(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys.putIfAbsent(index, () => GlobalKey());
if (key.currentContext != null && expandedIndex == index) {
Future.delayed(const Duration(milliseconds: 450), () {
if (key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: Duration(milliseconds: 350),
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
alignment: 0.1,
alignment: 0.0,
);
}
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${group.length} ${'doctors'.needTranslation}",
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),
Text(
displayName,
style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
],
}
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppCustomChipWidget(labelText: "${group.length} ${'doctors'.needTranslation}"),
Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
SizedBox(height: 8.h),
Text(
displayName,
style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
],
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: isExpanded
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h),
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(
AnimatedSwitcher(
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
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...group.map<Widget>((doctor) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
(doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
width: 24.h,
width: 24.w,
height: 24.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
SizedBox(height: 6.h),
],
),
child: (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
),
],
),
SizedBox(height: 8.h),
Row(
Wrap(
direction: Axis.horizontal,
spacing: 4.h,
runSpacing: 4.h,
children: [
CustomButton(
text: 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,
AppCustomChipWidget(
labelText: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""),
),
],
),
SizedBox(height: 8.h),
SizedBox(height: 12.h),
Row(
children: [
Expanded(
flex: 6,
flex: 2,
child: CustomButton(
icon: AppAssets.view_report_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
text: "View Profile".needTranslation.tr(context: context),
onPressed: () async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
@ -320,75 +310,31 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
);
});
},
backgroundColor: AppColors.bgRedLightColor,
borderColor: AppColors.primaryRedColor,
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
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(),
),
)
: SizedBox.shrink(),
),
],
);
}).toList(),
],
),
)
: const SizedBox.shrink(),
),
],
),
),
);
},

@ -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/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/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/lab/lab_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/prescriptions/prescriptions_list_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/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
@ -79,9 +83,14 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
late MedicalFileViewModel medicalFileViewModel;
late BookAppointmentsViewModel bookAppointmentsViewModel;
late LabViewModel labViewModel;
late HmgServicesViewModel hmgServicesViewModel;
int currentIndex = 0;
// Used to make the PageView height follow the card's intrinsic height
final GlobalKey _vitalSignMeasureKey = GlobalKey();
double? _vitalSignMeasuredHeight;
@override
void initState() {
appState = getIt.get<AppState>();
@ -92,11 +101,29 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
medicalFileViewModel.getPatientSickLeaveList();
medicalFileViewModel.onTabChanged(0);
// Load vital signs
hmgServicesViewModel.getPatientVitalSign();
}
});
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
Widget build(BuildContext context) {
labViewModel = Provider.of<LabViewModel>(context, listen: false);
@ -104,6 +131,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
hmgServicesViewModel = Provider.of<HmgServicesViewModel>(context, listen: false);
NavigationService navigationService = getIt.get<NavigationService>();
return CollapsingListView(
title: "Medical File".needTranslation,
@ -250,6 +278,111 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
),
).paddingSymmetrical(24.w, 0.0),
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(
labelText: LocaleKeys.search.tr(context: context),
hintText: "Type any record".needTranslation,
@ -268,7 +401,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
// Using CustomExpandableList
CustomExpandableList(
expansionMode: ExpansionMode.exactlyOne,
dividerColor: Color(0xFF2B353E1A),
dividerColor: Color(0xff2b353e1a),
itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h),
items: [
ExpandableListItem(
@ -485,47 +618,51 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
horizontalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: MedicalFileAppointmentCard(
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index],
myAppointmentsViewModel: myAppointmentsViewModel,
onRescheduleTap: () {
openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]);
},
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: MedicalFileAppointmentCard(
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index],
myAppointmentsViewModel: myAppointmentsViewModel,
onRescheduleTap: () {
openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]);
},
onAskDoctorTap: () async {
LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation);
await myAppointmentsViewModel.isDoctorAvailable(
projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID,
doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID,
clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID,
onSuccess: (value) async {
if (value) {
await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.askDoctor.tr(context: context),
child: AskDoctorRequestTypeSelect(
askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList,
myAppointmentsViewModel: myAppointmentsViewModel,
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index],
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
print("Doctor is not available");
}
});
projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID,
doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID,
clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID,
onSuccess: (value) async {
if (value) {
await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.askDoctor.tr(context: context),
child: AskDoctorRequestTypeSelect(
askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList,
myAppointmentsViewModel: myAppointmentsViewModel,
patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index],
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
LoaderBottomSheet.hideLoader();
print("Doctor is not available");
}
},
onError: (_) {
LoaderBottomSheet.hideLoader();
},
);
},
),
),
),
),
);
));
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h),
),
@ -643,58 +780,57 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
);
}),
),
));
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
SizedBox(height: 16.h),
const Divider(color: AppColors.dividerColor),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "All Prescriptions".needTranslation,
onPressed: () {
Navigator.of(context).push(
CustomPageRoute(
page: PrescriptionsListPage(),
),
);
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 12.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
height: 40.h,
icon: AppAssets.requests,
iconColor: AppColors.primaryRedColor,
iconSize: 16.w,
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
SizedBox(height: 16.h),
const Divider(color: AppColors.dividerColor),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "All Prescriptions".needTranslation,
onPressed: () {
Navigator.of(context).push(
CustomPageRoute(
page: PrescriptionsListPage(),
),
);
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 12.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
height: 40.h,
icon: AppAssets.requests,
iconColor: AppColors.primaryRedColor,
iconSize: 16.w,
),
),
SizedBox(width: 6.w),
Expanded(
child: CustomButton(
text: "All Medications".needTranslation,
onPressed: () {},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 12.f,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.all_medications_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
SizedBox(width: 6.w),
Expanded(
child: CustomButton(
text: "All Medications".needTranslation,
onPressed: () {},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 12.f,
fontWeight: FontWeight.w500,
borderRadius: 12.h,
height: 40.h,
icon: AppAssets.all_medications_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
),
),
),
],
),
],
],
),
],
),
),
).paddingSymmetrical(0.w, 0.h)
@ -826,8 +962,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
});
}),
),
),
);
));
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
),
@ -1065,7 +1200,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.medical_reports_icon,
isLargeText: true,
iconSize: 36.h,
iconSize: 36.w,
).onPress(() {
medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
medicalFileViewModel.getPatientMedicalReportList();
@ -1185,4 +1320,265 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
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,231 +190,173 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
itemBuilder: (context, index) {
final group = model.patientRadiologyOrdersViewList[index];
final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown');
final isExpanded = expandedIndex == index;
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 400),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Group header container with key so we can scroll to it
GestureDetector(
onTap: () {
setState(() {
expandedIndex = expandedIndex == index ? null : index;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys.putIfAbsent(index, () => GlobalKey());
if (key.currentContext != null && expandedIndex == index) {
// Delay scrolling to wait for expansion animation
Future.delayed(Duration(milliseconds: 450), () {
if (key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: Duration(milliseconds: 350),
curve: Curves.easeInOut,
alignment: 0.0,
);
}
});
}
});
},
child: Container(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
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: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: InkWell(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys.putIfAbsent(index, () => GlobalKey());
if (key.currentContext != null && expandedIndex == index) {
Future.delayed(const Duration(milliseconds: 450), () {
if (key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
alignment: 0.0,
);
}
});
}
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${group.length} ${'results'.needTranslation}",
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),
Text(
displayName,
style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"),
Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
),
],
SizedBox(height: 8.h),
Text(
displayName,
style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: expandedIndex == index
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h),
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 24.h,
height: 24.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(order.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
SizedBox(height: 6.h),
],
AnimatedSwitcher(
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
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...group.map<Widget>((order) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 24.w,
height: 24.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Expanded(
child: (order.doctorName ?? '').toString().toText14(weight: FontWeight.w500),
),
),
],
),
SizedBox(height: 8.h),
Row(children: [
CustomButton(
text: order.description!,
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: 6.h),
Row(
children: [
CustomButton(
text: DateUtil.formatDateToDate(order.orderDate ?? order.appointmentDate ?? "", false),
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(width: 8.h),
CustomButton(
text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.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),
Row(
children: [
Expanded(
flex: 6,
child: SizedBox(),
),
SizedBox(width: 8.h),
Expanded(
flex: 1,
child: Container(
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 12,
],
),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 4.h,
runSpacing: 4.h,
children: [
if ((order.description ?? '').isNotEmpty)
AppCustomChipWidget(
labelText: (order.description ?? '').toString(),
),
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,
),
),
AppCustomChipWidget(
labelText: DateUtil.formatDateToDate(
(order.orderDate ?? order.appointmentDate),
false,
),
).onPress(() {
model.navigationService.push(
CustomPageRoute(
page: RadiologyResultPage(patientRadiologyResponseModel: order),
),
);
}),
),
],
),
],
),
),
);
}).toList(),
),
)
: SizedBox.shrink(),
),
AppCustomChipWidget(
labelText: model.isSortByClinic ? (order.clinicDescription ?? '') : (order.projectName ?? ''),
),
],
),
SizedBox(height: 12.h),
Row(
children: [
Expanded(flex: 2, child: const SizedBox()),
Expanded(
flex: 2,
child: CustomButton(
icon: AppAssets.view_report_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
text: "View Results".needTranslation,
onPressed: () {
model.navigationService.push(
CustomPageRoute(
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(),
],
),
)
: const SizedBox.shrink(),
),
],
),
],
),
),
),
),

@ -1,4 +1,4 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.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/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/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';
class VitalSignPage extends StatefulWidget {
@ -22,22 +22,10 @@ class VitalSignPage extends StatefulWidget {
}
class _VitalSignPageState extends State<VitalSignPage> {
@override
void 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
@ -72,48 +60,40 @@ class _VitalSignPageState extends State<VitalSignPage> {
children: [
// BMI Card
_buildVitalSignCard(
icon: AppAssets.activity,
iconColor: AppColors.successColor,
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
icon: AppAssets.bmiVital,
label: 'BMI',
value: latestVitalSign?.bodyMassIndex?.toString() ?? '--',
unit: '',
chipText: _getBMIStatus(latestVitalSign?.bodyMassIndex),
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
chipTextColor: AppColors.successColor,
status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex),
onTap: () {},
),
SizedBox(height: 16.h),
// Height Card
_buildVitalSignCard(
icon: AppAssets.height,
iconColor: AppColors.infoColor,
iconBgColor: AppColors.infoColor.withValues(alpha: 0.1),
icon: AppAssets.heightVital,
label: 'Height',
value: latestVitalSign?.heightCm?.toString() ?? '--',
unit: 'cm',
status: null,
onTap: () {},
),
SizedBox(height: 16.h),
// Weight Card
_buildVitalSignCard(
icon: AppAssets.weight,
iconColor: AppColors.successColor,
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
icon: AppAssets.weightVital,
label: 'Weight',
value: latestVitalSign?.weightKg?.toString() ?? '--',
unit: 'kg',
chipText: 'Normal',
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
chipTextColor: AppColors.successColor,
status: (latestVitalSign?.weightKg != null) ? 'Normal' : null,
onTap: () {},
),
SizedBox(height: 16.h),
// Blood Pressure Card
_buildVitalSignCard(
icon: AppAssets.activity,
iconColor: AppColors.warningColor,
iconBgColor: AppColors.warningColor.withValues(alpha: 0.1),
icon: AppAssets.bloodPressure,
label: 'Blood Pressure',
value: latestVitalSign != null &&
latestVitalSign.bloodPressureHigher != null &&
@ -121,17 +101,22 @@ class _VitalSignPageState extends State<VitalSignPage> {
? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}'
: '--',
unit: '',
status: VitalSignUiModel.bloodPressureStatus(
systolic: latestVitalSign?.bloodPressureHigher,
diastolic: latestVitalSign?.bloodPressureLower,
),
onTap: () {},
),
SizedBox(height: 16.h),
// Temperature Card
_buildVitalSignCard(
icon: AppAssets.activity,
iconColor: AppColors.errorColor,
iconBgColor: AppColors.errorColor.withValues(alpha: 0.1),
icon: AppAssets.temperature,
label: 'Temperature',
value: latestVitalSign?.temperatureCelcius?.toString() ?? '--',
unit: '°C',
status: null,
onTap: () {},
),
],
),
@ -143,54 +128,80 @@ class _VitalSignPageState extends State<VitalSignPage> {
Expanded(
child: Column(
children: [
// Body anatomy image
Container(
height: 280.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Center(
child: Image.asset(
AppAssets.fullBodyFront,
height: 260.h,
fit: BoxFit.contain,
),
),
),
SizedBox(height: 16.h),
// Body anatomy image with Heart Rate card overlaid at bottom
SizedBox(
height: 480.h,
width: double.infinity,
child: Stack(
clipBehavior: Clip.none,
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,
],
),
),
),
),
),
),
],
),
),
// Heart Rate Card
_buildVitalSignCard(
icon: AppAssets.heart,
iconColor: AppColors.errorColor,
iconBgColor: AppColors.errorColor.withValues(alpha: 0.1),
label: 'Heart Rate',
value: latestVitalSign?.heartRate?.toString() ??
latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--',
unit: 'bpm',
chipText: 'Normal',
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
chipTextColor: AppColors.successColor,
// Overlay Heart Rate card
Positioned(
left: 0,
right: 0,
bottom: 12.h,
child: _buildVitalSignCard(
icon: AppAssets.heart,
label: 'Heart Rate',
value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--',
unit: 'bpm',
status: 'Normal',
onTap: () {},
),
),
],
),
),
SizedBox(height: 16.h),
SizedBox(height: 12.h),
// Respiratory rate Card
_buildVitalSignCard(
icon: AppAssets.activity,
iconColor: AppColors.successColor,
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
label: 'Respiratory rate',
value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--',
unit: 'bpm',
chipText: 'Normal',
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
chipTextColor: AppColors.successColor,
),
],
),
),
// Respiratory rate Card
_buildVitalSignCard(
icon: AppAssets.respRate,
label: 'Respiratory rate',
value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--',
unit: 'bpm',
status: 'Normal',
onTap: () {},
),
],
),
),
],
),
),
@ -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({
required String icon,
required Color iconColor,
required Color iconBgColor,
required String label,
required String value,
required String unit,
String? chipText,
Color? chipBgColor,
Color? chipTextColor,
required String? status,
required VoidCallback onTap,
}) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Icon with background
Container(
padding: EdgeInsets.all(8.h),
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(12.r),
final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label);
return GestureDetector(
onTap: onTap,
child: Container(
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,
),
),
child: Utils.buildSvgWithAssets(
icon: icon,
width: 16.w,
height: 16.h,
iconColor: iconColor,
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),
),
SizedBox(width: 8.w),
Expanded(
child: label.toText10(
color: AppColors.textColorLight,
weight: FontWeight.w500,
),
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(),
],
),
// Forward arrow
Utils.buildSvgWithAssets(
),
SizedBox(height: 8.h),
Align(
alignment: AlignmentDirectional.centerEnd,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 16.w,
height: 16.h,
width: 18.w,
height: 18.h,
iconColor: AppColors.textColorLight,
fit: BoxFit.contain,
),
],
),
SizedBox(height: 12.h),
// Value
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
value.toText18(
isBold: true,
color: AppColors.textColor,
),
if (unit.isNotEmpty) ...[
SizedBox(width: 4.w),
unit.toText12(
color: AppColors.textColorLight,
),
],
],
),
// Chip if available
if (chipText != null) ...[
SizedBox(height: 8.h),
AppCustomChipWidget(
labelText: chipText,
backgroundColor: chipBgColor,
textColor: chipTextColor,
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
),
],
],
),
),
),
);
}
}

@ -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