From e8319a0d3f1b0988285c33365c04bad3ab8673aa Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 8 Oct 2025 12:26:23 +0300 Subject: [PATCH 01/21] active medication --- .../active_medication_page.dart | 682 ++++++++++++++++++ lib/presentation/home/landing_page.dart | 11 +- 2 files changed, 692 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/active_medication/active_medication_page.dart diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart new file mode 100644 index 00000000..21e1a8bd --- /dev/null +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -0,0 +1,682 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; + +import '../../core/app_assets.dart'; +import '../../core/utils/utils.dart'; +import '../../generated/locale_keys.g.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import 'package:intl/intl.dart'; + +import '../../widgets/buttons/custom_button.dart'; +import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting + + + +class ActiveMedicationPage extends StatefulWidget { + @override + State createState() => _ActiveMedicationPageState(); +} + +class _ActiveMedicationPageState extends State { + + late DateTime currentDate; + late DateTime selectedDate; + +// Info for each day (customizable) + final Map> dayInfo = { + 0: {"text": "Medications", "icon": Icons.medication_outlined, "description": "Affected"}, + 1: {"text": "Doctor Appointment", "icon": Icons.local_hospital_outlined, "description": "Twice"}, + 2: {"text": "Rest Day", "icon": Icons.self_improvement_outlined, "description": "Daily"}, + 3: {"text": "Gym Session", "icon": Icons.fitness_center_outlined, "description": "Affected"}, + 4: {"text": "Meeting", "icon": Icons.meeting_room_outlined, "description": "Twice"}, + 5: {"text": "Shopping", "icon": Icons.shopping_bag_outlined, "description": "Daily"}, + 6: {"text": "Family Time", "icon": Icons.family_restroom_outlined, "description": "Affected"}, + }; + + @override + void initState() { + super.initState(); + currentDate = DateTime.now(); + selectedDate = currentDate; + } + +// Generate today + next 6 days + List getUpcomingDays() { + return List.generate(7, (index) => currentDate.add(Duration(days: index))); + } + + @override + Widget build(BuildContext context) { + + List days = getUpcomingDays(); + int dayIndex = selectedDate.difference(currentDate).inDays; + + String dateText = + "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; + String infoMed = dayInfo[dayIndex]?["text"] ?? "No Info"; + IconData infoImg= dayInfo[dayIndex]?["icon"] ?? Icons.info_outline; + String medDetails = dayInfo[dayIndex]?["description"] ?? "No Info"; + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () { + Navigator.of(context).pop(); + }, + onLanguageChanged: (lang) {}, + hideLogoAndLang: true, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 65, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + DateTime day = days[index]; + String label = DateFormat('E').format(day); // Mon, Tue + return Padding( + padding: const EdgeInsets.only(right: 12), + child: buildDayCard(label, day), + ); + }, + ), + ), + + const SizedBox(height: 20), + +// Show full date text + Text( + dateText, + style: TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500), + ), + const Text( + "Medications", + style: TextStyle( + color: AppColors.primaryRedBorderColor,fontSize: 12, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 16), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, + hasShadow: true,), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(1), + child: Container( + width: 59, + height: 59, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.spacerLineColor,// Border color + width: 1.0, ), + borderRadius: BorderRadius.circular(30),// Border width + ), + child: + Icon(infoImg, size: 26), + // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + ), + ), + const SizedBox(width: 12), + Text( + infoMed, + style: TextStyle( + fontSize: 16, + height: 1.2, + fontWeight: FontWeight.w700, + color: Colors.black87), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "Route: $medDetails", + ), + AppCustomChipWidget( + labelText: "Frequency: $medDetails", + ), + AppCustomChipWidget( + labelText: "Daily Does $medDetails", + ), + AppCustomChipWidget( + labelText: "Duration: $medDetails ", + ), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: Colors.grey,), + const SizedBox(width: 8), + Expanded( + child: Text( + "Remark: some remarks about the prescription will be here", + style: TextStyle( + fontSize: 10, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.visible, + ), + ) + ], + ), + ], + ).paddingAll(16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + // Reminder Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10),// Border width + ), + child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Set Reminder", + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, + color: AppColors.textColor), + ), + const Text( + "Notify me before the consumption time", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, + color: AppColors.textColorLight), + ), + ], + ), + ], + ), + // Switch( + // value: isActiveReminder, + // onChanged: (_) {}, + // activeColor: Colors.green, + // ), + ], + ).paddingOnly(left:16, right: 16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + +// Buttons + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.checkAvailability.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomButton( + text: LocaleKeys.readInstructions.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + ), + ), + ], + ).paddingAll(16), + ], + ), + ) +// Expanded( +// child: ListView( +// children: [ +// Container( +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, +// hasShadow: true,), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Row( +// children: [ +// ClipRRect( +// borderRadius: BorderRadius.circular(1), +// child: Container( +// width: 59, +// height: 59, +// decoration: BoxDecoration( +// border: Border.all( +// color: AppColors.spacerLineColor,// Border color +// width: 1.0, ), +// borderRadius: BorderRadius.circular(30),// Border width +// ), +// child: +// Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) +// ), +// ), +// const SizedBox(width: 12), +// const Expanded( +// child: Text( +// "Diclofenac Diethylamine 1% Topical Gel", +// style: TextStyle( +// fontSize: 16, +// height: 1.2, +// fontWeight: FontWeight.w700, +// color: Colors.black87), +// ), +// ), +// ], +// ), +// const SizedBox(height: 12), +// Wrap( +// direction: Axis.horizontal, +// spacing: 4.h, +// runSpacing: 4.h, +// children: [ +// AppCustomChipWidget( +// labelText: "Route: Affected Area ", +// ), +// AppCustomChipWidget( +// labelText: "Route: Affected Area ", +// ), +// AppCustomChipWidget( +// labelText: "Daily Does 2", +// ), +// AppCustomChipWidget( +// labelText: "Route: Affected Area ", +// ), +// ], +// ), +// const SizedBox(height: 12), +// Row( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Icon(Icons.info_outline, color: Colors.grey,), +// const SizedBox(width: 8), +// Expanded( +// child: Text( +// "Remark: some remarks about the prescription will be here", +// style: TextStyle( +// fontSize: 10, +// color: AppColors.greyTextColor, +// fontWeight: FontWeight.w500, +// ), +// overflow: TextOverflow.visible, +// ), +// ) +// ], +// ), +// ], +// ).paddingAll(16), +// const Divider( +// indent: 0, +// endIndent: 0, +// thickness: 1, +// color: AppColors.greyColor, +// ), +// // Reminder Row +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Row( +// children: [ +// Container( +// width: 40, +// height: 40, +// decoration: BoxDecoration( +// color: AppColors.greyColor, +// borderRadius: BorderRadius.circular(10),// Border width +// ), +// child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), +// const SizedBox(width: 8), +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// "Set Reminder", +// style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, +// color: AppColors.textColor), +// ), +// const Text( +// "Notify me before the consumption time", +// style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, +// color: AppColors.textColorLight), +// ), +// ], +// ), +// ], +// ), +// // Switch( +// // value: isActiveReminder, +// // onChanged: (_) {}, +// // activeColor: Colors.green, +// // ), +// ], +// ).paddingOnly(left:16, right: 16), +// const Divider( +// indent: 0, +// endIndent: 0, +// thickness: 1, +// color: AppColors.greyColor, +// ), +// +// // Buttons +// Row( +// children: [ +// Expanded( +// child: CustomButton( +// text: LocaleKeys.checkAvailability.tr(), +// fontSize: 14, +// onPressed: () async { +// }, +// backgroundColor: AppColors.secondaryLightRedColor, +// borderColor: AppColors.secondaryLightRedColor, +// textColor: AppColors.errorColor, +// ), +// ), +// const SizedBox(width: 12), +// Expanded( +// child: CustomButton( +// text: LocaleKeys.readInstructions.tr(), +// fontSize: 14, +// onPressed: () async { +// }, +// backgroundColor: AppColors.primaryRedColor, +// borderColor: AppColors.primaryRedColor, +// textColor: AppColors.whiteColor, +// ), +// ), +// ], +// ).paddingAll(16), +// ], +// ), +// ) +// // MedicationCard(), +// // SizedBox(height: 16), +// // MedicationCard(isActiveReminder: true), +// ], +// ), +// ), + ] + ), + + ), + ); + } + + Widget buildDayCard(String label, DateTime date) { + bool isSelected = selectedDate.day == date.day && + selectedDate.month == date.month && + selectedDate.year == date.year; + + return GestureDetector( + onTap: () { + setState(() { + selectedDate = date; + }); + }, + child: Container( + width: 57, + height: 65, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isSelected ? AppColors.secondaryLightRedBorderColor: AppColors.transparent, + border: Border.all( + color: isSelected ? AppColors.primaryRedBorderColor : AppColors.spacerLineColor, + width: 1.0, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day == currentDate.day ? "Today" : label, + style: TextStyle( + color: isSelected ? AppColors.primaryRedBorderColor : AppColors.greyTextColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 5), + Text( + date.day.toString(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected ? AppColors.primaryRedBorderColor : AppColors.textColor, + ), + ), + ], + ), + ), + ), + ); + } + + String getSuffix(int day) { + if (day == 1 || day == 21 || day == 31) return "st"; + if (day == 2 || day == 22) return "nd"; + if (day == 3 || day == 23) return "rd"; + return "th"; + } +} + +class MedicationCard extends StatelessWidget { + final bool isActiveReminder; + const MedicationCard({super.key, this.isActiveReminder = false}); + + Color get primaryRed => const Color(0xFFE84B3A); + + @override + Widget build(BuildContext context) { + return + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, + hasShadow: true,), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(1), + child: Container( + width: 59, + height: 59, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.spacerLineColor,// Border color + width: 1.0, ), + borderRadius: BorderRadius.circular(30),// Border width + ), + child: + Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + "Diclofenac Diethylamine 1% Topical Gel", + style: TextStyle( + fontSize: 16, + height: 1.2, + fontWeight: FontWeight.w700, + color: Colors.black87), + ), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "Route: Affected Area ", + ), + AppCustomChipWidget( + labelText: "Route: Affected Area ", + ), + AppCustomChipWidget( + labelText: "Daily Does 2", + ), + AppCustomChipWidget( + labelText: "Route: Affected Area ", + ), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: Colors.grey,), + const SizedBox(width: 8), + Expanded( + child: Text( + "Remark: some remarks about the prescription will be here", + style: TextStyle( + fontSize: 10, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.visible, + ), + ) + ], + ), + ], + ).paddingAll(16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + // Reminder Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10),// Border width + ), + child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Set Reminder", + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, + color: AppColors.textColor), + ), + const Text( + "Notify me before the consumption time", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, + color: AppColors.textColorLight), + ), + ], + ), + ], + ), + Switch( + value: isActiveReminder, + onChanged: (_) {}, + activeColor: Colors.green, + ), + ], + ).paddingOnly(left:16, right: 16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + +// Buttons + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.checkAvailability.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomButton( + text: LocaleKeys.readInstructions.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + ), + ), + ], + ).paddingAll(16), + ], + ), + ); + } +} + diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index cb2f76f4..2bb1b14a 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -44,6 +44,8 @@ import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dar import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; +import '../active_medication/active_medication_page.dart'; + class LandingPage extends StatefulWidget { const LandingPage({super.key}); @@ -379,7 +381,14 @@ class _LandingPageState extends State { "Services".toText16(isBold: true), Row( children: [ - "View all services".toText12(color: AppColors.primaryRedColor), + "View all services".toText12(color: AppColors.primaryRedColor).onPress(() { + Navigator.of(context) + .push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + ); + }), SizedBox(width: 2.h), Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), ], From de8c7bc60523fcd041507130e9de461fd239280e Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Tue, 21 Oct 2025 10:51:45 +0300 Subject: [PATCH 02/21] active medication --- lib/core/api_consts.dart | 6 +- lib/core/utils/utils.dart | 2 +- .../active_prescriptions_repo.dart | 99 +++ .../active_prescriptions_view_model.dart | 57 ++ .../active_prescriptions_response_model.dart | 149 ++++ lib/main.dart | 4 + .../active_medication_page.dart | 714 ++++++++---------- lib/presentation/home/landing_page.dart | 6 - .../medical_file/medical_file_page.dart | 13 +- lib/services/dialog_service.dart | 16 + .../reminder_timer_dialog.dart | 155 ++++ 11 files changed, 794 insertions(+), 427 deletions(-) create mode 100644 lib/features/active_prescriptions/active_prescriptions_repo.dart create mode 100644 lib/features/active_prescriptions/active_prescriptions_view_model.dart create mode 100644 lib/features/active_prescriptions/models/active_prescriptions_response_model.dart create mode 100644 lib/widgets/medication_reminder/reminder_timer_dialog.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9591cfe2..8d7ae290 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -419,7 +419,7 @@ var GET_WEIGHT_PRESSURE_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_Get var GET_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResult'; var ADD_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_AddWeightMeasurementResult'; -var ADD_ACTIVE_PRESCRIPTIONS_REPORT_BY_PATIENT_ID = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; +// var ADD_ACTIVE_PRESCRIPTIONS_REPORT_BY_PATIENT_ID = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; var GET_CALL_INFO_HOURS_RESULT = 'Services/Doctors.svc/REST/GetCallInfoHoursResult'; var GET_CALL_REQUEST_TYPE_LOV = 'Services/Doctors.svc/REST/GetCallRequestType_LOV'; @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -838,7 +838,7 @@ class ApiConsts { static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; - + static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; // static values for Api static final double appVersionID = 18.7; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index c4a2db84..fe5dd94b 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -643,7 +643,7 @@ class Utils { } /// Widget to build an SVG from network - static Widget buildImgWithNetwork({required String url, required Color iconColor, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover}) { + static Widget buildImgWithNetwork({required String url, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover}) { return Image.network( url, width: width, diff --git a/lib/features/active_prescriptions/active_prescriptions_repo.dart b/lib/features/active_prescriptions/active_prescriptions_repo.dart new file mode 100644 index 00000000..847d6e80 --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_repo.dart @@ -0,0 +1,99 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; + +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class ActivePrescriptionsRepo { + + Future>> getActivePrescriptionsDetails(); + +} + +class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + ActivePrescriptionsRepoImp({required this.loggerService, required this.apiClient}); + + @override + + Future>> getActivePrescriptionsDetails() async + { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getActivePrescriptionsDetails, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // final list = response['GetActivePrescriptionReportByPatientIDList']; + + // final prescriptionLists = list.map((item) => ActivePrescriptionsResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + return ['List_ActiveGetPrescriptionReportByPatientID']; + //apiResponse; + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + + + + // + // Future> getActiveMedications() { + // try { + // GenericApiModel? apiResponse; + // Failure? failure; + // return apiClient.post( + // ApiConsts.getActivePrescriptionsDetails, + // body: patientDeviceDataRequest, + // onFailure: (error, statusCode, {messageStatus, failureType}) { + // failure = failureType; + // }, + // onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + // try { + // apiResponse = GenericApiModel( + // messageStatus: messageStatus, + // statusCode: statusCode, + // errorMessage: errorMessage, + // data: response, + // ); + // } catch (e) { + // failure = DataParsingFailure(e.toString()); + // } + // }, + // ).then((_) { + // if (failure != null) return Left(failure!); + // if (apiResponse == null) return Left(ServerFailure("Unknown error")); + // return Right(apiResponse!); + // }); + // } catch (e) { + // return Future.value(Left(UnknownFailure(e.toString()))); + // } + // } +} \ No newline at end of file diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart new file mode 100644 index 00000000..d995f963 --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -0,0 +1,57 @@ + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class ActivePrescriptionsViewModel extends ChangeNotifier { + bool isActivePrescriptionsDetailsLoading = false; + + late ActivePrescriptionsRepo activePrescriptionsRepo; + late ErrorHandlerService errorHandlerService; + + // Prescription Orders Lists + List activePrescriptionsDetailsList = []; + + initActivePrescriptionsViewModel() { + getActiveMedications(); + notifyListeners(); + } + + setPrescriptionsDetailsLoading() { + isActivePrescriptionsDetailsLoading = true; + // activePrescriptionsDetailsList.clear(); + notifyListeners(); + } + + Future getActiveMedications( {Function(dynamic)? onSuccess, Function(String)? onError}) + async { + final result = await activePrescriptionsRepo.getActivePrescriptionsDetails(); + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + activePrescriptionsDetailsList = apiResponse.data!; + isActivePrescriptionsDetailsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + print(activePrescriptionsDetailsList.length); + } + } + }, + ); + } + + + + + + + + + + +} diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart new file mode 100644 index 00000000..878e1911 --- /dev/null +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; + +class ActivePrescriptionsResponseModel { + dynamic address; + int? appointmentNo; + dynamic clinic; + dynamic companyName; + int? days; + dynamic doctorName; + int? doseDailyQuantity; + String? frequency; + int? frequencyNumber; + dynamic image; + dynamic imageExtension; + dynamic imageSrcUrl; + String? imageString; + dynamic imageThumbUrl; + dynamic isCovered; + String? itemDescription; + int? itemId; + String? orderDate; + int? patientId; + dynamic patientName; + dynamic phoneOffice1; + dynamic prescriptionQr; + int? prescriptionTimes; + dynamic productImage; + String? productImageBase64; + String? productImageString; + int? projectId; + dynamic projectName; + dynamic remarks; + String? route; + String? sku; + int? scaleOffset; + String? startDate; + + ActivePrescriptionsResponseModel({ + this.address, + this.appointmentNo, + this.clinic, + this.companyName, + this.days, + this.doctorName, + this.doseDailyQuantity, + this.frequency, + this.frequencyNumber, + this.image, + this.imageExtension, + this.imageSrcUrl, + this.imageString, + this.imageThumbUrl, + this.isCovered, + this.itemDescription, + this.itemId, + this.orderDate, + this.patientId, + this.patientName, + this.phoneOffice1, + this.prescriptionQr, + this.prescriptionTimes, + this.productImage, + this.productImageBase64, + this.productImageString, + this.projectId, + this.projectName, + this.remarks, + this.route, + this.sku, + this.scaleOffset, + this.startDate, + }); + + factory ActivePrescriptionsResponseModel.fromRawJson(String str) => ActivePrescriptionsResponseModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ActivePrescriptionsResponseModel.fromJson(Map json) => ActivePrescriptionsResponseModel( + address: json["Address"], + appointmentNo: json["AppointmentNo"], + clinic: json["Clinic"], + companyName: json["CompanyName"], + days: json["Days"], + doctorName: json["DoctorName"], + doseDailyQuantity: json["DoseDailyQuantity"], + frequency: json["Frequency"], + frequencyNumber: json["FrequencyNumber"], + image: json["Image"], + imageExtension: json["ImageExtension"], + imageSrcUrl: json["ImageSRCUrl"], + imageString: json["ImageString"], + imageThumbUrl: json["ImageThumbUrl"], + isCovered: json["IsCovered"], + itemDescription: json["ItemDescription"], + itemId: json["ItemID"], + orderDate: json["OrderDate"], + patientId: json["PatientID"], + patientName: json["PatientName"], + phoneOffice1: json["PhoneOffice1"], + prescriptionQr: json["PrescriptionQR"], + prescriptionTimes: json["PrescriptionTimes"], + productImage: json["ProductImage"], + productImageBase64: json["ProductImageBase64"], + productImageString: json["ProductImageString"], + projectId: json["ProjectID"], + projectName: json["ProjectName"], + remarks: json["Remarks"], + route: json["Route"], + sku: json["SKU"], + scaleOffset: json["ScaleOffset"], + startDate: json["StartDate"], + ); + + Map toJson() => { + "Address": address, + "AppointmentNo": appointmentNo, + "Clinic": clinic, + "CompanyName": companyName, + "Days": days, + "DoctorName": doctorName, + "DoseDailyQuantity": doseDailyQuantity, + "Frequency": frequency, + "FrequencyNumber": frequencyNumber, + "Image": image, + "ImageExtension": imageExtension, + "ImageSRCUrl": imageSrcUrl, + "ImageString": imageString, + "ImageThumbUrl": imageThumbUrl, + "IsCovered": isCovered, + "ItemDescription": itemDescription, + "ItemID": itemId, + "OrderDate": orderDate, + "PatientID": patientId, + "PatientName": patientName, + "PhoneOffice1": phoneOffice1, + "PrescriptionQR": prescriptionQr, + "PrescriptionTimes": prescriptionTimes, + "ProductImage": productImage, + "ProductImageBase64": productImageBase64, + "ProductImageString": productImageString, + "ProjectID": projectId, + "ProjectName": projectName, + "Remarks": remarks, + "Route": route, + "SKU": sku, + "ScaleOffset": scaleOffset, + "StartDate": startDate, + }; +} diff --git a/lib/main.dart b/lib/main.dart index 20507d06..cf038f95 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.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/utils.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; @@ -125,6 +126,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index 21e1a8bd..6394002e 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -1,64 +1,72 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; - -import '../../core/app_assets.dart'; -import '../../core/utils/utils.dart'; +// import 'package:sizer/sizer.dart'; +import '../../core/dependencies.dart'; +import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; +import '../../features/active_prescriptions/models/active_prescriptions_response_model.dart'; import '../../generated/locale_keys.g.dart'; +import '../../services/dialog_service.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import 'package:intl/intl.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +// import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import '../../widgets/buttons/custom_button.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting +import 'package:provider/provider.dart'; class ActiveMedicationPage extends StatefulWidget { + //inal List activePrescriptionsResponseModel; + + ActiveMedicationPage({super.key, }); + + + + @override State createState() => _ActiveMedicationPageState(); } class _ActiveMedicationPageState extends State { - late DateTime currentDate; late DateTime selectedDate; -// Info for each day (customizable) - final Map> dayInfo = { - 0: {"text": "Medications", "icon": Icons.medication_outlined, "description": "Affected"}, - 1: {"text": "Doctor Appointment", "icon": Icons.local_hospital_outlined, "description": "Twice"}, - 2: {"text": "Rest Day", "icon": Icons.self_improvement_outlined, "description": "Daily"}, - 3: {"text": "Gym Session", "icon": Icons.fitness_center_outlined, "description": "Affected"}, - 4: {"text": "Meeting", "icon": Icons.meeting_room_outlined, "description": "Twice"}, - 5: {"text": "Shopping", "icon": Icons.shopping_bag_outlined, "description": "Daily"}, - 6: {"text": "Family Time", "icon": Icons.family_restroom_outlined, "description": "Affected"}, - }; + + ActivePrescriptionsViewModel? activePreVM; @override void initState() { + activePreVM = Provider.of(context, listen: false); + activePreVM?.getActiveMedications(); + print(activePreVM?.activePrescriptionsDetailsList); super.initState(); currentDate = DateTime.now(); selectedDate = currentDate; } + // Generate today + next 6 days List getUpcomingDays() { return List.generate(7, (index) => currentDate.add(Duration(days: index))); } + // on/off toggle + bool isOn = true; + get index => null; @override Widget build(BuildContext context) { - + // activePreVM = Provider.of(context, listen: false); List days = getUpcomingDays(); int dayIndex = selectedDate.difference(currentDate).inDays; - - String dateText = - "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; - String infoMed = dayInfo[dayIndex]?["text"] ?? "No Info"; - IconData infoImg= dayInfo[dayIndex]?["icon"] ?? Icons.info_outline; - String medDetails = dayInfo[dayIndex]?["description"] ?? "No Info"; + String dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( @@ -74,10 +82,11 @@ class _ActiveMedicationPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: 65, + height: 65.h, child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: days.length, + itemCount: days.length, + // itemCount: widget.details.length, itemBuilder: (context, index) { DateTime day = days[index]; String label = DateFormat('E').format(day); // Mon, Tue @@ -88,23 +97,22 @@ class _ActiveMedicationPageState extends State { }, ), ), - - const SizedBox(height: 20), + SizedBox(height: 20.h), // Show full date text Text( dateText, style: TextStyle( color: AppColors.textColor, - fontSize: 16, + fontSize: 16.fSize, fontWeight: FontWeight.w500), ), - const Text( - "Medications", + Text( + "Medications".needTranslation, style: TextStyle( - color: AppColors.primaryRedBorderColor,fontSize: 12, fontWeight: FontWeight.w500), + color: AppColors.primaryRedBorderColor,fontSize: 12.fSize, fontWeight: FontWeight.w500), ), - const SizedBox(height: 16), + SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, hasShadow: true,), @@ -119,61 +127,64 @@ class _ActiveMedicationPageState extends State { ClipRRect( borderRadius: BorderRadius.circular(1), child: Container( - width: 59, - height: 59, + width: 59.h, + height: 59.h, decoration: BoxDecoration( border: Border.all( color: AppColors.spacerLineColor,// Border color - width: 1.0, ), + width: 1.0.h, ), borderRadius: BorderRadius.circular(30),// Border width ), child: - Icon(infoImg, size: 26), - // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + Utils.buildImgWithNetwork(url: activePreVM!.activePrescriptionsDetailsList[index].productImageString.toString(),width: 26.h,) ), ), - const SizedBox(width: 12), + SizedBox(width: 12.h), Text( - infoMed, + activePreVM!.activePrescriptionsDetailsList[index].itemDescription.toString(), style: TextStyle( - fontSize: 16, - height: 1.2, + fontSize: 16.fSize, + height: 1.2.h, fontWeight: FontWeight.w700, color: Colors.black87), ), ], ), - const SizedBox(height: 12), + SizedBox(height: 12.h), + activePreVM!.activePrescriptionsDetailsList.length > 0 ? Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: "Route: $medDetails", + labelText: "Route: ${activePreVM?.activePrescriptionsDetailsList[index].route}", ), AppCustomChipWidget( - labelText: "Frequency: $medDetails", + labelText: "Frequency: ${activePreVM?.activePrescriptionsDetailsList[index].frequency}".needTranslation, ), AppCustomChipWidget( - labelText: "Daily Does $medDetails", + labelText: "Daily Does ${activePreVM?.activePrescriptionsDetailsList[index].doseDailyQuantity}".needTranslation, ), AppCustomChipWidget( - labelText: "Duration: $medDetails ", + labelText: "Duration: ${activePreVM?.activePrescriptionsDetailsList[index].days} ".needTranslation, ), ], + ): + Container( + child: Text("no data"), ), - const SizedBox(height: 12), + SizedBox(height: 12.h), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline, color: Colors.grey,), - const SizedBox(width: 8), + SizedBox(width: 8.h), Expanded( child: Text( - "Remark: some remarks about the prescription will be here", + "Remark: some remarks about the prescription will be here".needTranslation, style: TextStyle( - fontSize: 10, + fontSize: 10.fSize, color: AppColors.greyTextColor, fontWeight: FontWeight.w500, ), @@ -194,41 +205,96 @@ class _ActiveMedicationPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10),// Border width - ), - child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Set Reminder", - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, - color: AppColors.textColor), - ), - const Text( - "Notify me before the consumption time", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, - color: AppColors.textColorLight), - ), - ], + Container( + width: 40.h, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10),// Border width ), - ], + child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor) + // MedicalFileCard( + // label: "Vaccine Info".needTranslation, + // textColor: AppColors.blackColor, + // backgroundColor: AppColors.whiteColor, + // svgIcon: AppAssets..bell, + // isLargeText: true, + // iconSize: 36.h, + // ) + ), + SizedBox(width: 8.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Set Reminder", + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14.fSize, + color: AppColors.textColor), + ), + Text( + "Notify me before the consumption time", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12.fSize, + color: AppColors.textColorLight), + ), + ], + ).onPress(() { + DialogService dialogService = getIt.get(); + dialogService.showReminderBottomSheetWithoutHWithChild( + label: "Set the timer for reminder".needTranslation, + message: "", + child: ReminderTimerDialog(), + onOkPressed: () {}, + ); + }), + ), + GestureDetector( + onTap: () { + setState(() { + isOn = !isOn; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: isOn ? AppColors.lightGreenColor: AppColors.greyColor, + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: isOn ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isOn ? AppColors.textGreenColor : AppColors.greyTextColor, + ), + ), ), + ), + ), + ), + SizedBox(width: 2.h), // Switch( - // value: isActiveReminder, - // onChanged: (_) {}, - // activeColor: Colors.green, + // value: isOn, + // onChanged: (value){ + // setState(() { + // isOn = value; + // }); + // }, + // activeColor: AppColors.lightGreenColor, + // activeTrackColor: AppColors.lightGreenColor, + // activeThumbColor: AppColors.textGreenColor, + // inactiveThumbColor: AppColors.greyTextColor, + // inactiveTrackColor: AppColors.greyColor, // ), ], - ).paddingOnly(left:16, right: 16), + ).paddingAll(16), const Divider( indent: 0, endIndent: 0, @@ -242,7 +308,7 @@ class _ActiveMedicationPageState extends State { Expanded( child: CustomButton( text: LocaleKeys.checkAvailability.tr(), - fontSize: 14, + fontSize: 14.fSize, onPressed: () async { }, backgroundColor: AppColors.secondaryLightRedColor, @@ -250,11 +316,11 @@ class _ActiveMedicationPageState extends State { textColor: AppColors.errorColor, ), ), - const SizedBox(width: 12), + SizedBox(width: 12.h), Expanded( child: CustomButton( text: LocaleKeys.readInstructions.tr(), - fontSize: 14, + fontSize: 14.fSize, onPressed: () async { }, backgroundColor: AppColors.primaryRedColor, @@ -267,186 +333,15 @@ class _ActiveMedicationPageState extends State { ], ), ) -// Expanded( -// child: ListView( -// children: [ -// Container( -// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, -// hasShadow: true,), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Row( -// children: [ -// ClipRRect( -// borderRadius: BorderRadius.circular(1), -// child: Container( -// width: 59, -// height: 59, -// decoration: BoxDecoration( -// border: Border.all( -// color: AppColors.spacerLineColor,// Border color -// width: 1.0, ), -// borderRadius: BorderRadius.circular(30),// Border width -// ), -// child: -// Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) -// ), -// ), -// const SizedBox(width: 12), -// const Expanded( -// child: Text( -// "Diclofenac Diethylamine 1% Topical Gel", -// style: TextStyle( -// fontSize: 16, -// height: 1.2, -// fontWeight: FontWeight.w700, -// color: Colors.black87), -// ), -// ), -// ], -// ), -// const SizedBox(height: 12), -// Wrap( -// direction: Axis.horizontal, -// spacing: 4.h, -// runSpacing: 4.h, -// children: [ -// AppCustomChipWidget( -// labelText: "Route: Affected Area ", -// ), -// AppCustomChipWidget( -// labelText: "Route: Affected Area ", -// ), -// AppCustomChipWidget( -// labelText: "Daily Does 2", -// ), -// AppCustomChipWidget( -// labelText: "Route: Affected Area ", -// ), -// ], -// ), -// const SizedBox(height: 12), -// Row( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Icon(Icons.info_outline, color: Colors.grey,), -// const SizedBox(width: 8), -// Expanded( -// child: Text( -// "Remark: some remarks about the prescription will be here", -// style: TextStyle( -// fontSize: 10, -// color: AppColors.greyTextColor, -// fontWeight: FontWeight.w500, -// ), -// overflow: TextOverflow.visible, -// ), -// ) -// ], -// ), -// ], -// ).paddingAll(16), -// const Divider( -// indent: 0, -// endIndent: 0, -// thickness: 1, -// color: AppColors.greyColor, -// ), -// // Reminder Row -// Row( -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// Row( -// children: [ -// Container( -// width: 40, -// height: 40, -// decoration: BoxDecoration( -// color: AppColors.greyColor, -// borderRadius: BorderRadius.circular(10),// Border width -// ), -// child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), -// const SizedBox(width: 8), -// Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// const Text( -// "Set Reminder", -// style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, -// color: AppColors.textColor), -// ), -// const Text( -// "Notify me before the consumption time", -// style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, -// color: AppColors.textColorLight), -// ), -// ], -// ), -// ], -// ), -// // Switch( -// // value: isActiveReminder, -// // onChanged: (_) {}, -// // activeColor: Colors.green, -// // ), -// ], -// ).paddingOnly(left:16, right: 16), -// const Divider( -// indent: 0, -// endIndent: 0, -// thickness: 1, -// color: AppColors.greyColor, -// ), -// -// // Buttons -// Row( -// children: [ -// Expanded( -// child: CustomButton( -// text: LocaleKeys.checkAvailability.tr(), -// fontSize: 14, -// onPressed: () async { -// }, -// backgroundColor: AppColors.secondaryLightRedColor, -// borderColor: AppColors.secondaryLightRedColor, -// textColor: AppColors.errorColor, -// ), -// ), -// const SizedBox(width: 12), -// Expanded( -// child: CustomButton( -// text: LocaleKeys.readInstructions.tr(), -// fontSize: 14, -// onPressed: () async { -// }, -// backgroundColor: AppColors.primaryRedColor, -// borderColor: AppColors.primaryRedColor, -// textColor: AppColors.whiteColor, -// ), -// ), -// ], -// ).paddingAll(16), -// ], -// ), -// ) -// // MedicationCard(), -// // SizedBox(height: 16), -// // MedicationCard(isActiveReminder: true), -// ], -// ), -// ), ] ), - ), + + ), ); } - Widget buildDayCard(String label, DateTime date) { + Widget buildDayCard(String label, DateTime date,) { bool isSelected = selectedDate.day == date.day && selectedDate.month == date.month && selectedDate.year == date.year; @@ -458,14 +353,14 @@ class _ActiveMedicationPageState extends State { }); }, child: Container( - width: 57, - height: 65, + width: 57.h, + height: 65.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: isSelected ? AppColors.secondaryLightRedBorderColor: AppColors.transparent, border: Border.all( color: isSelected ? AppColors.primaryRedBorderColor : AppColors.spacerLineColor, - width: 1.0, + width: 1.0.h, ), ), child: Padding( @@ -474,18 +369,18 @@ class _ActiveMedicationPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - date.day == currentDate.day ? "Today" : label, + date.day == currentDate.day ? "Today".needTranslation : label, style: TextStyle( color: isSelected ? AppColors.primaryRedBorderColor : AppColors.greyTextColor, - fontSize: 12, + fontSize: 12.fSize, fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 5), + SizedBox(height: 5.h), Text( date.day.toString(), style: TextStyle( - fontSize: 16, + fontSize: 16.fSize, fontWeight: FontWeight.bold, color: isSelected ? AppColors.primaryRedBorderColor : AppColors.textColor, ), @@ -503,180 +398,167 @@ class _ActiveMedicationPageState extends State { if (day == 3 || day == 23) return "rd"; return "th"; } + + // Widget manageReminder(){ + // NavigationService navigationService = getIt(); + // return Container( + // width: 59, + // height: 59, + // decoration: BoxDecoration( + // border: Border.all( + // color: AppColors.spacerLineColor,// Border color + // width: 1.0, ), + // borderRadius: BorderRadius.circular(30),// Border width + // ), + // child: + // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + // ); + // } +} + +class ReminderTimerDialog extends StatefulWidget { + // final Function()? onSetReminderPress; + // final String message; + // + // const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); + const ReminderTimerDialog({super.key}); + + @override + State createState() => _ReminderTimerDialogState(); } -class MedicationCard extends StatelessWidget { - final bool isActiveReminder; - const MedicationCard({super.key, this.isActiveReminder = false}); +class _ReminderTimerDialogState extends State { + final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; + final List selectedTimes = ["Morning"]; // Default selection - Color get primaryRed => const Color(0xFFE84B3A); @override Widget build(BuildContext context) { - return - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, - hasShadow: true,), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return // + Column( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(1), - child: Container( - width: 59, - height: 59, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.spacerLineColor,// Border color - width: 1.0, ), - borderRadius: BorderRadius.circular(30),// Border width - ), - child: - Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - "Diclofenac Diethylamine 1% Topical Gel", + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, + hasShadow: true,), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + // Checkboxes list + children: options.map((time) => buildCircleCheckbox(time)).toList(), + ).paddingAll(16), + ), + SizedBox(height: 25.h), + // Buttons Row + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: AppColors.errorColor), + label: Text( + LocaleKeys.cancel.tr(), style: TextStyle( - fontSize: 16, - height: 1.2, - fontWeight: FontWeight.w700, - color: Colors.black87), + color: AppColors.errorColor, + fontWeight: FontWeight.w500, + fontSize: 14.fSize + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.secondaryLightRedColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), ), - ], - ), - const SizedBox(height: 12), - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - labelText: "Route: Affected Area ", - ), - AppCustomChipWidget( - labelText: "Route: Affected Area ", - ), - AppCustomChipWidget( - labelText: "Daily Does 2", - ), - AppCustomChipWidget( - labelText: "Route: Affected Area ", - ), - ], - ), - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.info_outline, color: Colors.grey,), - const SizedBox(width: 8), - Expanded( - child: Text( - "Remark: some remarks about the prescription will be here", + ), + SizedBox(width: 12.h), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context, selectedTimes); + }, + icon: const Icon(Icons.notifications_rounded), + label: Text( + LocaleKeys.setReminder.tr(), style: TextStyle( - fontSize: 10, - color: AppColors.greyTextColor, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w500, + fontSize: 14.fSize ), - overflow: TextOverflow.visible, ), - ) - ], - ), - ], - ).paddingAll(16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, - ), - // Reminder Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10),// Border width - ), - child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Set Reminder", - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, - color: AppColors.textColor), - ), - const Text( - "Notify me before the consumption time", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, - color: AppColors.textColorLight), - ), - ], + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - ], - ), - Switch( - value: isActiveReminder, - onChanged: (_) {}, - activeColor: Colors.green, + ), ), - ], - ).paddingOnly(left:16, right: 16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, + ), + ], ), + SizedBox(height: 30.h), + ], + ); + } -// Buttons - Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.checkAvailability.tr(), - fontSize: 14, - onPressed: () async { - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.errorColor, - ), - ), - const SizedBox(width: 12), - Expanded( - child: CustomButton( - text: LocaleKeys.readInstructions.tr(), - fontSize: 14, - onPressed: () async { - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - ), + Widget buildCircleCheckbox(String label) { + final bool isSelected = selectedTimes.contains(label); + return InkWell( + onTap: () { + setState(() { + if (isSelected) { + selectedTimes.remove(label); + } else { + selectedTimes.add(label); + } + }); + }, + borderRadius: BorderRadius.circular(25), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + // Custom circle checkbox + Container( + width: 15.h, + height: 15.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, + width: 1.h, ), - ], - ).paddingAll(16), - ], + color: isSelected ? AppColors.errorColor: AppColors.transparent, + ), + ), + SizedBox(width: 12.h), + // Label text + Text( + label, + style: TextStyle(fontSize: 16.fSize, color: Colors.black87), + ), + ], + ), ), ); } + + + void showCircleCheckboxDialog(BuildContext context) async { + final selected = await showDialog>( + context: context, + builder: (context) => const ReminderTimerDialog(), + ); + + if (selected != null && selected.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Reminders set for: ${selected.join(', ').needTranslation}')), + ); + } + } } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 8d233b0d..cf769f85 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -463,12 +463,6 @@ class _LandingPageState extends State { Row( children: [ "View all services".toText12(color: AppColors.primaryRedColor).onPress(() { - Navigator.of(context) - .push( - CustomPageRoute( - page: ActiveMedicationPage(), - ), - ); }), SizedBox(width: 2.h), Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 1eff927a..4dcf0c4d 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_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/insurance/insurance_view_model.dart'; @@ -23,6 +24,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/active_medication/active_medication_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; @@ -56,6 +58,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; +import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; import '../prescriptions/prescription_detail_page.dart'; import 'widgets/medical_file_appointment_card.dart'; @@ -73,6 +76,7 @@ class _MedicalFilePageState extends State { late MedicalFileViewModel medicalFileViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late LabViewModel labViewModel; + late ActivePrescriptionsViewModel activePrescriptionsViewModel; int currentIndex = 0; @@ -98,6 +102,7 @@ class _MedicalFilePageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); + NavigationService navigationService = getIt.get(); return CollapsingListView( title: "Medical File".needTranslation, @@ -528,7 +533,13 @@ class _MedicalFilePageState extends State { Expanded( child: CustomButton( text: "All Medications".needTranslation, - onPressed: () {}, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + ); + }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 7003a312..4d1b4be5 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -14,6 +14,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/family_files/family_file_add_widget.dart'; +import '../widgets/medication_reminder/reminder_timer_dialog.dart'; + abstract class DialogService { Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed, Function()? onCancelPressed}); @@ -29,6 +31,8 @@ abstract class DialogService { Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}); Future showAddFamilyFileSheet({String? label, String? message, required Function() onVerificationPress}); + + Future showReminderBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}); // TODO : Need to be Fixed showPhoneNumberPickerSheet ( From Login ADn Signup Bottom Sheet Move Here } @@ -133,6 +137,18 @@ class DialogServiceImp implements DialogService { ); } + @override + Future showReminderBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight( + context, + title: label ?? "", + child: child ?? SizedBox(), + callBackFunc: () {}, + ); + } + @override Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; diff --git a/lib/widgets/medication_reminder/reminder_timer_dialog.dart b/lib/widgets/medication_reminder/reminder_timer_dialog.dart new file mode 100644 index 00000000..62dbd963 --- /dev/null +++ b/lib/widgets/medication_reminder/reminder_timer_dialog.dart @@ -0,0 +1,155 @@ +// import 'package:easy_localization/easy_localization.dart'; +// import 'package:flutter/material.dart'; +// import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +// +// import '../../generated/locale_keys.g.dart'; +// import '../../theme/colors.dart'; +// +// class ReminderTimerDialog extends StatefulWidget { +// final Function()? onSetReminderPress; +// final String message; +// +// const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); +// +// +// @override +// State createState() => _ReminderTimerDialogState(); +// } +// +// class _ReminderTimerDialogState extends State { +// final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; +// final List selectedTimes = ["Morning"]; // Default selection +// +// +// @override +// Widget build(BuildContext context) { +// return // +// Column( +// children: [ +// Container( +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, +// hasShadow: true,), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// // Checkboxes list +// children: options.map((time) => buildCircleCheckbox(time)).toList(), +// ).paddingAll(16), +// ), +// const SizedBox(height: 25), +// // Buttons Row +// Row( +// children: [ +// Expanded( +// child: ElevatedButton.icon( +// onPressed: () => Navigator.pop(context), +// icon: const Icon(Icons.close, color: AppColors.errorColor), +// label: Text( +// LocaleKeys.cancel.tr(), +// style: TextStyle( +// color: AppColors.errorColor, +// fontWeight: FontWeight.w500, +// fontSize: 14 +// ), +// ), +// style: ElevatedButton.styleFrom( +// backgroundColor: AppColors.secondaryLightRedColor, +// elevation: 0, +// padding: const EdgeInsets.symmetric(vertical: 14), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12), +// ), +// ), +// ), +// ), +// const SizedBox(width: 12), +// Expanded( +// child: ElevatedButton.icon( +// onPressed: () { +// Navigator.pop(context, selectedTimes); +// }, +// icon: const Icon(Icons.notifications_rounded), +// label: Text( +// LocaleKeys.setReminder.tr(), +// style: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 14 +// ), +// ), +// style: ElevatedButton.styleFrom( +// backgroundColor: AppColors.successColor, +// foregroundColor: AppColors.whiteColor, +// elevation: 0, +// padding: const EdgeInsets.symmetric(vertical: 14), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12), +// ), +// ), +// ), +// ), +// ], +// ), +// const SizedBox(height: 30), +// ], +// ); +// } +// +// Widget buildCircleCheckbox(String label) { +// final bool isSelected = selectedTimes.contains(label); +// return InkWell( +// onTap: () { +// setState(() { +// if (isSelected) { +// selectedTimes.remove(label); +// } else { +// selectedTimes.add(label); +// } +// }); +// }, +// borderRadius: BorderRadius.circular(25), +// child: Padding( +// padding: const EdgeInsets.symmetric(vertical: 8.0), +// child: Row( +// children: [ +// // Custom circle checkbox +// Container( +// width: 15, +// height: 15, +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// border: Border.all( +// color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, +// width: 1, +// ), +// color: isSelected ? AppColors.errorColor: AppColors.transparent, +// ), +// ), +// const SizedBox(width: 12), +// // Label text +// Text( +// label, +// style: const TextStyle(fontSize: 16, color: Colors.black87), +// ), +// ], +// ), +// ), +// ); +// } +// +// +// void showCircleCheckboxDialog(BuildContext context) async { +// final selected = await showDialog>( +// context: context, +// builder: (context) => const ReminderTimerDialog(), +// ); +// +// if (selected != null && selected.isNotEmpty) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar(content: Text('Reminders set for: ${selected.join(', ')}')), +// ); +// } +// } +// } +// +// +// From eae16eec44f5a702b59801f4c36209279f780f0b Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Mon, 17 Nov 2025 09:38:20 +0300 Subject: [PATCH 03/21] active medication --- lib/core/api_consts.dart | 3 +- lib/core/dependencies.dart | 12 + lib/core/utils/calendar_utils.dart | 2 +- lib/core/utils/utils.dart | 2 +- .../active_prescriptions_repo.dart | 58 +- .../active_prescriptions_view_model.dart | 168 ++- .../active_prescriptions_response_model.dart | 94 +- lib/main.dart | 4 + .../active_medication_page.dart | 1129 ++++++++++------- .../call_ambulance/tracking_screen.dart | 14 +- .../widgets/nearestERItem.dart | 2 +- lib/presentation/home/landing_page.dart | 11 +- lib/theme/colors.dart | 2 + 13 files changed, 951 insertions(+), 550 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index ed2bba16..6dab797d 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -729,7 +729,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -848,6 +848,7 @@ class ApiConsts { static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; + static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; // static values for Api static final double appVersionID = 18.7; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index a82a9adb..67464b33 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -3,6 +3,8 @@ import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; @@ -45,6 +47,8 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../features/active_prescriptions/active_prescriptions_repo.dart'; + GetIt getIt = GetIt.instance; class AppDependencies { @@ -103,6 +107,7 @@ class AppDependencies { getIt.registerLazySingleton(() => MedicalFileRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ImmediateLiveCareRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => EmergencyServicesRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton @@ -202,6 +207,13 @@ class AppDependencies { ), ); + getIt.registerLazySingleton( + () => ActivePrescriptionsViewModel( + errorHandlerService: getIt(), + activePrescriptionsRepo: getIt() + ), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( // () => BookAppointmentsViewModel( diff --git a/lib/core/utils/calendar_utils.dart b/lib/core/utils/calendar_utils.dart index 2068db90..8c0db187 100644 --- a/lib/core/utils/calendar_utils.dart +++ b/lib/core/utils/calendar_utils.dart @@ -266,7 +266,7 @@ setCalender(BuildContext context, eventId: eventId + (i.toString() + j.toString()), location: '', //event id with varitions ); - + print("Creating event #$j for day $i → $actualDate"); actualDate = DateTime(actualDate.year, actualDate.month, actualDate.day, 8, 0); } actualDate = Jiffy.parseFromDateTime(actualDate).add(days: 1).dateTime; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index a88d9c22..1fc38065 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -670,7 +670,7 @@ class Utils { } /// Widget to build an SVG from network - static Widget buildImgWithNetwork({required String url, required Color iconColor, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover, ImageErrorWidgetBuilder? errorBuilder}) { + static Widget buildImgWithNetwork({required String url, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover, ImageErrorWidgetBuilder? errorBuilder}) { return Image.network( url, width: width, diff --git a/lib/features/active_prescriptions/active_prescriptions_repo.dart b/lib/features/active_prescriptions/active_prescriptions_repo.dart index 847d6e80..437f364b 100644 --- a/lib/features/active_prescriptions/active_prescriptions_repo.dart +++ b/lib/features/active_prescriptions/active_prescriptions_repo.dart @@ -2,7 +2,6 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; - import '../../core/api/api_client.dart'; import '../../core/api_consts.dart'; import '../../core/common_models/generic_api_model.dart'; @@ -11,7 +10,7 @@ import '../../services/logger_service.dart'; abstract class ActivePrescriptionsRepo { - Future>> getActivePrescriptionsDetails(); + Future>>> getActivePrescriptionsDetails(); } @@ -23,10 +22,10 @@ class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { @override - Future>> getActivePrescriptionsDetails() async + Future>>> getActivePrescriptionsDetails() async { try { - GenericApiModel? apiResponse; + GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( ApiConsts.getActivePrescriptionsDetails, @@ -36,18 +35,20 @@ class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - // final list = response['GetActivePrescriptionReportByPatientIDList']; - - // final prescriptionLists = list.map((item) => ActivePrescriptionsResponseModel.fromJson(item as Map)).toList().cast(); + var list = response['List_ActiveGetPrescriptionReportByPatientID']; + var res = list + .map( + (item) => ActivePrescriptionsResponseModel.fromJson(item)) + .toList(); - apiResponse = GenericApiModel( + apiResponse = GenericApiModel>( messageStatus: messageStatus, statusCode: statusCode, errorMessage: null, - data: response, + // data: response, + data: res ); - return ['List_ActiveGetPrescriptionReportByPatientID']; - //apiResponse; + return apiResponse; } catch (e) { failure = DataParsingFailure(e.toString()); } @@ -61,39 +62,4 @@ class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { } } - - - - // - // Future> getActiveMedications() { - // try { - // GenericApiModel? apiResponse; - // Failure? failure; - // return apiClient.post( - // ApiConsts.getActivePrescriptionsDetails, - // body: patientDeviceDataRequest, - // onFailure: (error, statusCode, {messageStatus, failureType}) { - // failure = failureType; - // }, - // onSuccess: (response, statusCode, {messageStatus, errorMessage}) { - // try { - // apiResponse = GenericApiModel( - // messageStatus: messageStatus, - // statusCode: statusCode, - // errorMessage: errorMessage, - // data: response, - // ); - // } catch (e) { - // failure = DataParsingFailure(e.toString()); - // } - // }, - // ).then((_) { - // if (failure != null) return Left(failure!); - // if (apiResponse == null) return Left(ServerFailure("Unknown error")); - // return Right(apiResponse!); - // }); - // } catch (e) { - // return Future.value(Left(UnknownFailure(e.toString()))); - // } - // } } \ No newline at end of file diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart index d995f963..03f84ea1 100644 --- a/lib/features/active_prescriptions/active_prescriptions_view_model.dart +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -4,13 +4,17 @@ import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_ import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; -class ActivePrescriptionsViewModel extends ChangeNotifier { +class ActivePrescriptionsViewModel extends ChangeNotifier { bool isActivePrescriptionsDetailsLoading = false; - late ActivePrescriptionsRepo activePrescriptionsRepo; + late ActivePrescriptionsRepo activePrescriptionsRepo; late ErrorHandlerService errorHandlerService; - // Prescription Orders Lists + ActivePrescriptionsViewModel({ + required this.activePrescriptionsRepo, + required this.errorHandlerService, + }); + List activePrescriptionsDetailsList = []; initActivePrescriptionsViewModel() { @@ -20,38 +24,172 @@ class ActivePrescriptionsViewModel extends ChangeNotifier { setPrescriptionsDetailsLoading() { isActivePrescriptionsDetailsLoading = true; - // activePrescriptionsDetailsList.clear(); notifyListeners(); } - Future getActiveMedications( {Function(dynamic)? onSuccess, Function(String)? onError}) - async { + // Get medications list + Future getActiveMedications({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { final result = await activePrescriptionsRepo.getActivePrescriptionsDetails(); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) { - if (apiResponse.messageStatus == 2) { - // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - activePrescriptionsDetailsList = apiResponse.data!; - isActivePrescriptionsDetailsLoading = false; + if (apiResponse.messageStatus == 1) { + activePrescriptionsDetailsList = apiResponse.data ?? []; notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - print(activePrescriptionsDetailsList.length); - } + if (onSuccess != null) onSuccess(apiResponse.data); } }, ); } + DateTime parseDate(String? date) { + if (date == null) return DateTime.now(); + final regex = RegExp(r"\/Date\((\d+)([+-]\d+)?\)\/"); + final match = regex.firstMatch(date); + if (match != null) { + final millis = int.parse(match.group(1)!); + return DateTime.fromMillisecondsSinceEpoch(millis); + } + return DateTime.tryParse(date) ?? DateTime.now(); + } + // Extract numeric value ( "3 / week" → 3) + int extractNumberFromFrequency(String? frequency) { + if (frequency == null) return 1; + final m = RegExp(r'(\d+)').firstMatch(frequency); + if (m != null) return int.tryParse(m.group(1)!) ?? 1; + return 1; + } + // Generate medication days based on frequency text + List generateMedicationDays(ActivePrescriptionsResponseModel med) { + final start = parseDate(med.startDate); + final duration = med.days ?? 0; + final frequency = (med.frequency ?? "").toLowerCase().trim(); + + List result = []; + if (duration <= 0) return result; + + // Every N hours ( "Every Six Hours", "Every 8 hours") + if (frequency.contains("hour")) { + final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); + int intervalHours = 0; + + if (match != null) { + intervalHours = int.tryParse(match.group(1)!) ?? 0; + } else { + // handle text numbers like "Every six hours" + final textNum = { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10, + "twelve": 12, + }; + for (var key in textNum.keys) { + if (frequency.contains(key)) { + intervalHours = textNum[key]!; + break; + } + } + } + if (intervalHours > 0) { + for (int day = 0; day < duration; day++) { + final dayStart = start.add(Duration(days: day)); + for (int hour = 0; hour < 24; hour += intervalHours) { + result.add(DateTime(dayStart.year, dayStart.month, dayStart.day, hour)); + } + } + return result; + } + } + + // Daily (every day) + if (frequency.contains("day") && + !frequency.contains("every other") && + !frequency.contains("every ")) { + for (int i = 0; i < duration; i++) { + result.add(start.add(Duration(days: i))); + } + } + + // Every other day + else if (frequency.contains("every other day")) { + for (int i = 0; i < duration; i += 2) { + result.add(start.add(Duration(days: i))); + } + } + + // Every N days → e.g. "Every 3 days", "Every 5 days" + else if (frequency.contains("every") && frequency.contains("day")) { + final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); + final interval = match != null ? int.tryParse(match.group(1)!) ?? 1 : 1; + for (int i = 0; i < duration; i += interval) { + result.add(start.add(Duration(days: i))); + } + } + + // Once or twice a week + else if (frequency.contains("once a week")) { + for (int i = 0; i < duration; i += 7) { + result.add(start.add(Duration(days: i))); + } + } else if (frequency.contains("twice a week")) { + for (int i = 0; i < duration; i += 3) { + result.add(start.add(Duration(days: i))); + } + } + + // Numeric frequency like "3 / week", "2 / week" + else if (frequency.contains("week")) { + int timesPerWeek = extractNumberFromFrequency(frequency); + double interval = 7 / timesPerWeek; + double dayPointer = 0; + + for (int i = 0; i < duration; i++) { + if (i >= dayPointer.floor()) { + result.add(start.add(Duration(days: i))); + dayPointer += interval; + } + } + } + + else { + result.add(start); + } + final unique = {}; + for (final d in result) { + unique["${d.year}-${d.month}-${d.day}"] = d; + } + return unique.values.toList()..sort((a, b) => a.compareTo(b)); + } + bool sameYMD(DateTime a, DateTime b) => + a.year == b.year && a.month == b.month && a.day == b.day; + // Filter medications for selected day + List getMedsForSelectedDay(DateTime selectedDate) { + final target = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + return activePrescriptionsDetailsList.where((med) { + final days = generateMedicationDays(med); + return days.any((d) => sameYMD(d, target)); + }).toList(); + } } + + + diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart index 878e1911..eb216a6b 100644 --- a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -7,7 +7,7 @@ class ActivePrescriptionsResponseModel { dynamic companyName; int? days; dynamic doctorName; - int? doseDailyQuantity; + int? doseDailyQuantity; // doses per day String? frequency; int? frequencyNumber; dynamic image; @@ -23,7 +23,7 @@ class ActivePrescriptionsResponseModel { dynamic patientName; dynamic phoneOffice1; dynamic prescriptionQr; - int? prescriptionTimes; + dynamic prescriptionTimes; dynamic productImage; String? productImageBase64; String? productImageString; @@ -35,6 +35,10 @@ class ActivePrescriptionsResponseModel { int? scaleOffset; String? startDate; + // ✅ Added for reminder feature + List selectedDoseTimes = []; + bool isReminderOn = false; // toggle status + ActivePrescriptionsResponseModel({ this.address, this.appointmentNo, @@ -69,47 +73,57 @@ class ActivePrescriptionsResponseModel { this.sku, this.scaleOffset, this.startDate, - }); - factory ActivePrescriptionsResponseModel.fromRawJson(String str) => ActivePrescriptionsResponseModel.fromJson(json.decode(str)); + // ✅ Default values for new fields (won’t break API) + List? selectedDoseTimes, + this.isReminderOn = false, + }) : selectedDoseTimes = selectedDoseTimes ?? []; + + factory ActivePrescriptionsResponseModel.fromRawJson(String str) => + ActivePrescriptionsResponseModel.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); - factory ActivePrescriptionsResponseModel.fromJson(Map json) => ActivePrescriptionsResponseModel( - address: json["Address"], - appointmentNo: json["AppointmentNo"], - clinic: json["Clinic"], - companyName: json["CompanyName"], - days: json["Days"], - doctorName: json["DoctorName"], - doseDailyQuantity: json["DoseDailyQuantity"], - frequency: json["Frequency"], - frequencyNumber: json["FrequencyNumber"], - image: json["Image"], - imageExtension: json["ImageExtension"], - imageSrcUrl: json["ImageSRCUrl"], - imageString: json["ImageString"], - imageThumbUrl: json["ImageThumbUrl"], - isCovered: json["IsCovered"], - itemDescription: json["ItemDescription"], - itemId: json["ItemID"], - orderDate: json["OrderDate"], - patientId: json["PatientID"], - patientName: json["PatientName"], - phoneOffice1: json["PhoneOffice1"], - prescriptionQr: json["PrescriptionQR"], - prescriptionTimes: json["PrescriptionTimes"], - productImage: json["ProductImage"], - productImageBase64: json["ProductImageBase64"], - productImageString: json["ProductImageString"], - projectId: json["ProjectID"], - projectName: json["ProjectName"], - remarks: json["Remarks"], - route: json["Route"], - sku: json["SKU"], - scaleOffset: json["ScaleOffset"], - startDate: json["StartDate"], - ); + factory ActivePrescriptionsResponseModel.fromJson(Map json) => + ActivePrescriptionsResponseModel( + address: json["Address"], + appointmentNo: json["AppointmentNo"], + clinic: json["Clinic"], + companyName: json["CompanyName"], + days: json["Days"], + doctorName: json["DoctorName"], + doseDailyQuantity: json["DoseDailyQuantity"], + frequency: json["Frequency"], + frequencyNumber: json["FrequencyNumber"], + image: json["Image"], + imageExtension: json["ImageExtension"], + imageSrcUrl: json["ImageSRCUrl"], + imageString: json["ImageString"], + imageThumbUrl: json["ImageThumbUrl"], + isCovered: json["IsCovered"], + itemDescription: json["ItemDescription"], + itemId: json["ItemID"], + orderDate: json["OrderDate"], + patientId: json["PatientID"], + patientName: json["PatientName"], + phoneOffice1: json["PhoneOffice1"], + prescriptionQr: json["PrescriptionQR"], + prescriptionTimes: json["PrescriptionTimes"], + productImage: json["ProductImage"], + productImageBase64: json["ProductImageBase64"], + productImageString: json["ProductImageString"], + projectId: json["ProjectID"], + projectName: json["ProjectName"], + remarks: json["Remarks"], + route: json["Route"], + sku: json["SKU"], + scaleOffset: json["ScaleOffset"], + startDate: json["StartDate"], + + // ✅ Ensure local reminder values are not overwritten by API + selectedDoseTimes: [], + isReminderOn: false, + ); Map toJson() => { "Address": address, @@ -145,5 +159,7 @@ class ActivePrescriptionsResponseModel { "SKU": sku, "ScaleOffset": scaleOffset, "StartDate": startDate, + + }; } diff --git a/lib/main.dart b/lib/main.dart index 259ce3b5..5a33d028 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.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/utils.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; @@ -129,6 +130,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index 6394002e..aa2abe59 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -1,355 +1,538 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -// import 'package:sizer/sizer.dart'; -import '../../core/dependencies.dart'; +import 'package:flutter/cupertino.dart'; +import '../../core/app_assets.dart'; +import '../../core/utils/calendar_utils.dart'; import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; import '../../features/active_prescriptions/models/active_prescriptions_response_model.dart'; import '../../generated/locale_keys.g.dart'; -import '../../services/dialog_service.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import 'package:intl/intl.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -// import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; - import '../../widgets/buttons/custom_button.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting import 'package:provider/provider.dart'; - +import '../../widgets/loader/bottomsheet_loader.dart'; class ActiveMedicationPage extends StatefulWidget { - //inal List activePrescriptionsResponseModel; - - ActiveMedicationPage({super.key, }); - - - + const ActiveMedicationPage({super.key}); @override State createState() => _ActiveMedicationPageState(); } class _ActiveMedicationPageState extends State { - late DateTime currentDate; - late DateTime selectedDate; - - + late DateTime currentDate; + late DateTime selectedDate; + List selectedDayMeds = []; ActivePrescriptionsViewModel? activePreVM; + Map medReminderStatus = {}; @override - void initState() { - activePreVM = Provider.of(context, listen: false); - activePreVM?.getActiveMedications(); - print(activePreVM?.activePrescriptionsDetailsList); + void initState() { super.initState(); currentDate = DateTime.now(); selectedDate = currentDate; + WidgetsBinding.instance.addPostFrameCallback((_) async { + activePreVM = Provider.of(context, listen: false); + LoaderBottomSheet.showLoader(); + await activePreVM!.getActiveMedications( + onSuccess: (_) { + LoaderBottomSheet.hideLoader(); + final todayMeds = activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() { + selectedDayMeds = todayMeds; + }); + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); + activePreVM!.addListener(() { + if (!mounted) return; + final medsForDay = activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = medsForDay); + }); + }); } -// Generate today + next 6 days - List getUpcomingDays() { - return List.generate(7, (index) => currentDate.add(Duration(days: index))); - } - // on/off toggle - bool isOn = true; - - get index => null; + List getUpcomingDays() => List.generate(7, (index) => currentDate.add(Duration(days: index))); @override Widget build(BuildContext context) { - // activePreVM = Provider.of(context, listen: false); - List days = getUpcomingDays(); - int dayIndex = selectedDate.difference(currentDate).inDays; - String dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; - return Scaffold( + final days = getUpcomingDays(); + final dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)}"; + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( - onBackPressed: () { - Navigator.of(context).pop(); - }, - onLanguageChanged: (lang) {}, - hideLogoAndLang: true, + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 65.h, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: days.length, - // itemCount: widget.details.length, - itemBuilder: (context, index) { - DateTime day = days[index]; - String label = DateFormat('E').format(day); // Mon, Tue - return Padding( - padding: const EdgeInsets.only(right: 12), - child: buildDayCard(label, day), - ); - }, - ), - ), - SizedBox(height: 20.h), - -// Show full date text - Text( - dateText, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Active Medications".needTranslation, style: TextStyle( color: AppColors.textColor, - fontSize: 16.fSize, - fontWeight: FontWeight.w500), + fontSize: 27.f, + fontWeight: FontWeight.w600)), + SizedBox(height: 16.h), + SizedBox( + height: 65.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + final day = days[index]; + final label = DateFormat('E').format(day); + return Padding( + padding: const EdgeInsets.only(right: 12), + child: buildDayCard(label, day), + ); + }, ), - Text( - "Medications".needTranslation, - style: TextStyle( - color: AppColors.primaryRedBorderColor,fontSize: 12.fSize, fontWeight: FontWeight.w500), - ), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, - hasShadow: true,), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + SizedBox(height: 20.h), + RichText( + text: TextSpan( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(1), - child: Container( - width: 59.h, - height: 59.h, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.spacerLineColor,// Border color - width: 1.0.h, ), - borderRadius: BorderRadius.circular(30),// Border width - ), - child: - Utils.buildImgWithNetwork(url: activePreVM!.activePrescriptionsDetailsList[index].productImageString.toString(),width: 26.h,) - ), - ), - SizedBox(width: 12.h), - Text( - activePreVM!.activePrescriptionsDetailsList[index].itemDescription.toString(), - style: TextStyle( - fontSize: 16.fSize, - height: 1.2.h, - fontWeight: FontWeight.w700, - color: Colors.black87), - ), - ], - ), - SizedBox(height: 12.h), - activePreVM!.activePrescriptionsDetailsList.length > 0 ? - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - labelText: "Route: ${activePreVM?.activePrescriptionsDetailsList[index].route}", - ), - AppCustomChipWidget( - labelText: "Frequency: ${activePreVM?.activePrescriptionsDetailsList[index].frequency}".needTranslation, - ), - AppCustomChipWidget( - labelText: "Daily Does ${activePreVM?.activePrescriptionsDetailsList[index].doseDailyQuantity}".needTranslation, - ), - AppCustomChipWidget( - labelText: "Duration: ${activePreVM?.activePrescriptionsDetailsList[index].days} ".needTranslation, - ), - ], - ): - Container( - child: Text("no data"), - ), - SizedBox(height: 12.h), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.info_outline, color: Colors.grey,), - SizedBox(width: 8.h), - Expanded( - child: Text( - "Remark: some remarks about the prescription will be here".needTranslation, - style: TextStyle( - fontSize: 10.fSize, - color: AppColors.greyTextColor, - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.visible, - ), - ) - ], - ), - ], - ).paddingAll(16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, + TextSpan( + text: "${selectedDate.day}", + style: TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), - // Reminder Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 40.h, - height: 40.h, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10),// Border width - ), - child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor) - // MedicalFileCard( - // label: "Vaccine Info".needTranslation, - // textColor: AppColors.blackColor, - // backgroundColor: AppColors.whiteColor, - // svgIcon: AppAssets..bell, - // isLargeText: true, - // iconSize: 36.h, - // ) + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -4), + child: Text( + getSuffix(selectedDate.day), + style: const TextStyle( + fontSize: 12, + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), ), - SizedBox(width: 8.h), - Expanded( + ), + ), + TextSpan( + text: " ${DateFormat.MMMM().format(selectedDate)}", + style: const TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + Text("Medications".needTranslation, + style: TextStyle( + color: AppColors.primaryRedBorderColor, + fontSize: 12.f, + fontWeight: FontWeight.w500)), + SizedBox(height: 16.h), + Expanded( + child: SingleChildScrollView( + child: selectedDayMeds.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedDayMeds.length, + itemBuilder: (context, index) { + final med = selectedDayMeds[index]; + final doses = med.doseDailyQuantity ?? 1; + med.selectedDoseTimes ??= List.filled(doses, null); + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + margin: EdgeInsets.all(10), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Set Reminder", - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14.fSize, - color: AppColors.textColor), - ), - Text( - "Notify me before the consumption time", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12.fSize, - color: AppColors.textColorLight), + _buildMedHeader(med), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + color: AppColors.lightGreyTextColor, + size: 20, + ), + SizedBox(width: 6.h), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Remarks: ".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + fontSize: 10, + ), + ), + TextSpan( + text: "some remarks about the prescription will be here".needTranslation, + style: TextStyle( + color: AppColors.lightGreyTextColor, + fontWeight: FontWeight.normal, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ).paddingOnly(left: 16, right: 16), + const Divider(color: AppColors.greyColor), + // Reminder Section + GestureDetector( + onTap: () => showDoseDialog(med, index), + child: Row( + children: [ + Container( + width: 40.h, + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10), + ), + child: Utils.buildSvgWithAssets( + icon: AppAssets.bell, + height: 24.h, + width: 24.h, + iconColor: AppColors.greyTextColor, + ), + ), + SizedBox(width: 12.h), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text("Set Reminder".needTranslation, + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor)), + Text("Notify me before the consumption time".needTranslation, + style: TextStyle( + fontSize: 12.f, + color: AppColors.textColorLight, + )), + ], + ), + ), + _buildToggle(index) + ], + ).paddingAll(16), ), + const Divider(color: AppColors.greyColor), + _buildButtons(), ], - ).onPress(() { - DialogService dialogService = getIt.get(); - dialogService.showReminderBottomSheetWithoutHWithChild( - label: "Set the timer for reminder".needTranslation, - message: "", - child: ReminderTimerDialog(), - onOkPressed: () {}, - ); - }), + ), + ); + }, + ) + : Utils.getNoDataWidget(context, + noDataText: "No medications today".needTranslation), + ), + ), + ], + ).paddingAll(16), + ); + } + + //medicine card + Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 59.h, + height: 59.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.spacerLineColor, + borderRadius: 30.r, + hasShadow: false, + ), + child: Utils.buildImgWithNetwork( + url: med.productImageString ?? "" ).circle(52.h) + ), + ), + SizedBox(width: 12.h), + Expanded( + child: Text( + med.itemDescription ?? "", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor), + ), + ), + ]), + SizedBox(height: 12.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget(labelText: "Route: ${med.route}".needTranslation), + AppCustomChipWidget(labelText: "Frequency: ${med.frequency}".needTranslation), + AppCustomChipWidget(labelText: "Daily Dose: ${med.doseDailyQuantity}".needTranslation), + AppCustomChipWidget(labelText: "Duration: ${med.days}".needTranslation), + ], + ), + ], + ), + ); + + Widget _buildButtons() => Padding( + padding: EdgeInsets.all(16), + child: Row(children: [ + Expanded( + child: CustomButton( + text: "Check Availability".needTranslation, + fontSize: 13.f, + onPressed: () {}, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + SizedBox(width: 12.h), + Expanded( + child: CustomButton( + text: "Read Instructions".needTranslation, fontSize: 13.f, onPressed: () {})), + ]), + ); + + Widget _buildToggle(int index) { + final value = medReminderStatus[index] ?? false; + + return GestureDetector( + onTap: () async { + await showDoseDialog(selectedDayMeds[index], index); + setState(() { + if ((selectedDayMeds[index].selectedDoseTimes ?? []).any((t) => t != null)) { + medReminderStatus[index] = true; + } + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: value ? AppColors.lightGreenColor : AppColors.greyColor.withOpacity(0.3), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: value ? AppColors.textGreenColor : AppColors.greyTextColor, + ), + ), + ), + ), + ), + ); + } + + Future showDoseDialog(ActivePrescriptionsResponseModel med, int medIndex) { + final doses = med.frequencyNumber ?? 1; + if (med.selectedDoseTimes.length != doses) { + med.selectedDoseTimes = List.generate(doses, (_) => null); + } + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 520.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + hasShadow: true, + ), + + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Reminders".needTranslation, + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, ), - GestureDetector( - onTap: () { - setState(() { - isOn = !isOn; - }); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 50.h, - height: 28.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: isOn ? AppColors.lightGreenColor: AppColors.greyColor, ), - child: AnimatedAlign( - duration: const Duration(milliseconds: 200), - alignment: isOn ? Alignment.centerRight : Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(3), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, color:AppColors.blackBgColor), + ), + ], + ), + SizedBox(height: 20.h), + Expanded( + child: ListView.builder( + itemCount: doses, + itemBuilder: (context, doseIndex) { + final badgeColor = [ + AppColors.textGreenColor, + AppColors.infoColor, + AppColors.labelColorYellow, + AppColors.purpleBg + ][doseIndex % 4]; + + final doseLabel = "${doseIndex + 1}${getSuffix(doseIndex + 1)}"; + final time = med.selectedDoseTimes[doseIndex] ?? "Not set yet"; + + return GestureDetector( + onTap: () { + Navigator.pop(context); + showTimePickerSheet(med, medIndex, doseIndex); + }, child: Container( - width: 22.h, - height: 22.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isOn ? AppColors.textGreenColor : AppColors.greyTextColor, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 14), + decoration: BoxDecoration( + color: badgeColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + doseLabel, + style: TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Set reminder for $doseLabel dose", + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + ), + Icon(Icons.arrow_forward_outlined, + size: 24.w, color: AppColors.textColor), + ], + ), + SizedBox(height: 4.h), + Text( + time, + style: TextStyle( + fontSize: 12.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - ), - ), + ); + }, ), ), - SizedBox(width: 2.h), - // Switch( - // value: isOn, - // onChanged: (value){ - // setState(() { - // isOn = value; - // }); - // }, - // activeColor: AppColors.lightGreenColor, - // activeTrackColor: AppColors.lightGreenColor, - // activeThumbColor: AppColors.textGreenColor, - // inactiveThumbColor: AppColors.greyTextColor, - // inactiveTrackColor: AppColors.greyColor, - // ), - ], - ).paddingAll(16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, - ), - -// Buttons - Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.checkAvailability.tr(), - fontSize: 14.fSize, - onPressed: () async { - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.errorColor, - ), - ), - SizedBox(width: 12.h), - Expanded( - child: CustomButton( - text: LocaleKeys.readInstructions.tr(), - fontSize: 14.fSize, - onPressed: () async { - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - ), - ), - ], - ).paddingAll(16), - ], - ), - ) - ] - ), + ], + ), + ), + ), + ); + } + void showTimePickerSheet( + ActivePrescriptionsResponseModel med, int medIndex, int doseIndex) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 460.h, + decoration: BoxDecoration( + color: AppColors.bottomSheetBgColor, + borderRadius: + BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + ), + child: ReminderTimerDialog( + med: med, + frequencyNumber: med.doseDailyQuantity ?? 1, + doseIndex: doseIndex, + onTimeSelected: (String time) { + setState(() { + med.selectedDoseTimes[doseIndex] = time; + medReminderStatus[medIndex] = true; + }); + }, + ), ), ); } - Widget buildDayCard(String label, DateTime date,) { - bool isSelected = selectedDate.day == date.day && + Widget buildDayCard(String label, DateTime date) { + final isSelected = selectedDate.day == date.day && selectedDate.month == date.month && selectedDate.year == date.year; - return GestureDetector( onTap: () { + final vm = + Provider.of(context, listen: false); setState(() { selectedDate = date; + selectedDayMeds = vm.getMedsForSelectedDay(date); }); }, child: Container( @@ -357,36 +540,38 @@ class _ActiveMedicationPageState extends State { height: 65.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: isSelected ? AppColors.secondaryLightRedBorderColor: AppColors.transparent, + color: isSelected + ? AppColors.secondaryLightRedBorderColor + : Colors.transparent, border: Border.all( - color: isSelected ? AppColors.primaryRedBorderColor : AppColors.spacerLineColor, - width: 1.0.h, - ), + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.spacerLineColor, + width: 1), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - date.day == currentDate.day ? "Today".needTranslation : label, - style: TextStyle( - color: isSelected ? AppColors.primaryRedBorderColor : AppColors.greyTextColor, - fontSize: 12.fSize, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 5.h), - Text( - date.day.toString(), - style: TextStyle( - fontSize: 16.fSize, - fontWeight: FontWeight.bold, - color: isSelected ? AppColors.primaryRedBorderColor : AppColors.textColor, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day == currentDate.day ? "Today" : label, + style: TextStyle( + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.greyTextColor, + fontSize: 11.f, + fontWeight: FontWeight.w500), ), - ), - ], - ), + SizedBox(height: 5.h), + Text("${date.day}", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.bold, + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.textColor)) + ]), ), ), ); @@ -398,167 +583,235 @@ class _ActiveMedicationPageState extends State { if (day == 3 || day == 23) return "rd"; return "th"; } - - // Widget manageReminder(){ - // NavigationService navigationService = getIt(); - // return Container( - // width: 59, - // height: 59, - // decoration: BoxDecoration( - // border: Border.all( - // color: AppColors.spacerLineColor,// Border color - // width: 1.0, ), - // borderRadius: BorderRadius.circular(30),// Border width - // ), - // child: - // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) - // ); - // } } + + class ReminderTimerDialog extends StatefulWidget { - // final Function()? onSetReminderPress; - // final String message; - // - // const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); - const ReminderTimerDialog({super.key}); + final int frequencyNumber; + final int doseIndex; + final Function(String) onTimeSelected; + final ActivePrescriptionsResponseModel med; + + const ReminderTimerDialog({ + super.key, + required this.frequencyNumber, + required this.doseIndex, + required this.onTimeSelected, + required this.med, + }); @override State createState() => _ReminderTimerDialogState(); } class _ReminderTimerDialogState extends State { - final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; - final List selectedTimes = ["Morning"]; // Default selection + TimeOfDay selectedTime = TimeOfDay.now(); + String? _selectedTime; + String bigTimeText = "00:00"; + bool showPicker = false; + final List> presetTimes = [ + ["06:00 AM", "07:00 AM", "08:00 AM", "09:00 AM"], // Morning + ["12:00 PM", "01:00 PM", "02:00 PM", "03:00 PM"], // Noon + ["06:00 PM", "07:00 PM", "08:00 PM", "09:00 PM"], // Evening + ]; @override Widget build(BuildContext context) { - return // - Column( - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, - hasShadow: true,), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - // Checkboxes list - children: options.map((time) => buildCircleCheckbox(time)).toList(), - ).paddingAll(16), - ), - SizedBox(height: 25.h), - // Buttons Row - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close, color: AppColors.errorColor), - label: Text( - LocaleKeys.cancel.tr(), - style: TextStyle( - color: AppColors.errorColor, - fontWeight: FontWeight.w500, - fontSize: 14.fSize + final int bucket = widget.doseIndex.clamp(0, 2); + final List times = presetTimes[bucket]; + return Padding( + padding: const EdgeInsets.all(16), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Time for ${widget.doseIndex + 1} dose".needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.bold), + ), + SizedBox(height: 12.h), + // Preset times + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + children: times.map((t) { + bool selected = _selectedTime == t; + return AppCustomChipWidget( + labelText: t, + backgroundColor: selected + ? AppColors.lightGreenButtonColor + : AppColors.transparent, + textColor: AppColors.textColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: selected + ? AppColors.successColor + : AppColors.spacerLineColor, + width: 1.2, ), + borderRadius: BorderRadius.circular(12), ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.secondaryLightRedColor, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 14), + onChipTap: () { + setState(() { + _selectedTime = t; + selectedTime = _parseTime(t); + bigTimeText = selectedTime.format(context).split(" ")[0]; + showPicker = false; + }); + }, + ); + }).toList(), + ), + SizedBox(height: 25.h), + GestureDetector( + onTap: () { + setState(() { + showPicker = !showPicker; + }); + }, + child: Center( + child: Column( + children: [ + Text( + bigTimeText, + style: TextStyle( + fontSize: 48.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor + ), ), - ), + Text( + selectedTime.period == DayPeriod.am ? "AM" : "PM", + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.bold, + color: AppColors.greyTextColor, + ), + ), + ], ), ), - SizedBox(width: 12.h), - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.pop(context, selectedTimes); + ), + SizedBox(height: 15.h), + // Time picker + if (showPicker) + SizedBox( + height: 100.h, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: false, + initialDateTime: DateTime( + 2024, + 1, + 1, + selectedTime.hour, + selectedTime.minute, + ), + onDateTimeChanged: (DateTime newTime) { + setState(() { + _selectedTime = null; + selectedTime = TimeOfDay( + hour: newTime.hour, + minute: newTime.minute, + ); + bigTimeText = + selectedTime.format(context).split(" ")[0]; + }); }, - icon: const Icon(Icons.notifications_rounded), - label: Text( - LocaleKeys.setReminder.tr(), - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14.fSize + ), + ), + SizedBox(height: 25.h), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.successColor, - foregroundColor: AppColors.whiteColor, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + onPressed: () async { + final selectedFormattedTime = + selectedTime.format(context); + widget.onTimeSelected(selectedFormattedTime); + try { + final parts = selectedFormattedTime.split(":"); + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1].split(" ")[0]); + bool isPM = selectedFormattedTime.contains("PM"); + if (isPM && hour != 12) hour += 12; + if (!isPM && hour == 12) hour = 0; + int totalMinutes = hour * 60 + minute; + // Call setCalender() + await setCalender( + context, + eventId: widget.med.itemId.toString(), + selectedMinutes: totalMinutes, + frequencyNumber: widget.frequencyNumber, + days: widget.med.days ?? 1, + orderDate: widget.med.orderDate ?? "", + itemDescriptionN: widget.med.itemDescription ?? "", + route: widget.med.route ?? "", + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Reminder added to calendar ✅".needTranslation)), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text("Error while setting calendar: $e".needTranslation)), + ); + } + Navigator.pop(context); + }, + child: Text( + LocaleKeys.save.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.f, + ), ), ), ), - ), - ], - ), - SizedBox(height: 30.h), - ], - ); - } - - Widget buildCircleCheckbox(String label) { - final bool isSelected = selectedTimes.contains(label); - return InkWell( - onTap: () { - setState(() { - if (isSelected) { - selectedTimes.remove(label); - } else { - selectedTimes.add(label); - } - }); - }, - borderRadius: BorderRadius.circular(25), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - // Custom circle checkbox - Container( - width: 15.h, - height: 15.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, - width: 1.h, - ), - color: isSelected ? AppColors.errorColor: AppColors.transparent, - ), - ), - SizedBox(width: 12.h), - // Label text - Text( - label, - style: TextStyle(fontSize: 16.fSize, color: Colors.black87), + ], ), ], - ), + ).paddingAll(16), ), ); } - void showCircleCheckboxDialog(BuildContext context) async { - final selected = await showDialog>( - context: context, - builder: (context) => const ReminderTimerDialog(), - ); - - if (selected != null && selected.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Reminders set for: ${selected.join(', ').needTranslation}')), - ); + TimeOfDay _parseTime(String t) { + try { + int hour = int.parse(t.split(":")[0]); + int minute = int.parse(t.split(":")[1].split(" ")[0]); + bool pm = t.contains("PM"); + if (pm && hour != 12) hour += 12; + if (!pm && hour == 12) hour = 0; + return TimeOfDay(hour: hour, minute: minute); + } catch (e) { + return TimeOfDay.now(); } } } + + + diff --git a/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart b/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart index a48ca10a..28a55baf 100644 --- a/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart +++ b/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart @@ -119,7 +119,7 @@ class TrackingScreen extends StatelessWidget { backgroundColor: AppColors.lightRedButtonColor, borderColor: Colors.transparent, text: "Share Your Live Locatin on Whatsapp".needTranslation, - fontSize: 12.fSize, + fontSize: 12.f, textColor: AppColors.primaryRedColor, iconColor: AppColors.primaryRedColor, onPressed: () {}, @@ -170,7 +170,7 @@ class TrackingScreen extends StatelessWidget { return Row( spacing: 16.h, children: [ - Utils.buildImgWithNetwork(url: "", iconColor: Colors.transparent) + Utils.buildImgWithNetwork(url: "",) .circle(52.h), Expanded( child: Column( @@ -244,7 +244,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "Please wait for the call".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.textColor, ), @@ -252,7 +252,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "...".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.errorColor, ), @@ -265,7 +265,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "15:30".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.textColor, ), @@ -273,7 +273,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: " mins ".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.errorColor, ), @@ -281,7 +281,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "to hospital".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.textColor, ), diff --git a/lib/presentation/emergency_services/widgets/nearestERItem.dart b/lib/presentation/emergency_services/widgets/nearestERItem.dart index 3dc2aa1f..6a8fdbcb 100644 --- a/lib/presentation/emergency_services/widgets/nearestERItem.dart +++ b/lib/presentation/emergency_services/widgets/nearestERItem.dart @@ -49,7 +49,7 @@ class NearestERItem extends StatelessWidget { ).toShimmer2(isShow: isLoading) : Utils.buildImgWithNetwork( url: nearestERItem.projectImageURL ?? '', - iconColor: Colors.transparent, + // iconColor: Colors.transparent, ).circle(24.h).toShimmer2(isShow: isLoading), const SizedBox(width: 12), Expanded( diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 2f72227a..0e9eb30d 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -351,7 +351,16 @@ class _LandingPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Quick Links".needTranslation.toText16(isBold: true), + CustomButton(text: "Quick Links".needTranslation, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + ); + }, + ), + // "Quick Links".needTranslation.toText16(isBold: true), Row( children: [ "View medical file".needTranslation.toText12(color: AppColors.primaryRedColor), diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index ce2f87c1..3aab1de6 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -77,5 +77,7 @@ static const Color calenderTextColor = Color(0xFFD0D0D0); static const Color lightGreenButtonColor = Color(0x2618C273); static const Color lightRedButtonColor = Color(0x1AED1C2B); +static const Color lightGreyTextColor = Color(0xFF959595); +static const Color labelColorYellow = Color(0xFFFBCB6E); } From 77242825bbfd2653b310de8836409303d1fd91c3 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Tue, 18 Nov 2025 10:54:57 +0300 Subject: [PATCH 04/21] fix toggle --- .../active_prescriptions_view_model.dart | 168 +--- .../active_prescriptions_response_model.dart | 2 +- .../active_medication_page.dart | 730 ++++++++++++------ 3 files changed, 512 insertions(+), 388 deletions(-) diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart index 03f84ea1..e4da04dd 100644 --- a/lib/features/active_prescriptions/active_prescriptions_view_model.dart +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -5,36 +5,24 @@ import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescri import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class ActivePrescriptionsViewModel extends ChangeNotifier { - bool isActivePrescriptionsDetailsLoading = false; - late ActivePrescriptionsRepo activePrescriptionsRepo; late ErrorHandlerService errorHandlerService; + List activePrescriptionsDetailsList = []; ActivePrescriptionsViewModel({ required this.activePrescriptionsRepo, required this.errorHandlerService, }); - List activePrescriptionsDetailsList = []; - - initActivePrescriptionsViewModel() { - getActiveMedications(); - notifyListeners(); - } - - setPrescriptionsDetailsLoading() { - isActivePrescriptionsDetailsLoading = true; - notifyListeners(); - } - - // Get medications list Future getActiveMedications({ Function(dynamic)? onSuccess, Function(String)? onError, }) async { - final result = await activePrescriptionsRepo.getActivePrescriptionsDetails(); + final result = + await activePrescriptionsRepo.getActivePrescriptionsDetails(); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async => + await errorHandlerService.handleError(failure: failure), (apiResponse) { if (apiResponse.messageStatus == 1) { activePrescriptionsDetailsList = apiResponse.data ?? []; @@ -56,140 +44,58 @@ class ActivePrescriptionsViewModel extends ChangeNotifier { return DateTime.tryParse(date) ?? DateTime.now(); } - // Extract numeric value ( "3 / week" → 3) - int extractNumberFromFrequency(String? frequency) { - if (frequency == null) return 1; - final m = RegExp(r'(\d+)').firstMatch(frequency); - if (m != null) return int.tryParse(m.group(1)!) ?? 1; - return 1; - } - - // Generate medication days based on frequency text List generateMedicationDays(ActivePrescriptionsResponseModel med) { final start = parseDate(med.startDate); final duration = med.days ?? 0; - final frequency = (med.frequency ?? "").toLowerCase().trim(); - - List result = []; - if (duration <= 0) return result; - - // Every N hours ( "Every Six Hours", "Every 8 hours") - if (frequency.contains("hour")) { - final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); - int intervalHours = 0; - - if (match != null) { - intervalHours = int.tryParse(match.group(1)!) ?? 0; - } else { - // handle text numbers like "Every six hours" - final textNum = { - "one": 1, - "two": 2, - "three": 3, - "four": 4, - "five": 5, - "six": 6, - "seven": 7, - "eight": 8, - "nine": 9, - "ten": 10, - "twelve": 12, - }; - for (var key in textNum.keys) { - if (frequency.contains(key)) { - intervalHours = textNum[key]!; - break; - } - } - } - - if (intervalHours > 0) { - for (int day = 0; day < duration; day++) { - final dayStart = start.add(Duration(days: day)); - for (int hour = 0; hour < 24; hour += intervalHours) { - result.add(DateTime(dayStart.year, dayStart.month, dayStart.day, hour)); - } - } - return result; - } - } - - // Daily (every day) - if (frequency.contains("day") && - !frequency.contains("every other") && - !frequency.contains("every ")) { - for (int i = 0; i < duration; i++) { - result.add(start.add(Duration(days: i))); - } - } - - // Every other day - else if (frequency.contains("every other day")) { - for (int i = 0; i < duration; i += 2) { - result.add(start.add(Duration(days: i))); - } + if (duration <= 0) return []; + final f = (med.frequency ?? "").toLowerCase().trim(); + int intervalDays = 1; + + if (f.contains("every six hours") || + f.contains("every 6 hours") || + f.contains("every four hours") || + f.contains("every 4 hours") || + f.contains("every eight hours") || + f.contains("every 8 hours") || + f.contains("every 12 hours") || + f.contains("every twelve hours") || + f.contains("every 24 hours") || + f.contains("3 times a day") || + f.contains("once a day")) { + intervalDays = 1; } - - // Every N days → e.g. "Every 3 days", "Every 5 days" - else if (frequency.contains("every") && frequency.contains("day")) { - final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); - final interval = match != null ? int.tryParse(match.group(1)!) ?? 1 : 1; - for (int i = 0; i < duration; i += interval) { - result.add(start.add(Duration(days: i))); - } + else if (f.contains("once a week")) { + intervalDays = 7; } - - // Once or twice a week - else if (frequency.contains("once a week")) { - for (int i = 0; i < duration; i += 7) { - result.add(start.add(Duration(days: i))); - } - } else if (frequency.contains("twice a week")) { - for (int i = 0; i < duration; i += 3) { - result.add(start.add(Duration(days: i))); - } + else if (f.contains("every 3 days")) { + intervalDays = 3; } - - // Numeric frequency like "3 / week", "2 / week" - else if (frequency.contains("week")) { - int timesPerWeek = extractNumberFromFrequency(frequency); - double interval = 7 / timesPerWeek; - double dayPointer = 0; - - for (int i = 0; i < duration; i++) { - if (i >= dayPointer.floor()) { - result.add(start.add(Duration(days: i))); - dayPointer += interval; - } - } + else if (f.contains("every other day")) { + intervalDays = 2; } - else { - result.add(start); - } - - - final unique = {}; - for (final d in result) { - unique["${d.year}-${d.month}-${d.day}"] = d; + List result = []; + for (int offset = 0; offset < duration; offset += intervalDays) { + result.add(start.add(Duration(days: offset))); } - return unique.values.toList()..sort((a, b) => a.compareTo(b)); + return result; } - bool sameYMD(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; - // Filter medications for selected day - List getMedsForSelectedDay(DateTime selectedDate) { - final target = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + List getMedsForSelectedDay( + DateTime selectedDate) { + final clean = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + return activePrescriptionsDetailsList.where((med) { final days = generateMedicationDays(med); - return days.any((d) => sameYMD(d, target)); + return days.any((d) => sameYMD(d, clean)); }).toList(); } } + diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart index eb216a6b..42faafa9 100644 --- a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -35,7 +35,7 @@ class ActivePrescriptionsResponseModel { int? scaleOffset; String? startDate; - // ✅ Added for reminder feature + // Added for reminder feature List selectedDoseTimes = []; bool isReminderOn = false; // toggle status diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index aa2abe59..d27b35db 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -18,6 +18,7 @@ import '../../widgets/buttons/custom_button.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting import 'package:provider/provider.dart'; import '../../widgets/loader/bottomsheet_loader.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ActiveMedicationPage extends StatefulWidget { @@ -32,43 +33,98 @@ class _ActiveMedicationPageState extends State { late DateTime selectedDate; List selectedDayMeds = []; ActivePrescriptionsViewModel? activePreVM; - Map medReminderStatus = {}; + + + Map medReminderStatus = {}; + + String _buildMedKey(ActivePrescriptionsResponseModel med) { + return "${med.itemId}_${med.startDate}_${med.days}_${med.frequency}"; + } + + int _getDosesCount(ActivePrescriptionsResponseModel med) { + return med.frequencyNumber ?? 1; + } @override - void initState() { + void initState() { super.initState(); currentDate = DateTime.now(); selectedDate = currentDate; + WidgetsBinding.instance.addPostFrameCallback((_) async { - activePreVM = Provider.of(context, listen: false); + activePreVM = + Provider.of(context, listen: false); LoaderBottomSheet.showLoader(); await activePreVM!.getActiveMedications( - onSuccess: (_) { + onSuccess: (_) async { LoaderBottomSheet.hideLoader(); - final todayMeds = activePreVM!.getMedsForSelectedDay(selectedDate); - setState(() { - selectedDayMeds = todayMeds; + + final todayMeds = + activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = todayMeds); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await loadSavedReminders(); }); }, onError: (_) { LoaderBottomSheet.hideLoader(); }, ); - activePreVM!.addListener(() { + + activePreVM!.addListener(() { if (!mounted) return; - final medsForDay = activePreVM!.getMedsForSelectedDay(selectedDate); + final medsForDay = + activePreVM!.getMedsForSelectedDay(selectedDate); setState(() => selectedDayMeds = medsForDay); }); }); } + Future loadSavedReminders() async { + final prefs = await SharedPreferences.getInstance(); + + for (final med in activePreVM!.activePrescriptionsDetailsList) { + final medKey = _buildMedKey(med); + final doses = _getDosesCount(med); + + med.selectedDoseTimes = + List.filled(doses, null, growable: false); + + for (int i = 0; i < doses; i++) { + final saved = prefs.getString("doseTime_${medKey}_$i"); + if (saved != null) { + med.selectedDoseTimes[i] = saved; + } + } + + final reminderOn = + prefs.getBool("reminderStatus_$medKey") ?? false; + med.isReminderOn = reminderOn; + medReminderStatus[medKey] = reminderOn; + } + + setState(() {}); + } + + Future saveReminderStatus(String medKey, bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool("reminderStatus_$medKey", value); + } + + Future saveDoseTime( + String medKey, int doseIndex, String time) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString("doseTime_${medKey}_$doseIndex", time); + } + + List getUpcomingDays() => + List.generate(7, (index) => currentDate.add(Duration(days: index))); - List getUpcomingDays() => List.generate(7, (index) => currentDate.add(Duration(days: index))); @override Widget build(BuildContext context) { final days = getUpcomingDays(); - final dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)}"; - return Scaffold( + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( onBackPressed: () => Navigator.of(context).pop(), @@ -99,13 +155,13 @@ class _ActiveMedicationPageState extends State { }, ), ), - SizedBox(height: 20.h), + SizedBox(height: 20.h), RichText( text: TextSpan( children: [ TextSpan( text: "${selectedDate.day}", - style: TextStyle( + style: TextStyle( color: AppColors.textColor, fontSize: 16, fontWeight: FontWeight.w500, @@ -115,7 +171,7 @@ class _ActiveMedicationPageState extends State { child: Transform.translate( offset: const Offset(0, -4), child: Text( - getSuffix(selectedDate.day), + _getSuffix(selectedDate.day), style: const TextStyle( fontSize: 12, color: AppColors.textColor, @@ -135,121 +191,163 @@ class _ActiveMedicationPageState extends State { ], ), ), - Text("Medications".needTranslation, + Text("Medications".needTranslation, style: TextStyle( color: AppColors.primaryRedBorderColor, fontSize: 12.f, fontWeight: FontWeight.w500)), - SizedBox(height: 16.h), + SizedBox(height: 16.h), Expanded( child: SingleChildScrollView( - child: selectedDayMeds.isNotEmpty - ? ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: selectedDayMeds.length, - itemBuilder: (context, index) { - final med = selectedDayMeds[index]; - final doses = med.doseDailyQuantity ?? 1; - med.selectedDoseTimes ??= List.filled(doses, null); - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - margin: EdgeInsets.all(10), - child: Column( - children: [ - _buildMedHeader(med), - Row( - crossAxisAlignment: CrossAxisAlignment.center, + child: selectedDayMeds.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedDayMeds.length, + itemBuilder: (context, index) { + final med = selectedDayMeds[index]; + final doses = _getDosesCount(med); + if (med.selectedDoseTimes.length != doses) { + final old = med.selectedDoseTimes; + med.selectedDoseTimes = + List.filled(doses, null, + growable: false); + for (int i = 0; + i < old.length && i < doses; + i++) { + med.selectedDoseTimes[i] = old[i]; + } + } + + return Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + margin: EdgeInsets.all(10), + child: Column( + children: [ + _buildMedHeader(med), + Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + // Utils.buildSvgWithAssets( + // icon: AppAssets., + // height: 18.h, + // width: 18.h, + // iconColor: + // AppColors.lightGreyTextColor, + // ), + Icon( + Icons.info_outline, + color: AppColors + .lightGreyTextColor, + size: 18, + ), + SizedBox(width: 6.h), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Remarks: " + .needTranslation, + style: TextStyle( + color: + AppColors.textColor, + fontWeight: + FontWeight.w600, + fontSize: 10, + ), + ), + TextSpan( + text: + "some remarks about the prescription will be here" + .needTranslation, + style: TextStyle( + color: AppColors + .lightGreyTextColor, + fontWeight: + FontWeight.normal, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ).paddingOnly(left: 16, right: 16), + const Divider( + color: AppColors.greyColor), + GestureDetector( + onTap: () => showDoseDialog(med), + child: Row( children: [ - Icon( - Icons.info_outline, - color: AppColors.lightGreyTextColor, - size: 20, + Container( + width: 40.h, + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: + BorderRadius.circular(10), + ), + child: + Utils.buildSvgWithAssets( + icon: AppAssets.bell, + height: 24.h, + width: 24.h, + iconColor: + AppColors.greyTextColor, + ), ), - SizedBox(width: 6.h), + SizedBox(width: 12.h), Expanded( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: "Remarks: ".needTranslation, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Set Reminder" + .needTranslation, style: TextStyle( - color: AppColors.textColor, - fontWeight: FontWeight.w600, - fontSize: 10, - ), - ), - TextSpan( - text: "some remarks about the prescription will be here".needTranslation, + fontSize: 14.f, + fontWeight: + FontWeight.w600, + color: AppColors + .textColor)), + Text( + "Notify me before the consumption time" + .needTranslation, style: TextStyle( - color: AppColors.lightGreyTextColor, - fontWeight: FontWeight.normal, - fontSize: 10, - ), - ), - ], - ), + fontSize: 12.f, + color: AppColors + .textColorLight, + )), + ], ), ), + _buildToggle(med), ], - ).paddingOnly(left: 16, right: 16), - const Divider(color: AppColors.greyColor), - // Reminder Section - GestureDetector( - onTap: () => showDoseDialog(med, index), - child: Row( - children: [ - Container( - width: 40.h, - height: 40.h, - alignment: Alignment.center, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10), - ), - child: Utils.buildSvgWithAssets( - icon: AppAssets.bell, - height: 24.h, - width: 24.h, - iconColor: AppColors.greyTextColor, - ), - ), - SizedBox(width: 12.h), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text("Set Reminder".needTranslation, - style: TextStyle( - fontSize: 14.f, - fontWeight: FontWeight.w600, - color: AppColors.textColor)), - Text("Notify me before the consumption time".needTranslation, - style: TextStyle( - fontSize: 12.f, - color: AppColors.textColorLight, - )), - ], - ), - ), - _buildToggle(index) - ], - ).paddingAll(16), - ), - const Divider(color: AppColors.greyColor), - _buildButtons(), - ], - ), - ); - }, - ) - : Utils.getNoDataWidget(context, - noDataText: "No medications today".needTranslation), + ).paddingAll(16), + ), + const Divider( + color: AppColors.greyColor), + _buildButtons(), + ], + ), + ); + }, + ) + : Utils.getNoDataWidget( + context, + noDataText: + "No medications today".needTranslation, + ), ), ), ], @@ -257,55 +355,67 @@ class _ActiveMedicationPageState extends State { ); } - //medicine card - Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - width: 59.h, - height: 59.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.spacerLineColor, - borderRadius: 30.r, - hasShadow: false, + // medicine card + Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 59.h, + height: 59.h, + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.spacerLineColor, + borderRadius: 30.r, + hasShadow: false, + ), + child: Utils.buildImgWithNetwork( + url: med.productImageString ?? "", + iconColor: Colors.transparent) + .circle(52.h)), + ), + SizedBox(width: 12.h), + Expanded( + child: Text( + med.itemDescription ?? "", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor), ), - child: Utils.buildImgWithNetwork( - url: med.productImageString ?? "" ).circle(52.h) - ), - ), - SizedBox(width: 12.h), - Expanded( - child: Text( - med.itemDescription ?? "", - style: TextStyle( - fontSize: 16.f, - fontWeight: FontWeight.w600, - color: AppColors.textColor), + ), + ]), + SizedBox(height: 12.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget( + labelText: + "Route: ${med.route}".needTranslation), + AppCustomChipWidget( + labelText: + "Frequency: ${med.frequency}".needTranslation), + AppCustomChipWidget( + labelText: + "Daily Dose: ${med.doseDailyQuantity}" + .needTranslation), + AppCustomChipWidget( + labelText: + "Duration: ${med.days}".needTranslation), + ], ), - ), - ]), - SizedBox(height: 12.h), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - AppCustomChipWidget(labelText: "Route: ${med.route}".needTranslation), - AppCustomChipWidget(labelText: "Frequency: ${med.frequency}".needTranslation), - AppCustomChipWidget(labelText: "Daily Dose: ${med.doseDailyQuantity}".needTranslation), - AppCustomChipWidget(labelText: "Duration: ${med.days}".needTranslation), ], ), - ], - ), - ); + ); Widget _buildButtons() => Padding( - padding: EdgeInsets.all(16), + padding: EdgeInsets.all(16), child: Row(children: [ Expanded( child: CustomButton( @@ -317,24 +427,28 @@ class _ActiveMedicationPageState extends State { textColor: AppColors.errorColor, ), ), - SizedBox(width: 12.h), + SizedBox(width: 12.h), Expanded( child: CustomButton( - text: "Read Instructions".needTranslation, fontSize: 13.f, onPressed: () {})), + text: "Read Instructions".needTranslation, + fontSize: 13.f, + onPressed: () {})), ]), ); - Widget _buildToggle(int index) { - final value = medReminderStatus[index] ?? false; + Widget _buildToggle(ActivePrescriptionsResponseModel med) { + final medKey = _buildMedKey(med); + final value = medReminderStatus[medKey] ?? false; return GestureDetector( onTap: () async { - await showDoseDialog(selectedDayMeds[index], index); - setState(() { - if ((selectedDayMeds[index].selectedDoseTimes ?? []).any((t) => t != null)) { - medReminderStatus[index] = true; - } - }); + await showDoseDialog(med); + final hasTime = + (med.selectedDoseTimes).any((t) => t != null); + medReminderStatus[medKey] = hasTime; + await saveReminderStatus(medKey, hasTime); + + setState(() {}); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -342,11 +456,14 @@ class _ActiveMedicationPageState extends State { height: 28.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: value ? AppColors.lightGreenColor : AppColors.greyColor.withOpacity(0.3), + color: value + ? AppColors.lightGreenColor + : AppColors.greyColor.withOpacity(0.3), ), child: AnimatedAlign( duration: const Duration(milliseconds: 200), - alignment: value ? Alignment.centerRight : Alignment.centerLeft, + alignment: + value ? Alignment.centerRight : Alignment.centerLeft, child: Padding( padding: const EdgeInsets.all(3), child: Container( @@ -354,7 +471,9 @@ class _ActiveMedicationPageState extends State { height: 22.h, decoration: BoxDecoration( shape: BoxShape.circle, - color: value ? AppColors.textGreenColor : AppColors.greyTextColor, + color: value + ? AppColors.textGreenColor + : AppColors.greyTextColor, ), ), ), @@ -363,10 +482,16 @@ class _ActiveMedicationPageState extends State { ); } - Future showDoseDialog(ActivePrescriptionsResponseModel med, int medIndex) { - final doses = med.frequencyNumber ?? 1; + + Future showDoseDialog(ActivePrescriptionsResponseModel med) { + final doses = _getDosesCount(med); if (med.selectedDoseTimes.length != doses) { - med.selectedDoseTimes = List.generate(doses, (_) => null); + final old = med.selectedDoseTimes; + med.selectedDoseTimes = + List.filled(doses, null, growable: false); + for (int i = 0; i < old.length && i < doses; i++) { + med.selectedDoseTimes[i] = old[i]; + } } return showModalBottomSheet( @@ -378,19 +503,22 @@ class _ActiveMedicationPageState extends State { height: 520.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bottomSheetBgColor, - customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + customBorder: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), hasShadow: true, ), - child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - Text( + Text( "Reminders".needTranslation, style: TextStyle( fontSize: 20.f, @@ -400,11 +528,17 @@ class _ActiveMedicationPageState extends State { ), GestureDetector( onTap: () => Navigator.pop(context), - child: Icon(Icons.close, color:AppColors.blackBgColor), + child: Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + height: 24.h, + width: 24.h, + iconColor: + AppColors.blackBgColor, + ), ), ], ), - SizedBox(height: 20.h), + SizedBox(height: 20.h), Expanded( child: ListView.builder( itemCount: doses, @@ -415,64 +549,91 @@ class _ActiveMedicationPageState extends State { AppColors.labelColorYellow, AppColors.purpleBg ][doseIndex % 4]; - - final doseLabel = "${doseIndex + 1}${getSuffix(doseIndex + 1)}"; - final time = med.selectedDoseTimes[doseIndex] ?? "Not set yet"; - + final doseLabel = + "${doseIndex + 1}${_getSuffix(doseIndex + 1)}"; + final time = + med.selectedDoseTimes[doseIndex] ?? + "Not set yet"; return GestureDetector( onTap: () { Navigator.pop(context); - showTimePickerSheet(med, medIndex, doseIndex); + showTimePickerSheet(med, doseIndex); }, child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 16.r, hasShadow: false, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Container( - padding: const EdgeInsets.symmetric( - vertical: 6, horizontal: 14), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 14), decoration: BoxDecoration( color: badgeColor, borderRadius: BorderRadius.circular(12), ), - child: Text( - doseLabel, - style: TextStyle( - color: AppColors.whiteColor, - fontWeight: FontWeight.bold, - fontSize: 16.f, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${doseIndex + 1}", + style: TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -4), + child: Text( + _getSuffix(doseIndex + 1), + style: TextStyle( + color: AppColors.whiteColor, + fontSize: 10.f, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], ), ), ), SizedBox(height: 8.h), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( "Set reminder for $doseLabel dose", - style: TextStyle( + style: TextStyle( color: AppColors.textColor, fontWeight: FontWeight.bold, fontSize: 16.f, ), ), ), - Icon(Icons.arrow_forward_outlined, - size: 24.w, color: AppColors.textColor), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + height: 24.h, + width: 24.h, + iconColor: + AppColors.textColor, + ), ], ), - SizedBox(height: 4.h), + SizedBox(height: 4.h), Text( time, - style: TextStyle( + style: TextStyle( fontSize: 12.f, color: AppColors.greyTextColor, fontWeight: FontWeight.w500, @@ -494,7 +655,7 @@ class _ActiveMedicationPageState extends State { void showTimePickerSheet( - ActivePrescriptionsResponseModel med, int medIndex, int doseIndex) { + ActivePrescriptionsResponseModel med, int doseIndex) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -502,20 +663,24 @@ class _ActiveMedicationPageState extends State { builder: (_) => Container( width: double.infinity, height: 460.h, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: AppColors.bottomSheetBgColor, - borderRadius: - BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), ), child: ReminderTimerDialog( med: med, - frequencyNumber: med.doseDailyQuantity ?? 1, + frequencyNumber: _getDosesCount(med), doseIndex: doseIndex, - onTimeSelected: (String time) { - setState(() { - med.selectedDoseTimes[doseIndex] = time; - medReminderStatus[medIndex] = true; - }); + onTimeSelected: (String time) async { + final medKey = _buildMedKey(med); + med.selectedDoseTimes[doseIndex] = time; + await saveDoseTime(medKey, doseIndex, time); + medReminderStatus[medKey] = true; + await saveReminderStatus(medKey, true); + setState(() {}); }, ), ), @@ -529,7 +694,8 @@ class _ActiveMedicationPageState extends State { return GestureDetector( onTap: () { final vm = - Provider.of(context, listen: false); + Provider.of(context, + listen: false); setState(() { selectedDate = date; selectedDayMeds = vm.getMedsForSelectedDay(date); @@ -544,10 +710,11 @@ class _ActiveMedicationPageState extends State { ? AppColors.secondaryLightRedBorderColor : Colors.transparent, border: Border.all( - color: isSelected - ? AppColors.primaryRedBorderColor - : AppColors.spacerLineColor, - width: 1), + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.spacerLineColor, + width: 1, + ), ), child: Padding( padding: const EdgeInsets.all(8.0), @@ -563,7 +730,7 @@ class _ActiveMedicationPageState extends State { fontSize: 11.f, fontWeight: FontWeight.w500), ), - SizedBox(height: 5.h), + SizedBox(height: 5.h), Text("${date.day}", style: TextStyle( fontSize: 16.f, @@ -577,14 +744,14 @@ class _ActiveMedicationPageState extends State { ); } - String getSuffix(int day) { + String _getSuffix(int day) { if (day == 1 || day == 21 || day == 31) return "st"; if (day == 2 || day == 22) return "nd"; if (day == 3 || day == 23) return "rd"; return "th"; } -} +} class ReminderTimerDialog extends StatefulWidget { @@ -617,32 +784,94 @@ class _ReminderTimerDialogState extends State { ["06:00 PM", "07:00 PM", "08:00 PM", "09:00 PM"], // Evening ]; + String _getSuffix(int number) { + if (number == 1 || number == 21 || number == 31) return "st"; + if (number == 2 || number == 22) return "nd"; + if (number == 3 || number == 23) return "rd"; + return "th"; + } + @override Widget build(BuildContext context) { final int bucket = widget.doseIndex.clamp(0, 2); final List times = presetTimes[bucket]; + return Padding( padding: const EdgeInsets.all(16), child: Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bottomSheetBgColor, - customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + customBorder: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), hasShadow: true, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Time for ${widget.doseIndex + 1} dose".needTranslation, - style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.bold), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Time for ", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + TextSpan( + text: "${widget.doseIndex + 1}", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -6), + child: Text( + _getSuffix(widget.doseIndex + 1), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + ), + ), + TextSpan( + text: " reminder", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child:Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + height: 24.h, + width: 24.h, + iconColor: + AppColors.blackBgColor, + ),), + ], ), - SizedBox(height: 12.h), - // Preset times + SizedBox(height: 12.h), Wrap( spacing: 8, runSpacing: 8, - alignment: WrapAlignment.start, children: times.map((t) { bool selected = _selectedTime == t; return AppCustomChipWidget( @@ -660,7 +889,7 @@ class _ReminderTimerDialogState extends State { ), borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 14), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), onChipTap: () { setState(() { _selectedTime = t; @@ -672,27 +901,25 @@ class _ReminderTimerDialogState extends State { ); }).toList(), ), - SizedBox(height: 25.h), + SizedBox(height: 25.h), GestureDetector( onTap: () { - setState(() { - showPicker = !showPicker; - }); + setState(() => showPicker = !showPicker); }, child: Center( child: Column( children: [ Text( bigTimeText, - style: TextStyle( + style: TextStyle( fontSize: 48.f, fontWeight: FontWeight.bold, - color: AppColors.textColor + color: AppColors.textColor, ), ), Text( selectedTime.period == DayPeriod.am ? "AM" : "PM", - style: TextStyle( + style: TextStyle( fontSize: 20.f, fontWeight: FontWeight.bold, color: AppColors.greyTextColor, @@ -702,8 +929,7 @@ class _ReminderTimerDialogState extends State { ), ), ), - SizedBox(height: 15.h), - // Time picker + SizedBox(height: 15.h), if (showPicker) SizedBox( height: 100.h, @@ -717,20 +943,16 @@ class _ReminderTimerDialogState extends State { selectedTime.hour, selectedTime.minute, ), - onDateTimeChanged: (DateTime newTime) { + onDateTimeChanged: (newTime) { setState(() { _selectedTime = null; - selectedTime = TimeOfDay( - hour: newTime.hour, - minute: newTime.minute, - ); - bigTimeText = - selectedTime.format(context).split(" ")[0]; + selectedTime = TimeOfDay(hour: newTime.hour, minute: newTime.minute); + bigTimeText = selectedTime.format(context).split(" ")[0]; }); }, ), ), - SizedBox(height: 25.h), + SizedBox(height: 25.h), Row( children: [ Expanded( @@ -745,8 +967,7 @@ class _ReminderTimerDialogState extends State { ), ), onPressed: () async { - final selectedFormattedTime = - selectedTime.format(context); + final selectedFormattedTime = selectedTime.format(context); widget.onTimeSelected(selectedFormattedTime); try { final parts = selectedFormattedTime.split(":"); @@ -756,7 +977,6 @@ class _ReminderTimerDialogState extends State { if (isPM && hour != 12) hour += 12; if (!isPM && hour == 12) hour = 0; int totalMinutes = hour * 60 + minute; - // Call setCalender() await setCalender( context, eventId: widget.med.itemId.toString(), @@ -768,21 +988,18 @@ class _ReminderTimerDialogState extends State { route: widget.med.route ?? "", ); ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Reminder added to calendar ✅".needTranslation)), + SnackBar(content: Text("Reminder added to calendar ✅".needTranslation)), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text("Error while setting calendar: $e".needTranslation)), + SnackBar(content: Text("Error while setting calendar: $e".needTranslation)), ); } Navigator.pop(context); }, child: Text( LocaleKeys.save.tr(), - style: TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.f, ), @@ -797,7 +1014,6 @@ class _ReminderTimerDialogState extends State { ); } - TimeOfDay _parseTime(String t) { try { int hour = int.parse(t.split(":")[0]); @@ -806,7 +1022,7 @@ class _ReminderTimerDialogState extends State { if (pm && hour != 12) hour += 12; if (!pm && hour == 12) hour = 0; return TimeOfDay(hour: hour, minute: minute); - } catch (e) { + } catch (_) { return TimeOfDay.now(); } } @@ -815,3 +1031,5 @@ class _ReminderTimerDialogState extends State { + + From e17d3cde87a0fe0a2060b8c98531ca0c8eff074b Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Tue, 18 Nov 2025 11:19:15 +0300 Subject: [PATCH 05/21] fix toggle --- lib/presentation/emergency_services/widgets/nearestERItem.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/emergency_services/widgets/nearestERItem.dart b/lib/presentation/emergency_services/widgets/nearestERItem.dart index 6a8fdbcb..3dc2aa1f 100644 --- a/lib/presentation/emergency_services/widgets/nearestERItem.dart +++ b/lib/presentation/emergency_services/widgets/nearestERItem.dart @@ -49,7 +49,7 @@ class NearestERItem extends StatelessWidget { ).toShimmer2(isShow: isLoading) : Utils.buildImgWithNetwork( url: nearestERItem.projectImageURL ?? '', - // iconColor: Colors.transparent, + iconColor: Colors.transparent, ).circle(24.h).toShimmer2(isShow: isLoading), const SizedBox(width: 12), Expanded( From 0b3fea230f75bcda90ded25e29d3a93cf9adf610 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Mon, 15 Dec 2025 10:28:48 +0300 Subject: [PATCH 06/21] monthly report --- assets/images/jpg/report.jpg | Bin 0 -> 37509 bytes lib/core/api_consts.dart | 1 + lib/core/app_assets.dart | 4 + lib/core/dependencies.dart | 6 + .../active_prescriptions_response_model.dart | 205 ++++--------- .../hmg_services_component_model.dart | 2 + .../terms_conditions_repo.dart | 60 ++++ .../terms_conditions_view_model.dart | 45 +++ lib/main.dart | 6 +- .../active_medication_page.dart | 2 +- .../hmg_services/services_page.dart | 25 +- .../hmg_services/services_view.dart | 21 +- .../monthly_reports/monthly_reports_page.dart | 283 ++++++++++++++++++ .../monthly_reports/user_agreement_page.dart | 117 ++++++++ lib/routes/app_routes.dart | 7 +- pubspec.yaml | 2 +- 16 files changed, 632 insertions(+), 154 deletions(-) create mode 100644 assets/images/jpg/report.jpg create mode 100644 lib/features/terms_conditions/terms_conditions_repo.dart create mode 100644 lib/features/terms_conditions/terms_conditions_view_model.dart create mode 100644 lib/presentation/monthly_reports/monthly_reports_page.dart create mode 100644 lib/presentation/monthly_reports/user_agreement_page.dart diff --git a/assets/images/jpg/report.jpg b/assets/images/jpg/report.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5846cd538dec27f12011b3de3ecfbdae4d411f29 GIT binary patch literal 37509 zcmdpdWmH^EwrJx{Xj}s{(6}_N!QI^hbT{tq3GVJ5f+e_X2*I_H;GWgVj{Z0%)5 z=jZI?;wkDU0s0fU=wtnlW^NGOpIy8hB|y@DP^B|c*P@ek^{}QB;1cA7@dygh35jq) z1cZe6g*fPVc_0GZJbc`IJe<6IqWnCf{JeC3FQ7+i9&j5`9XW-+(|UZ80R3GkUteD? zUp_8Z4_j`Ch=>R`4=*<_FXv+qPEUUqFDpMz7f*)2FvwYZ!aVHVyzE_F=>EWH1$FiI zk^nt&`Zp1r-Ts00-|Fh0I;yMx&t09J|4_i6Xg$4jtpAJOe~Z{t&)?0OTgTed)!PGR z{n(x14`4S@Sr2O~FINveS68RMw5Vn0>gDQb=jui$EBmL`=vWNxUEr?1o~(busH=-A zyLft8xxlQI>uXf?Dj}m&f3G?#~QBS;p$BHr`V$Q|B)A2`TvOT@38Ry z$cwDPe}v_JWQO~X!TqPf{;TQHg8r!f!*m}X{$c#qE{_)P@o3r)X8`;^4Hf|Jef)%w z9@YR60HptYLH@6T@{jU&aAYK;$G4Baf4unLRvvl*Kun}oBy(K9oMb#l^zJK*7X$tRtbK z0f2-===8)G5Re{;blR+LvscEvfPN;aHZOx``drI`fmP~9ovcMna`#XfK1LrhrdGk% zjeQU-(2{=HQu!f;HY}k>=8yr8JRl=uprfJv!4(n^nVt{@6(X%|MdS`b3*}9f z`Su72B_{c(;}OQdw{ipNiwr~o0we*yfAgbA{a;_`%n~F6Ik&7x zOen+q$OCb{%YI1;!f#x`A^qKv=sC594v=Uu8u41z$hqW5OTN>TS?2XwEY9 z-X2D0Jt6Rt3@J{4f_xKEUn>qi%^rQOAwYUsw^M_rtlzC+kYU(rQ^c~OEPRwT=Y!0IL^O)r8V7$Yfys-95cw{p6!uYx0mr_qScft;5|{M zD5;D#n&b(wl(JyG>E~C_Oi+up&5Vw*ajhyNLKyC1ZnNqSyGB*m;_(Nz7n9-2*3xl8knkM(k+PH z6Nu&dleVY@PO86pSmI&Qa?yBHSLkLw>d{rs%R)%z+xQ<3`W2 z9_^mPvaTXm>bEIG7#sVOh!VLJFRP%W{?c($)d*48Z9b$tK{i?2JM^BD&Mc2pHy}?e zL835AHL*x{Yq|;8%~9+KL8}-UHPN|;+#WV8%uSg9v8&a@u&z5))lESNdj^OYX=)+ z6Xylqw;fzscPT5Zv7xMZs8ncmsTlBRP779-+K;1K+=Jr|#)kNy!V^bFmCf8sUSeJs zLeB4tV?S(OtT`i9Z+G)(u=~7`aH}gk3C>!k_F=+t4iG-6 zpqm*4zrA#}7_rWM(lQ{FUu;YSq^88=WWQqe@kpz=QJgnh6{`K5^q$asy%lX9&CL2t zYF*z}Fm8QyiwFHMHr`BYP%R&DqoSA{~T!H0x;&z^c(7!KY$>ztE!Tdih>fvp%}lPafKrGBwd zngFPQW>jwQhI^sK<|X`{(nZZr!m4Sv&NlxonsTY+=VbI8!UL~;h%4)S$K(?2IvaeotqUp@Mx%9-t(UV;J|k>DUb z>M^rQq*Aj$b# zq32hblU8!d?Fnr$If=-eVu{?c)fZm(iP(Ruc?Tu}&iLQs*N-in@uPIiUWtBSHtd+e z-%*`8`dRpVM@n{7yL_En)3bU-1MvVj`*K70@|QXiJIz%vZS2B#zzosfoW1Vm4_%DA z;|D;4kd#BP+*vVxc^$%kH#>27i-Fa~@nl?$@e_RgEy7D*$cFoQaixv{zFygz%6{h^1icY!9KXf7 z47_%QH|ax`-%^b2l6O(RBVSmda1LtaOScY)(0zg;kl`*xj5J%Xe=T-mG-6PUnd;F{Q2dZ}oMv&P&TMwd$juDyr{Qr0zL;cwL8`Huq7d zqC=a&=L-qma_xCLoiSCHe4kn`I-Zg!lL3^!WKQQrv?mxNbfyykSs25}W|qxqJcOV8 zVsmqn#%3za0W*02a*^DzkB>0pwuf&<;`)okacaOtrYhz>$ve`TNx&(Vv&R?gw8{6WhChjXvhGFpGW*NpL@N8ml)yP)i6+drHj-Y16n ztrY4c9Q4ZKX}R1oIizf3n*8tzbltV z;h{k4DZ=F?Y`X$!mQrb^o@Waa9YG86aV@bh4|!uUGHeg~OiO-lJMvh}u-w?wi#spY z;G`eVk_V52FzvANkcughidnw~vxfh?egG^}6|?dFG9<6Lre@UoD(_OoNrk+f?Lc~e zF8dpYmL2>lZ%%yaL>fm;*=O1&B8}zxH^FkCe8y}YHhZUIT;BJpL@`4)zNfwnc}y9y z_S1xIq01xFn?5tjSpk=50#p66%no|_xE>BWn~UHfzh;yq`{9Lf2K!d>RxaZMYDFix z!BM66i_{^5x8myA9|R1k97?!^A%peCX4QH`zTPXMlS(Vw!l4pnenIcMu(){qUr*BS z3DBxcvU8ONyz?_^*{B=PosD<=nb?4g_kF~b|8YiKx74}(536PjU4R|sGV)M^5_vN@ z507vv9fT#7p1!bxxd|pERcS~Yqr;+AL3~WXAHuejTG#wae5?Ycw+TM=k?EP&jY3Fm zaKba*(>0Q+Me)uo8nV~aDIH2<@W66ir<+$S&u{pgOJmw^Y=be~)2DRI6Kn*vy3_c4 z&Y%UV9^?^e44L+VGJae6Kmu}qC=?2l$;#%Ro>EjEHVH_TE#Bf+$a+ul8>{HT+E zT08M=Pe$MGfCs<{-VA|9`D|ax?mh=>E)@Zzij6es#PseqHRT!aC;Yr+`usOJ%W}*( zI@s23CWoBtV=#Z&q-;5t>SSF*ykJcSx3OSmSgXCw7qVqtBRRD8-R}ocAi-t2hNlIC zlWuiaO`j7C!>lKs1mIc2*cP~ ztwVX-X{}AI+XsAf-gSN%6{ym_FX02gfgg-M6jz;45sM3B=o;a4gV<*gA$ZvdmjxTs zCP=3~QM4%~&T2F9iP`D&JyFlZIc}j&-~J3eh*6Cw!?903NjTZ! z5l2B`rlEjn#eTlb`;__6SX{X=Ts6e$Ylhrcz}Z4hiqcH0) z%0|#k$CME5>>YAcX;)rddQ`_kobaaO6^`11a3`aNdxKL2gKM-~h+QZDbAehWPniftp+aG+W$sO|e~s@T}d zWRrg_J^ZQB-WwH?`=`jrMYpp#lG7HyLc35brmyxjSu+Bw`6!M~iZJFySUO3uXNe7s zEl+iH7L@{rxL7QB!bnHME=&DpQXEdc-3#6uxV&RX#woZ*46jLVM#E?VpQH{+GRwg& z+06q?LM%1;;^?M~nfe-}J*JYnh1H%5z9Z->ci4+3e|M)Ae?IwDYFUcwI-u1#tkDGm zB^xJ*P3&F?gKpUn^vX@>>SOOwI_~W=?S>(oL&SL`i>J+;ER(CqcEcYAtwnD-dqjl z96vd?{CFD|-H2-GOg-SdwlI!?o)~u?BU_X2Am7iIC@(D)02?hV|Hcd8A(3v|40-5r zI_L6dqZloqUaYsNjg={NDxVvNU<$? z)h{Y==tTHx4Aw=&z+Fy(Rf}Zt>fR~Gpmt$GDn;V3+gEghK6W}gf37zczZ3l1^@h1c zTB|0g7Jg9X9m+SPHT_qDe7YQGpk&sZQ1d(8Pk}__ba$WCHbxi63iqk*d z0L5J;5TSjdmcq}Kn9iL*f+GABn{1?3`2X!l|Fr_3e5JkLM8351WqaZI-brL$r8$u* z)=ZeY`b3GRLlt?~^^#abj*`MjF3VG7f;Z!Sp$VmF@57r&56$K3EH_NX8qTN4$}W}E z9d7Vh4ZAwbDCDpZXJW%)+SU;^%7r3n1(l}kvd2gdfCMbl*X~Aem_1+}WaCrV!rBb$ zNiIDX{e(=3Z1cC^FYuF!JI(h4HlG-8m>UVO^p>D& z*qP^T>Q!4Z(~L;tFD3Pn7CkRrbt%5g4wc7_HLnAJoL+@{PTWh?Y@K zsyAZM7DbCt9b zy{PAj*E=x?IND&rra*t)i;qpVF4M{v<(Ff|2jnq(47Z7=9I}?tQ6ZM2=n=KAhk49! zu};zP@6|M&G$MKb9tQs{82+bwlu-2qlG?MA#pPg!qt+%W=gh&xbZw^`cBBak;ndi5 zsO?xW>rN%R5VtIazPq*1ap#V|OALVRv?Z!apM4vIzCd5Jyc#Hxh=jtL(z|bdq!#Jm zPx(3}?-ut2wfgA-^aTFVZW%(DMNPLv2xffdbJ=zdB4bNHr(E(eg)HB98pxudUR5pZ zsC1wkw9$(6mvi}q#sW`oYGYi*-5yCkD=91$f5NxTBoK?%l4dV7|w_gy&+1 zKO~82ztI#T<~Vt+P0SF25FLy5~mzjQ>G`g$*t713_R?&74@ z=ZdOLkD3?E5IV9^8&Ie~pHYseN@;iP0#9ZX2Kttib!km35sZ;`d`@@(P};`X!0Abh z)$p-5rPf5S`E!%RR&bP~YHAp`=l%9AtKqfj&MuKn84bjB05U%N*mZu?byxYV#%Ag9 zc&88Ym^H?N8!f&rhIJii`fWS*`IQxN$_}|(#`x!JOpF9hG0uJrWp4(|mGy^uowJsW z8gNp4Fe{9~TV~ThpR74i;`D##mLGjJieBTaUftd_XTapKLJ!smT}`)Pf|IIxVr~B> z1+RN-#OPTGW@ypVQ9}F<4q6TRW?%B#UFr(zp?nI{T#)K3ZtD`P;HbE`41} zcjpx(wdjYhIrt!%n-tIIABW9B76->6X1E$RPRhlc^ubn(Nfm zw?)hc_*@q%hs~0iaFbxq{gMgzDNt4y>R*Zi>VPA@-ArwvHMtYJ<%oH)pvco3tfVeC zLie&;X3R!bTJ4h>Zl8Ya1Zu+N6g~neiJSkY#TapCQYJ@_buM^~zWPXMBoc3&z2~A% zbWhc=pW!z>e*x5yoPK%R#y55qHQeyYlO}I0_BScj8$^cGd6~;Ui}5LcIb4T*($d=U z6q{0*$goI<$9;!_Ri`X-q_ak%Z)3hh2Bjtvh$eSx`{PC`h%w^uH0R?)P@v!5u{=eJ z!OVF8*x7)*2sEd?BobOr7Je+*&o*Zky{hM8i`Xw>r~GdHgLI#qdWr?)%$HbE8@rSE zIYEgM^sc1Y82gqaZOkw&uDUiZ!wm64df{AkgdMy?MH1uE@Hv+UBHqc>54yY}kZ<1l zZQx=q{z9WoLXGV_ew2QNd~Az#!A5F_LZ7$)?NM8{haOy)uz_zC+bReNZIP@rnWA^U zzeYeGy)~eIY?pJXoO8VJxKY0;u9}$eE~xSBn~dsK+E(ri6GpL}_P?_u=2BsFZK^OF}JQ~|rA5apfO6q9sBtjof z$fje86f*KktFCtJ>q3bEi;N*vbIiV)7@eAGK(N4kZF_=?Ew{mshC8fwmAIs z_}L*#!azcn6+18`37!LW%DFdY?+~voNTW3|6=5yQw=*||B}X7YPK(A(g#E%Ip{zTf zx3*6|$z*r@_my5YGq^e@UpyaAh1P*aU}gfn$5}9JG%HomP5H(Z2qA*>giIgn1qf=M z)Fe-~pe(++1!FTvMQlt4x3TB5w_M?LGNH=1qgDIG3f7wjw8b1&8%1J@k_Lg zyE$E%)Ttc5V`ErEUvGdYP>3Zl3hbNn6H?5`blCLT+n23hGd{G^Qb4|62d%$1jc^N+ zQ~0I{=8Tjr1%v6O?2E+fd=7yxLmR@Z;ugi_A=C=}p)of$P=Oin{8ktG)C zI!KOzsS{HXS8Z@=-<|eLP9oALDerJ(5E3LE|0yIamTo(7BnImHl;t$>XBSFah#fAH zO|$X1c?6`p!{I`#*d9>QR%nH(IqC>;o<$102nG z0yRMMWX`M-V_-pooc~AQBl4zb2p)c*dsz|pxJ>mb>FB9sj3T@rC%C&&A`kgZ_4(BM zD51G}L600$eb4WisJNZk=bZ5neQJem$Se>@&_@#oNhIRZ5a;j$Vg& z7H^u;Y{Ib<@Twx2`1;v(>M+oBBrzW#eBIwCE_2C@DyIW=HrISUsZl;?)6m)^$fuC! zSFQicU@gZiir~Uyq-0SAnJoNW5;INaha7P;aWGyIIk|!TjgvPTaX7E*(fDGzWBF2C zu+iCalX}t{`Jm#x#hikm<>n__4x7AVZ>2Z7?^tI@4Isbts_3B}=4$VR4Cigr8S+=q zz}oYfCtD8nG%-0J6y6uuH+A#TqByW?5|8nq7DKq}L~2K_@Ct^XO-VfMDl-Jj@YuE`mJKzeL$PNDh7jSKE8 zu12N?G8+Z6p)lOtcNsw*kr4{vL?qk@^K|Lex>tne{!12|mGCYWl5)O!33-*e&SZS6JxX*NT;EhxDb; zS%wlI2*_M5Df2Exf~+T2G~ewal68#-3`cf2%``ac;=Z5cCoxX?Ae_Lp+B?Shy^B(* zn0*AHalZq_QpyXeRe=j6+15Ujh8_Y>2u}hw$rp62k?X>njt==6T(si3V#>;3)6cg! zCKU&%BR}by)y@XXAV|iQ#SBJBQk**+Q=HiO(^$e9lPl)q#+9TO1A8F=N0dU;%wTSr zeMzCY)_m>xv-5*mQ@!!x5%d!Vb%TFwAKXnR`j(tjgj`}iG01rF^ zXA8z&F=Jq`j&H5C2j6~@N%iyW`4xL7FkN_vf$#gNQ4-vF`5v$}b$`RWyY$Srm|Eo1 zi@~3H2UAxl(PCehU!|MfZHBlV1m6=~IKC2_!KeQE0s2DS^B@3epz!uYbF41ttCa9R zP~r+|ipG5n5+XFqXzk`R*_HO_O^$wXE#}&uZ9hZWS}4eF7?h0KiR~4kNsQYF_24m8 z_$hAsk1zY5m$>K2(%=gHSPRTSsG=jCE=nM77yQwtiynB_9Q zIH#tP6UNdU_$|cpo>UKBXkMOv{xILe_yDkJ{IE$wm%HTtJGh(dNA}>WlSQHXfim3V zjt9WAO`Kn<+nP6yRKK=n(Y@DSzH&wTU4G7dFOc~A+ZPnkGyQizoxda5f)ZMj(vC;q zRS9^3wO=@tm(_4HB5EcidBHubTinAm2lj~vN-zo4|4!A`d!1*7{yn1}XoZguUz*eWaSI`qS$>sn1f-58L|jA8FnTUR|pA2J=y5 zL|b|TX>(pZ0E#8QKL7?a@5Cx4 zzt(89Byno6+?A4ij<&FE5UwljOV)(j?NyhIe>`5juA9RlKk5{yx*$grIJq@7y_krY z4)b^OkYY?1G5u_@@BqMj%4vG$sE_~ZYc`j>@$ip3&wwsekt`2f{IA$~^I1%YFo7{1 zOEQjhidh8%KkuVvMf%i{bm@b{F}QoQm5gOI?(jx1sgaT(eQ|c7KTlW;3q?%P#d3OGxoEtRr z>C1uMq+wjQbmH|a`-rY~yI}hphcXXtWW>AgwCNrx}zy?f9eXKF7SS1xk8aL+B#2~v_AK9I6?iu9`GJbc>W+R z_Fw>!cO7xEl9ISj{5Fff4Jnp25$UM;Gi^-MkN%V|m*p>pC_7FP3z{M4)^{c~ow2o! zRM1p-!yV3;cC29wPM#~4dWotZ>Xua=x51U9ju)Rh%aOws+XoSqDOK_+e-aCronE`} zt%)C}qo^Z$ucH|+YAzdZ%;!)Ldhz_V(&NcnAg!h!(b9Y4gdyu69&YWdyYBNB2JuWD z!#E4XeKcDw<&-kcRaPUXi+O#d>%kFZ0>sV6ZNWnt2 zP;DlE#aOdsUXHm|%bP{aR5}D)W$J!`m1je_t$m1J1D!`*%m~Y-c7`MI8oWMrw30q( z&v-u(5wyK#cbYTvbkkz%b`S?&$+E_4%AeF9N&7N~8s$vlYutKX2V!=?J**N^~Z)h{MFBD;p{4b0x*7U%f7BUNFr2j>(G3mTeBF**saqQ2Hi50nIS z>!9g5K^yAxmyUF5s#f+IyYmqN4mQtm--cC&z#M5wvU=Sxuu^)7Nz-_n$36Htw>^wG z6r`aAQ4I9HWWk`IUj38cD804@n*jtMAP%)HiRE;yT`&5`_B+`F;5Wmf#pQv{1K`(7 zq?+cef@g2mh5rhCUN_nN%dXiI1nmQ0Mgia#y7h_};rekyxvgLR0dQ>e>S*w^mLXm@ z?ip^p84r}SuEvOmgSh8HZqU|^kc3rGfZGlI*Is3iG#F^>DKF4}H4wb>$ zOGX2ZI0GytiH2n`C#z8i?p-kh5D271X7;nVPO@FeMMog+`S=+4nE3W`PjhGR13qBA zNG#fs5;^+G^h5CR?bMT z_{pbSR4c&n=)PXl(FAG}x8>H6{?VPUJ~EUWxBG2$6CN>zNb{$c*y__wH)NSv8VgYx zA5ngvUkv=zj=8u4Nuq}w|3wZ%!Xn~>!nAua_4NzFUsMvqAzwc|v5M&ovLt0iq7oW3 z2oWH_=~~r+b}XartAM~ZUKqwr%)DxjtDfydG=kJ~t8|KqwL4bdVg?m1#c<^m z;R+!;MfLp_rM8bpNgIHC zoZMUnt5mgAiueRXoQ)C!0Y@O-c(CT?=5E>sBaN{jk3%VyJ$S2L92Tv z?)&lT1+(dAS*;bmKTjahB)*pOye#@~kK%=v&c5U|*oozopoZHh!gO%|>V#fXCx5F`<)lu^zTCGG3p8GBB1$aL#d9j+dvM#8 zk4>$64aR0+v}J-xNT7vzu|Bd6xLj@#w0x>Oh$*M>154;d-h0irjjH26QqHRbYvR~; z?S>q?BZadnmh0w2Une4Mr(Tu_oq&=yuq|#WpGO<+9zB}mx_tnXMf#g{J3RFb;~Aw~ z(mgjfEj*_hD}^Z)r%2hCoQ`1i7lG(#`EJx|N{D2ahzuK7ohwmW8*8_xO1UZ422zMH zV;qnfHBPxEQRMV1_Vt^supHvUy2?Yvn7pQCWK%SF#km1-4-LoekS{@84bhgIDnVL0 z`8IiKPA>8R^-p|b7Z*XuE!p!vegATt9QE?BJi9E^=G2%nUwI3d*&x=crhazy@q7;7dSB;Z#IM^@qbIqSc_7271 zbCGX{6pqB%fW25>KI)XlBE5Ru4#U0t(EEi(y?e31%|U#qv&u!-N^cc_x`A=t1PDsI%-BzXs4_s2T9b8_{=aJ zIZZwI#3eFAePrWHcVna9;`UccZIcOw2KuHJQ|3eiT1+*}?yBbK7b~9&k?jA$37$tl zbV<~QK=C3p@7DrOsyXBFNcBELRjtoGMxUa#UYS$tHVm@t+tArqGPV~RnU(85ABn{A zrX^x)h$vfhV*;7+^_>tP?9eXyp+|-539w2%Q}(wf#wNtCovX1cjXmHHYS*T3Mv55; zB@As|=$M^za_Ye5h+L_@XY}*7X}tXgCqY~T8c?b)#^2(D;1l5pO- z)h*ZCXyuDX5#YwO0WGl=gSWATW+>_^l}9W{I2|Y|?aB<0QTSMdbC;nE!KG60|7ok1 zj1pz)g8X?8c$b-IK4&%DWnqys`>36fy>5}qu3Qh#QQoLh2a6A2jVC!lOPo=MI@s`~ zD%jcJdilt#j_tRAw}W9Zf2F)BehZHAO4l+HpsoqT)ZZ@g-Lf=H4AV?~?vJ?v40=of z@GBaNJ&ldM7#t3n*PSV3iT2IhCL;}eQ@xE??i9qJMMbUg>vHl$PqZ_V$t3E1jT9Oa zGn0}Oy7d51WG!B>w67044a#N;nt6-Jl;f&33$MZfHJ>e2)fqMPtA@mZZLobY6^*~$ zmnACKts9EIcO*HTuUpHGB|g=S!c!F>+O%XhuBq_o3nhReGZ3{XgyDtLM!$}?9?>(U zzauSQDq8A1ZJMWJXH5X-Dw3DG6G4DXZI|6&MTsZU>!F*X*Ctu{DhqL0L?+uxNUhI@ z2r7@k;9e}&bTlG5_m$m3qyznL&sY{^^=ND(;gi9~lLcZ<_4la~f$TbW%LZU~<88_;*e$4BW%I ze0#cFDTE`>W+chlMW!)q{;~zCZmRbOPj#DTI6ls8$`{Q$Q)%#DgvvXpI{#C16(zvn)U(wumGJXuPNc| z=T2!*tGV8ZAl_Tz3ER9&p75P#pzs)$BCZWA%oUyvTuF^3XEjcenVN&lbeY(+9(zuQ zb+jnAVv@HC03Tq?Nz4orHC8Td>DM5Q>E5Eq|0%0LV3r>GSt&{w=UgfJ*W1VB*~uhl z#yk)I3fVyfDKp)wh{)Ii+Ot!a9}I$enx)Rd7jg@Ai)Q-OzUBkwj07WP{sV-av9lEB z)pNOFpW=RTKF|16^K)3A!P{sDH<+Esl5=&)Wa1-*>Le3;SKfLf_hSrHi*JjWO`0<7 za3x&1{XxyNd1LEN{)rDe5I%1fCV;czj3oZ?Qzu(@YD1ZH{vdnOV_<7ofxZ@UMEPDh z5w}Mm%OSMt36aWoR;zCNF2|AGKZ@(;db~%%CE)OZ_P2N zB+fsSN$dy5#B4tKp$^+LKrCn;VA# zK7QV225lz5Nx1-ZeLi5l(7T|@+8q%phOF~`ou)@q{3Wh^j7xm-i7387C?dv&g#y!) zQITD*f~*sl7s`S7rWP^$TVw1eB^3Osgs)=t#W3JT|35LSsNfR>&fcRm#wq z6FU|A5^G@CkGY~&0Cmr$uD)NJODJ`?*VZdLHNvh0PX-h_Az5dSe&eu#VvaNT&G4t? z@MAy5y0uI6*y8J|(EEHQG;D>SMS#&iYG{TV6q=$!XDJy3fYbaum=a0o8?r)QY%maaf{<^*JXm5*a~1{#^vZL6 zeTk!qIzC(O1v%|w5+kru+}$yW69(0W=OB{dYKqN`NwkB`M$2hXe9~`ZW0Xcx93&l< zyBUvX@-dthawS3OlsQ>!KM2)g8jZ0)prWrXLab?p@49bX`S@y=Li?)H+GwzH{tBZi zht>W}ghA2_{GvvCQ2)Dk11atHe&_*kMD}~$QBdTnLsEB&7w34&MhP!p;Gi$#JPL3bL9h41Cxg zN!OZZXpvJ)LxnAe#sCvP021UBA}Jr!DLzV05bY&BI?3egLp)S*-Y>xBPd2rSYYBea z&;gge3>c}sb%0ifREa{%k{Jpk(;Uh{E+S(xoq~95UYzT&H9J@46Uc+aqqYnYp06{k zkSX>0`M~FGrxbZe3S+!HM>rQYu#Hj$DiobD&c?R6&DMWj(DU(3Ua4{GlgjES+#|8l_Who#!RTg6D=5mO zPDV~ut|t3A7iRBr%1R>nPrHS53IgBg_aaccOBhHx(e+u{{b~onA=@?w#aUBeU8dh# z$OpheE+%>I&a8r*>DCVzFtq7jc<4R&htf#i*o9KuLaj+hPDBhKJ?pq8)asm|$`aiY z=FC`CDIy5Wr3g!Gr-)6whxrxm_?>JD&GXN{6~EpjyYk$YJ9_?^f3a>)K{_p-2az(Jo`8wx&)_4wkhNv)p}ZEd0}Mt>JqdIkk6?21FrGzKr}cJ3i~$JC)1D{mJuOU+%H0%5QLn@p;X&>0leibmjcu|W ziJ($Ic>^gFFqu@d2;5z3Gc^6gNi*VE&RWCp1TL%WD1)k)q_)p6#X4_uRJ@{kSQEX^ z^lkZIaUwONZiokyjDDw#`5UKzTSd&oVO`?+8Y-@F29_Li&A;Nocc65c3 zu;&2)aoF$yrz=v}3*QgtGf{I;-ZA^yCw7z%K!6Zx6KmOt7ZvQk>HS_NSANRPegmx! zz-C-!cRs{P8aA8Iuah+**P3W$c?A~ySt=|ez8#aLC*F4Ct~xyb{%NI{lJ^o28Q!~&(F3kQF6Yh+{GDp#)I4< zIEy2R7*(NrxJBy_zKTDz>qKsrMfR5c**=f|LTbRic%B{9@{@d1HsMeM*gid(=Vo~X znz#+MepK8wmsq*0|C9Y$f(wgkn%Va5=u7)QRFL$=bkp$Xs?1Z>Yg?Mtx_gi8p0(4J z$6wFd*R~&jG{fHMe~QlNVTs9oI(d_u6T23ClbcBYgy~0f(Ic$vUCSRrj(F96W&iE` zM|0x_d*Jv3;9o}QpXdvpwc6N-sBVfkwmZ4*e){$&Cpj)PWJ%rZXw-vh3 zrd#Am!LD5yK4xKe$tW{W?r0R8pM$8Eg7WBnVR*XSR9tOE+62>g_!&x~#a@yJXOF%~ zaZ@5jOwC}fKRdm;8GQD7z%4_LIG`y*S`IfsZiJ!5?=ck>Jl7T(HeRdJk|t1TP_NVB z+8bAC?iEcuMuwe1hcc+!eM-|@%xH%X7K%+;-h8~T!d~`08$5i%_5{nFX4`3b9DYjX zW+Rkx_*So?ds#iwSk&4q!{0vRiRk=qoxU-O5ugtihU8T#sRp5s?^`FZi(&UnqZbW) zs#Ww#A+DaXVkk)L_>Ip3R*2wefl>-?uXKbsQz=QcKF5s-nS4v3b_{k*>jS_xe&KoR zgj=bP>{BHtS*@{D`-EKoAyG`BqE}zedjqDk-IH-M4?hTOvDWM4+cXhuRb&q4^$!ps z=+o&5Im1w)3^G|F?hSr5G4uMiCTBk2rrITT@0@dO!qDrGMOR~92DdN@Zt!`rcuBFh zvsS~tpVgdtlfGr>gb%o|y5!(x|2Aho&{_#f8hY>{VTO+$MARsU72A4s#&Qw!!Ld@* z5G#>%)1vC!I%%Y9D1b#BhQ8_MIX|KP0arM5m6QjvCWeT93O+U|H*w6EtBI>jhgs{V zwQw2DraKA~gT$Rga#`2>b2q(M8Kn3z@=d?Z(Yi9Ej}}=#h|(wYCrC8>NK{-j0V`+h zJZZ9Z4E*!Litr6-n z6$?&c`|EbIj0o7IPTTf3SI<=1)R_N77xTNtSh zG`Do;mh(C?j9cBk{pU!z8jLOXzBhxkT3WK>`_E z;_dm_tu}SPMXo!Fk4}4y2vW&WmEl)n5!n1S{1ijk`h3Rs-P7;SX1ub*y~GC2G}HYr z{E0qf2_?f;k~WyqnefGiIxjSSy?fb}%X2QN|RdF@I|hyLeuQ(SC*mqi_*cCTBG*{A8@ijOFLGPesa?x!t56s!u$Sk zSr8N7O0^)^oHw8=pK{CijaZbOP@I9|C=5aI7)tfRpmH68g*Qc`vAHLID8#lj|GBby zFiuQc#9FI-l2RWR=d3MwR_-RomZ`@n<&5u{a-p)d-dqq?;n{T0lNK7L>=EhBll#y2 z(tikb(7pP*y>aOGnIzfR(C@@wg-q!qvPI{vKQ@(fUX3YcG59B64Jtg!8BJs}@2|q( zzba&=a7oil(}R%v!r^!5zIhV(2P)$rFOjc;?Q8Mv?ai4~Hwu?OKZbn_gkc*50;EYbYdcB4I#wPRzGkboGsiOe z%isL3LZQLSYsd|4M*Jp)(Ghq`;n)!m=w}qB9;tFx7@sEmt7$Uk{eZ;<1UNV{o-^Z5;Lom0N z^u$5!`oxi?^bDOULQK7ej$ArkdC0JN^V;6LV44b0QYQJ}0bsB;axp@z^p$pkh!SPu z0q|vCzAkI+=p-{z9jtq1=8v|(Tv)bR(5VEEJWp>KJnx~~61UN61g;9ecl3@BuiQLN z+|-jgCYS3!u`k23d0g=LmujM|94Sw@inY4tebcPo7QI6zLW7_UrW~$qTUkWkwJz0# z9J9CPnzxesRu^e6@!D5EA=4fwNl0cnhQ~Inp*NDLmQ`S5MA0*_3z?CEhC#eC$1p2H zm&oyjVP}5#oeiC+Mg9C*=cq++b*yR;P|AtngVW)Pqd`ZLjU`h~LNT7o9Wonfhn=Y) zSNU8d5Y3cBrC>%jiB3fh-{%$M5re_!X{vHvG9xD=*D%uI(HVihS(91PFQS9$LFOu1 zX1XRj_YL_(ylf~TX>e(|;VH~u5h}qa<)RjnCYK)1X<*;xW$94jN?)`_nl?o0t2Hw? zQ#+thZ7woG89eF<(Ri5{D!~iPv4=)KopJSFd`YaT@-ClhWYsH3=;=G}Y~~Q8PRF_P ziHV6>1u+$s{5j-2C8Uw`EG}k}%JxH#jqC(`Xa1g@sem1dt(@ zXZ#=X-ZCn#E?XOg5Fkhrg1bxb;0_5IpnxESyF=mbBv^2Fm!L)AZV6Dh1*yUb?hu@y zx%KurZ+G9@efstt_l)m5V|;tmk2R|Ho_powr`tsG%MpHng}-w3?r3iu zRkMbWlq@bi=rX=*fu*hnSY%}v!rTDU!5zwouvtzw$>5AIPc-Lzqm88v;&iRM;#Tkw zhrGt(zN*jA+*(Ao)xK`|pdaOwh}u848Z5LP9xh+3nLUp7o#%<9I=Lzy9-X*kc+S<) ziikVr=?CvDA0k29WX`I3tSajQLyb3=IVMHEOb!&vb9z|@d%cMs&x*xMTb4+xth6>WE--{$Bk?=&PC=cle<85PpKwDZ*zZZMk3YF1YMe zz%{*fKy`Hf8(-RMZ3o%Rf~NLPGQ}3RS=^Zuj`I&#Zl{i~lZfx_H53_t3Gx-~Dc2+f zYekR%b-4n)?Ka9ANcxBT&EaU+p`@1!YCN=zQ}RwfJEVI6o6ZBN?g@xT-Ktei|xs!0QdS-Yxo z%u0=;&@Q43IT}OxelR6iRy_v-VP6 zvbW=Y$m9c&#B9w|MQx^+~mb--hsg4K)tOoM@8roUYuYO9vYEiJ=^@~UHhOZsfF7MpE#u8&zq zw-WTjv{1ACGJ4TApJbu31vi700A82$>^uz3(yZ<;w~Gii%s4sG-q?tsS@RhQH(qm* zYEZ#vSy zneu-=AfT*E&x4{{0`Axx=lYkQM1lehU^0Lr}W=RBVxxEs1w#Kto|{5QOn|N1@y z_O#jdCm75TVC#;(h@34{N~qi_OaWVWd|W5(PZSZ1`yaPf&iSG!Y28%U=`OV!EkVq>DX11 zQ)7R=cfW9?%9f+vw?Zbk>ZHYbxm{0laWhJ-EtBGa$G}CV;_&h2HxhaEb5O5cnv8pj z8YI?{0aMmx%VM(K?nXV9s8jhe7cgZpOmN+~*^j_1>}5Z1FS+F@Xe*3qn*6rKmF|;4 zsVo$lIP~5Ap@PwV320)PrCcvL&2ofVFZt{$Xzg{VJY{ug&xdc*EP|`X)Q9s;e*JiW zJo_*3v{&AAksjeO?Q|zUxjCOKBJ82e#l4kw2SvvH2Sb*A2`;rO?k`_tE-FUZwJL;) zH2V915Wn}zy-zK;`yCexxNa%z+^!AZ)PBz`(KW56=HThzZNIb~t7t5aU~>zfbI}~( zsG(;ZP@xM^Q~SW|KRs9VWkBD1>)bh|#Ot;Zj*d5mRo3kN*-lU+rP6M?rodc}yw zKski7*ucESzL$r~U>mVp#L^QDjk!f(o2SBCmahQWEtar`?%gd)T zq)qD3U4mcS%{auD>P}TIR>f0nWO&PO%B!rie^K3eyZik8DJ84RY6`4V=%tTrhHXo+im5i#Q-y}W=h=E75)S+{;JHP8I7a>pEOoIR+q6GcMo-WO|W4)tTMD1 z7;dWCKa=&$E9(s)Pqg!sVs_l*br+M>s>)lgA99nwavVQBNB##d+|xxud3stJR4BLJ z5Wv0x>ibGKrh7=iQcZ?Djbu0o$39|>J_?%6K+V3!AbW_rMNT+Pb~37xXzI?8c{*D@ zy2LPg%AlC);7ae;YJ~)SX2)3p45_(Z)Vdm0sq;P=r1(!NOL?XfWcm4 zH2PvS)ds>Pk^V)*Hv3>^of}o;Bq%B*94oX#pOL2r#6g@(h%5``S>T!HDlu+I>f_0M zobY++D82*eJLM?)TE7y!IMF)&=Kp)U6weRglB+Zov6RoIdqt^}m>wP;*DIE3$rRx( z6t%;Hf>chM_=?zJQ=01MQ(4;g5xzH*6h)K<2(!t3-}9%Y7-`^L;ND~DD#E^)EssTX z9QtHi)?9%mYmnT&gHfN;zxl@{{U72h-+UsQrIbc< z4r5)*oKn*Qxx?#Y8ODNB&8KvHzEt{Tq_8yAt+L2>X<349B5106)VQa(*V|kRg&Uvu zyi|8rb`C!#nGHSQXB9KL(sQ!IVS!Uf zr0NS9eeTb3ks%WoV{#-k21DcoO}kf`5wWCw%}+gS@U&w_&&8 zo3;O~!-YWxq$A$-u4@osmyqsM2G~D>Hr;Z>?S%#GBVYPq-_>vbHQfnSa{3)R%u|$-V(Jz zGwgkJ=Kh%!*{HPicp;zlXVI7F{Y5<&j;=qMbZEI?E<}W-I`t+Rs@|hm{<2Xs<-d{o zC^bxKUaa%is&%g(uEm%-Rdy&fBTsb-<$)b%#H8Th{ta$Ch-x zNqybb48aAq0bbEnl7ug%!A@}s-D@mv4}>2|9BdZ@9_1@*B$Y&Th(-=yy$^>T2aF!e zh#;Irm7`fe;Htj0we|BS3-=1yv9c^W-(s2IDH-A1z<`_mrW4d;bL2wjS8`!0P8q(N zO(ax(QJ)5WK3Kh^!nJfh)jyT`ra+!WBG_XPQnsHzT%%A>)@s+<6zi*oaUTTLtL;TT z?k4hjvtLtpx=;AQ-(gJ`XFofXs8Vuyt{x`@?c89-0v+nYjU9nZ5;G!)T4nS3+x+Lx zk%G-@D}qfw0x0f05XlRT&s(DfbFE=swMdFeu^F_yA9Rm!Xbp6Sk2kk3)8O$ytEw?s z^<_{_fvs^!uu3f7r@o(S4Ni zLLHmVQyuDwPw6hn-APP0g*9pqMk$8LIfntB(jN?p+ZjY#)lHh%)OBc7xoXG?lIBRu zbj?t7&F(eVf|WT7TVsfhKW}nkTA%P_=$WZMZ_d7$V7uR{1Q>TD2^(8Fq76)+&(viN z%&f%$>pR89h?}I!OTsm)I#cKGTchl;UI_h2`NUpAXB|UQF=_^#ANez8)41vk^8X>} z@Mk74gk1U`!Qyy!noKzH*)N}sDxdOno#P!M_B>1s92NPY`zfbM3~pPWJF*lqGmcMQgCBaP z38rdeLanPXvrue8 zY^+O~T94SCUyd*FX1uBgF(nsc`*1m;_fBZ{Moula|)>rPTv)$l@6J!>A%6d>N;N zZUR|@z*VVJ?TQmwE6uR18S*$xkyaIK=vuwid$YGhc$@=htmYmM@CUL(G7H=K2R!u4 zC_BTl?BuJSNJYvDb*wC}8LtR~Fb~m~z3zk*b--`#_}R>lgL=kFb({sqK2{G!AM1Iy z%Z74ZpyPE-H#mxTNRKa0)RG;ESifY7ZjN+xt90_(Pw`F4SQ%6F4<8Qgd$+Nky^YT# zBfpIaaUSq`6}*b^ngSzY!Bw2OcC~EJ52z**{(iz2i_eVIy&|QuI4b9eK8vsJsbXY& zAtUX3=w=XgFdk<8%4;Q`9SS~Rv(QX0oxu=hBN!%rPC}M`Ad|Myu`uxpf0p`*sh6_y zvl419f!v%{1Z@({=uEOC$44`W3)&aMFYcf^VjyPU=fWO+>F4Wn3<;v0J+y95V@uVj zrZZ-A>s?Z`*10k29Vy$Ne>H5cXhblxmgCvl2Sr-=wQK}0$TizzPN4TQAvIl>cnzhI z44`RFcy_oS%$GZieBma^j>t${);B4kR!0F4s?z<6R|Txvr~47gy#a^!c$LEl&0hMp zN>DE(SAObDNi%`H`>DaWTcG)^WgSyv_fsM@o<>i#`jn3=+9)c~jDzeru!&pXjIG%* zDvQadTO7E<*Y^!_6(?dHNB`{b4RatO2tNj~xR=IGRU+{5CHLCDv90J|W|U94caPPb zh@rS-gDk%c&pun>zn3BR7);Nh$~;BVi^Vs_b@59WdiO;LOD)T=F$%a$qRrtCwxfNj zd{QCBp2RQxI+%p>gs82sZxLg{OjQ**uA;B0Qkve;6FgiO%iU;_N`qL}NQggaPhbGD zkB}ppcGZWMvrk2lQoB<6@#OJoA?5YBiLC-IQG=ds1zuUYd7h+dcxaYL{dGbiZxHZ3 zpS=T;DDBAViD^xV*W;dE6-k6opoZV8^nyugm8)o7MAhl~8m^#FOu7B294SR2?7e6; z?i<(kckgr;dm4e$tO{%?X}mv-#+3#2hKV_a&R&Vzjea97xa156>5KZH^3(Jhr)l%R z5*PBr(cgKKj7Njk%&-_k!L=DxEHg@K%qio|wnDS*FNMC-=jXz+dIB8J*j{^6l66kpu> zTlAdIsn-I~8iL2C62iw zp@MNTm%R@1rz>b&+ECNSxcw%7$J7Ttql{hjiuubfy$e`?RX!!y3V!1_4ZO+y=mW{q z+xn`cwe5raf6}SKJTH{=@V)ICg5Rb(zX0P=o6)Lvs7J)C`^PRL+ZI33XsBW&EDj1{ zw6sJvd&0#dGCGRwZS_a;^&c44&?^v(ScLzT{J}RGEKry*ibI9|3pRuqz|h zVw(ZV0gyg#waN`|JB8*h~qAfHUo=uTA=A5m<;ApsOx4y(G3WW zLO$#fdeKY_k%tGUc*j>Vb@sHB@BVk7fyq>5kwn5Tw&sAjMtIAPx@n&?qrOzfAuaG0nh^uZe(fzIXK?m(fcS`uftCS}hQ3llJIt|6 zkw7zL_@YzhAVhaitzIcD)w93Ym)ARC#GQ!M<-CPdBsw07Z%x1RGHcwj#Fg)0gJ2qW zaZSh9us*jf5>T)0fK;8P*0E4)6k)w)GF-y=UfJs@p?E0#rG%nr7NCL<)D}}-?32?M z1evKF0>6;~ZLE0^fsB19Da96nOL7Li z>i>9fV5bH5`i+zdf3W=Mvq*56E3wu16~PtT)aP#%cVq7eNANGy8tH62>gio(BLB}h zTgXqtV_b#m6-7OdN3#_I;gQb-g+c*{!x4|7mP=ed;>y$wUHC7{2y@*NhNSLpY<$=1 z2|MYlYqYEWNBQrBC&+jmNYH_Llvffr4QREXk}M(5ZD%3yFRM+F^sVn9Q?2R@_|nOr z6*y;WmpS^pL-Bxg{RPCYnUNB3F(+Cy!pY=YMHANEy0}L7#<}ToDUVdFJBSgYHT%@q zU>!w%U+1bu=Xxdb^4U91u1W2~RwM7Z8gS}Qv$_axj$-Ka*Qml#f6Jgc?xa#dceWKu?vEprqn6{+hbEV!O*KRFYAE zlKB~H#V^;`s!QJEra20Hv$Lq?IH=%R%AR_*JH?R_aebU=?K!6TEJH7~apBXJ@E|Nd zD*0H_6C+D2)?|A>g%e(&(*de&`WNk}XBa@+y`MxIA@5R^&F82^pvvboPjz+^2+X+} z&s%+RL=vMznxDmP(*awBKy3X2bjzJ43e{rJZU6}q`KatjuKrU(1HNg3HXAJ&LK2gC zR)Qma@t+DQ0scM&T$4+?W(V+cdHPIKwI@97&mt`?-7wzO4)^$)Ke6<+G&gHT;A=w3 z8}H_PRUc!JwyIZp7ve6g(b)s8C}#qT-U@-PYp#kONRda3IzKm3?S6lu`*0{$_8Up| zqs#11lOwrXUX;|YzAtShCiDM$q|j_?eAQaHPY*oy_&P`BC9(OP;`A@hG^3CI%$atu ze9)?YOpol@Hg{}p@*KHwNc>o~m9O^+SpP_{p&x)zJ9j4<*&dU*qWD3(NRH)pjRAlz!M30rx_yO5gCs;D2#KoDAN?}cg9+fRmn%tNWd z7|M>w+xXWXslf*--*u0Grxz%Hx`zH4$(p|P%iKEc-$+CpraJ`hyfcYZ;Uv+Cbt?OQ za$IwILW)M746T^SrX+~m>!?~s624zKaax|+HN(;^6^$Lhu+)m;1ORU25;a`?u%tO7TR8lB``3d{Q)wLaY=n)1^Bh50%2a>k6yHM+vOdqh|RCIt>h>0 z_LtpR-ykpPic{NM)Es|>v%G$P(df!9!du|2QeS0M~jgPeD|6F@*GjijY) zf|XT!8ef+pxBc+y9K{m16xG>*&bE6C&PfZ9<%w~x+T^$w1f^s|@Dbo~9rXPiW`BLi zf?BwFqyi!}K6b1#$9R+e#9iyl#c!lR7l{rxl;Da^rqV~LgollMath#{n8X=*+)oOm z)%|Muz28Xjn}J@t=;%Nn6_xwXbaw-91%<&=K7pG;R4c;B-7v$X>EB2+gO8`*U%aTe z$j!TXA$J}7(dmD%{P$%-ziRNjf4PEd&(VtYDry;eZ^TjD2RLZ4Mq0*CfAKyY{dauM zh+QdyPi2+~F@9d&w@loP`JkBnjpmTlK?_P5*L^8YR#ewIJaZc*Uhct3-t9Ir5DhOg z>z2{2o#!L|qHV%X{hDH%Z+Yt1(Q(s+dSG$M%C!6)B#f#`Um;yvAqiaGwXo-4Q$N$D zm!r~X7I&;3twQ{5sc}j~>YvPke|y{`IANZ<1fDwmMjDC!jnpxl&oA~TM<0Ht%nWgE za`%>Sl#sQJsWSnI%TZ7SZD_SM0fSj<*H~CQM1eR71VA7$3;E_2j@+BuBkes7_?wCg zIFEMI2!GLMd6A36__J5VG?R|=mKA@Uj`G9ZK9L)Kelk(!XX>E{;US!#Hqe7+`3s{{ ztT)SRmeN1w?IGqmRM+lYq2HuvN2W9p_J%b)&*KMs^SptdA&lKI1Y*FK_HvuujaaY+DOvv zUaipTm0Pvf%uT%HzoxI?FzsmN-?NoMzMqOdX1e3p9}(j%K~wuEr&g`JVvq`PwXv19 zsz%GaZ%YnXRyb?6jFYqIcyvhBO*uWjSTRrt^jL128X?uoJkHqj*`=8L2U@HrlybXw zKk({LnDYNcPS2B#x3&~k)7QdjR51k#Gn2`eXn1tC>p!=+t`elEoa~k(+51}27Ck=T zCZOZb&L_$;^rG-X!0@5f?5>k+PH@B{$qw?DJ=={8INqMmRXVm z@@`Dg^?xV_`oO0>-D4I;Jl z5gH+ElPGrC6!f)_BOP9rfu)yn2~igE#(0&xmioviy)K2)^K%C&kxo&GxNe8}N**CO*Q$-_KrJRM*W4=R|> zz_yp3DxdLyC&&JfT^CGu6$OB>T955*Muz}bqvIWv#2o0g1BBiahQEAg25cw-Cv^&c zBbDVyD5V^r>~B;|?^lu_;Nyzh41DnC+ussYUA@AJ3)dGzq717Ce)4MObct*-+*^V( z6zq%2yFuz;eNLkZfZ!MJ#pNn+r|05py(2!HC3Xd8ACJoo#Q;6C?s~J`fWq9aZ**dx z_w6$2GWT^v4GuMFgaDL=8E19Gb`YD=KHUc*Xax2ZzMWJ{GT>66UCcgg?Wl2Wo*o8{ zany=&lpAl-F1>J?ma_u2O|4k7`;!3eOL7Szsf3U;R^aL^%y^prH`3q!vbIv={~v#v zm{!IkW9yXz-5;iC$HXb$M7=W=&-a&qlmA~03Hq6WM46BF*;`rVuLM~}i(}uDC)+Ww zRqHs!VjRUp()#BhbN*XkN#{Q%Cf^VuI?#)N1M)nc8-A=Cu{hNxckk!|(Wwwb6_&e? zhDr8R%v2Q|Zh)5F)~;C6()n*&(h6kEa>Rx;?P#`<`7GSRoxni$TYBgH{XJ`&P6H5& z4N%d($4Gsf{O7Ouud(~1@4oMjLVi3y{u1UY7c)pFk;2T6z2%hNbe1XDaPpart=OUE zscdH$dep8~&wW5H{8l7qJ2r0&Oy95!myt-lmM^x7ZaAClX(kudI?Gx-nAMW92M0zC zFLr5V3~WR61q>(o)n>5+KJOVe>^U?Q2vMeusg2A&S*>|y9y-i%6j7a!ziY7MpOBsd z-}>a?&A0BzFVY!S8s}MRt5dGtUDa5ZIqwO!zVuieYdXugn$=Q_c2+4;!N23pXaJRY zyuX6LigacBeFK}lQc!b{g5JgZF^Dd3 z<%orYeKl?7Mxu|C(#Mx&*dKjL2jo9pPELG3t8c1lpHPZy*$ zl|_{nwfT9`M=2S9xv#SuHmT_SQc8~I+E&%A25yw&PLhyaMnovv9_cC8Mh$Xv9frzt zIekmPhNeE`y=%OQ$<6EQOuciwAMa$lR~{$8`)oTLRWa-l*`enr^s7BCX6eT!;eicg zb*j!#I=P*XS(hBv63VYl-;&0)_9Idgig_LJpjk(QYYb}-`trt=(n(rl?EkqAC#`pShaq=AU z6@N=qUCf~5r=oZ9)>QJeEzC$wP^z(p-0dit#++L*t zWq!0$X6(TMA?A$6UKO9l4qw{CWS&fa^<*lU^npYD_? zjRdJH6+E^JG>X)Oi|9H8+(J2is^+(Tg}*N-Eo&a_!vV*CCdsYC5GaM zY4MNHP4G5XvP+|2dy`~^{p|W;q}oZa{si`@NA%Bm9@3i>cGH4${psyIw-c+56ED-R zwwn(mCKZzZuIfivF@DSID$$p>MAfkAOCO(YIRVE~oJ>MT9Ya>+6&Iq>1H9wRWs8P` zYnPo&!XDf;ee48B%HxSO8^|gA#($-gw~4n)k3OeS$V7Kor*Z+PRNg=R>`cIj*`IwM zF}GqG1w1QZO<-j|BN^O<^ls_;CZ^ul!UNGz0MVxoLY^ zS;_p8{NaPgY-PN!ycE`Agn^7-%XkH_w8TrGlcs&WKa!-^^Osx@osj?5=Fmepsw zL@&_fD5hp@Oxj|rWKoG9zZ$75nGM_V1c_YHQTP+zjF=UQND496qN3M8@l_xQ0-G3+ z-On@9E(7*4qu)rG%_<^~SPPH}y(k9*no{mK;fWo$YrgsEf${>2$2+i&)+tPfZ6xsn zgeQc@v2^uOuG}!X)X2hxLdBBajzc(Ss8OK^%k&4u6h7KlfZ2PdAvR#@K%Khq?vL$j zVA0gC+r7gJN~%3hU!y*-%}k&iaPDY&z3HlMW@X_n$0F`F&or>Nq%dl~@-DZk`o*ov z7?2?E@2POXx{yJ`(6Sq9E{4A^h4C}xHHhe`f`?iGl!nbZC7|rWo_}~g*Oxsfy*n&HkELFA#RFP!$%L64K|ti( zPn;9?b~6g6(lg*~WaRaw|9lMeEB#OkQP!cIr?0=k%e_Fxh54qH+(LDGtS)}VsOjbh zCGkLa{{t#c&qnw({IQ@$@#afnry#p-l}ho!4d=*FbK_ctLKm{JTlEo zEKugWsJL3K%TH78nYgotI$i@@>?ikRP()hTg+P{Af3MTkVsdJsd`UJO<^{iK#!?%> zpDhZ@G#INokn=8VydZP4FUgG39Eq`O)vZSR>c(R~t9|&FXWI~`^9puJDREPg6{GSs zP}Qs#DrIEf@Ik?{E4Cfk1odwOO2ntfLpLNSr;E}DBUjZ7`$wz_N(&y;H+OCbtc&rY zK+6FN7Ztye%FBDesK>UVV=Ftlw_m5PUfnOMgG50^ayJn8#F_fJs8OR?Uj99QpQ$r# z`vqg4)phe2+Yp-aiX>c_(IsQ0#2323IwMrh%v7^3jb>u<1mzV4#mcpF7oz%;K-CoN zLk0M#DhG5mt;E^XxJ5k9K9bbjw6JUMCXqU&Ub4f}#Buz?z3Mj-PqR?Z8*S1^1JE}> z;qH<}Q~;KxLu9QtIvJZt&tdVbQ#C=n^APVXOV(Qzz@r?s4EKU=Flf z!(|-t3FA{VF);uqJy|*eL-s^cRRlVaW+dusuK#Zu%jdt5UbV?yYp-)@)TRZK#P_>% z5Q(VMju0Wc=|p>H=&%r2vThH7%ul8p8?^|hT*e|Y9u`kmIA~|c} zGi{lCJ*LhogtgIJv7(gNXOHnGW);G|kb_H^Z$V#zEdAyt*I1ScNvt_mK&|Hb8 z&t;mHs}Nc%QlpZpAtO_|@KgZrPVm@g`Wz5YW`e;k7h<%$S)VPqp{l#za+j*(cL>YL z`N05UY*;W{O8HQVGI{RL95Zp5qPeN(HeN#1ss72r!GJ$ZvgK5hP{V`+kV>!KCo(Eq zP-J!2HL*@vcw?cGZjHWt6^L_5G{SN}@K*e1{jbkuT{>FZ!pPTtJNCE1WmYyT0?o=E z>1O~(NsruRq(84TO5go2uk_D@ipiV}f93lAviQOh8 z<2#9PpQx>r0JD55&-r_)86!6EImO=#f0-xx{v@z+dZtnMx@RT7SzXXPabknYs|+k; zTGPY2Qse&4&2YMi!C+~V`@Nt7Q@Hzu$jgjHn|uF9p3Msgq`*d_ZTM#)Bx&yiWOX$j4e*QLqpTbSLe$92u(y*^AN@G+b00Y`EO;xm9ay`ZVX?ZWe_J zyGr z$0}1uFTdB`TMCVpU}?+MGjNf@nEe%5Y)V007|f27bo`ArTA+rCmgXS2u`W8VosWCB z@AL}WiiBoJSm7$<6q4jbKI&*ga}-`jFT~t^Vwb7Kt<@@ew+9bf{YU=zUK=Xz^S>wa!9#d6oqi zyypf=Vfvq$c}e2ysqe=rpO(ChHJyr<#Rwt2zPkI3^l52YU2E&=xV=wblt8J)R%Pch&OZXOXtsf-KHG5 z3&b*z@(a?4sP@)=>`o*mcVw7p`}!;TzR#SO_f)8=+wuBzlG+yrGe46GdGz%$+YMyH zO1V|-XwU$N$D^TLoV{uB($8q-A{9fkrlN(ufKz5<*MV69Ty8DdC;uLErY05#>DipL zsc6D0()^Kp(kFQWAA2t;#Md$l^k{TRUutWA;8r_XK^~+fp>HuRq5f>TU@33}(`>Y2 zwbc-`SJ>T%JUpQTI%hB;%CN0ns&`+no4e~?I8BrTO*1cRf2wInL}Fp?JmGo_pJDzh zWb~iM1)eqiRLg(OI?FOm#-E^2t5=h`VR}>)3M-3n4&jHTs$jxJGSbYY$Fuc0I3EM+ zUk!WHlJfjw)l^q*0^wP1MRXGE)6prxjRgB6T|OkUL*e6M{nU@dxvGG7_B@5x%vCH* zdtBm4A(t=FU!rqeM#O3N3*&4v{j?sRZeE7VKK4J>8(IEF`ux^T_aDFme?<}eN8?{$ z3I4vm7zZy}9z2I_rD|ZKb}gCQ)Yht&Br4vk(nFgY=&WhFw4P3anmvuBjFeFmfrYht zj!A3f(yP|&1+Oxz26(NmU4ux}vg^ha)x#|C4hBxux>}^dyswpiQAO^JSG*_&XZTUO z%%0OtU)FlRwDAWe{6`g3XhXC=>w5JQvs_!8|UMPlDt^;Vhs@OH#T#K2rbvEE>c@wT8X{qXat znxn(BJ5o;fx=Op`2jR!O7E#uW2gtx|0j_DlRrHaF|B|`mq2mGHmE$irw1I!{um3;h z#lP)tN6dqa-Hrf|MC3jek&CqlPwTlg%-y&kCE8+d*wl=cz2ZQ`%tf8Uh^BmldCiL0 zyR9{wrNcVcyiJcCoK;z|FwDr6o*dl*R81;;sqdtm>hJfMhd;xo9JMt_+-v8eF8F1xvv~f$*HDWg+Jg zyalfSnmaRok`j>t;tP(){fnKMnLsJ3>3qKC*^`AXO;yvMDq4e#N8(wcW-jjds_CNz z-T+IX3vwj`MNlo2bq z)HznMzMtfuSsU%f_V@j5e|-XPQUfitPB*&n9+{mu^tUsq zq6vwA*=)y>xlsO=yjf=)acn|2YCUxFPI|mA4%->DpK93&^q(jkKoHk#;axbg1jNqy z{NFlTfO&nfBL}95)jKdta2fu|7>eS=;&>8dWSs_xW!$hS7uACT%AS?hF##EWxfq>1 zKmr&(}1ngKtsjR zh$L><=5u_EpwWs9jFU8`CQX0Y>C2d#Hl4KgAAH2qsTuCsPl+XTB}sNr0ACTF$Y;Okl;J8j`-exz32N7J`bG= z`AAv0?WKFIU{ezd*{4}%#%&;yItn$XX`$abahAs6GAJh5T zo?hY6(+V&XCtm9@IClVCW51Rjzg*P+i3w0)=0E&@w@nlFUyYW*<;$!SpDhMZD10$c+dt+OWi`ZqHO4eRbSoVPPJz}+MufpS+yh`YXnu;|6 z^MN8t5BQwD*Qr^EiI%T0nr&|?j9V=xSlNLHPjYk}xAx(;axH1D(c$&b zX9Js5Dhad)DX+`+A)(wRy1l79IR0t!og^dUUS}Rk{A~i-;C(P~aJlMm3%UvXjl|-T zHZ^T=uT($HhfOLjmH+03qrT65@z}CgBm{zA+Y?;`j8-B<{oelJG@eJZr%v4kW3!u4 zoIb3E4?MSgqOHzYyyz7@z*D#PmCT{TIUuG~bJo)cbfA%7kONLR zuQECtH}53q9Qjo%#`z(vGq7aOye+u&Hge?Xx++ssp9_}Shsy0>*=DO#Z|==~%?AWh zw}fF1q^r})^wNQ>^e1~@ad60I@cY(WKvbBG{~B*sq!EhA3{CZ>k4vtK*mUPRi>vhs zK)-hFWpW=XPk?FqcmF6UW~Ro7@_yEE;}BQI_X@`5_Iluk0Nbrwh2{-DBPkt(tfSGa zpBTk=MkvNJ+%_atpL%!Nicny)4wkT#y)L5)$yD7weIK)IxZdXvM)zW0b;>)k$mhfv zkiMM&oNZJtjxSlSg$Jfc<7UMym;|wW_sbm{g>Hvk4?^6+W=tJ!Q8dObVz7@LOI(}m zH(7P}Kpqb2n9r3|8#Gl1g|82vdv`2(%VoRwefx|PU8C}mUu)5`ti(X~D9*66G`P<+ zv~y+<5yH0>Yr*TaUWVE7@86E;`E68~bkLYs*QNBcCD!ddf4wF~ zDu(ib6kWs1aXQB!yZe$*&4qQu?VwD6$TFs-QXW$$Q>X;SIYdGs3a&j+>T~Qi?Qj}( zd?ICy@DI)uMODueez@~3MTC~aVivBN;yyhj{_wV8zQ_0B&mQ`^Pt2QJx~p#mH0MgI zBjT1fAV$?2n6Y?QoKcpYD#2q|BA2In_0Ih){R^)`JI>Ijc-na87=>a36(+)htISsL zr3%4Fg(bwwo|9vu9LL4S~doZV1{ln{0qET{XaJRe^=dErdhUl6?IW^1xwKQ z9rPIe6&(~6?AMd?-T!fq zB;2iE2+eOEXla%CknIfjv`S$Ge zSiJy{Oa;dnm{;2J1jF{G8FVvKh$fo(wFPi6m8DY8kC(WcCYYV)8Sq3^kxUhD*R!?M zL5#n8_IR_XV|jMw6Nfc?pX`1Uz>~AC>c)+VJ><~7nPQ|ju`(3y*<|hGYkL?7pNmXQ zj(kBGC*h>!M{L+lV14GmW9XBD{Yb&Jn^@0Kd*s9877;Q6F|Jb1Qb{ev3V(P8u?wd+W& z0N~RQnE*Xj9aB3^)q47;pFKn7iI(d2&3a9#-?`ITYO$}-y^9#z`4t4yuOc}3@&$+Q zaAPXSMl`l{;HB>%W|B@Yhu*k(3^~$Uw)OOn&`z=(b)jDQ4GqEf9V~ib>io(LGoEBa zIp-m)!O)ivKX{9?TPVU5-$f_Sj6gBn*iZ|=@ipy^GMb4XsY99|&6I>Xmchukv_scA zZ{H5G&?JZ@kz&gmuQaQ@x%^C}v~&x>Zg(c1*UdJ0ihSa6;oDP?7P;mN=`Nd}x$X zj;{`uh-p@3%?TM7Uj<6JLV;AD0rMaxmD+~Y$Bs4nN{ia3GTq1#!j0o~Al>VY_;5{J zW3I(ndff?z3K1Oese@sg{R;+7I^>{;ZEkdHk|zvd?yX7P)}*Io3+kY25Kc{MwYh(-Iwv=o>-{b>)a}KjqDb02V(swa@TP$-M~TbI%XhdZb9G(Jl;)`Y|oD# zoC`}37A&N3MJ`aT>bulkCfYa0)YV?H_XU#f$ADrhO4wqwgrjG`HnGlm0zsNR;p@+c z!#{B`XqLmj&GN&wNKZ|xBoC^}INC_%>zBf@5(t&?cU7oJMhOqE1Wi#W&DBcRpD5Qb zu}3wOo0arJU>^jZs(twMu`+}$(Nf&6e*pV+`Z=1c6fP~Z`?Bs0i8`nj9&{N=XSwCk z)j8WxY*=GA899bZ9fnKLZHq?|6hRX45t@GQsg*07g42d5-U32Su1k}4InrrxsOuv; z;Jc5g?7=*#dZI{g>507fE&?-^6>{n(MWMV!XW$!7TJ0U07%knc%1+yh;9D`xAdUuavxGv>iq!gsw2n35DcTzx$G&@=-IH_uUsL4QdQWz~n*gd1H6ri6Ys znPA~>!%Mw1&ts_?FkJ35cMPkSkCoN>kb&(VM?oZSxms?W@SY}WLYlkAVsY(wm7pHZD(>${Y)VK z$%|*}B-l!yU&Iz*`sqZ$^t@9)Au>mCBQMrbA}elq^kbm|gh$LY>qnnH%^9m@%(NA7 zwt4+}-w9taxVbJqs0$;TAjMPe(d8}QFo`E5Oh*$ma}sCH_#T3l?$DUUV9s4@t@G~J zHAJ~ueyNl?oxK20CWU1gI|7OaP`0dFrxnT~3?fMpeC%Qg%hJ(RFoo&1$ck#v?-F9` z*R^ZzYnx^dPu2;Bd69MG;REc56JIG-D%>=V=VvxDZxY37&zVFl_)yz4eaDp4t$iI~ zN1hx39aO>I<)YRqn$4fgJt?SY=kdW`yv%Tz>4K$FC^}*rF@O{nxM&jy20wm#KS8(_ z(YCt5A(+XP%d(ReG7!Mh?t-x-(-78!@_%i=exB|k_U_uWwn literal 0 HcmV?d00001 diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index de917b76..88164bda 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -826,6 +826,7 @@ class ApiConsts { static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; + static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 5fccc6e7..b34b47ec 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -1,6 +1,7 @@ class AppAssets { static const String svgBasePath = 'assets/images/svg'; static const String pngBasePath = 'assets/images/png'; + static const String jpgBasePath = 'assets/images/jpg'; static const String hmg = '$svgBasePath/hmg.svg'; static const String arrow_back = '$svgBasePath/arrow-back.svg'; @@ -206,6 +207,9 @@ class AppAssets { static const String dummy_user = '$pngBasePath/dummy_user.png'; static const String comprehensiveCheckupEn = '$pngBasePath/cc_en.png'; static const String comprehensiveCheckupAr = '$pngBasePath/cc_er.png'; + + // JPGS // + static const String report = '$jpgBasePath/hmg_logo.jpg'; } class AppAnimations { diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index d4afbd98..c167aa05 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -55,6 +55,8 @@ import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; +import '../features/terms_conditions/terms_conditions_repo.dart'; +import '../features/terms_conditions/terms_conditions_view_model.dart'; GetIt getIt = GetIt.instance; @@ -121,6 +123,10 @@ class AppDependencies { getIt.registerLazySingleton(() => ContactUsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => HmgServicesRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), + ), + ); // ViewModels // Global/shared VMs → LazySingleton diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart index 42faafa9..dc859a96 100644 --- a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -1,165 +1,78 @@ -import 'dart:convert'; class ActivePrescriptionsResponseModel { - dynamic address; - int? appointmentNo; - dynamic clinic; - dynamic companyName; - int? days; - dynamic doctorName; - int? doseDailyQuantity; // doses per day + String? itemId; + String? itemDescription; + String? route; String? frequency; int? frequencyNumber; - dynamic image; - dynamic imageExtension; - dynamic imageSrcUrl; - String? imageString; - dynamic imageThumbUrl; - dynamic isCovered; - String? itemDescription; - int? itemId; + int? doseDailyQuantity; + int? days; + String? startDate; + String? endDate; String? orderDate; - int? patientId; - dynamic patientName; - dynamic phoneOffice1; - dynamic prescriptionQr; - dynamic prescriptionTimes; - dynamic productImage; - String? productImageBase64; String? productImageString; - int? projectId; - dynamic projectName; - dynamic remarks; - String? route; - String? sku; - int? scaleOffset; - String? startDate; - - // Added for reminder feature + bool isReminderOn; List selectedDoseTimes = []; - bool isReminderOn = false; // toggle status ActivePrescriptionsResponseModel({ - this.address, - this.appointmentNo, - this.clinic, - this.companyName, - this.days, - this.doctorName, - this.doseDailyQuantity, + this.itemId, + this.itemDescription, + this.route, this.frequency, this.frequencyNumber, - this.image, - this.imageExtension, - this.imageSrcUrl, - this.imageString, - this.imageThumbUrl, - this.isCovered, - this.itemDescription, - this.itemId, + this.doseDailyQuantity, + this.days, + this.startDate, + this.endDate, this.orderDate, - this.patientId, - this.patientName, - this.phoneOffice1, - this.prescriptionQr, - this.prescriptionTimes, - this.productImage, - this.productImageBase64, this.productImageString, - this.projectId, - this.projectName, - this.remarks, - this.route, - this.sku, - this.scaleOffset, - this.startDate, - - // ✅ Default values for new fields (won’t break API) - List? selectedDoseTimes, this.isReminderOn = false, - }) : selectedDoseTimes = selectedDoseTimes ?? []; - - factory ActivePrescriptionsResponseModel.fromRawJson(String str) => - ActivePrescriptionsResponseModel.fromJson(json.decode(str)); - - String toRawJson() => json.encode(toJson()); - - factory ActivePrescriptionsResponseModel.fromJson(Map json) => - ActivePrescriptionsResponseModel( - address: json["Address"], - appointmentNo: json["AppointmentNo"], - clinic: json["Clinic"], - companyName: json["CompanyName"], - days: json["Days"], - doctorName: json["DoctorName"], - doseDailyQuantity: json["DoseDailyQuantity"], - frequency: json["Frequency"], - frequencyNumber: json["FrequencyNumber"], - image: json["Image"], - imageExtension: json["ImageExtension"], - imageSrcUrl: json["ImageSRCUrl"], - imageString: json["ImageString"], - imageThumbUrl: json["ImageThumbUrl"], - isCovered: json["IsCovered"], - itemDescription: json["ItemDescription"], - itemId: json["ItemID"], - orderDate: json["OrderDate"], - patientId: json["PatientID"], - patientName: json["PatientName"], - phoneOffice1: json["PhoneOffice1"], - prescriptionQr: json["PrescriptionQR"], - prescriptionTimes: json["PrescriptionTimes"], - productImage: json["ProductImage"], - productImageBase64: json["ProductImageBase64"], - productImageString: json["ProductImageString"], - projectId: json["ProjectID"], - projectName: json["ProjectName"], - remarks: json["Remarks"], - route: json["Route"], - sku: json["SKU"], - scaleOffset: json["ScaleOffset"], - startDate: json["StartDate"], + List? selectedDoseTimes, + }) { + this.selectedDoseTimes = selectedDoseTimes ?? []; + } - // ✅ Ensure local reminder values are not overwritten by API - selectedDoseTimes: [], - isReminderOn: false, - ); + /// ========== JSON FROM ========== + factory ActivePrescriptionsResponseModel.fromJson(Map json) { + return ActivePrescriptionsResponseModel( + itemId: json["ItemID"]?.toString() ?? "", + itemDescription: json["ItemDescription"] ?? "", + route: json["Route"] ?? "", + frequency: json["Frequency"] ?? "", + frequencyNumber: json["FrequencyNumber"], + doseDailyQuantity: json["DoseDailyQuantity"] ?? 1, + days: json["Days"] ?? 0, + startDate: json["StartDate"] ?? "", + endDate: json["EndDate"] ?? "", + orderDate: json["OrderDate"] ?? "", + productImageString: json["ProductImageString"] ?? "", + isReminderOn: json["IsReminderOn"] == true, + selectedDoseTimes: + (json["SelectedDoseTimes"] as List?) + ?.map((e) => e?.toString()) + .toList() ?? + [], + ); + } - Map toJson() => { - "Address": address, - "AppointmentNo": appointmentNo, - "Clinic": clinic, - "CompanyName": companyName, - "Days": days, - "DoctorName": doctorName, - "DoseDailyQuantity": doseDailyQuantity, - "Frequency": frequency, - "FrequencyNumber": frequencyNumber, - "Image": image, - "ImageExtension": imageExtension, - "ImageSRCUrl": imageSrcUrl, - "ImageString": imageString, - "ImageThumbUrl": imageThumbUrl, - "IsCovered": isCovered, - "ItemDescription": itemDescription, - "ItemID": itemId, - "OrderDate": orderDate, - "PatientID": patientId, - "PatientName": patientName, - "PhoneOffice1": phoneOffice1, - "PrescriptionQR": prescriptionQr, - "PrescriptionTimes": prescriptionTimes, - "ProductImage": productImage, - "ProductImageBase64": productImageBase64, - "ProductImageString": productImageString, - "ProjectID": projectId, - "ProjectName": projectName, - "Remarks": remarks, - "Route": route, - "SKU": sku, - "ScaleOffset": scaleOffset, - "StartDate": startDate, - }; + /// ========== JSON TO ========== + Map toJson() { + return { + "ItemID": itemId, + "ItemDescription": itemDescription, + "Route": route, + "Frequency": frequency, + "FrequencyNumber": frequencyNumber, + "DoseDailyQuantity": doseDailyQuantity, + "Days": days, + "StartDate": startDate, + "EndDate": endDate, + "OrderDate": orderDate, + "ProductImageString": productImageString, + "IsReminderOn": isReminderOn, + "SelectedDoseTimes": selectedDoseTimes, + }; + } } diff --git a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart index d5180aec..6c249984 100644 --- a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart +++ b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart @@ -10,6 +10,7 @@ class HmgServicesComponentModel { Color bgColor; Color textColor; String route; + bool isExternalLink; HmgServicesComponentModel( this.action, @@ -21,5 +22,6 @@ class HmgServicesComponentModel { this.bgColor = Colors.white, this.textColor = Colors.black, this.route = '', + this.isExternalLink = false, }); } diff --git a/lib/features/terms_conditions/terms_conditions_repo.dart b/lib/features/terms_conditions/terms_conditions_repo.dart new file mode 100644 index 00000000..a5d3f95b --- /dev/null +++ b/lib/features/terms_conditions/terms_conditions_repo.dart @@ -0,0 +1,60 @@ +import 'package:dartz/dartz.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class TermsConditionsRepo { + Future> getTermsConditions(); +} + +class TermsConditionsRepoImp implements TermsConditionsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + TermsConditionsRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future> getTermsConditions() async { + Failure? failure; + String? html; + + try { + await apiClient.post( + ApiConsts.getTermsConditions, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType ?? ServerFailure(error.toString()); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + final content = response['UserAgreementContent']; + + if (content is String && content.isNotEmpty) { + html = content; + } else { + failure = DataParsingFailure( + 'UserAgreementContent is null or not String'); + } + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + } catch (e) { + failure = UnknownFailure(e.toString()); + } + + if (failure != null) return Left(failure!); + if (html == null || html!.isEmpty) { + return Left(ServerFailure('No terms and conditions returned')); + } + + return Right(html!); + } +} + diff --git a/lib/features/terms_conditions/terms_conditions_view_model.dart b/lib/features/terms_conditions/terms_conditions_view_model.dart new file mode 100644 index 00000000..5d67ae78 --- /dev/null +++ b/lib/features/terms_conditions/terms_conditions_view_model.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class TermsConditionsViewModel extends ChangeNotifier { + final TermsConditionsRepo termsConditionsRepo; + final ErrorHandlerService errorHandlerService; + + String? termsConditionsHtml; + bool isLoading = false; + + TermsConditionsViewModel({ + required this.termsConditionsRepo, + required this.errorHandlerService, + }); + + Future getTermsConditions({ + Function()? onSuccess, + Function(String)? onError, + }) async { + isLoading = true; + notifyListeners(); + + final result = await termsConditionsRepo.getTermsConditions(); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + isLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message ?? 'Something went wrong'); + } + }, + (html) { + termsConditionsHtml = html; + isLoading = false; + notifyListeners(); + if (onSuccess != null) onSuccess(); + }, + ); + } +} + + diff --git a/lib/main.dart b/lib/main.dart index f1274001..e9ceec76 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,6 +38,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'core/utils/size_utils.dart'; +import 'features/terms_conditions/terms_conditions_view_model.dart'; import 'firebase_options.dart'; @pragma('vm:entry-point') @@ -146,9 +147,12 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ) + ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index d27b35db..d0720fb2 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -547,7 +547,7 @@ class _ActiveMedicationPageState extends State { AppColors.textGreenColor, AppColors.infoColor, AppColors.labelColorYellow, - AppColors.purpleBg + AppColors.mainPurple ][doseIndex % 4]; final doseLabel = "${doseIndex + 1}${_getSuffix(doseIndex + 1)}"; diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index af576aa6..bc638767 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_s import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -41,8 +42,30 @@ class ServicesPage extends StatelessWidget { textColor: AppColors.blackColor, route: AppRoutes.homeHealthCarePage, ), + HmgServicesComponentModel( + 12, + "Latest News".needTranslation, + "".needTranslation, + AppAssets.news, + true, + bgColor: AppColors.bgGreenColor, + textColor: AppColors.blackColor, + route: "https://twitter.com/HMG", + isExternalLink: true, + ), + HmgServicesComponentModel( + 12, + "Monthly Reports".needTranslation, + "".needTranslation, + AppAssets.report_icon, + true, + bgColor: AppColors.bgGreenColor, + textColor: AppColors.blackColor, + route: AppRoutes.monthlyReports, + ), ]; + @override Widget build(BuildContext context) { return CollapsingListView( @@ -72,7 +95,7 @@ class ServicesPage extends StatelessWidget { return ServiceGridViewItem(hmgServices[index], index, false); }, ), - ) + ), ], ), ), diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 225bd963..3a0c2119 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -5,6 +5,7 @@ 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/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServiceGridViewItem extends StatelessWidget { final HmgServicesComponentModel hmgServiceComponentModel; @@ -12,12 +13,18 @@ class ServiceGridViewItem extends StatelessWidget { final bool isHomePage; final bool isLocked; - const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false}); + const ServiceGridViewItem( + this.hmgServiceComponentModel, this.index, this.isHomePage, + {super.key, this.isLocked = false}); @override Widget build(BuildContext context) { return InkWell( - onTap: () => getIt.get().pushPageRoute(hmgServiceComponentModel.route), + onTap: () => hmgServiceComponentModel.isExternalLink + ? _openLink(hmgServiceComponentModel.route) + : getIt + .get() + .pushPageRoute(hmgServiceComponentModel.route), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -47,4 +54,14 @@ class ServiceGridViewItem extends StatelessWidget { ], )); } + + Future _openLink(String link) async { + final Uri url = Uri.parse(link); + + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + throw "Could not launch $url"; + } + } } diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart new file mode 100644 index 00000000..97cc0e34 --- /dev/null +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -0,0 +1,283 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_reports/user_agreement_page.dart'; + +import '../../generated/locale_keys.g.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/input_widget.dart'; + +class MonthlyReportsPage extends StatefulWidget { + const MonthlyReportsPage({super.key}); + + @override + State createState() => _MonthlyReportsPageState(); +} + +class _MonthlyReportsPageState extends State { + bool isHealthSummaryEnabled = false; + bool isTermsAccepted = false; + + final TextEditingController emailController = TextEditingController(); + + @override + void dispose() { + emailController.dispose(); + super.dispose(); + } + + void _showError(String message) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + + void _onSavePressed() { + if (!isTermsAccepted) { + _showError("Please accept the terms and conditions".needTranslation); + return; + } + + final email = emailController.text.trim(); + if (email.isEmpty) { + _showError("Please enter your email".needTranslation); + return; + } + + setState(() { + isHealthSummaryEnabled = true; + }); + + // TODO: هنا حطي API/logic حق الحفظ + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Monthly Reports".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + height: 54.h, + alignment: Alignment.center, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + borderRadius: (12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Patient Health Summary Report".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + _buildToggle(), + ], + ), + ), + + SizedBox(height: 16.h), + + TextInputWidget( + controller: emailController, + labelText: "Eamil*".needTranslation, + hintText: "email@email.com", + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + keyboardType: TextInputType.emailAddress, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + onChange: (value) { + setState(() {}); + }, + ).paddingOnly(top: 8.h, bottom: 8.h), + + Row( + children: [ + Text( + "To View The Terms and Conditions".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + InkWell( + child: Text( + "Click here".needTranslation, + style: TextStyle( + color: AppColors.errorColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const UserAgreementPage(), + ), + ); + }, + ), + ], + ), + + SizedBox(height: 12.h), + + GestureDetector( + onTap: () => setState(() => isTermsAccepted = !isTermsAccepted), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 24.h, + width: 24.h, + decoration: BoxDecoration( + color: isTermsAccepted + ? AppColors.textGreenColor + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isTermsAccepted + ? AppColors.lightGreenColor + : AppColors.greyColor, + width: 2.h, + ), + ), + child: isTermsAccepted + ? Icon(Icons.check, size: 16.f, color: Colors.white) + : null, + ), + SizedBox(width: 12.h), + Text( + "I agree to the terms and conditions".needTranslation, + style: context.dynamicTextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: const Color(0xFF2E3039), + ), + ), + ], + ), + ), + + SizedBox(height: 12.h), + + Text( + "This is monthly health summary report".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 10.f, + fontWeight: FontWeight.w600, + ), + ), + + SizedBox(height: 12.h), + + Image.asset('assets/images/jpg/report.jpg'), + + SizedBox(height: 16.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _onSavePressed, + child: Text( + LocaleKeys.save.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.f, + ), + ), + ), + ), + ], + ), + ], + ), + ).paddingAll(16), + ); + } + + Widget _buildToggle() { + final value = isHealthSummaryEnabled; + + return AbsorbPointer( + absorbing: true, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: value + ? AppColors.lightGreenColor + : AppColors.greyColor.withOpacity(0.3), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: value + ? AppColors.textGreenColor + : AppColors.greyTextColor, + ), + ), + ), + ), + ), + ); + } +} + + diff --git a/lib/presentation/monthly_reports/user_agreement_page.dart b/lib/presentation/monthly_reports/user_agreement_page.dart new file mode 100644 index 00000000..f6379ad1 --- /dev/null +++ b/lib/presentation/monthly_reports/user_agreement_page.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_view_model.dart'; +import 'package:provider/provider.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; + +class UserAgreementPage extends StatefulWidget { + const UserAgreementPage({super.key}); + + @override + State createState() => _UserAgreementPageState(); +} + +class _UserAgreementPageState extends State { + late final WebViewController _webViewController; + bool _isLoading = true; + String? _errorMessage; + + @override + void initState() { + super.initState(); + + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onPageStarted: (_) { + setState(() { + _isLoading = true; + }); + }, + onPageFinished: (_) { + setState(() { + _isLoading = false; + }); + }, + onWebResourceError: (error) { + }, + ), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final vm = + Provider.of(context, listen: false); + + vm.getTermsConditions( + onSuccess: () { + final htmlString = vm.termsConditionsHtml ?? ''; + + if (htmlString.isNotEmpty) { + setState(() { + _errorMessage = null; + _isLoading = true; + }); + _webViewController.loadHtmlString(htmlString); + } else { + setState(() { + _isLoading = false; + _errorMessage = 'لا توجد شروط متاحة حالياً'.needTranslation; + }); + } + }, + onError: (msg) { + setState(() { + _isLoading = false; + _errorMessage = msg; + }); + }, + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Stack( + children: [ + WebViewWidget(controller: _webViewController), + + if (_errorMessage != null) + Center( + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _errorMessage!, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index a0ee1e59..73307448 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -9,6 +9,8 @@ import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../presentation/monthly_reports/monthly_reports_page.dart'; + class AppRoutes { static const String initialRoute = '/initialRoute'; static const String loginScreen = '/loginScreen'; @@ -19,7 +21,7 @@ class AppRoutes { static const String eReferralPage = '/erReferralPage'; static const String comprehensiveCheckupPage = '/comprehensiveCheckupPage'; static const String homeHealthCarePage = '/homeHealthCarePage'; - + static const String monthlyReports = '/monthlyReportsPage'; static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -29,6 +31,7 @@ class AppRoutes { medicalFilePage: (context) => MedicalFilePage(), eReferralPage: (context) => EReferralPage(), comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), - homeHealthCarePage: (context) => HhcProceduresPage() + homeHealthCarePage: (context) => HhcProceduresPage(), + monthlyReports: (context) => MonthlyReportsPage() }; } diff --git a/pubspec.yaml b/pubspec.yaml index 3d6604cb..0de3829d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,7 +79,7 @@ dependencies: path_provider: ^2.0.8 open_filex: ^4.7.0 flutter_swiper_view: ^1.1.8 - + webview_flutter: ^4.9.0 location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 From c9de23347a69da4ad2ae3bbcf559e94752cee5cc Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Mon, 15 Dec 2025 12:30:42 +0300 Subject: [PATCH 07/21] fix error --- lib/core/app_assets.dart | 15 ++++++++++++--- lib/core/dependencies.dart | 2 +- lib/core/utils/calender_utils_new.dart | 1 + .../ui_models/hmg_services_component_model.dart | 3 ++- lib/presentation/hmg_services/services_view.dart | 6 +++--- lib/routes/app_routes.dart | 4 ++-- lib/splashPage.dart | 1 + pubspec.yaml | 1 - 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 4e5c7b88..40add266 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -1,7 +1,6 @@ class AppAssets { static const String svgBasePath = 'assets/images/svg'; static const String pngBasePath = 'assets/images/png'; - static const String jpgBasePath = 'assets/images/jpg'; static const String hmg = '$svgBasePath/hmg.svg'; static const String arrow_back = '$svgBasePath/arrow-back.svg'; @@ -235,6 +234,17 @@ class AppAssets { static const String heart = '$svgBasePath/heart.svg'; static const String alertSquare = '$svgBasePath/alert-square.svg'; static const String arrowRight = '$svgBasePath/arrow-right.svg'; + static const String tickIcon = '$svgBasePath/tick.svg'; + + // Symptoms Checker + static const String calendarGrey = '$svgBasePath/calendar-grey.svg'; + static const String weightScale = '$svgBasePath/weight-scale.svg'; + static const String rulerIcon = '$svgBasePath/ruler.svg'; + static const String genderIcon = '$svgBasePath/gender.svg'; + static const String bodyIcon = '$svgBasePath/body_icon.svg'; + static const String rotateIcon = '$svgBasePath/rotate_icon.svg'; + static const String refreshIcon = '$svgBasePath/refresh.svg'; + static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; @@ -257,8 +267,6 @@ class AppAssets { static const String fullBodyFront = '$pngBasePath/full_body_front.png'; static const String fullBodyBack = '$pngBasePath/full_body_back.png'; - // JPGS // - static const String report = '$jpgBasePath/hmg_logo.jpg'; } class AppAnimations { @@ -280,3 +288,4 @@ class AppAnimations { static const String ambulanceAlert = '$lottieBasePath/ambulance_alert.json'; static const String rrtAmbulance = '$lottieBasePath/rrt_ambulance.json'; } + diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 674cd029..a6699c68 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -134,7 +134,7 @@ class AppDependencies { getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), - ), + ),); // ViewModels // Global/shared VMs → LazySingleton diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 5a43d788..5e9e91b2 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:device_calendar_plus/device_calendar_plus.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:jiffy/jiffy.dart' show Jiffy; +import 'package:manage_calendar_events/manage_calendar_events.dart' hide Calendar; class CalenderUtilsNew { final DeviceCalendar calender = DeviceCalendar.instance; diff --git a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart index 5e531c75..ebc95117 100644 --- a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart +++ b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart @@ -9,7 +9,8 @@ class HmgServicesComponentModel { bool isLogin; bool isLocked; Color textColor; - String route; + Color bgColor; + String? route; bool isExternalLink; Function? onTap; diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 237b4cd5..59efb73f 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -19,16 +19,16 @@ class ServiceGridViewItem extends StatelessWidget { const ServiceGridViewItem( this.hmgServiceComponentModel, this.index, this.isHomePage, - {super.key, this.isLocked = false}); + {super.key, this.isLocked = false, required this.isHealthToolIcon, this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: () => hmgServiceComponentModel.isExternalLink - ? _openLink(hmgServiceComponentModel.route) + ? _openLink(hmgServiceComponentModel.route!) : getIt .get() - .pushPageRoute(hmgServiceComponentModel.route), + .pushPageRoute(hmgServiceComponentModel.route!), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f32a0031..81c5ee4a 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -82,8 +82,8 @@ class AppRoutes { huaweiHealthExample: (context) => HuaweiHealthExample(), // - healthCalculatorsPage: (context) => HealthCalculatorsPage() - monthlyReports: (context) => MonthlyReportsPage() + healthCalculatorsPage: (context) => HealthCalculatorsPage(), + monthlyReports: (context) => MonthlyReportsPage() }; } diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 9aa16eea..2327ab5a 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -35,6 +35,7 @@ import 'core/utils/local_notifications.dart'; import 'core/utils/push_notification_handler.dart'; import 'widgets/routes/custom_page_route.dart'; + class SplashPage extends StatefulWidget { @override _SplashScreenState createState() => _SplashScreenState(); diff --git a/pubspec.yaml b/pubspec.yaml index 9f0be5fa..5590c9e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,6 @@ dependencies: google_maps_flutter: ^2.13.1 flutter_zoom_videosdk: 2.1.10 dart_jsonwebtoken: ^3.2.0 - webview_flutter: ^4.9.0 dartz: ^0.10.1 equatable: ^2.0.7 google_api_availability: ^5.0.1 From 68f044de523a526e5f641b9eaa411dc25ebb9705 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 24 Dec 2025 09:54:39 +0300 Subject: [PATCH 08/21] added monthly report --- lib/core/dependencies.dart | 7 +- .../monthly_reports/monthly_reports_repo.dart | 96 +++++++++++++++++++ .../monthly_reports_view_model.dart | 33 +++++++ .../terms_conditions_repo.dart | 0 .../terms_conditions_view_model.dart | 2 +- lib/main.dart | 2 +- .../monthly_reports/monthly_reports_page.dart | 51 +++++++--- .../monthly_reports/user_agreement_page.dart | 8 +- lib/routes/app_routes.dart | 14 ++- 9 files changed, 192 insertions(+), 21 deletions(-) create mode 100644 lib/features/monthly_reports/monthly_reports_repo.dart create mode 100644 lib/features/monthly_reports/monthly_reports_view_model.dart rename lib/features/{terms_conditions => monthly_reports}/terms_conditions_repo.dart (100%) rename lib/features/{terms_conditions => monthly_reports}/terms_conditions_view_model.dart (92%) diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index a6699c68..8cc29dfe 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/monthly_reports_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; @@ -48,6 +49,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_v import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -62,8 +64,8 @@ import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; -import '../features/terms_conditions/terms_conditions_repo.dart'; -import '../features/terms_conditions/terms_conditions_view_model.dart'; +import '../features/monthly_reports/terms_conditions_repo.dart'; +import '../features/monthly_reports/terms_conditions_view_model.dart'; GetIt getIt = GetIt.instance; @@ -135,6 +137,7 @@ class AppDependencies { getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), ),); + getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton diff --git a/lib/features/monthly_reports/monthly_reports_repo.dart b/lib/features/monthly_reports/monthly_reports_repo.dart new file mode 100644 index 00000000..4ace6ec1 --- /dev/null +++ b/lib/features/monthly_reports/monthly_reports_repo.dart @@ -0,0 +1,96 @@ +import 'package:dartz/dartz.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class MonthlyReportsRepo { + Future>> saveMonthlyReport({ + String? email, + }); +} + +class MonthlyReportsRepoImp implements MonthlyReportsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MonthlyReportsRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>> saveMonthlyReport({ + String? email, + }) async { + try { + Failure? failure; + + GenericApiModel? reportApiResponse; + + await apiClient.post( + ApiConsts.getMonthlyReports, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + reportApiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (reportApiResponse == null) return Left(ServerFailure("Unknown error")); + + if ((reportApiResponse!.messageStatus ?? 0) != 1) { + return Right(reportApiResponse!); + } + + GenericApiModel? emailApiResponse; + + final Map emailRequest = {}; + + if (email != null && email.trim().isNotEmpty) { + emailRequest["Email"] = email.trim(); + } + + await apiClient.post( + ApiConsts.updatePatientEmail, + body: emailRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + emailApiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (emailApiResponse == null) return Left(ServerFailure("Unknown error")); + + return Right(emailApiResponse!); + } catch (e) { + loggerService.logError("MonthlyReportsRepo.saveMonthlyReport error: $e"); + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/monthly_reports/monthly_reports_view_model.dart b/lib/features/monthly_reports/monthly_reports_view_model.dart new file mode 100644 index 00000000..4fd82da1 --- /dev/null +++ b/lib/features/monthly_reports/monthly_reports_view_model.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'monthly_reports_repo.dart'; +class MonthlyReportsViewModel extends ChangeNotifier { + final MonthlyReportsRepo monthlyReportsRepo; + final ErrorHandlerService errorHandlerService; + + bool isLoading = false; + + MonthlyReportsViewModel({ + required this.monthlyReportsRepo, + required this.errorHandlerService, + }); + + Future saveMonthlyReport({String? email}) async { + isLoading = true; + notifyListeners(); + + final result = await monthlyReportsRepo.saveMonthlyReport(email: email); + + final success = result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + return false; + }, + (apiResponse) => (apiResponse.messageStatus ?? 0) == 1, + ); + + isLoading = false; + notifyListeners(); + return success; + } +} diff --git a/lib/features/terms_conditions/terms_conditions_repo.dart b/lib/features/monthly_reports/terms_conditions_repo.dart similarity index 100% rename from lib/features/terms_conditions/terms_conditions_repo.dart rename to lib/features/monthly_reports/terms_conditions_repo.dart diff --git a/lib/features/terms_conditions/terms_conditions_view_model.dart b/lib/features/monthly_reports/terms_conditions_view_model.dart similarity index 92% rename from lib/features/terms_conditions/terms_conditions_view_model.dart rename to lib/features/monthly_reports/terms_conditions_view_model.dart index 5d67ae78..bd70b873 100644 --- a/lib/features/terms_conditions/terms_conditions_view_model.dart +++ b/lib/features/monthly_reports/terms_conditions_view_model.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_repo.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/terms_conditions_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class TermsConditionsViewModel extends ChangeNotifier { diff --git a/lib/main.dart b/lib/main.dart index 801a54c8..714053f6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,7 +43,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'core/utils/size_utils.dart'; -import 'features/terms_conditions/terms_conditions_view_model.dart'; +import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; @pragma('vm:entry-point') diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart index 97cc0e34..78c3f304 100644 --- a/lib/presentation/monthly_reports/monthly_reports_page.dart +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -3,12 +3,15 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/monthly_reports_view_model.dart'; import 'package:hmg_patient_app_new/presentation/monthly_reports/user_agreement_page.dart'; +import 'package:provider/provider.dart'; import '../../generated/locale_keys.g.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/input_widget.dart'; +import '../../widgets/loader/bottomsheet_loader.dart'; class MonthlyReportsPage extends StatefulWidget { const MonthlyReportsPage({super.key}); @@ -39,7 +42,25 @@ class _MonthlyReportsPageState extends State { ); } - void _onSavePressed() { + void _showSuccessSnackBar() { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Successfully updated".needTranslation, + style: const TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.w600, + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: AppColors.textGreenColor, + duration: const Duration(seconds: 2), + ), + ); + } + + Future _onSavePressed() async { if (!isTermsAccepted) { _showError("Please accept the terms and conditions".needTranslation); return; @@ -51,11 +72,18 @@ class _MonthlyReportsPageState extends State { return; } - setState(() { - isHealthSummaryEnabled = true; - }); + final vm = context.read(); - // TODO: هنا حطي API/logic حق الحفظ + // LoaderBottomSheet.showLoader(); + final ok = await vm.saveMonthlyReport(email: email); + // LoaderBottomSheet.hideLoader(); + + if (ok) { + setState(() => isHealthSummaryEnabled = true); + _showSuccessSnackBar(); + } else { + _showError("Failed to update".needTranslation); + } } @override @@ -87,7 +115,7 @@ class _MonthlyReportsPageState extends State { height: 54.h, alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: (12.r), ), child: Row( @@ -139,7 +167,7 @@ class _MonthlyReportsPageState extends State { child: Text( "Click here".needTranslation, style: TextStyle( - color: AppColors.errorColor, + color: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, ), @@ -179,7 +207,7 @@ class _MonthlyReportsPageState extends State { ), ), child: isTermsAccepted - ? Icon(Icons.check, size: 16.f, color: Colors.white) + ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor,) : null, ), SizedBox(width: 12.h), @@ -188,7 +216,7 @@ class _MonthlyReportsPageState extends State { style: context.dynamicTextStyle( fontSize: 12.f, fontWeight: FontWeight.w500, - color: const Color(0xFF2E3039), + color: AppColors.textColor, ), ), ], @@ -198,7 +226,8 @@ class _MonthlyReportsPageState extends State { SizedBox(height: 12.h), Text( - "This is monthly health summary report".needTranslation, + "This monthly Health Summary Report reflects the health indicators and analysis results of the latest visits. Please note that this will be sent automatically from the system and it's not considered as an official report so no medical decisions should be taken based on it" + .needTranslation, style: TextStyle( color: AppColors.textColor, fontSize: 10.f, @@ -279,5 +308,3 @@ class _MonthlyReportsPageState extends State { ); } } - - diff --git a/lib/presentation/monthly_reports/user_agreement_page.dart b/lib/presentation/monthly_reports/user_agreement_page.dart index f6379ad1..73ea5643 100644 --- a/lib/presentation/monthly_reports/user_agreement_page.dart +++ b/lib/presentation/monthly_reports/user_agreement_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/terms_conditions_view_model.dart'; import 'package:provider/provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -93,14 +93,14 @@ class _UserAgreementPageState extends State { margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(8), ), child: Text( _errorMessage!, textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.red, + style: TextStyle( + color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, ), ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 81c5ee4a..11c6c00f 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -23,7 +23,12 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_sele import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../core/dependencies.dart'; +import '../features/monthly_reports/monthly_reports_repo.dart'; +import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../presentation/monthly_reports/monthly_reports_page.dart'; +import '../services/error_handler_service.dart'; +import 'package:provider/provider.dart'; class AppRoutes { static const String initialRoute = '/initialRoute'; @@ -83,7 +88,14 @@ class AppRoutes { // healthCalculatorsPage: (context) => HealthCalculatorsPage(), - monthlyReports: (context) => MonthlyReportsPage() + // monthlyReports: (context) => MonthlyReportsPage() + monthlyReports: (context) => ChangeNotifierProvider( + create: (_) => MonthlyReportsViewModel( + monthlyReportsRepo: getIt(), + errorHandlerService: getIt(), + ), + child: const MonthlyReportsPage(), + ), }; } From 7a03242396d96e49544c26f765a86128076c8395 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 24 Dec 2025 11:26:19 +0300 Subject: [PATCH 09/21] added monthly report --- lib/core/api_consts.dart | 4 +++- lib/generated/locale_keys.g.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 778f7518..390e448d 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -703,7 +703,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -829,6 +829,8 @@ class ApiConsts { static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; + static final String getMonthlyReports = 'Services/Patients.svc/Rest/UpdatePateintHealthSummaryReport'; + static final String updatePatientEmail = 'Services/Patients.svc/Rest/UpdatePateintEmail'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index d76422bf..44d671f5 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -476,7 +476,7 @@ abstract class LocaleKeys { static const shareReview = 'shareReview'; static const review = 'review'; static const viewMedicalFile = 'viewMedicalFile'; - static const viewAllServices = 'viewAllServices'; + static String get viewAllServices => 'viewAllServices'; static const medicalFile = 'medicalFile'; static const verified = 'verified'; static const checkup = 'checkup'; From a53602dc8cc40d962f6e4b50ed4d5627fd844a6e Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 24 Dec 2025 11:26:32 +0300 Subject: [PATCH 10/21] added monthly report --- android/app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 6c4ac3df..328e8fc8 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -19,5 +19,5 @@ Geofence requests happened too frequently. - sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg + From 62bb667cb8edf7acec48982eaa021400bab47655 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 31 Dec 2025 11:38:51 +0300 Subject: [PATCH 11/21] added parking part --- lib/core/dependencies.dart | 2 +- .../hmg_services/services_page.dart | 121 ++++++++------- .../monthly_reports/monthly_reports_page.dart | 2 +- lib/presentation/parking/paking_page.dart | 118 +++++++++++++++ lib/presentation/parking/parking_slot.dart | 142 ++++++++++++++++++ lib/routes/app_routes.dart | 2 +- 6 files changed, 333 insertions(+), 54 deletions(-) create mode 100644 lib/presentation/parking/paking_page.dart create mode 100644 lib/presentation/parking/parking_slot.dart diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 2f45f375..555ce292 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -48,7 +48,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_r import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart'; +// import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/presentation/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index bfd9f30b..7ae79165 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -21,6 +21,7 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_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'; @@ -31,6 +32,7 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import '../../core/dependencies.dart' show getIt; @@ -108,27 +110,27 @@ class ServicesPage extends StatelessWidget { true, route: AppRoutes.homeHealthCarePage, ), - HmgServicesComponentModel( - 12, - "Latest News".needTranslation, - "".needTranslation, - AppAssets.news, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: "https://twitter.com/HMG", - isExternalLink: true, - ), - HmgServicesComponentModel( - 12, - "Monthly Reports".needTranslation, - "".needTranslation, - AppAssets.report_icon, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: AppRoutes.monthlyReports, - ), + // HmgServicesComponentModel( + // 12, + // "Latest News".needTranslation, + // "".needTranslation, + // AppAssets.news, + // true, + // bgColor: AppColors.bgGreenColor, + // textColor: AppColors.blackColor, + // route: "https://twitter.com/HMG", + // isExternalLink: true, + // ), + // HmgServicesComponentModel( + // 12, + // "Monthly Reports".needTranslation, + // "".needTranslation, + // AppAssets.report_icon, + // true, + // bgColor: AppColors.bgGreenColor, + // textColor: AppColors.blackColor, + // route: AppRoutes.monthlyReports, + // ), ]; late final List hmgHealthToolServices = [ @@ -169,17 +171,17 @@ class ServicesPage extends StatelessWidget { route: AppRoutes.smartWatches, // route: AppRoutes.huaweiHealthExample, ), - HmgServicesComponentModel( - 12, - "Latest News".needTranslation, - "".needTranslation, - AppAssets.news, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: "https://twitter.com/HMG", - isExternalLink: true, - ), + // HmgServicesComponentModel( + // 12, + // "Latest News".needTranslation, + // "".needTranslation, + // AppAssets.news, + // true, + // bgColor: AppColors.bgGreenColor, + // textColor: AppColors.blackColor, + // route: "https://twitter.com/HMG", + // isExternalLink: true, + // ), HmgServicesComponentModel( 12, "Monthly Reports".needTranslation, @@ -460,25 +462,42 @@ class ServicesPage extends StatelessWidget { ), SizedBox(width: 16.w), Expanded( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.car_parking_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) - ], + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ParkingPage(), + ), + ); + }, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ).onPress(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ParkingPage(), + ), + ); + }), ), ), ), diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart index 78c3f304..d1a4d0ce 100644 --- a/lib/presentation/monthly_reports/monthly_reports_page.dart +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -82,7 +82,7 @@ class _MonthlyReportsPageState extends State { setState(() => isHealthSummaryEnabled = true); _showSuccessSnackBar(); } else { - _showError("Failed to update".needTranslation); + // _showError("Failed to update".needTranslation); } } diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart new file mode 100644 index 00000000..ce9b6ab7 --- /dev/null +++ b/lib/presentation/parking/paking_page.dart @@ -0,0 +1,118 @@ + + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/parking/parking_slot.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/routes/custom_page_route.dart'; + +class ParkingPage extends StatefulWidget { + const ParkingPage({super.key}); + + @override + State createState() => _ParkingPageState(); +} + +class _ParkingPageState extends State { + String? scannedCode; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Parking".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600)), + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + // margin: EdgeInsets.all(10), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Text( + "Dr. Sulaiman Al Habib hospital are conduction a test for the emerging corona" + " virus and issuing travel certificates 24/7 in a short time and with high accuracy." + " Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches " + "to conduct a corona test within few minutes. Dr. Sulaiman Al Habib hospital are conduction" + " a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. " + "Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes.", + style: TextStyle( + color: AppColors.textColor, + fontSize: 12, height: 1.4, fontWeight: FontWeight.w500), + ), + ), + ).paddingOnly( top: 16, bottom: 16), + + ], + ), + ), + ), + + /// Bottom button + Container + ( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryRedColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: ParkingSlot(), + ) ); + }, + child: Text( + "Read Barcodes", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/parking/parking_slot.dart b/lib/presentation/parking/parking_slot.dart new file mode 100644 index 00000000..094dcb70 --- /dev/null +++ b/lib/presentation/parking/parking_slot.dart @@ -0,0 +1,142 @@ + + + + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/chip/app_custom_chip_widget.dart'; + +class ParkingSlot extends StatefulWidget { + const ParkingSlot({super.key}); + + @override + State createState() => _ParkingSlotState(); +} + +class _ParkingSlotState extends State { + String? scannedCode; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: LayoutBuilder( + builder: (context, constraints) { + final maxW = constraints.maxWidth; + final contentW = maxW > 600 ? 600.0 : maxW; // حد أقصى للتابلت + + return Align( + alignment: Alignment.topCenter, + child: SizedBox( + width: contentW, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Parking Slot Details", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 16.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget(labelText: "Slot: B-24".needTranslation), + AppCustomChipWidget(labelText: "Basement: Zone B".needTranslation), + AppCustomChipWidget(labelText: "Date: 16 Dec 2025".needTranslation), + AppCustomChipWidget(labelText: "Parked Since: 10:32 AM".needTranslation), + ], + ), + ], + ), + ), + ), + + SizedBox(height: 24.h), + + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryRedColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () {}, + child: Text( + "Get Direction", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + const Spacer(), + SizedBox( + width: double.infinity, + height: 48.h, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppColors.primaryRedColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () { + // Reset direction logic + }, + child: Text( + "Reset Direction", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryRedColor, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 970d2b97..db38e78f 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -89,7 +89,7 @@ class AppRoutes { huaweiHealthExample: (context) => HuaweiHealthExample(), // - healthCalculatorsPage: (context) => HealthCalculatorsPage(), + // healthCalculatorsPage: (context) => HealthCalculatorsPage(), // monthlyReports: (context) => MonthlyReportsPage() monthlyReports: (context) => ChangeNotifierProvider( create: (_) => MonthlyReportsViewModel( From 597579e6778d98de2b1a2edc11493e94868d3bc8 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Thu, 1 Jan 2026 15:17:27 +0300 Subject: [PATCH 12/21] refactoring --- .../water_monitor_view_model.dart | 9 +++--- .../widgets/water_action_buttons_widget.dart | 32 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/features/water_monitor/water_monitor_view_model.dart b/lib/features/water_monitor/water_monitor_view_model.dart index 01167fef..18ffdddc 100644 --- a/lib/features/water_monitor/water_monitor_view_model.dart +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -1065,9 +1065,8 @@ class WaterMonitorViewModel extends ChangeNotifier { notifyListeners(); return false; }, - (apiModel) { + (apiModel) async { log("Insert user activity success: ${apiModel.data.toString()}"); - // Update consumed amount and goal from the response if (apiModel.data != null && apiModel.data is List && (apiModel.data as List).isNotEmpty) { final progressData = (apiModel.data as List).first; if (progressData is Map) { @@ -1090,7 +1089,7 @@ class WaterMonitorViewModel extends ChangeNotifier { } // Refresh progress data to ensure consistency - fetchUserProgressForMonitoring(); + await fetchUserProgressForMonitoring(); } _isLoading = false; @@ -1140,7 +1139,7 @@ class WaterMonitorViewModel extends ChangeNotifier { notifyListeners(); return false; }, - (apiModel) { + (apiModel) async { log("Undo user activity success: ${apiModel.data.toString()}"); // Update consumed amount and goal from the response @@ -1164,8 +1163,8 @@ class WaterMonitorViewModel extends ChangeNotifier { } } } + await fetchUserProgressForMonitoring(); } - fetchUserProgressForMonitoring(); _isLoading = false; notifyListeners(); return true; diff --git a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart index 6546f776..97795629 100644 --- a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart +++ b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart @@ -18,23 +18,29 @@ class WaterActionButtonsWidget extends StatelessWidget { return Consumer(builder: (context, vm, _) { final cupAmount = vm.selectedCupCapacityMl; final isGoalAchieved = vm.progressPercent >= 100 || vm.nextDrinkTime.toLowerCase().contains('goal achieved'); + final isDisabled = vm.isLoading || isGoalAchieved; return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - InkWell( - onTap: () async { - if (cupAmount > 0) { - await vm.undoUserActivity(); - } - }, - child: Utils.buildSvgWithAssets( - icon: AppAssets.minimizeIcon, - height: 20.h, - width: 20.h, - iconColor: AppColors.textColor, + Opacity( + opacity: vm.isLoading ? 0.4 : 1.0, + child: InkWell( + onTap: vm.isLoading + ? null + : () async { + if (cupAmount > 0) { + await vm.undoUserActivity(); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.minimizeIcon, + height: 20.h, + width: 20.h, + iconColor: AppColors.textColor, + ), ), ), Container( @@ -51,9 +57,9 @@ class WaterActionButtonsWidget extends StatelessWidget { ), ), Opacity( - opacity: isGoalAchieved ? 0.4 : 1.0, + opacity: isDisabled ? 0.4 : 1.0, child: InkWell( - onTap: isGoalAchieved + onTap: isDisabled ? null : () async { if (cupAmount > 0) { From 49d091547a6d6e366b6a11406293610b8f01dfee Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 4 Jan 2026 22:30:43 +0300 Subject: [PATCH 13/21] Till Triage Completed --- contexts/AuthContext.tsx | 31 ++ lib/core/api_consts.dart | 6 +- lib/core/utils/utils.dart | 22 -- .../symptoms_user_details_response_model.dart | 188 +++++++++ .../resp_models/triage_response_model.dart | 209 ++++++++++ .../symptoms_checker_repo.dart | 213 +++++++++-- .../symptoms_checker_view_model.dart | 197 ++++++++++ .../organ_selector_screen.dart | 29 +- .../possible_conditions_screen.dart | 14 +- .../symptoms_checker/risk_factors_screen.dart | 12 +- .../symptoms_checker/suggestions_screen.dart | 12 +- .../symptoms_selector_screen.dart | 12 +- .../symptoms_checker/triage_screen.dart | 359 ++++++++++++++---- .../pages/age_selection_page.dart | 3 - .../water_monitor_settings_screen.dart | 37 +- lib/services/dialog_service.dart | 18 +- services/api.ts | 1 + types/user.ts | 57 +++ utils/apiHelpers.ts | 18 + 19 files changed, 1252 insertions(+), 186 deletions(-) create mode 100644 contexts/AuthContext.tsx create mode 100644 lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart create mode 100644 lib/features/symptoms_checker/models/resp_models/triage_response_model.dart create mode 100644 services/api.ts create mode 100644 types/user.ts create mode 100644 utils/apiHelpers.ts diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx new file mode 100644 index 00000000..48571a86 --- /dev/null +++ b/contexts/AuthContext.tsx @@ -0,0 +1,31 @@ +// ...existing imports... +import { apiService } from '../services/api'; + +// ...existing code... + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + // ...existing state... + + const login = async (nationalId: string, password: string) => { + try { + setIsLoading(true); + + // Call the real API + const user = await apiService.getUserDetails(nationalId, password); + + // Store user data + await AsyncStorage.setItem('user', JSON.stringify(user)); + await AsyncStorage.setItem('authToken', user.authToken); + + setUser(user); + } catch (error) { + console.error('Login error:', error); + throw new Error('Invalid credentials or network error'); + } finally { + setIsLoading(false); + } + }; + + // ...existing code... +}; + diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 0febdbee..65ac1ef9 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -825,11 +825,13 @@ class ApiConsts { static final String updateHHCOrder = 'api/hhc/update'; static final String addHHCOrder = 'api/HHC/add'; - // SYMPTOMS CHECKER + // SYMPTOMS CHECKER API + + static final String symptomsUserLogin = '$symptomsCheckerApi/user_login'; static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName'; static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors'; static final String getSuggestions = '$symptomsCheckerApi/GetSuggestion'; - static final String diagnosis = '$symptomsCheckerApi/diagnosis'; + static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis'; static final String explain = '$symptomsCheckerApi/explain'; //E-REFERRAL SERVICES diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 0302818b..a30763f8 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -218,16 +218,6 @@ class Utils { return await prefs.remove(key); } - static void showLoading({bool isNeedBinding = true}) { - if (isNeedBinding) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showLoadingDialog(); - }); - } else { - showLoadingDialog(); - } - } - static void showLoadingDialog() { _isLoadingVisible = true; showDialog( @@ -244,18 +234,6 @@ class Utils { ); } - static void hideLoading() { - try { - if (_isLoadingVisible) { - _isLoadingVisible = false; - Navigator.of(navigationService.navigatorKey.currentContext!).pop(); - } - _isLoadingVisible = false; - } catch (e) { - log("errr: ${e.toString()}"); - } - } - static List uniqueBy(List list, K Function(T) keySelector) { final seenKeys = {}; return list.where((item) => seenKeys.add(keySelector(item))).toList(); diff --git a/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart new file mode 100644 index 00000000..c0466c4e --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart @@ -0,0 +1,188 @@ +class SymptomsUserDetailsResponseModel { + final TokenDetails? tokenDetails; + final UserDetails? userDetails; + final String? sessionId; + + SymptomsUserDetailsResponseModel({ + this.tokenDetails, + this.userDetails, + this.sessionId, + }); + + factory SymptomsUserDetailsResponseModel.fromJson(Map json) { + return SymptomsUserDetailsResponseModel( + tokenDetails: json['tokenDetails'] != null ? TokenDetails.fromJson(json['tokenDetails']) : null, + userDetails: json['userDetails'] != null ? UserDetails.fromJson(json['userDetails']) : null, + sessionId: json['sessionId'], + ); + } + + Map toJson() { + return { + 'tokenDetails': tokenDetails?.toJson(), + 'userDetails': userDetails?.toJson(), + 'sessionId': sessionId, + }; + } +} + +class TokenDetails { + final String? id; + final String? authToken; + final int? expiresIn; + + TokenDetails({ + this.id, + this.authToken, + this.expiresIn, + }); + + factory TokenDetails.fromJson(Map json) { + return TokenDetails( + id: json['id'], + authToken: json['auth_token'], + expiresIn: json['expires_in'], + ); + } + + Map toJson() { + return { + 'id': id, + 'auth_token': authToken, + 'expires_in': expiresIn, + }; + } +} + +class UserDetails { + final String? fileNo; + final String? nationalId; + final String? email; + final String? dateOfBirth; + final String? dateOfBirthHijri; + final int? age; + final UserName? name; + final int? maritalStatus; + final String? maritalStatusCode; + final String? nationality; + final String? nationalityIsoCode; + final String? occupation; + final int? idType; + final int? gender; + final String? jwtToken; + final String? countryDialCode; + final String? phoneNo; + + UserDetails({ + this.fileNo, + this.nationalId, + this.email, + this.dateOfBirth, + this.dateOfBirthHijri, + this.age, + this.name, + this.maritalStatus, + this.maritalStatusCode, + this.nationality, + this.nationalityIsoCode, + this.occupation, + this.idType, + this.gender, + this.jwtToken, + this.countryDialCode, + this.phoneNo, + }); + + factory UserDetails.fromJson(Map json) { + return UserDetails( + fileNo: json['FileNo'], + nationalId: json['national_id'], + email: json['email'], + dateOfBirth: json['date_of_birth'], + dateOfBirthHijri: json['date_of_birth_hijri'], + age: json['age'], + name: json['name'] != null ? UserName.fromJson(json['name']) : null, + maritalStatus: json['marital_status'], + maritalStatusCode: json['marital_status_code'], + nationality: json['nationality'], + nationalityIsoCode: json['nationality_iso_code'], + occupation: json['occupation'], + idType: json['id_type'], + gender: json['gender'], + jwtToken: json['jwt_token'], + countryDialCode: json['country_dial_code'], + phoneNo: json['phone_no'], + ); + } + + Map toJson() { + return { + 'FileNo': fileNo, + 'national_id': nationalId, + 'email': email, + 'date_of_birth': dateOfBirth, + 'date_of_birth_hijri': dateOfBirthHijri, + 'age': age, + 'name': name?.toJson(), + 'marital_status': maritalStatus, + 'marital_status_code': maritalStatusCode, + 'nationality': nationality, + 'nationality_iso_code': nationalityIsoCode, + 'occupation': occupation, + 'id_type': idType, + 'gender': gender, + 'jwt_token': jwtToken, + 'country_dial_code': countryDialCode, + 'phone_no': phoneNo, + }; + } + + // Helper method to get full name + String getFullName(bool isArabic) { + if (name == null) return ''; + if (isArabic) { + return '${name!.firstNameAr ?? ''} ${name!.middleNameAr ?? ''} ${name!.lastNameAr ?? ''}'.trim(); + } + return '${name!.firstName ?? ''} ${name!.middleName ?? ''} ${name!.lastName ?? ''}'.trim(); + } +} + +class UserName { + final String? firstName; + final String? middleName; + final String? lastName; + final String? firstNameAr; + final String? middleNameAr; + final String? lastNameAr; + + UserName({ + this.firstName, + this.middleName, + this.lastName, + this.firstNameAr, + this.middleNameAr, + this.lastNameAr, + }); + + factory UserName.fromJson(Map json) { + return UserName( + firstName: json['first_name'], + middleName: json['middle_name'], + lastName: json['last_name'], + firstNameAr: json['first_name_ar'], + middleNameAr: json['middle_name_ar'], + lastNameAr: json['last_name_ar'], + ); + } + + Map toJson() { + return { + 'first_name': firstName, + 'middle_name': middleName, + 'last_name': lastName, + 'first_name_ar': firstNameAr, + 'middle_name_ar': middleNameAr, + 'last_name_ar': lastNameAr, + }; + } +} diff --git a/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart new file mode 100644 index 00000000..f0d7e904 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart @@ -0,0 +1,209 @@ +class TriageDataDetails { + final TriageQuestion? question; + final List? conditions; + final bool? hasEmergencyEvidence; + final bool? shouldStop; + final String? interviewToken; + final String? message; + final List? errorList; + final int? id; + final String? language; + final String? generalId; + final String? createDate; + final String? lastEditDate; + final String? createdBy; + final String? lastEditBy; + final bool? active; + final int? sortOrder; + final int? userType; + final String? userId; + + TriageDataDetails({ + this.question, + this.conditions, + this.hasEmergencyEvidence, + this.shouldStop, + this.interviewToken, + this.message, + this.errorList, + this.id, + this.language, + this.generalId, + this.createDate, + this.lastEditDate, + this.createdBy, + this.lastEditBy, + this.active, + this.sortOrder, + this.userType, + this.userId, + }); + + factory TriageDataDetails.fromJson(Map json) { + return TriageDataDetails( + question: json['question'] != null ? TriageQuestion.fromJson(json['question']) : null, + conditions: json['conditions'] != null ? (json['conditions'] as List).map((item) => TriageCondition.fromJson(item)).toList() : null, + hasEmergencyEvidence: json['has_emergency_evidence'], + shouldStop: json['should_stop'], + interviewToken: json['interview_token'], + message: json['Message'], + errorList: json['ErrorList'] != null ? List.from(json['ErrorList']) : null, + id: json['Id'], + language: json['language'], + generalId: json['generalId'], + createDate: json['CreateDate'], + lastEditDate: json['LastEditDate'], + createdBy: json['CreatedBy'], + lastEditBy: json['LastEditBy'], + active: json['Active'], + sortOrder: json['SortOrder'], + userType: json['userType'], + userId: json['userId'], + ); + } + + Map toJson() { + return { + 'question': question?.toJson(), + 'conditions': conditions?.map((item) => item.toJson()).toList(), + 'has_emergency_evidence': hasEmergencyEvidence, + 'should_stop': shouldStop, + 'interview_token': interviewToken, + 'Message': message, + 'ErrorList': errorList, + 'Id': id, + 'language': language, + 'generalId': generalId, + 'CreateDate': createDate, + 'LastEditDate': lastEditDate, + 'CreatedBy': createdBy, + 'LastEditBy': lastEditBy, + 'Active': active, + 'SortOrder': sortOrder, + 'userType': userType, + 'userId': userId, + }; + } +} + +class TriageQuestion { + final int? type; + final String? text; + final List? items; + + TriageQuestion({ + this.type, + this.text, + this.items, + }); + + factory TriageQuestion.fromJson(Map json) { + return TriageQuestion( + type: json['type'], + text: json['text'], + items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'type': type, + 'text': text, + 'items': items?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageQuestionItem { + final String? id; + final String? name; + final List? choices; + + TriageQuestionItem({ + this.id, + this.name, + this.choices, + }); + + factory TriageQuestionItem.fromJson(Map json) { + return TriageQuestionItem( + id: json['id'], + name: json['name'], + choices: json['choices'] != null ? (json['choices'] as List).map((item) => TriageChoice.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'choices': choices?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageChoice { + final String? id; + final String? label; + + TriageChoice({ + this.id, + this.label, + }); + + factory TriageChoice.fromJson(Map json) { + return TriageChoice( + id: json['id'], + label: json['label'], + ); + } + + Map toJson() { + return { + 'id': id, + 'label': label, + }; + } +} + +class TriageCondition { + final String? id; + final String? name; + final String? commonName; + final double? probability; + final dynamic conditionDetails; + + TriageCondition({ + this.id, + this.name, + this.commonName, + this.probability, + this.conditionDetails, + }); + + factory TriageCondition.fromJson(Map json) { + return TriageCondition( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + probability: json['probability']?.toDouble(), + conditionDetails: json['condition_details'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'common_name': commonName, + 'probability': probability, + 'condition_details': conditionDetails, + }; + } + + /// Get probability as percentage + String getProbabilityPercentage() { + if (probability == null) return '0%'; + return '${(probability! * 100).toStringAsFixed(1)}%'; + } +} diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 954d414a..2d9a99b7 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; @@ -8,11 +7,20 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class SymptomsCheckerRepo { + Future>> getUserDetails({ + required String userName, + required String password, + }); + Future>> getBodySymptomsByName({ required List organNames, + required String userSessionToken, + required int gender, }); Future>> getRiskFactors({ @@ -20,6 +28,9 @@ abstract class SymptomsCheckerRepo { required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required int gender, + required String sessionId, }); Future>> getSuggestions({ @@ -27,6 +38,19 @@ abstract class SymptomsCheckerRepo { required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required String sessionId, + required int gender, + }); + + Future>> getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, }); } @@ -37,11 +61,71 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService}); @override - Future>> getBodySymptomsByName({required List organNames}) async { - log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}"); - log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}"); + Future>> getUserDetails({ + required String userName, + required String password, + }) async { + Map body = {"userName": userName, "password": password}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.symptomsUserLogin, + body: body, + isExternal: true, + isAllowAny: true, + isBodyPlainText: false, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("getUserDetails API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = response is String ? jsonDecode(response) : response; - Map headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'}; + SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = SymptomsUserDetailsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: symptomsUserDetailsResponseModel, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing getUserDetails response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getUserDetails: $e"); + loggerService.logError("StackTrace: $stackTrace"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getBodySymptomsByName({ + required List organNames, + required String userSessionToken, + required int gender, + }) async { + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + Map body = { + 'bodyPartName': organNames, + 'gender': gender, + }; try { GenericApiModel? apiResponse; @@ -50,21 +134,15 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { await apiClient.post( ApiConsts.getBodySymptomsByName, apiHeaders: headers, - body: jsonEncode(organNames), + body: body, isExternal: true, isAllowAny: true, - isBodyPlainText: true, + isBodyPlainText: false, onFailure: (error, statusCode, {messageStatus, failureType}) { - loggerService.logError("GetBodySymptomsByName API Failed: $error"); - log("GetBodySymptomsByName Failed: $error, Status: $statusCode"); failure = failureType ?? ServerFailure(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - log("GetBodySymptomsByName Response Status: $statusCode"); - loggerService.logInfo("GetBodySymptomsByName API Success: $response"); - log("GetBodySymptomsByName Response: $response"); - BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response); apiResponse = GenericApiModel( @@ -76,7 +154,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Error parsing GetBodySymptomsByName response: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); failure = DataParsingFailure(e.toString()); } }, @@ -88,7 +165,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Exception in getBodySymptomsByName: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); return Left(UnknownFailure(e.toString())); } } @@ -99,6 +175,9 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required int gender, + required String sessionId, }) async { final Map body = { "age": { @@ -107,6 +186,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { "sex": sex, "evidence": evidenceIds.map((id) => {"id": id}).toList(), "language": language, + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', }; try { @@ -115,17 +200,16 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { await apiClient.post( ApiConsts.getRiskFactors, + apiHeaders: headers, body: body, isExternal: true, isAllowAny: true, onFailure: (error, statusCode, {messageStatus, failureType}) { - log("GetRiskFactors Failed: $error, Status: $statusCode"); + loggerService.logError("GetRiskFactors API Failed: $error"); failure = failureType ?? ServerFailure(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - log("GetRiskFactors Response: $response"); - // Parse response if it's a string final Map responseData = response is String ? jsonDecode(response) : response; @@ -140,7 +224,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Error parsing GetRiskFactors response: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); failure = DataParsingFailure(e.toString()); } }, @@ -152,7 +235,79 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Exception in getRiskFactors: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, + }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + "suggest_method": "diagnosis", + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.diagnosis, + apiHeaders: headers, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("getDiagnosisForTriage API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = response is String ? jsonDecode(response) : response; + + final updatedResponseData = responseData['dataDetails']; + + TriageDataDetails riskFactorsResponse = TriageDataDetails.fromJson(updatedResponseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing getDiagnosisForTriage response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getDiagnosisForTriage: $e"); + loggerService.logError("StackTrace: $stackTrace"); return Left(UnknownFailure(e.toString())); } } @@ -163,6 +318,9 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required String sessionId, + required int gender, }) async { final Map body = { "age": { @@ -171,6 +329,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { "sex": sex, "evidence": evidenceIds.map((id) => {"id": id}).toList(), "language": language, + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', }; try { @@ -179,17 +343,16 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { await apiClient.post( ApiConsts.getSuggestions, + apiHeaders: headers, body: body, isExternal: true, isAllowAny: true, onFailure: (error, statusCode, {messageStatus, failureType}) { - log("getSuggestions Failed: $error, Status: $statusCode"); + loggerService.logError("GetSuggestions API Failed: $error"); failure = failureType ?? ServerFailure(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - log("getSuggestions Response: $response"); - // Parse response if it's a string final Map responseData = response is String ? jsonDecode(response) : response; @@ -202,9 +365,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { data: riskFactorsResponse, ); } catch (e, stackTrace) { - loggerService.logError("Error parsing getSuggestions response: $e"); + loggerService.logError("Error parsing GetSuggestions response: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); failure = DataParsingFailure(e.toString()); } }, @@ -216,7 +378,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Exception in getSuggestions: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); return Left(UnknownFailure(e.toString())); } } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index da439c6c..b41bab29 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -7,6 +7,8 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -35,11 +37,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier { bool isBodySymptomsLoading = false; bool isRiskFactorsLoading = false; bool isSuggestionsLoading = false; + bool isTriageDiagnosisLoading = false; // API data storage - using API models directly + SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel; BodySymptomResponseModel? bodySymptomResponse; RiskAndSuggestionsResponseModel? riskFactorsResponse; RiskAndSuggestionsResponseModel? suggestionsResponse; + TriageDataDetails? triageDataDetails; + + // Triage state + int? _selectedTriageChoiceIndex; // Selected risk factors tracking final Set _selectedRiskFactorIds = {}; @@ -92,6 +100,23 @@ class SymptomsCheckerViewModel extends ChangeNotifier { String? get tooltipOrganId => _tooltipOrganId; + String get currentSessionAuthToken => symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? ""; + + String get currentSessionId => symptomsUserDetailsResponseModel?.sessionId ?? ""; + + // Triage-related getters + bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false; + + bool get hasEmergencyEvidence => triageDataDetails?.hasEmergencyEvidence ?? false; + + String? get currentInterviewToken => triageDataDetails?.interviewToken; + + TriageQuestion? get currentTriageQuestion => triageDataDetails?.question; + + List? get currentConditions => triageDataDetails?.conditions; + + int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex; + /// Get organs for current view List get currentOrgans => OrganData.getOrgansForView(_currentView); @@ -391,6 +416,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { age: _selectedAge!, sex: _selectedGender!.toLowerCase(), evidenceIds: evidenceIds, + sessionId: currentSessionId, language: appState.isArabic() ? 'ar' : 'en', onSuccess: (response) { if (onSuccess != null) { @@ -409,6 +435,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { Future getRiskFactors({ required int age, required String sex, + required String sessionId, required List evidenceIds, required String language, Function(RiskAndSuggestionsResponseModel)? onSuccess, @@ -422,6 +449,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { sex: sex, evidenceIds: evidenceIds, language: language, + sessionId: sessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( @@ -502,6 +532,25 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } + /// Get all evidence IDs (symptoms + risk factors + suggestions) for triage/diagnosis + List getAllEvidenceIds() { + List evidenceIds = []; + + // Add selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + evidenceIds.addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!)); + + // Add selected risk factors (excluding "not_applicable") + final selectedRiskFactors = getAllSelectedRiskFactors(); + evidenceIds.addAll(selectedRiskFactors.where((rf) => rf.id != null && rf.id != "not_applicable").map((rf) => rf.id!)); + + // Add selected suggestions (excluding "not_applicable") + final selectedSuggestions = getAllSelectedSuggestions(); + evidenceIds.addAll(selectedSuggestions.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!)); + + return evidenceIds; + } + /// Fetch risk factors based on selected symptoms Future fetchSuggestions({ Function()? onSuccess, @@ -571,6 +620,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { sex: sex, evidenceIds: evidenceIds, language: language, + sessionId: currentSessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( @@ -612,6 +664,109 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); } + /// Call Diagnosis API for Triage - This is called iteratively until shouldStop is true + Future getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + required String language, + Function(TriageDataDetails)? onSuccess, + Function(String)? onError, + }) async { + isTriageDiagnosisLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getDiagnosisForTriage( + age: age, + sex: sex, + evidenceIds: evidenceIds, + language: language, + sessionId: currentSessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, + ); + + result.fold( + (failure) async { + isTriageDiagnosisLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isTriageDiagnosisLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + triageDataDetails = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch diagnosis'); + } + } + }, + ); + } + + /// Convenience method to start or continue the triage process + /// This automatically uses all selected evidence (symptoms + risk factors + suggestions) + Future startOrContinueTriage({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Get all evidence IDs + final evidenceIds = getAllEvidenceIds(); + + if (evidenceIds.isEmpty) { + if (onError != null) { + onError('No evidence selected'); + } + return; + } + + await getDiagnosisForTriage( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Select a triage choice option + void selectTriageChoice(int choiceIndex) { + _selectedTriageChoiceIndex = choiceIndex; + notifyListeners(); + } + + /// Reset triage choice selection + void resetTriageChoice() { + _selectedTriageChoiceIndex = null; + notifyListeners(); + } + void reset() { _currentView = BodyView.front; _selectedOrganIds.clear(); @@ -621,6 +776,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { bodySymptomResponse = null; riskFactorsResponse = null; suggestionsResponse = null; + triageDataDetails = null; + isTriageDiagnosisLoading = false; + _selectedTriageChoiceIndex = null; _isBottomSheetExpanded = false; _tooltipTimer?.cancel(); _tooltipOrganId = null; @@ -725,6 +883,43 @@ class SymptomsCheckerViewModel extends ChangeNotifier { }; } + Future getSymptomsUserDetails({ + required String userName, + required String password, + Function()? onSuccess, + Function(String)? onError, + }) async { + isBodySymptomsLoading = true; + notifyListeners(); + final result = await symptomsCheckerRepo.getUserDetails(userName: userName, password: password); + + result.fold( + (failure) async { + isBodySymptomsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isBodySymptomsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + symptomsUserDetailsResponseModel = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms'); + } + } + }, + ); + } + Future getBodySymptomsByName({ required List organNames, Function(BodySymptomResponseModel)? onSuccess, @@ -735,6 +930,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { final result = await symptomsCheckerRepo.getBodySymptomsByName( organNames: organNames, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index c23b956f..4e0ec10e 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -10,9 +10,11 @@ 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/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.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:provider/provider.dart'; class OrganSelectorPage extends StatefulWidget { @@ -24,25 +26,38 @@ class OrganSelectorPage extends StatefulWidget { class _OrganSelectorPageState extends State { late final AppState _appState; + late final DialogService dialogService; @override void initState() { super.initState(); _appState = getIt.get(); + dialogService = getIt(); } - void _onNextPressed(SymptomsCheckerViewModel viewModel) { + void _onNextPressed(SymptomsCheckerViewModel viewModel) async { if (!viewModel.validateSelection()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one organ'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one organ'.needTranslation, ); return; } + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + + final String userName = 'guest_user'; + final String password = '123456'; - context.navigateWithName(AppRoutes.symptomsSelectorScreen); + await viewModel.getSymptomsUserDetails( + userName: userName, + password: password, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + context.navigateWithName(AppRoutes.symptomsSelectorScreen); + }, + onError: (String error) { + LoaderBottomSheet.hideLoader(); + }, + ); } @override diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 2c995156..188d07bf 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/models/conditions_ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.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'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -44,7 +45,7 @@ class PossibleConditionsScreen extends StatelessWidget { ); } - Widget _buildPredictionsList(List conditions) { + Widget _buildPredictionsList(BuildContext context, List conditions) { if (conditions.isEmpty) { return Center( child: Padding( @@ -60,6 +61,8 @@ class PossibleConditionsScreen extends StatelessWidget { ); } + final dialogService = getIt(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -82,11 +85,8 @@ class PossibleConditionsScreen extends StatelessWidget { description: conditionModel.description, possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum, onActionPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('We are not available for a week. May you Rest In Peace :('), - backgroundColor: AppColors.primaryRedColor, - ), + dialogService.showErrorBottomSheet( + message: 'We are not available for a week. May you Rest In Peace :(', ); }, ); @@ -168,7 +168,7 @@ class PossibleConditionsScreen extends StatelessWidget { if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) { return _buildLoadingShimmer(); } - return _buildPredictionsList(dummyConditions); + return _buildPredictionsList(context, dummyConditions); }, ), ), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index d4ff2cb5..aef7ce79 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -2,11 +2,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; @@ -20,9 +22,12 @@ class RiskFactorsScreen extends StatefulWidget { } class _RiskFactorsScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Fetch risk factors based on selected symptoms WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -38,11 +43,8 @@ class _RiskFactorsScreenState extends State { if (viewModel.hasSelectedRiskFactors) { context.navigateWithName(AppRoutes.suggestionsScreen); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one risk before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one risk before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index f2aa71ec..b5b43880 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; @@ -19,9 +21,12 @@ class SuggestionsScreen extends StatefulWidget { } class _SuggestionsScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -40,11 +45,8 @@ class _SuggestionsScreenState extends State { // Navigate to triage screen context.navigateWithName(AppRoutes.triageScreen); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one option before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 522c5f82..39509743 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -8,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; @@ -23,9 +25,12 @@ class SymptomsSelectorScreen extends StatefulWidget { } class _SymptomsSelectorScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -38,11 +43,8 @@ class _SymptomsSelectorScreenState extends State { // Navigate to triage screen context.navigateWithName(AppRoutes.riskFactorsScreen); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one symptom before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one symptom before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index ff0482e1..8262c677 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -1,18 +1,23 @@ +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart'; -import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; class TriageScreen extends StatefulWidget { const TriageScreen({super.key}); @@ -22,114 +27,293 @@ class TriageScreen extends StatefulWidget { } class _TriageScreenState extends State { - late List triageQuestions; - int currentQuestionIndex = 0; + List answeredEvidenceIds = []; // Track user's answers + late SymptomsCheckerViewModel viewModel; + late DialogService dialogService; @override void initState() { super.initState(); - triageQuestions = TriageQuestionsData.getSampleTriageQuestions(); - } - - TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex]; - - bool get isFirstQuestion => currentQuestionIndex == 0; + viewModel = context.read(); + dialogService = getIt.get(); - bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1; - - void _onOptionSelected(int optionIndex) { - setState(() { - currentQuestion.selectOption(optionIndex); + // Start triage process when screen loads + WidgetsBinding.instance.addPostFrameCallback((_) { + _startTriage(); }); } - void _onPreviousPressed() { - if (!isFirstQuestion) { - setState(() { - currentQuestionIndex--; - }); - } + void _startTriage() { + viewModel.startOrContinueTriage( + onSuccess: () { + _handleTriageResponse(); + }, + onError: (error) { + dialogService.showErrorBottomSheet( + message: error, + onOkPressed: () => context.pop(), + ); + }, + ); } - void _onNextPressed() { - if (currentQuestion.isAnswered) { - currentQuestion.confirmSelection(); - if (isLastQuestion) { - context.navigateWithName(AppRoutes.possibleConditionsScreen); - } else { - setState(() { - currentQuestionIndex++; - }); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select an option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), - ); + void _handleTriageResponse() { + if (viewModel.hasEmergencyEvidence) { + _showEmergencyDialog(); + return; + } + + if (viewModel.shouldStopTriage) { + // Navigate to results/possible conditions screen + context.navigateWithName(AppRoutes.possibleConditionsScreen); + return; } + + // Question is loaded, reset selection for new question + viewModel.resetTriageChoice(); } - _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { - return showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), + void _showEmergencyDialog() { + showCommonBottomSheetWithoutHeight( context, + title: "Emergency".needTranslation, child: Utils.getWarningWidget( - loadingText: "Are you sure you want to restart the organ selection?".needTranslation, + loadingText: "Emergency evidence detected. Please seek immediate medical attention.".needTranslation, isShowActionButtons: true, onCancelTap: () => Navigator.pop(context), - onConfirmTap: () => onConfirm(), + onConfirmTap: () { + Navigator.pop(context); + context.pop(); + }, ), - callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); } + bool get isFirstQuestion => answeredEvidenceIds.isEmpty; + + void _onOptionSelected(int choiceIndex) { + viewModel.selectTriageChoice(choiceIndex); + } + + void _onPreviousPressed() { + context.pop(); + } + + void _onNextPressed() { + // Check if user has selected an option + if (viewModel.selectedTriageChoiceIndex == null) { + dialogService.showErrorBottomSheet(message: 'Please select an option before proceeding'.needTranslation); + return; + } + + // Get the selected choice from the current question + final currentQuestion = viewModel.currentTriageQuestion; + if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) { + dialogService.showErrorBottomSheet( + message: 'No question items available'.needTranslation, + ); + return; + } + + final questionItem = currentQuestion.items!.first; + if (questionItem.choices == null || viewModel.selectedTriageChoiceIndex! >= questionItem.choices!.length) { + dialogService.showErrorBottomSheet( + message: 'Invalid choice selection'.needTranslation, + ); + return; + } + + final selectedChoice = questionItem.choices![viewModel.selectedTriageChoiceIndex!]; + + final evidenceId = selectedChoice.label ?? ""; + if (evidenceId.isNotEmpty) { + answeredEvidenceIds.add(evidenceId); + } + + // Get all previous evidence IDs + List allEvidenceIds = viewModel.getAllEvidenceIds(); + allEvidenceIds.addAll(answeredEvidenceIds); + + log("allEvidences: ${allEvidenceIds.toString()}"); + + // Call API with updated evidence + viewModel.getDiagnosisForTriage( + age: viewModel.selectedAge!, + sex: viewModel.selectedGender!.toLowerCase(), + evidenceIds: allEvidenceIds, + language: viewModel.appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + _handleTriageResponse(); + }, + onError: (error) { + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: "Triage".needTranslation, - // onLeadingTapped: () => _buildConfirmationBottomSheet( - // context: context, - // onConfirm: () => { - // context.pop(), - // context.pop(), - // }), - - leadingCallback: () => context.pop(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - _buildQuestionCard(), - ], + body: Consumer( + builder: (context, viewModel, child) { + // Show normal question UI + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Triage".needTranslation, + leadingCallback: () => _showConfirmationBeforeExit(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildQuestionCard(viewModel), + ], + ), + ), ), + _buildStickyBottomCard(context, viewModel), + ], + ); + }, + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(1, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Suggestion', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); + }), ), - _buildStickyBottomCard(context), ], ), ); } - Widget _buildQuestionCard() { + Widget _buildErrorState() { + return CollapsingListView( + title: "Triage".needTranslation, + leadingCallback: () => context.pop(), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64.f, color: AppColors.errorColor), + SizedBox(height: 16.h), + "No question available".needTranslation.toText16(weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: "Go Back".needTranslation, + onPressed: () => context.pop(), + backgroundColor: AppColors.primaryRedColor, + ).paddingSymmetrical(48.w, 0), + ], + ), + ), + ); + } + + void _showConfirmationBeforeExit(BuildContext context) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to exit? Your progress will be lost.".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () { + Navigator.pop(context); + context.pop(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + Widget _buildQuestionCard(SymptomsCheckerViewModel viewModel) { + if (viewModel.isTriageDiagnosisLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.currentTriageQuestion == null) { + return Center( + child: "No question available".needTranslation.toText16(weight: FontWeight.w500), + ); + } + + final question = viewModel.currentTriageQuestion; + if (question == null || question.items == null || question.items!.isEmpty) { + return SizedBox.shrink(); + } + + final questionItem = question.items!.first; + final choices = questionItem.choices ?? []; + return AnimatedSwitcher( duration: const Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { final offsetAnimation = Tween( begin: const Offset(1.0, 0.0), end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeInOut, - )); + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + ), + ); return SlideTransition( position: offsetAnimation, @@ -140,7 +324,7 @@ class _TriageScreenState extends State { ); }, child: Container( - key: ValueKey(currentQuestionIndex), + key: ValueKey(questionItem.id ?? answeredEvidenceIds.length.toString()), width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 24.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -148,11 +332,11 @@ class _TriageScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - currentQuestion.question.toText16(weight: FontWeight.w500), + (question.text ?? "").toText16(weight: FontWeight.w500), SizedBox(height: 24.h), - ...List.generate(currentQuestion.options.length, (index) { - bool selected = currentQuestion.selectedOptionIndex == index; - return _buildOptionItem(index, selected, currentQuestion.options[index].text); + ...List.generate(choices.length, (index) { + bool selected = viewModel.selectedTriageChoiceIndex == index; + return _buildOptionItem(index, selected, choices[index].label ?? ""); }), ], ), @@ -188,9 +372,21 @@ class _TriageScreenState extends State { ); } - Widget _buildStickyBottomCard(BuildContext context) { - final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions); - final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore); + Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { + // Get the top condition with highest probability + final conditions = viewModel.currentConditions ?? []; + String suggestedCondition = "Analyzing..."; + double probability = 0.0; + + if (conditions.isNotEmpty) { + // Sort by probability descending + final sortedConditions = List.from(conditions); + sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); + + final topCondition = sortedConditions.first; + suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown"; + probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage + } return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -223,14 +419,14 @@ class _TriageScreenState extends State { ), SizedBox(height: 16.h), CustomRoundedProgressBar( - percentage: currentScore, + percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( - text: "$currentScore% ", + text: "${probability.toStringAsFixed(1)}% ", style: TextStyle( color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, @@ -256,7 +452,7 @@ class _TriageScreenState extends State { child: CustomButton( text: "Previous".needTranslation, onPressed: isFirstQuestion ? () {} : _onPreviousPressed, - isDisabled: isFirstQuestion, + isDisabled: isFirstQuestion || viewModel.isTriageDiagnosisLoading, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, @@ -266,7 +462,8 @@ class _TriageScreenState extends State { SizedBox(width: 12.w), Expanded( child: CustomButton( - text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation, + text: "Next".needTranslation, + isDisabled: viewModel.isTriageDiagnosisLoading, onPressed: _onNextPressed, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart index d73f3877..8366545e 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -35,7 +33,6 @@ class AgeSelectionPage extends StatelessWidget { initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1), onDateChanged: (date) { symptomsViewModel.setDateOfBirth(date); - log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}'); }, ) ], diff --git a/lib/presentation/water_monitor/water_monitor_settings_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_screen.dart index 1b783ca7..3470344b 100644 --- a/lib/presentation/water_monitor/water_monitor_settings_screen.dart +++ b/lib/presentation/water_monitor/water_monitor_settings_screen.dart @@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; class WaterMonitorSettingsScreen extends StatefulWidget { @@ -20,6 +21,14 @@ class WaterMonitorSettingsScreen extends StatefulWidget { } class _WaterMonitorSettingsScreenState extends State { + late DialogService dialogService; + + @override + void initState() { + super.initState(); + dialogService = getIt.get(); + } + // No need to call initialize() here since it's already called in water_consumption_screen // The ViewModel is shared via Provider, so data is already loaded @@ -59,7 +68,6 @@ class _WaterMonitorSettingsScreenState extends State required Function(String) onSelected, bool useUpperCase = false, }) { - final dialogService = getIt.get(); dialogService.showFamilyBottomSheetWithoutHWithChild( label: title.needTranslation, @@ -263,9 +271,20 @@ class _WaterMonitorSettingsScreenState extends State onPressed: () async { final success = await viewModel.saveSettings(); if (!success && viewModel.validationError != null) { - _showSnackBar(context, viewModel.validationError!); + dialogService.showErrorBottomSheet( + message: viewModel.validationError!, + ); } else if (success) { - _showSnackBar(context, "Settings saved successfully"); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: "Settings saved successfully".needTranslation, + ), + callBackFunc: () {}, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); } }, borderRadius: 12.r, @@ -340,16 +359,4 @@ class _WaterMonitorSettingsScreenState extends State ), ); } - - // Show snackbar for validation errors and success messages - void _showSnackBar(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - duration: const Duration(seconds: 3), - behavior: SnackBarBehavior.floating, - backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor, - ), - ); - } } diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 497a0092..3c009f3b 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -62,18 +62,16 @@ class DialogServiceImp implements DialogService { message: message, showCancel: onCancelPressed != null ? true : false, onOkPressed: () { - print('ok button is pressed'); if (onOkPressed != null) { - print('onOkPressed is not null'); onOkPressed(); - }else { + } else { Navigator.pop(context); } }, onCancelPressed: () { if (onCancelPressed != null) { onCancelPressed(); - }else { + } else { Navigator.pop(context); } }, @@ -108,7 +106,8 @@ class DialogServiceImp implements DialogService { } @override - Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + Future showCommonBottomSheetWithoutH( + {String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight( @@ -162,7 +161,8 @@ class DialogServiceImp implements DialogService { } @override - Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { + Future showPhoneNumberPickerSheet( + {String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight(context, @@ -184,7 +184,8 @@ class DialogServiceImp implements DialogService { } } -Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { +Widget exceptionBottomSheetWidget( + {required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { return Column( children: [ (message).toText16(isBold: false, color: AppColors.textColor), @@ -239,7 +240,8 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin ); } -Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { +Widget showPhoneNumberPickerWidget( + {required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { return Column( children: [ diff --git a/services/api.ts b/services/api.ts new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/services/api.ts @@ -0,0 +1 @@ + diff --git a/types/user.ts b/types/user.ts new file mode 100644 index 00000000..a2158efe --- /dev/null +++ b/types/user.ts @@ -0,0 +1,57 @@ +export interface UserName { + first_name: string; + middle_name: string; + last_name: string; + first_name_ar: string; + middle_name_ar: string; + last_name_ar: string; +} + +export interface UserDetails { + FileNo: string; + national_id: string; + email: string; + date_of_birth: string; + date_of_birth_hijri: string | null; + age: number; + name: UserName; + marital_status: number; + marital_status_code: string | null; + nationality: string; + nationality_iso_code: string; + occupation: string | null; + id_type: number; + gender: number; + jwt_token: string | null; + country_dial_code: string; + phone_no: string; +} + +export interface TokenDetails { + id: string; + auth_token: string; + expires_in: number; +} + +export interface GetUserDetailsResponse { + tokenDetails: TokenDetails; + userDetails: UserDetails; + sessionId: string; +} + +// Simplified User type for app usage +export interface User { + id: string; + fileNo: string; + nationalId: string; + email: string; + name: string; + nameAr: string; + phoneNo: string; + dateOfBirth: string; + nationality: string; + gender: number; + authToken: string; + sessionId: string; +} + diff --git a/utils/apiHelpers.ts b/utils/apiHelpers.ts new file mode 100644 index 00000000..3fae819d --- /dev/null +++ b/utils/apiHelpers.ts @@ -0,0 +1,18 @@ +export function safeJsonParse(data: any): T { + if (typeof data === 'string') { + try { + return JSON.parse(data); + } catch (error) { + console.error('Failed to parse JSON string:', error); + throw new Error('Invalid JSON response from server'); + } + } + return data; +} + +export function logApiResponse(endpoint: string, response: any) { + if (__DEV__) { + console.log(`[API Response - ${endpoint}]`, JSON.stringify(response, null, 2)); + } +} + From 8c91b6830ae1af16c67e290ba07ca41045813823 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Mon, 5 Jan 2026 15:32:10 +0300 Subject: [PATCH 14/21] Triage Completed. Explain API left --- contexts/AuthContext.tsx | 31 --- lib/core/api/api_client.dart | 2 +- .../symptoms_checker_repo.dart | 85 +++++--- .../symptoms_checker_view_model.dart | 155 +++++++++++--- .../symptoms_checker/triage_screen.dart | 202 ++++++++++++------ services/api.ts | 1 - types/user.ts | 57 ----- utils/apiHelpers.ts | 18 -- 8 files changed, 320 insertions(+), 231 deletions(-) delete mode 100644 contexts/AuthContext.tsx delete mode 100644 services/api.ts delete mode 100644 types/user.ts delete mode 100644 utils/apiHelpers.ts diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx deleted file mode 100644 index 48571a86..00000000 --- a/contexts/AuthContext.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// ...existing imports... -import { apiService } from '../services/api'; - -// ...existing code... - -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - // ...existing state... - - const login = async (nationalId: string, password: string) => { - try { - setIsLoading(true); - - // Call the real API - const user = await apiService.getUserDetails(nationalId, password); - - // Store user data - await AsyncStorage.setItem('user', JSON.stringify(user)); - await AsyncStorage.setItem('authToken', user.authToken); - - setUser(user); - } catch (error) { - console.error('Login error:', error); - throw new Error('Invalid credentials or network error'); - } finally { - setIsLoading(false); - } - }; - - // ...existing code... -}; - diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 039787b8..f3663294 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient { final int statusCode = response.statusCode; log("uri: ${Uri.parse(url.trim())}"); log("body: ${json.encode(body)}"); - log("response.body: ${response.body}"); + // log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 2d9a99b7..c55c2f0c 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -12,18 +12,21 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class SymptomsCheckerRepo { - Future>> getUserDetails({ + Future>> + getUserDetails({ required String userName, required String password, }); - Future>> getBodySymptomsByName({ + Future>> + getBodySymptomsByName({ required List organNames, required String userSessionToken, required int gender, }); - Future>> getRiskFactors({ + Future>> + getRiskFactors({ required int age, required String sex, required List evidenceIds, @@ -33,7 +36,8 @@ abstract class SymptomsCheckerRepo { required String sessionId, }); - Future>> getSuggestions({ + Future>> + getSuggestions({ required int age, required String sex, required List evidenceIds, @@ -43,10 +47,12 @@ abstract class SymptomsCheckerRepo { required int gender, }); - Future>> getDiagnosisForTriage({ + Future>> + getDiagnosisForTriage({ required int age, required String sex, required List evidenceIds, + List>? triageEvidence, required String language, required String userSessionToken, required int gender, @@ -58,10 +64,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { final ApiClient apiClient; final LoggerService loggerService; - SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService}); + SymptomsCheckerRepoImp( + {required this.apiClient, required this.loggerService}); @override - Future>> getUserDetails({ + Future>> + getUserDetails({ required String userName, required String password, }) async { @@ -84,9 +92,11 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { // Parse response if it's a string - final Map responseData = response is String ? jsonDecode(response) : response; + final Map responseData = + response is String ? jsonDecode(response) : response; - SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = SymptomsUserDetailsResponseModel.fromJson(responseData); + SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = + SymptomsUserDetailsResponseModel.fromJson(responseData); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -113,7 +123,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } @override - Future>> getBodySymptomsByName({ + Future>> + getBodySymptomsByName({ required List organNames, required String userSessionToken, required int gender, @@ -143,7 +154,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response); + BodySymptomResponseModel bodySymptomResponse = + BodySymptomResponseModel.fromJson(response); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -152,7 +164,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { data: bodySymptomResponse, ); } catch (e, stackTrace) { - loggerService.logError("Error parsing GetBodySymptomsByName response: $e"); + loggerService + .logError("Error parsing GetBodySymptomsByName response: $e"); loggerService.logError("StackTrace: $stackTrace"); failure = DataParsingFailure(e.toString()); } @@ -170,7 +183,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } @override - Future>> getRiskFactors({ + Future>> + getRiskFactors({ required int age, required String sex, required List evidenceIds, @@ -211,9 +225,11 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { // Parse response if it's a string - final Map responseData = response is String ? jsonDecode(response) : response; + final Map responseData = + response is String ? jsonDecode(response) : response; - RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); + RiskAndSuggestionsResponseModel riskFactorsResponse = + RiskAndSuggestionsResponseModel.fromJson(responseData); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -239,22 +255,37 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } } - @override - Future>> getDiagnosisForTriage({ + Future>> + getDiagnosisForTriage({ required int age, required String sex, required List evidenceIds, + List>? + triageEvidence, // Additional triage-specific evidence required String language, required String userSessionToken, required int gender, required String sessionId, }) async { + // Build evidence list: combine initial symptoms with triage evidence + List> evidenceList = []; + + // Add initial evidence as simple IDs + for (var id in evidenceIds) { + evidenceList.add({"id": id}); + } + + // Add triage evidence with proper format (id, choice_id, source) + if (triageEvidence != null && triageEvidence.isNotEmpty) { + evidenceList.addAll(triageEvidence); + } + final Map body = { "age": { "value": age, }, "sex": sex, - "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "evidence": evidenceList, "language": language, "suggest_method": "diagnosis", "generalId": sessionId, @@ -282,11 +313,13 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { // Parse response if it's a string - final Map responseData = response is String ? jsonDecode(response) : response; + final Map responseData = + response is String ? jsonDecode(response) : response; final updatedResponseData = responseData['dataDetails']; - TriageDataDetails riskFactorsResponse = TriageDataDetails.fromJson(updatedResponseData); + TriageDataDetails riskFactorsResponse = + TriageDataDetails.fromJson(updatedResponseData); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -295,7 +328,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { data: riskFactorsResponse, ); } catch (e, stackTrace) { - loggerService.logError("Error parsing getDiagnosisForTriage response: $e"); + loggerService + .logError("Error parsing getDiagnosisForTriage response: $e"); loggerService.logError("StackTrace: $stackTrace"); failure = DataParsingFailure(e.toString()); } @@ -313,7 +347,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } @override - Future>> getSuggestions({ + Future>> + getSuggestions({ required int age, required String sex, required List evidenceIds, @@ -354,9 +389,11 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { // Parse response if it's a string - final Map responseData = response is String ? jsonDecode(response) : response; + final Map responseData = + response is String ? jsonDecode(response) : response; - RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); + RiskAndSuggestionsResponseModel riskFactorsResponse = + RiskAndSuggestionsResponseModel.fromJson(responseData); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index b41bab29..73768fe0 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -47,7 +47,13 @@ class SymptomsCheckerViewModel extends ChangeNotifier { TriageDataDetails? triageDataDetails; // Triage state - int? _selectedTriageChoiceIndex; + int? + _selectedTriageChoiceIndex; // Deprecated - keeping for backward compatibility + final Map _selectedTriageChoicesByItemId = + {}; // Map of itemId -> choiceIndex for multi-item questions + final List> _triageEvidenceList = + []; // Store triage evidence with proper format + int _triageQuestionCount = 0; // Track number of triage questions answered // Selected risk factors tracking final Set _selectedRiskFactorIds = {}; @@ -60,7 +66,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // User Info Flow State int _userInfoCurrentPage = 0; - bool _isSinglePageEditMode = false; // Track if editing single page or full flow + bool _isSinglePageEditMode = + false; // Track if editing single page or full flow String? _selectedGender; DateTime? _dateOfBirth; int? _selectedAge; @@ -100,14 +107,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier { String? get tooltipOrganId => _tooltipOrganId; - String get currentSessionAuthToken => symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? ""; + String get currentSessionAuthToken => + symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? ""; - String get currentSessionId => symptomsUserDetailsResponseModel?.sessionId ?? ""; + String get currentSessionId => + symptomsUserDetailsResponseModel?.sessionId ?? ""; // Triage-related getters bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false; - bool get hasEmergencyEvidence => triageDataDetails?.hasEmergencyEvidence ?? false; + bool get hasEmergencyEvidence => + triageDataDetails?.hasEmergencyEvidence ?? false; String? get currentInterviewToken => triageDataDetails?.interviewToken; @@ -117,8 +127,34 @@ class SymptomsCheckerViewModel extends ChangeNotifier { int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex; + /// Get the number of triage questions answered + int get triageQuestionCount => _triageQuestionCount; + + /// Get choice index for a specific item + int? getTriageChoiceForItem(String itemId) { + return _selectedTriageChoicesByItemId[itemId]; + } + + /// Check if all items in current question have been answered + bool get areAllTriageItemsAnswered { + if (currentTriageQuestion?.items == null || + currentTriageQuestion!.items!.isEmpty) { + return false; + } + + // Check if we have an answer for each item + for (var item in currentTriageQuestion!.items!) { + if (item.id != null && + !_selectedTriageChoicesByItemId.containsKey(item.id)) { + return false; + } + } + return true; + } + /// Get organs for current view - List get currentOrgans => OrganData.getOrgansForView(_currentView); + List get currentOrgans => + OrganData.getOrgansForView(_currentView); /// Get all selected organs from both views List get selectedOrgans { @@ -126,7 +162,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ...OrganData.frontViewOrgans, ...OrganData.backViewOrgans, ]; - return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList(); + return allOrgans + .where((organ) => _selectedOrganIds.contains(organ.id)) + .toList(); } /// Check if any organs are selected @@ -143,11 +181,13 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } int get totalSelectedSymptomsCount { - return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); + return _selectedSymptomsByOrgan.values + .fold(0, (sum, symptomIds) => sum + symptomIds.length); } bool get hasSelectedSymptoms { - return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty); + return _selectedSymptomsByOrgan.values + .any((symptomIds) => symptomIds.isNotEmpty); } /// Get risk factors list @@ -173,7 +213,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } void toggleView() { - _currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front; + _currentView = + _currentView == BodyView.front ? BodyView.back : BodyView.front; notifyListeners(); } @@ -276,7 +317,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { return; } - List organNames = selectedOrgans.map((organ) => organ.name).toList(); + List organNames = + selectedOrgans.map((organ) => organ.name).toList(); await getBodySymptomsByName( organNames: organNames, @@ -326,7 +368,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } } - if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { + if (matchingOrganId != null && + _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!; if (organResult.bodySymptoms != null) { @@ -377,7 +420,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Get all selected risk factors List getAllSelectedRiskFactors() { - return riskFactorsList.where((factor) => factor.id != null && _selectedRiskFactorIds.contains(factor.id)).toList(); + return riskFactorsList + .where((factor) => + factor.id != null && _selectedRiskFactorIds.contains(factor.id)) + .toList(); } /// Clear all risk factor selections @@ -410,7 +456,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } // Extract symptom IDs - List evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + List evidenceIds = + selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); await getRiskFactors( age: _selectedAge!, @@ -468,8 +515,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { if (apiResponse.messageStatus == 1 && apiResponse.data != null) { riskFactorsResponse = apiResponse.data; - if (riskFactorsResponse != null && riskFactorsResponse!.dataDetails != null) { - RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( + if (riskFactorsResponse != null && + riskFactorsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = + RiskAndSuggestionsItemModel( id: "not_applicable", commonName: "Not Applicable", name: "Not Applicable", @@ -523,7 +572,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Get all selected risk factors List getAllSelectedSuggestions() { - return suggestionsList.where((factor) => factor.id != null && _selectedSuggestionsIds.contains(factor.id)).toList(); + return suggestionsList + .where((factor) => + factor.id != null && _selectedSuggestionsIds.contains(factor.id)) + .toList(); } /// Clear all risk factor selections @@ -538,15 +590,20 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Add selected symptoms final selectedSymptoms = getAllSelectedSymptoms(); - evidenceIds.addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!)); + evidenceIds + .addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!)); // Add selected risk factors (excluding "not_applicable") final selectedRiskFactors = getAllSelectedRiskFactors(); - evidenceIds.addAll(selectedRiskFactors.where((rf) => rf.id != null && rf.id != "not_applicable").map((rf) => rf.id!)); + evidenceIds.addAll(selectedRiskFactors + .where((rf) => rf.id != null && rf.id != "not_applicable") + .map((rf) => rf.id!)); // Add selected suggestions (excluding "not_applicable") final selectedSuggestions = getAllSelectedSuggestions(); - evidenceIds.addAll(selectedSuggestions.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!)); + evidenceIds.addAll(selectedSuggestions + .where((s) => s.id != null && s.id != "not_applicable") + .map((s) => s.id!)); return evidenceIds; } @@ -575,13 +632,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } // Extract symptom IDs - List evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + List evidenceIds = + selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); // Get all selected symptoms final selectedRisks = getAllSelectedRiskFactors(); if (selectedRisks.isNotEmpty) { - List evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList(); + List evidenceRisksIds = selectedRisks + .where((s) => s.id != null && s.id != "not_applicable") + .map((s) => s.id!) + .toList(); evidenceIds.addAll(evidenceRisksIds); } @@ -639,8 +700,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { if (apiResponse.messageStatus == 1 && apiResponse.data != null) { suggestionsResponse = apiResponse.data; - if (suggestionsResponse != null && suggestionsResponse!.dataDetails != null) { - RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( + if (suggestionsResponse != null && + suggestionsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = + RiskAndSuggestionsItemModel( id: "not_applicable", commonName: "Not Applicable", name: "Not Applicable", @@ -669,6 +732,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { required int age, required String sex, required List evidenceIds, + List>? triageEvidence, required String language, Function(TriageDataDetails)? onSuccess, Function(String)? onError, @@ -680,6 +744,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { age: age, sex: sex, evidenceIds: evidenceIds, + triageEvidence: triageEvidence, language: language, sessionId: currentSessionId, userSessionToken: currentSessionAuthToken, @@ -755,15 +820,44 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); } - /// Select a triage choice option + /// Select a triage choice option (for backward compatibility with single-item questions) void selectTriageChoice(int choiceIndex) { _selectedTriageChoiceIndex = choiceIndex; notifyListeners(); } - /// Reset triage choice selection + /// Select a choice for a specific item (for multi-item questions) + void selectTriageChoiceForItem(String itemId, int choiceIndex) { + _selectedTriageChoicesByItemId[itemId] = choiceIndex; + notifyListeners(); + } + + /// Reset triage choice selection and increment question count void resetTriageChoice() { _selectedTriageChoiceIndex = null; + _selectedTriageChoicesByItemId.clear(); + _triageQuestionCount++; // Increment question count + notifyListeners(); + } + + /// Add triage evidence in the proper format + void addTriageEvidence(String itemId, String choiceId) { + _triageEvidenceList.add({ + "id": itemId, + "choice_id": choiceId, + "source": "triage", + }); + notifyListeners(); + } + + /// Get all triage evidence + List> getTriageEvidence() { + return List.from(_triageEvidenceList); + } + + /// Clear triage evidence + void clearTriageEvidence() { + _triageEvidenceList.clear(); notifyListeners(); } @@ -773,6 +867,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { _selectedSymptomsByOrgan.clear(); _selectedRiskFactorIds.clear(); _selectedSuggestionsIds.clear(); + _triageEvidenceList.clear(); + _selectedTriageChoicesByItemId.clear(); + _triageQuestionCount = 0; // Reset question count bodySymptomResponse = null; riskFactorsResponse = null; suggestionsResponse = null; @@ -844,7 +941,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Calculate age from date of birth final now = DateTime.now(); int age = now.year - dateOfBirth.year; - if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { + if (now.month < dateOfBirth.month || + (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { age--; } _selectedAge = age; @@ -891,7 +989,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { }) async { isBodySymptomsLoading = true; notifyListeners(); - final result = await symptomsCheckerRepo.getUserDetails(userName: userName, password: password); + final result = await symptomsCheckerRepo.getUserDetails( + userName: userName, password: password); result.fold( (failure) async { diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 8262c677..ba159bdf 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -2,6 +2,7 @@ import 'dart:developer'; 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_export.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -17,6 +18,7 @@ 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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; class TriageScreen extends StatefulWidget { @@ -27,7 +29,6 @@ class TriageScreen extends StatefulWidget { } class _TriageScreenState extends State { - List answeredEvidenceIds = []; // Track user's answers late SymptomsCheckerViewModel viewModel; late DialogService dialogService; @@ -58,43 +59,96 @@ class _TriageScreenState extends State { } void _handleTriageResponse() { + // Case 1: Emergency evidence detected if (viewModel.hasEmergencyEvidence) { _showEmergencyDialog(); return; } - if (viewModel.shouldStopTriage) { + // Get the highest probability condition + final conditions = viewModel.currentConditions ?? []; + double highestProbability = 0.0; + + if (conditions.isNotEmpty) { + final sortedConditions = List.from(conditions); + sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); + highestProbability = (sortedConditions.first.probability ?? 0.0) * 100; + } + + // Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered + if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 7) { // Navigate to results/possible conditions screen context.navigateWithName(AppRoutes.possibleConditionsScreen); return; } - // Question is loaded, reset selection for new question + // Continue triage - question is loaded, reset selection for new question viewModel.resetTriageChoice(); } void _showEmergencyDialog() { showCommonBottomSheetWithoutHeight( context, - title: "Emergency".needTranslation, - child: Utils.getWarningWidget( - loadingText: "Emergency evidence detected. Please seek immediate medical attention.".needTranslation, - isShowActionButtons: true, - onCancelTap: () => Navigator.pop(context), - onConfirmTap: () { - Navigator.pop(context); - context.pop(); - }, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.primaryRedColor, + borderRadius: 24.h, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: AppColors.whiteColor, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + "Emergency".needTranslation.toText28(color: AppColors.whiteColor, isBold: true), + SizedBox(height: 8.h), + "Emergency evidence detected. Please seek medical attention." + .needTranslation + .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async => Navigator.of(context).pop(), + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.whiteColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), + ), ), isFullScreen: false, - isCloseButtonVisible: true, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () {}, ); } - bool get isFirstQuestion => answeredEvidenceIds.isEmpty; + bool get isFirstQuestion => viewModel.getTriageEvidence().isEmpty; - void _onOptionSelected(int choiceIndex) { - viewModel.selectTriageChoice(choiceIndex); + void _onOptionSelectedForItem(String itemId, int choiceIndex) { + viewModel.selectTriageChoiceForItem(itemId, choiceIndex); } void _onPreviousPressed() { @@ -102,13 +156,6 @@ class _TriageScreenState extends State { } void _onNextPressed() { - // Check if user has selected an option - if (viewModel.selectedTriageChoiceIndex == null) { - dialogService.showErrorBottomSheet(message: 'Please select an option before proceeding'.needTranslation); - return; - } - - // Get the selected choice from the current question final currentQuestion = viewModel.currentTriageQuestion; if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) { dialogService.showErrorBottomSheet( @@ -117,32 +164,43 @@ class _TriageScreenState extends State { return; } - final questionItem = currentQuestion.items!.first; - if (questionItem.choices == null || viewModel.selectedTriageChoiceIndex! >= questionItem.choices!.length) { - dialogService.showErrorBottomSheet( - message: 'Invalid choice selection'.needTranslation, - ); + // Check if all items have been answered + if (!viewModel.areAllTriageItemsAnswered) { + dialogService.showErrorBottomSheet(message: 'Please answer all questions before proceeding'.needTranslation); return; } - final selectedChoice = questionItem.choices![viewModel.selectedTriageChoiceIndex!]; + // Collect all evidence from all items + for (var item in currentQuestion.items!) { + final itemId = item.id ?? ""; + if (itemId.isEmpty) continue; - final evidenceId = selectedChoice.label ?? ""; - if (evidenceId.isNotEmpty) { - answeredEvidenceIds.add(evidenceId); + final selectedChoiceIndex = viewModel.getTriageChoiceForItem(itemId); + if (selectedChoiceIndex == null) continue; + + if (item.choices != null && selectedChoiceIndex < item.choices!.length) { + final selectedChoice = item.choices![selectedChoiceIndex]; + final choiceId = selectedChoice.id ?? ""; + + if (choiceId.isNotEmpty) { + viewModel.addTriageEvidence(itemId, choiceId); + } + } } - // Get all previous evidence IDs - List allEvidenceIds = viewModel.getAllEvidenceIds(); - allEvidenceIds.addAll(answeredEvidenceIds); + // Get all evidence: initial symptoms + risk factors + suggestions + triage evidence + List initialEvidenceIds = viewModel.getAllEvidenceIds(); + List> triageEvidence = viewModel.getTriageEvidence(); - log("allEvidences: ${allEvidenceIds.toString()}"); + log("initialEvidenceIds: ${initialEvidenceIds.toString()}"); + log("triageEvidence: ${triageEvidence.toString()}"); // Call API with updated evidence viewModel.getDiagnosisForTriage( age: viewModel.selectedAge!, sex: viewModel.selectedGender!.toLowerCase(), - evidenceIds: allEvidenceIds, + evidenceIds: initialEvidenceIds, + triageEvidence: triageEvidence, language: viewModel.appState.isArabic() ? 'ar' : 'en', onSuccess: (response) { _handleTriageResponse(); @@ -241,29 +299,6 @@ class _TriageScreenState extends State { ); } - Widget _buildErrorState() { - return CollapsingListView( - title: "Triage".needTranslation, - leadingCallback: () => context.pop(), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 64.f, color: AppColors.errorColor), - SizedBox(height: 16.h), - "No question available".needTranslation.toText16(weight: FontWeight.w500), - SizedBox(height: 24.h), - CustomButton( - text: "Go Back".needTranslation, - onPressed: () => context.pop(), - backgroundColor: AppColors.primaryRedColor, - ).paddingSymmetrical(48.w, 0), - ], - ), - ), - ); - } - void _showConfirmationBeforeExit(BuildContext context) { showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: context), @@ -299,9 +334,6 @@ class _TriageScreenState extends State { return SizedBox.shrink(); } - final questionItem = question.items!.first; - final choices = questionItem.choices ?? []; - return AnimatedSwitcher( duration: const Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { @@ -324,7 +356,7 @@ class _TriageScreenState extends State { ); }, child: Container( - key: ValueKey(questionItem.id ?? answeredEvidenceIds.length.toString()), + key: ValueKey(question.items!.first.id ?? viewModel.getTriageEvidence().length.toString()), width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 24.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -332,11 +364,36 @@ class _TriageScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (question.text ?? "").toText16(weight: FontWeight.w500), + // Main question text + (question.text ?? "").toText16(weight: FontWeight.w600, color: AppColors.textColor), SizedBox(height: 24.h), - ...List.generate(choices.length, (index) { - bool selected = viewModel.selectedTriageChoiceIndex == index; - return _buildOptionItem(index, selected, choices[index].label ?? ""); + + // Show all items with dividers + ...List.generate(question.items!.length, (itemIndex) { + final item = question.items![itemIndex]; + final itemId = item.id ?? ""; + final choices = item.choices ?? []; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Item name (sub-question) + (item.name ?? "").toText14(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 8.h), + // Choices for this item + ...List.generate(choices.length, (choiceIndex) { + bool selected = viewModel.getTriageChoiceForItem(itemId) == choiceIndex; + return _buildOptionItem(itemId, choiceIndex, selected, choices[choiceIndex].label ?? ""); + }), + + // Add divider between items (but not after the last one) + if (itemIndex < question.items!.length - 1) ...[ + SizedBox(height: 8.h), + Divider(color: AppColors.bottomNAVBorder, thickness: 1), + SizedBox(height: 10.h), + ], + ], + ); }), ], ), @@ -344,9 +401,9 @@ class _TriageScreenState extends State { ); } - Widget _buildOptionItem(int index, bool selected, String optionText) { + Widget _buildOptionItem(String itemId, int choiceIndex, bool selected, String optionText) { return GestureDetector( - onTap: () => _onOptionSelected(index), + onTap: () => _onOptionSelectedForItem(itemId, choiceIndex), child: Container( margin: EdgeInsets.only(bottom: 12.h), child: Row( @@ -365,7 +422,7 @@ class _TriageScreenState extends State { child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), - Expanded(child: optionText.toText14(weight: FontWeight.w500)), + Expanded(child: optionText.toText13(weight: FontWeight.w500)), ], ), ), @@ -387,6 +444,7 @@ class _TriageScreenState extends State { suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown"; probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage } + // final bool isHighConfidence = probability >= 70.0; return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -445,6 +503,8 @@ class _TriageScreenState extends State { ), ), ), + // Show high confidence message + SizedBox(height: 12.h), Row( children: [ diff --git a/services/api.ts b/services/api.ts deleted file mode 100644 index 8b137891..00000000 --- a/services/api.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/types/user.ts b/types/user.ts deleted file mode 100644 index a2158efe..00000000 --- a/types/user.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface UserName { - first_name: string; - middle_name: string; - last_name: string; - first_name_ar: string; - middle_name_ar: string; - last_name_ar: string; -} - -export interface UserDetails { - FileNo: string; - national_id: string; - email: string; - date_of_birth: string; - date_of_birth_hijri: string | null; - age: number; - name: UserName; - marital_status: number; - marital_status_code: string | null; - nationality: string; - nationality_iso_code: string; - occupation: string | null; - id_type: number; - gender: number; - jwt_token: string | null; - country_dial_code: string; - phone_no: string; -} - -export interface TokenDetails { - id: string; - auth_token: string; - expires_in: number; -} - -export interface GetUserDetailsResponse { - tokenDetails: TokenDetails; - userDetails: UserDetails; - sessionId: string; -} - -// Simplified User type for app usage -export interface User { - id: string; - fileNo: string; - nationalId: string; - email: string; - name: string; - nameAr: string; - phoneNo: string; - dateOfBirth: string; - nationality: string; - gender: number; - authToken: string; - sessionId: string; -} - diff --git a/utils/apiHelpers.ts b/utils/apiHelpers.ts deleted file mode 100644 index 3fae819d..00000000 --- a/utils/apiHelpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function safeJsonParse(data: any): T { - if (typeof data === 'string') { - try { - return JSON.parse(data); - } catch (error) { - console.error('Failed to parse JSON string:', error); - throw new Error('Invalid JSON response from server'); - } - } - return data; -} - -export function logApiResponse(endpoint: string, response: any) { - if (__DEV__) { - console.log(`[API Response - ${endpoint}]`, JSON.stringify(response, null, 2)); - } -} - From 4ee0d1d3c8dd76db95e70759798cf453c9a31f0e Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Mon, 5 Jan 2026 15:47:41 +0300 Subject: [PATCH 15/21] only explain api left. --- .../symptoms_checker/user_info_selection.dart | 121 +++++++++++++----- 1 file changed, 89 insertions(+), 32 deletions(-) diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index 91f3d36d..e5163f5b 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.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/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -17,7 +18,8 @@ class UserInfoSelectionScreen extends StatefulWidget { const UserInfoSelectionScreen({super.key}); @override - State createState() => _UserInfoSelectionScreenState(); + State createState() => + _UserInfoSelectionScreenState(); } class _UserInfoSelectionScreenState extends State { @@ -51,7 +53,7 @@ class _UserInfoSelectionScreenState extends State { if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { try { - DateTime dob = DateTime.parse(user.dateofBirth!); + DateTime dob = DateUtil.convertStringToDate(user.dateofBirth!); viewModel.setDateOfBirth(dob); } catch (e) { // If date parsing fails, ignore and let user fill manually @@ -85,19 +87,25 @@ class _UserInfoSelectionScreenState extends State { width: 40.h, margin: EdgeInsets.only(right: 10.h), padding: EdgeInsets.all(8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.greyColor), - child: Utils.buildSvgWithAssets(icon: leadingIcon, iconColor: iconColor)), + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + borderRadius: 12.r, color: AppColors.greyColor), + child: Utils.buildSvgWithAssets( + icon: leadingIcon, iconColor: iconColor)), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ title.toText16(weight: FontWeight.w500), - subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + subTitle.toText14( + color: AppColors.primaryRedColor, + weight: FontWeight.w500), ], ), ], ), ), - Utils.buildSvgWithAssets(icon: trailingIcon, height: 24.h, width: 24.h), + Utils.buildSvgWithAssets( + icon: trailingIcon, height: 24.h, width: 24.h), ], ), ); @@ -114,8 +122,27 @@ class _UserInfoSelectionScreenState extends State { AppState appState = getIt.get(); String name = ""; + int? userAgeFromDOB; + if (appState.isAuthenticated) { - name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} "; + final user = appState.getAuthenticatedUser(); + name = "${user!.firstName!} ${user.lastName!} "; + + // Calculate age from authenticated user's DOB if available + if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { + try { + DateTime dob = DateUtil.convertStringToDate(user.dateofBirth!); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || + (now.month == dob.month && now.day < dob.day)) { + age--; + } + userAgeFromDOB = age; + } catch (e) { + // If date parsing fails, ignore + } + } } else { name = "Guest"; } @@ -132,12 +159,15 @@ class _UserInfoSelectionScreenState extends State { // Get display values String genderText = viewModel.selectedGender ?? "Not set"; - // Show age calculated from DOB, not the DOB itself - String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set"; - String heightText = - viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set"; - String weightText = - viewModel.selectedWeight != null ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" : "Not set"; + // Show age calculated from DOB (prefer viewModel's age, fallback to calculated from user's DOB) + int? displayAge = viewModel.selectedAge ?? userAgeFromDOB; + String ageText = displayAge != null ? "$displayAge Years" : "Not set"; + String heightText = viewModel.selectedHeight != null + ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" + : "Not set"; + String weightText = viewModel.selectedWeight != null + ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" + : "Not set"; return Column( children: [ @@ -150,11 +180,17 @@ class _UserInfoSelectionScreenState extends State { children: [ Container( width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r), + padding: EdgeInsets.symmetric( + vertical: 24.h, horizontal: 16.w), child: Column( children: [ - "Hello $name, Is your information up to date?".needTranslation.toText18( + "Hello $name, Is your information up to date?" + .needTranslation + .toText18( weight: FontWeight.w600, color: AppColors.textColor, ), @@ -165,8 +201,10 @@ class _UserInfoSelectionScreenState extends State { title: "Gender".needTranslation, subTitle: genderText, onTap: () { - viewModel.setUserInfoPage(0, isSinglePageEdit: true); - context.navigateWithName(AppRoutes.userInfoFlowManager); + viewModel.setUserInfoPage(0, + isSinglePageEdit: true); + context.navigateWithName( + AppRoutes.userInfoFlowManager); }, trailingIcon: AppAssets.edit_icon, ), @@ -178,8 +216,10 @@ class _UserInfoSelectionScreenState extends State { subTitle: ageText, iconColor: AppColors.greyTextColor, onTap: () { - viewModel.setUserInfoPage(1, isSinglePageEdit: true); - context.navigateWithName(AppRoutes.userInfoFlowManager); + viewModel.setUserInfoPage(1, + isSinglePageEdit: true); + context.navigateWithName( + AppRoutes.userInfoFlowManager); }, trailingIcon: AppAssets.edit_icon, ), @@ -190,8 +230,10 @@ class _UserInfoSelectionScreenState extends State { title: "Height".needTranslation, subTitle: heightText, onTap: () { - viewModel.setUserInfoPage(2, isSinglePageEdit: true); - context.navigateWithName(AppRoutes.userInfoFlowManager); + viewModel.setUserInfoPage(2, + isSinglePageEdit: true); + context.navigateWithName( + AppRoutes.userInfoFlowManager); }, trailingIcon: AppAssets.edit_icon, ), @@ -202,8 +244,10 @@ class _UserInfoSelectionScreenState extends State { title: "Weight".needTranslation, subTitle: weightText, onTap: () { - viewModel.setUserInfoPage(3, isSinglePageEdit: true); - context.navigateWithName(AppRoutes.userInfoFlowManager); + viewModel.setUserInfoPage(3, + isSinglePageEdit: true); + context.navigateWithName( + AppRoutes.userInfoFlowManager); }, trailingIcon: AppAssets.edit_icon, ), @@ -225,7 +269,8 @@ class _UserInfoSelectionScreenState extends State { Widget _buildBottomCard(BuildContext context, bool hasEmptyFields) { return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, borderRadius: 24.r), child: SafeArea( top: false, child: Column( @@ -240,10 +285,13 @@ class _UserInfoSelectionScreenState extends State { icon: AppAssets.edit_icon, iconColor: AppColors.primaryRedColor, onPressed: () { - context.read().setUserInfoPage(0, isSinglePageEdit: false); + context + .read() + .setUserInfoPage(0, isSinglePageEdit: false); context.navigateWithName(AppRoutes.userInfoFlowManager); }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: + AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, @@ -254,13 +302,22 @@ class _UserInfoSelectionScreenState extends State { child: CustomButton( text: "Yes, It is".needTranslation, icon: AppAssets.tickIcon, - iconColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, + iconColor: hasEmptyFields + ? AppColors.greyTextColor + : AppColors.whiteColor, onPressed: hasEmptyFields ? () {} // Empty function for disabled state - : () => context.navigateWithName(AppRoutes.organSelectorPage), - backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, - borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, - textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, + : () => context + .navigateWithName(AppRoutes.organSelectorPage), + backgroundColor: hasEmptyFields + ? AppColors.greyLightColor + : AppColors.primaryRedColor, + borderColor: hasEmptyFields + ? AppColors.greyLightColor + : AppColors.primaryRedColor, + textColor: hasEmptyFields + ? AppColors.greyTextColor + : AppColors.whiteColor, fontSize: 16.f, ), ), From cc255e9c9ab93df54e5d0ab2e3d4b33710935f7c Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Wed, 7 Jan 2026 15:57:32 +0300 Subject: [PATCH 16/21] vital sign detail page --- .../vital_sign/vital_sign_details_page.dart | 620 ++++++++++++++++++ .../vital_sign/vital_sign_page.dart | 80 ++- 2 files changed, 693 insertions(+), 7 deletions(-) create mode 100644 lib/presentation/vital_sign/vital_sign_details_page.dart diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart new file mode 100644 index 00000000..fbbea64e --- /dev/null +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -0,0 +1,620 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:provider/provider.dart'; + +/// Which vital sign is being shown in the details screen. +enum VitalSignMetric { + bmi, + height, + weight, + bloodPressure, + temperature, + heartRate, + respiratoryRate, +} + +class VitalSignDetailsArgs { + final VitalSignMetric metric; + final String title; + final String icon; + final String unit; + + /// Optional bounds used for graph shading and labels. + final double? low; + final double? high; + + const VitalSignDetailsArgs({ + required this.metric, + required this.title, + required this.icon, + required this.unit, + this.low, + this.high, + }); +} + +class VitalSignDetailsPage extends StatefulWidget { + final VitalSignDetailsArgs args; + + const VitalSignDetailsPage({super.key, required this.args}); + + @override + State createState() => _VitalSignDetailsPageState(); +} + +class _VitalSignDetailsPageState extends State { + bool _isGraphVisible = true; + + VitalSignDetailsArgs get args => widget.args; + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: 'Vital Sign Details'.needTranslation, + child: Consumer( + builder: (context, viewModel, child) { + final latest = viewModel.vitalSignList.isNotEmpty ? viewModel.vitalSignList.first : null; + + final history = _buildSeries(viewModel.vitalSignList, args); + final latestValueText = _latestValueText(latest); + final status = _statusForLatest(latest); + final scheme = VitalSignUiModel.scheme(status: status, label: args.title); + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _headerCard( + context, + title: args.title, + icon: args.icon, + valueText: latestValueText, + status: status, + scheme: scheme, + latestDate: latest?.vitalSignDate, + ), + SizedBox(height: 16.h), + + _whatIsThisResultCard(context), + SizedBox(height: 16.h), + + _historyCard(context, history: history), + SizedBox(height: 16.h), + + _nextStepsCard(context), + SizedBox(height: 32.h), + ], + ).paddingAll(24.h), + ); + }, + ), + ); + } + + Widget _headerCard( + BuildContext context, { + required String title, + required String icon, + required String valueText, + required String? status, + required VitalSignUiModel scheme, + required DateTime? latestDate, + }) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + title.toText18(isBold: true, weight: FontWeight.w600), + ], + ), + if (status != null) + Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), + decoration: BoxDecoration( + color: scheme.chipBg, + borderRadius: BorderRadius.circular(100.r), + ), + child: status.toText11( + color: scheme.chipFg, + weight: FontWeight.w500, + ), + ), + ], + ), + SizedBox(height: 10.h), + ( + latestDate != null + ? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation) + : ('Result of --'.needTranslation) + ).toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), + SizedBox(height: 12.h), + + valueText.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -2), + + if (args.low != null || args.high != null) ...[ + SizedBox(height: 8.h), + Text( + _referenceText(context), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ) + ] + ], + ), + ); + } + + String _referenceText(BuildContext context) { + if (args.low != null && args.high != null) { + return 'Reference range: ${args.low} – ${args.high} ${args.unit}'.needTranslation; + } + if (args.low != null) { + return 'Reference range: ≥ ${args.low} ${args.unit}'.needTranslation; + } + if (args.high != null) { + return 'Reference range: ≤ ${args.high} ${args.unit}'.needTranslation; + } + return ''; + } + + Widget _whatIsThisResultCard(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600), + SizedBox(height: 8.h), + _descriptionText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10), + SizedBox(height: 12.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bulb, width: 16.w, height: 16.h, iconColor: AppColors.greyTextColor), + SizedBox(width: 6.w), + Expanded( + child: 'This information is for monitoring and not a diagnosis.'.needTranslation + .toText11(color: AppColors.greyTextColor, weight: FontWeight.w500, maxLine: 3), + ), + ], + ) + ], + ), + ); + } + + Widget _historyCard(BuildContext context, {required List history}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 'History flowchart'.needTranslation.toText16(weight: FontWeight.w600), + Row( + children: [ + // toggle graph/list similar to lab result details + Utils.buildSvgWithAssets( + icon: _isGraphVisible ? AppAssets.graphIcon : AppAssets.listIcon, + width: 18.w, + height: 18.h, + iconColor: AppColors.greyTextColor, + ).onPress(() { + setState(() { + _isGraphVisible = !_isGraphVisible; + }); + }), + SizedBox(width: 10.w), + Utils.buildSvgWithAssets(icon: AppAssets.calendarGrey, width: 18.w, height: 18.h, iconColor: AppColors.greyTextColor), + ], + ), + ], + ), + SizedBox(height: 12.h), + if (history.isEmpty) + Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) + else if (_isGraphVisible) + _buildHistoryGraph(history) + else + _buildHistoryList(context, history), + ], + ), + ); + } + + Widget _buildHistoryGraph(List history) { + final minY = _minY(history); + final maxY = _maxY(history); + return CustomGraph( + dataPoints: history, + makeGraphBasedOnActualValue: true, + leftLabelReservedSize: 40, + showGridLines: true, + leftLabelInterval: _leftInterval(history), + maxY: maxY, + minY: minY, + maxX: history.length.toDouble() - .75, + horizontalInterval: .1, + leftLabelFormatter: (value) { + // Match the lab screen behavior: only show High/Low labels. + final v = double.parse(value.toStringAsFixed(1)); + if (args.high != null && v == args.high) { + return _axisLabel('High'.needTranslation); + } + if (args.low != null && v == args.low) { + return _axisLabel('Low'.needTranslation); + } + return const SizedBox.shrink(); + }, + getDrawingHorizontalLine: (value) { + value = double.parse(value.toStringAsFixed(1)); + if ((args.high != null && value == args.high) || (args.low != null && value == args.low)) { + return FlLine( + color: AppColors.bgGreenColor.withValues(alpha: 0.6), + strokeWidth: 1, + ); + } + return const FlLine(color: Colors.transparent, strokeWidth: 1); + }, + graphColor: AppColors.blackColor, + graphShadowColor: Colors.transparent, + graphGridColor: AppColors.graphGridColor.withValues(alpha: .4), + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return const SizedBox.shrink(); + if (value == 0) return _bottomLabel(data[value.toInt()].label); + if (value == data.length - 1) return _bottomLabel(data[value.toInt()].label); + if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); + return const SizedBox.shrink(); + }, + rangeAnnotations: _rangeAnnotations(history), + minX: (history.length == 1) ? null : -.2, + scrollDirection: Axis.horizontal, + height: 180.h, + ); + } + + Widget _buildHistoryList(BuildContext context, List history) { + final items = history.reversed.toList(); + final height = items.length < 3 ? items.length * 64.0 : 180.h; + return SizedBox( + height: height, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: items.length, + separatorBuilder: (_, __) => Divider( + color: AppColors.borderOnlyColor.withValues(alpha: 0.1), + height: 1, + ), + itemBuilder: (context, index) { + final dp = items[index]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + ('${dp.actualValue} ${dp.unitOfMeasurement ?? ''}').toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + ), + ], + ), + ); + }, + ), + ); + } + + double _minY(List points) { + // IMPORTANT: y-axis uses actual numeric values (from actualValue). + final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + final min = values.reduce((a, b) => a < b ? a : b); + final double boundLow = args.low ?? min; + return (min < boundLow ? min : boundLow) - 1; + } + + double _maxY(List points) { + // IMPORTANT: y-axis uses actual numeric values (from actualValue). + final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + final max = values.reduce((a, b) => a > b ? a : b); + final double boundHigh = args.high ?? max; + return (max > boundHigh ? max : boundHigh) + 1; + } + + double _leftInterval(List points) { + // Keep it stable; graph will mostly show just two labels. + final range = (_maxY(points) - _minY(points)).abs(); + if (range <= 0) return 1; + return (range / 4).clamp(1, 20); + } + + RangeAnnotations? _rangeAnnotations(List points) { + if (args.low == null && args.high == null) return null; + + final minY = _minY(points); + final maxY = _maxY(points); + + final List ranges = []; + + if (args.low != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: minY, + y2: args.low!, + color: AppColors.highAndLow.withValues(alpha: 0.05), + ), + ); + } + + if (args.low != null && args.high != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: args.low!, + y2: args.high!, + color: AppColors.bgGreenColor.withValues(alpha: 0.05), + ), + ); + } + + if (args.high != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: args.high!, + y2: maxY, + color: AppColors.criticalLowAndHigh.withValues(alpha: 0.05), + ), + ); + } + + return RangeAnnotations(horizontalRangeAnnotations: ranges); + } + + List _buildSeries(List vitals, VitalSignDetailsArgs args) { + final List points = []; + + // Build a chronological series (oldest -> newest), skipping null/zero values. + final sorted = List.from(vitals); + sorted.sort((a, b) { + final ad = a.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + final bd = b.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + return ad.compareTo(bd); + }); + + double? metricValue(VitalSignResModel v) { + switch (args.metric) { + case VitalSignMetric.bmi: + return _toDouble(v.bodyMassIndex); + case VitalSignMetric.height: + return _toDouble(v.heightCm); + case VitalSignMetric.weight: + return _toDouble(v.weightKg); + case VitalSignMetric.temperature: + return _toDouble(v.temperatureCelcius); + case VitalSignMetric.heartRate: + return _toDouble(v.heartRate ?? v.pulseBeatPerMinute); + case VitalSignMetric.respiratoryRate: + return _toDouble(v.respirationBeatPerMinute); + case VitalSignMetric.bloodPressure: + // Graph only systolic for now (simple single-series). + return _toDouble(v.bloodPressureHigher); + } + } + + double index = 0; + for (final v in sorted) { + final mv = metricValue(v); + if (mv == null) continue; + if (mv == 0) continue; + + final dt = v.vitalSignDate ?? DateTime.now(); + final label = '${dt.day}/${dt.month}'; + + points.add( + DataPoint( + value: index, + label: label, + actualValue: mv.toStringAsFixed(0), + time: dt, + displayTime: '${dt.day}/${dt.month}/${dt.year}', + unitOfMeasurement: args.unit, + ), + ); + index += 1; + } + + return points; + } + + double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is num) return v.toDouble(); + return double.tryParse(v.toString()); + } + + String _latestValueText(VitalSignResModel? latest) { + if (latest == null) return '--'; + + switch (args.metric) { + case VitalSignMetric.bmi: + final v = _toDouble(latest.bodyMassIndex); + return v == null ? '--' : v.toStringAsFixed(0); + case VitalSignMetric.height: + final v = _toDouble(latest.heightCm); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.weight: + final v = _toDouble(latest.weightKg); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.temperature: + final v = _toDouble(latest.temperatureCelcius); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.heartRate: + final v = _toDouble(latest.heartRate ?? latest.pulseBeatPerMinute); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.respiratoryRate: + final v = _toDouble(latest.respirationBeatPerMinute); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.bloodPressure: + final s = _toDouble(latest.bloodPressureHigher); + final d = _toDouble(latest.bloodPressureLower); + if (s == null || d == null) return '--'; + return '${s.toStringAsFixed(0)}/${d.toStringAsFixed(0)}'; + } + } + + String? _statusForLatest(VitalSignResModel? latest) { + if (latest == null) return null; + + switch (args.metric) { + case VitalSignMetric.bmi: + return VitalSignUiModel.bmiStatus(latest.bodyMassIndex); + case VitalSignMetric.bloodPressure: + return VitalSignUiModel.bloodPressureStatus(systolic: latest.bloodPressureHigher, diastolic: latest.bloodPressureLower); + case VitalSignMetric.height: + return null; + case VitalSignMetric.weight: + return latest.weightKg != null ? 'Normal' : null; + case VitalSignMetric.temperature: + return null; + case VitalSignMetric.heartRate: + return (latest.heartRate ?? latest.pulseBeatPerMinute) != null ? 'Normal' : null; + case VitalSignMetric.respiratoryRate: + return latest.respirationBeatPerMinute != null ? 'Normal' : null; + } + } + + String _descriptionText(BuildContext context) { + switch (args.metric) { + case VitalSignMetric.bmi: + return 'BMI is a measurement based on height and weight that estimates body fat.'.needTranslation; + case VitalSignMetric.height: + return 'Height is measured in centimeters and is used to calculate BMI and dosage recommendations.'.needTranslation; + case VitalSignMetric.weight: + return 'Weight helps track overall health, nutrition, and changes over time.'.needTranslation; + case VitalSignMetric.bloodPressure: + return 'Blood pressure reflects the force of blood against artery walls. It is shown as systolic/diastolic.'.needTranslation; + case VitalSignMetric.temperature: + return 'Body temperature reflects how hot your body is and may change with infection or inflammation.'.needTranslation; + case VitalSignMetric.heartRate: + return 'Heart rate refers to the number of heart beats per minute.'.needTranslation; + case VitalSignMetric.respiratoryRate: + return 'Respiratory rate is the number of breaths taken per minute.'.needTranslation; + } + } + + String _nextStepsText(BuildContext context) { + switch (args.metric) { + case VitalSignMetric.bmi: + return 'Maintain a balanced diet and regular activity. If your BMI is high or low, consider consulting your doctor.'.needTranslation; + case VitalSignMetric.height: + return 'No action is needed unless your measurement looks incorrect. Update it during your next visit.'.needTranslation; + case VitalSignMetric.weight: + return 'Monitor weight changes. Sudden gain or loss may require medical advice.'.needTranslation; + case VitalSignMetric.bloodPressure: + return 'Keep tracking your blood pressure. High or low readings should be discussed with your doctor.'.needTranslation; + case VitalSignMetric.temperature: + return 'If you have a persistent fever or symptoms, contact your healthcare provider.'.needTranslation; + case VitalSignMetric.heartRate: + return 'Track your heart rate trends. If you feel dizziness or chest pain, seek medical care.'.needTranslation; + case VitalSignMetric.respiratoryRate: + return 'If you notice shortness of breath or abnormal breathing, seek medical advice.'.needTranslation; + } + } + + Widget _nextStepsCard(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 'What should I do next?'.needTranslation.toText16(weight: FontWeight.w600), + SizedBox(height: 8.h), + _nextStepsText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10), + ], + ), + ); + } + + Widget _axisLabel(String value) { + return Text( + value, + style: TextStyle( + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + fontSize: 8.f, + color: AppColors.textColor, + ), + ); + } + + Widget _bottomLabel(String label) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: 8.f, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.labelTextColor, + ), + ), + ); + } +} diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index bd25641a..d8b6d7e3 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_details_page.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; class VitalSignPage extends StatefulWidget { @@ -22,6 +24,13 @@ class VitalSignPage extends StatefulWidget { } class _VitalSignPageState extends State { + void _openDetails(VitalSignDetailsArgs args) { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignDetailsPage(args: args), + ), + ); + } @override void initState() { @@ -65,7 +74,14 @@ class _VitalSignPageState extends State { value: latestVitalSign?.bodyMassIndex?.toString() ?? '--', unit: '', status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex), - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.bmi, + title: 'BMI', + icon: AppAssets.bmiVital, + unit: '', + ), + ), ), SizedBox(height: 16.h), @@ -76,7 +92,14 @@ class _VitalSignPageState extends State { value: latestVitalSign?.heightCm?.toString() ?? '--', unit: 'cm', status: null, - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.height, + title: 'Height', + icon: AppAssets.heightVital, + unit: 'cm', + ), + ), ), SizedBox(height: 16.h), @@ -87,7 +110,14 @@ class _VitalSignPageState extends State { value: latestVitalSign?.weightKg?.toString() ?? '--', unit: 'kg', status: (latestVitalSign?.weightKg != null) ? 'Normal' : null, - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.weight, + title: 'Weight', + icon: AppAssets.weightVital, + unit: 'kg', + ), + ), ), SizedBox(height: 16.h), @@ -105,7 +135,16 @@ class _VitalSignPageState extends State { systolic: latestVitalSign?.bloodPressureHigher, diastolic: latestVitalSign?.bloodPressureLower, ), - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.bloodPressure, + title: 'Blood Pressure', + icon: AppAssets.bloodPressure, + unit: 'mmHg', + low: 90, + high: 140, + ), + ), ), SizedBox(height: 16.h), @@ -116,7 +155,16 @@ class _VitalSignPageState extends State { value: latestVitalSign?.temperatureCelcius?.toString() ?? '--', unit: '°C', status: null, - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.temperature, + title: 'Temperature', + icon: AppAssets.temperature, + unit: '°C', + low: 36.1, + high: 37.2, + ), + ), ), ], ), @@ -182,7 +230,16 @@ class _VitalSignPageState extends State { value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--', unit: 'bpm', status: 'Normal', - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.heartRate, + title: 'Heart Rate', + icon: AppAssets.heart, + unit: 'bpm', + low: 60, + high: 100, + ), + ), ), ), ], @@ -197,7 +254,16 @@ class _VitalSignPageState extends State { value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', unit: 'bpm', status: 'Normal', - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.respiratoryRate, + title: 'Respiratory rate', + icon: AppAssets.respRate, + unit: 'bpm', + low: 12, + high: 20, + ), + ), ), ], ), From 8ae81d2b248f4508c951e0920f8e2e82fb07a187 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Thu, 8 Jan 2026 10:37:59 +0300 Subject: [PATCH 17/21] fixed parking qr --- lib/core/api_consts.dart | 5 +- lib/core/dependencies.dart | 19 ++ .../models/qr_parking_response_model.dart | 183 ++++++++++++++++++ lib/features/qr_parking/qr_parking_repo.dart | 74 +++++++ .../qr_parking/qr_parking_view_model.dart | 144 ++++++++++++++ .../hmg_services/services_page.dart | 109 ++++------- .../medical_file/medical_file_page.dart | 4 +- lib/presentation/parking/paking_page.dart | 95 +++++---- lib/presentation/parking/parking_slot.dart | 177 +++++++++++++---- lib/routes/app_routes.dart | 14 +- 10 files changed, 670 insertions(+), 154 deletions(-) create mode 100644 lib/features/qr_parking/models/qr_parking_response_model.dart create mode 100644 lib/features/qr_parking/qr_parking_repo.dart create mode 100644 lib/features/qr_parking/qr_parking_view_model.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 66b9b7ec..b0fb2a83 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -207,7 +207,7 @@ var GET_APPOINTMENT_DETAILS_BY_NO = 'Services/MobileNotifications.svc/REST/GetAp var NEW_RATE_APPOINTMENT_URL = "Services/Doctors.svc/REST/AppointmentsRating_InsertAppointmentRate"; var NEW_RATE_DOCTOR_URL = "Services/Doctors.svc/REST/DoctorsRating_InsertDoctorRate"; -var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; +//var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; //URL to get clinic list var GET_CLINICS_LIST_URL = "Services/lists.svc/REST/GetClinicCentralized"; @@ -700,7 +700,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -828,6 +828,7 @@ class ApiConsts { static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; static final String getMonthlyReports = 'Services/Patients.svc/Rest/UpdatePateintHealthSummaryReport'; static final String updatePatientEmail = 'Services/Patients.svc/Rest/UpdatePateintEmail'; + static final String getQrParkingDetails = 'Services/SWP.svc/REST/GetQRParkingByID'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 555ce292..872fe69b 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -41,6 +41,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; @@ -63,6 +64,7 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; @@ -140,6 +142,15 @@ class AppDependencies { getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), ),); getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => QrParkingRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + // ViewModels // Global/shared VMs → LazySingleton @@ -245,6 +256,14 @@ class AppDependencies { activePrescriptionsRepo: getIt() ), ); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( diff --git a/lib/features/qr_parking/models/qr_parking_response_model.dart b/lib/features/qr_parking/models/qr_parking_response_model.dart new file mode 100644 index 00000000..2e90da17 --- /dev/null +++ b/lib/features/qr_parking/models/qr_parking_response_model.dart @@ -0,0 +1,183 @@ + + +class QrParkingResponseModel { + dynamic totalRecords; + dynamic nRowID; + int? qRParkingID; + String? description; + String? descriptionN; + dynamic qRCompare; + dynamic qRValue; + String? imagePath; + bool? isActive; + int? parkingID; + int? branchID; + int? companyID; + int? buildingID; + int? rowID; + int? gateID; + int? floorID; + dynamic imagePath1; + int? createdBy; + String? createdOn; + dynamic editedBy; + dynamic editedOn; + String? parkingDescription; + String? parkingDescriptionN; + String? gateDescription; + String? gateDescriptionN; + String? branchDescription; + String? branchDescriptionN; + String? companyDescription; + String? companyDescriptionN; + String? rowDescription; + String? rowDescriptionN; + String? floorDescription; + String? floorDescriptionN; + String? buildingDescription; + String? buildingDescriptionN; + String? qRParkingCode; + String? parkingCode; + double? latitude; + double? longitude; + String? qRImageStr; + + QrParkingResponseModel({ + this.totalRecords, + this.nRowID, + this.qRParkingID, + this.description, + this.descriptionN, + this.qRCompare, + this.qRValue, + this.imagePath, + this.isActive, + this.parkingID, + this.branchID, + this.companyID, + this.buildingID, + this.rowID, + this.gateID, + this.floorID, + this.imagePath1, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.parkingDescription, + this.parkingDescriptionN, + this.gateDescription, + this.gateDescriptionN, + this.branchDescription, + this.branchDescriptionN, + this.companyDescription, + this.companyDescriptionN, + this.rowDescription, + this.rowDescriptionN, + this.floorDescription, + this.floorDescriptionN, + this.buildingDescription, + this.buildingDescriptionN, + this.qRParkingCode, + this.parkingCode, + this.latitude, + this.longitude, + this.qRImageStr, + }); + + QrParkingResponseModel.fromJson(Map json) { + totalRecords = json['TotalRecords']; + nRowID = json['nRowID']; + qRParkingID = json['QRParkingID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + qRCompare = json['QRCompare']; + qRValue = json['QRValue']; + imagePath = json['ImagePath']; + isActive = json['IsActive']; + parkingID = json['ParkingID']; + branchID = json['BranchID']; + companyID = json['CompanyID']; + buildingID = json['BuildingID']; + rowID = json['RowID']; + gateID = json['GateID']; + floorID = json['FloorID']; + imagePath1 = json['ImagePath1']; + createdBy = json['CreatedBy']; + createdOn = json['CreatedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + parkingDescription = json['ParkingDescription']; + parkingDescriptionN = json['ParkingDescriptionN']; + gateDescription = json['GateDescription']; + gateDescriptionN = json['GateDescriptionN']; + branchDescription = json['BranchDescription']; + branchDescriptionN = json['BranchDescriptionN']; + companyDescription = json['CompanyDescription']; + companyDescriptionN = json['CompanyDescriptionN']; + rowDescription = json['RowDescription']; + rowDescriptionN = json['RowDescriptionN']; + floorDescription = json['FloorDescription']; + floorDescriptionN = json['FloorDescriptionN']; + buildingDescription = json['BuildingDescription']; + buildingDescriptionN = json['BuildingDescriptionN']; + qRParkingCode = json['QRParkingCode']; + parkingCode = json['ParkingCode']; + latitude = _toDouble(json['Latitude']); + longitude = _toDouble(json['Longitude']); + qRImageStr = json['QRImageStr']; + } + + Map toJson() { + final Map data = {}; + data['TotalRecords'] = totalRecords; + data['nRowID'] = nRowID; + data['QRParkingID'] = qRParkingID; + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['QRCompare'] = qRCompare; + data['QRValue'] = qRValue; + data['ImagePath'] = imagePath; + data['IsActive'] = isActive; + data['ParkingID'] = parkingID; + data['BranchID'] = branchID; + data['CompanyID'] = companyID; + data['BuildingID'] = buildingID; + data['RowID'] = rowID; + data['GateID'] = gateID; + data['FloorID'] = floorID; + data['ImagePath1'] = imagePath1; + data['CreatedBy'] = createdBy; + data['CreatedOn'] = createdOn; + data['EditedBy'] = editedBy; + data['EditedOn'] = editedOn; + data['ParkingDescription'] = parkingDescription; + data['ParkingDescriptionN'] = parkingDescriptionN; + data['GateDescription'] = gateDescription; + data['GateDescriptionN'] = gateDescriptionN; + data['BranchDescription'] = branchDescription; + data['BranchDescriptionN'] = branchDescriptionN; + data['CompanyDescription'] = companyDescription; + data['CompanyDescriptionN'] = companyDescriptionN; + data['RowDescription'] = rowDescription; + data['RowDescriptionN'] = rowDescriptionN; + data['FloorDescription'] = floorDescription; + data['FloorDescriptionN'] = floorDescriptionN; + data['BuildingDescription'] = buildingDescription; + data['BuildingDescriptionN'] = buildingDescriptionN; + data['QRParkingCode'] = qRParkingCode; + data['ParkingCode'] = parkingCode; + data['Latitude'] = latitude; + data['Longitude'] = longitude; + data['QRImageStr'] = qRImageStr; + return data; + } + + static double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is double) return v; + if (v is int) return v.toDouble(); + return double.tryParse(v.toString()); + } +} + diff --git a/lib/features/qr_parking/qr_parking_repo.dart b/lib/features/qr_parking/qr_parking_repo.dart new file mode 100644 index 00000000..1ec905f1 --- /dev/null +++ b/lib/features/qr_parking/qr_parking_repo.dart @@ -0,0 +1,74 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + + +abstract class QrParkingRepo { + Future>>> + getQrParking({ + required int qrParkingId, + }); +} + +class QrParkingRepoImp implements QrParkingRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + QrParkingRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>>> + getQrParking({required int qrParkingId}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getQrParkingDetails, // GetQRParkingByID + body: {'QRParkingID': qrParkingId}, + onFailure: (error, statusCode, + {messageStatus, failureType}) { + failure = failureType ?? + StatusCodeFailure("$error ($statusCode)"); + }, + onSuccess: (response, statusCode, + {messageStatus, errorMessage}) { + final list = + (response['List_SWP_QRParkingModel'] as List?) ?? []; + + final res = list + .map((e) => QrParkingResponseModel.fromJson( + Map.from(e), + )) + .toList(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: res, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) { + return Left(ServerFailure("Unknown error")); + } + + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} + diff --git a/lib/features/qr_parking/qr_parking_view_model.dart b/lib/features/qr_parking/qr_parking_view_model.dart new file mode 100644 index 00000000..b0b688fd --- /dev/null +++ b/lib/features/qr_parking/qr_parking_view_model.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; + +import '../../services/cache_service.dart'; +import '../../services/error_handler_service.dart'; +import 'models/qr_parking_response_model.dart'; + + +class QrParkingViewModel extends ChangeNotifier { + final QrParkingRepo qrParkingRepo; + final ErrorHandlerService errorHandlerService; + final CacheService cacheService; + String IS_GO_TO_PARKING = 'IS_GO_TO_PARKING'; + + bool isLoading = false; + String? error; + + bool isSavePark = false; + QrParkingResponseModel? qrParkingModel; + List qrParkingList = []; + + QrParkingViewModel({ + required this.qrParkingRepo, + required this.errorHandlerService, + required this.cacheService, + }); + + + Future scanAndGetParking() async { + try { + error = null; + isLoading = true; + notifyListeners(); + + final result = await BarcodeScanner.scan(); + + if (result.type != ResultType.Barcode) { + isLoading = false; + notifyListeners(); + return null; + } + + final raw = result.rawContent.trim(); + if (raw.isEmpty) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final qrParkingId = _extractQrParkingId(raw); + if (qrParkingId == null) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final apiResult = + await qrParkingRepo.getQrParking(qrParkingId: qrParkingId); + + final model = apiResult.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + error = failure.toString(); + return null; + }, + (apiResponse) { + qrParkingList = apiResponse.data ?? []; + if (qrParkingList.isNotEmpty) { + return qrParkingList.first; + } + error = "Invalid Qr Code"; + return null; + }, + ); + + if (model != null) { + qrParkingModel = model; + isSavePark = true; + + await cacheService.saveObject( + key: IS_GO_TO_PARKING, + value: model.toJson(), + ); + } + + isLoading = false; + notifyListeners(); + return model; + } catch (e) { + error = "Scan error"; + isLoading = false; + notifyListeners(); + return null; + } + } + + /// Load saved parking + Future getIsSaveParking() async { + isLoading = true; + notifyListeners(); + + final parking = + await cacheService.getObject(key: IS_GO_TO_PARKING); + + if (parking != null) { + isSavePark = true; + qrParkingModel = QrParkingResponseModel.fromJson( + Map.from(parking), + ); + } else { + isSavePark = false; + qrParkingModel = null; + } + + isLoading = false; + notifyListeners(); + } + + /// Reset parking + Future clearParking() async { + await cacheService.remove(key: IS_GO_TO_PARKING); + isSavePark = false; + qrParkingModel = null; + notifyListeners(); + } + + int? _extractQrParkingId(String raw) { + try { + if (raw.startsWith("{")) { + final data = jsonDecode(raw); + return int.tryParse(data['QRParkingID'].toString()); + } + return int.tryParse(raw); + } catch (_) { + return null; + } + } +} + + diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 87a0e0e5..a3d5d76c 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -21,7 +21,7 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; - +import 'package:hmg_patient_app_new/presentation/parking/paking_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'; @@ -31,10 +31,9 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import '../../core/dependencies.dart' show getIt; +import '../../features/qr_parking/qr_parking_view_model.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -89,30 +88,18 @@ class ServicesPage extends StatelessWidget { route: null, onTap: () async { LoaderBottomSheet.showLoader(loadingText: "Fetching Data..."); await bloodDonationViewModel.getRegionSelectedClinics(onSuccess: (val) async { - // await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { + await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { LoaderBottomSheet.hideLoader(); Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( CustomPageRoute( page: BloodDonationPage(), ), ); - // }, onError: (err) { - // LoaderBottomSheet.hideLoader(); - // }); + }); }, onError: (err) { LoaderBottomSheet.hideLoader(); }); }), - HmgServicesComponentModel( - 11, - "Covid 19 Test".needTranslation, - "".needTranslation, - AppAssets.covid19icon, - bgColor: AppColors.covid29Color, - true, - route: AppRoutes.covid19Test, - ) - // HmgServicesComponentModel( // 3, // "Home Health Care".needTranslation, @@ -176,30 +163,8 @@ class ServicesPage extends StatelessWidget { route: AppRoutes.smartWatches, // route: AppRoutes.huaweiHealthExample, ), - // HmgServicesComponentModel( - // 12, - // "Latest News".needTranslation, - // "".needTranslation, - // AppAssets.news, - // true, - // bgColor: AppColors.bgGreenColor, - // textColor: AppColors.blackColor, - // route: "https://twitter.com/HMG", - // isExternalLink: true, - // ), - HmgServicesComponentModel( - 12, - "Monthly Reports".needTranslation, - "".needTranslation, - AppAssets.report_icon, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: AppRoutes.monthlyReports, - ), ]; - @override Widget build(BuildContext context) { bloodDonationViewModel = Provider.of(context); @@ -467,43 +432,37 @@ class ServicesPage extends StatelessWidget { ), SizedBox(width: 16.w), Expanded( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ParkingPage(), - ), - ); - }, - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.car_parking_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ).onPress(() { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ParkingPage(), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ).onPress(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), ), - ); - }), - ), + ), + ); + + }), ), ), ), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 52dc9ff3..80a45ac3 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1069,7 +1069,9 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.monthly_reports_icon, isLargeText: true, iconSize: 36.h, - ), + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.monthlyReports); + }), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart index ce9b6ab7..cd1e8bcb 100644 --- a/lib/presentation/parking/paking_page.dart +++ b/lib/presentation/parking/paking_page.dart @@ -1,16 +1,18 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/presentation/parking/parking_slot.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:provider/provider.dart'; +import '../../features/qr_parking/qr_parking_view_model.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/routes/custom_page_route.dart'; + class ParkingPage extends StatefulWidget { const ParkingPage({super.key}); @@ -19,10 +21,29 @@ class ParkingPage extends StatefulWidget { } class _ParkingPageState extends State { - String? scannedCode; + Future _readQR(BuildContext context) async { + final vm = context.read(); + + final model = await vm.scanAndGetParking(); + + if (model == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(vm.error ?? "Invalid Qr Code")), + ); + return; + } + + Navigator.of(context).push( + CustomPageRoute( + page: ParkingSlot(model: model), + ), + ); + } @override Widget build(BuildContext context) { + final vm = context.watch(); // عشان loading + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( @@ -38,22 +59,24 @@ class _ParkingPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Parking".needTranslation, - style: TextStyle( - color: AppColors.textColor, - fontSize: 27.f, - fontWeight: FontWeight.w600)), - Container( - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - // margin: EdgeInsets.all(10), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Text( + Text( + "Parking".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Text( "Dr. Sulaiman Al Habib hospital are conduction a test for the emerging corona" " virus and issuing travel certificates 24/7 in a short time and with high accuracy." " Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches " @@ -62,19 +85,20 @@ class _ParkingPageState extends State { "Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes.", style: TextStyle( color: AppColors.textColor, - fontSize: 12, height: 1.4, fontWeight: FontWeight.w500), - ), - ), - ).paddingOnly( top: 16, bottom: 16), - + fontSize: 12, + height: 1.4, + fontWeight: FontWeight.w500, + ), + ), + ), + ).paddingOnly(top: 16, bottom: 16), ], ), ), ), /// Bottom button - Container - ( + Container( decoration: RoundedRectangleBorder() .toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -93,13 +117,17 @@ class _ParkingPageState extends State { borderRadius: BorderRadius.circular(10), ), ), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: ParkingSlot(), - ) ); - }, - child: Text( + onPressed: vm.isLoading ? null : () => _readQR(context), + child: vm.isLoading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( "Read Barcodes", style: TextStyle( fontSize: 18, @@ -116,3 +144,4 @@ class _ParkingPageState extends State { ); } } + diff --git a/lib/presentation/parking/parking_slot.dart b/lib/presentation/parking/parking_slot.dart index 094dcb70..013bb6f2 100644 --- a/lib/presentation/parking/parking_slot.dart +++ b/lib/presentation/parking/parking_slot.dart @@ -1,26 +1,106 @@ - - import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../features/qr_parking/qr_parking_view_model.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:provider/provider.dart'; + class ParkingSlot extends StatefulWidget { - const ParkingSlot({super.key}); + final QrParkingResponseModel model; + + const ParkingSlot({ + super.key, + required this.model, + }); @override State createState() => _ParkingSlotState(); } class _ParkingSlotState extends State { - String? scannedCode; + + void _openDirection() { + final lat = widget.model.latitude; + final lng = widget.model.longitude; + + final valid = lat != null && + lng != null && + !(lat == 0.0 && lng == 0.0) && + lat >= -90 && lat <= 90 && + lng >= -180 && lng <= 180; + + if (!valid) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Parking location not available")), + ); + return; + } + + MapsLauncher.launchCoordinates(lat, lng); + } + + Future _resetDirection() async { + final vm = context.read(); + await vm.clearParking(); + Navigator.of(context).popUntil((route) => route.isFirst); + } + + DateTime? _parseDotNetDate(String? value) { + if (value == null || value.isEmpty) return null; + + final regExp = RegExp(r'Date\((\d+)([+-]\d+)?\)'); + final match = regExp.firstMatch(value); + if (match == null) return null; + + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds == null) return null; + + return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true) + .toLocal(); + } + + + String _formatPrettyDate(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + + final day = date.day; + final month = months[date.month - 1]; + final year = date.year; + + return "$day $month $year"; + } + + + String _formatPrettyTime(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + int hour = date.hour; + final minute = date.minute.toString().padLeft(2, '0'); + + final isPM = hour >= 12; + final period = isPM ? 'PM' : 'AM'; + + hour = hour % 12; + if (hour == 0) hour = 12; + + return "${hour.toString().padLeft(2, '0')}:$minute $period"; + } @override Widget build(BuildContext context) { @@ -34,7 +114,7 @@ class _ParkingSlotState extends State { body: LayoutBuilder( builder: (context, constraints) { final maxW = constraints.maxWidth; - final contentW = maxW > 600 ? 600.0 : maxW; // حد أقصى للتابلت + final contentW = maxW > 600 ? 600.0 : maxW; return Align( alignment: Alignment.topCenter, @@ -45,9 +125,11 @@ class _ParkingSlotState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Container( width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, @@ -58,7 +140,7 @@ class _ParkingSlotState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Parking Slot Details", + "Parking Slot Details".needTranslation, style: TextStyle( fontSize: 16.f, fontWeight: FontWeight.w600, @@ -70,10 +152,26 @@ class _ParkingSlotState extends State { spacing: 4, runSpacing: 4, children: [ - AppCustomChipWidget(labelText: "Slot: B-24".needTranslation), - AppCustomChipWidget(labelText: "Basement: Zone B".needTranslation), - AppCustomChipWidget(labelText: "Date: 16 Dec 2025".needTranslation), - AppCustomChipWidget(labelText: "Parked Since: 10:32 AM".needTranslation), + AppCustomChipWidget( + labelText: + "Slot: ${widget.model.qRParkingCode ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Basement: ${widget.model.floorDescription ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Date: ${_formatPrettyDate(widget.model.createdOn)}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Parked Since: ${_formatPrettyTime(widget.model.createdOn)}" + .needTranslation, + ), ], ), ], @@ -93,42 +191,40 @@ class _ParkingSlotState extends State { borderRadius: BorderRadius.circular(10), ), ), - onPressed: () {}, + onPressed: _openDirection, child: Text( - "Get Direction", + "Get Direction".needTranslation, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.whiteColor, ), ), ), ), - const Spacer(), - SizedBox( - width: double.infinity, - height: 48.h, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - side: BorderSide(color: AppColors.primaryRedColor), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: () { - // Reset direction logic - }, - child: Text( - "Reset Direction", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.primaryRedColor, - ), - ), - ), - ), + // const Spacer(), + // SizedBox( + // width: double.infinity, + // height: 48.h, + // child: OutlinedButton( + // style: OutlinedButton.styleFrom( + // side: BorderSide(color: AppColors.primaryRedColor), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // ), + // onPressed: _resetDirection, + // child: Text( + // "Reset Direction".needTranslation, + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.w600, + // color: AppColors.primaryRedColor, + // ), + // ), + // ), + // ), ], ), ), @@ -136,7 +232,8 @@ class _ParkingSlotState extends State { ); }, ), - ); } } + + diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index dd381809..5b18bce2 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -24,12 +24,14 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_sele import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/covid19test/covid19_landing_page.dart'; import '../core/dependencies.dart'; import '../features/monthly_reports/monthly_reports_repo.dart'; import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../presentation/monthly_reports/monthly_reports_page.dart'; +import '../presentation/parking/paking_page.dart'; import '../services/error_handler_service.dart'; import 'package:provider/provider.dart'; @@ -65,6 +67,7 @@ class AppRoutes { static const String userInfoSelection = '/userInfoSelection'; static const String userInfoFlowManager = '/userInfoFlowManager'; static const String monthlyReports = '/monthlyReportsPage'; + static const String qrParking = '/qrParkingPage'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -92,7 +95,6 @@ class AppRoutes { covid19Test: (context) => Covid19LandingPage(), // // healthCalculatorsPage: (context) => HealthCalculatorsPage(), - // monthlyReports: (context) => MonthlyReportsPage() monthlyReports: (context) => ChangeNotifierProvider( create: (_) => MonthlyReportsViewModel( monthlyReportsRepo: getIt(), @@ -102,6 +104,12 @@ class AppRoutes { ), healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), - healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter) - }; + healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), + qrParking: (context) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), + ), + + + }; } From 3f5bd0a759bcbda7da91e9745a72d22427cfb1bc Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 11 Jan 2026 17:01:07 +0300 Subject: [PATCH 18/21] Completed the health Tracker Module, Only the email part is left. --- assets/images/svg/blood_pressure_icon.svg | 4 + assets/images/svg/blood_sugar_only_icon.svg | 3 + assets/images/svg/low_indicator_icon.svg | 3 + .../images/svg/normal_status_green_icon.svg | 7 + assets/images/svg/send_email_icon.svg | 3 + assets/images/svg/weight_icon.svg | 3 + lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 40 +- lib/core/app_assets.dart | 19 +- lib/core/dependencies.dart | 17 +- lib/core/enums.dart | 18 +- lib/core/utils/size_utils.dart | 4 +- .../health_trackers/health_trackers_repo.dart | 752 +++++++++++ .../blood_pressure/blood_pressure_result.dart | 87 ++ .../month_blood_pressure_result_average.dart | 58 + .../week_blood_pressure_result_average.dart | 24 + .../year_blood_pressure_result_average.dart | 37 + .../blood_sugar/diabetic_patient_result.dart | 99 ++ .../month_diabetic_result_average.dart | 37 + .../week_diabetic_result_average.dart | 21 + .../year_diabetic_result_average.dart | 35 + ...nth_weight_measurement_result_average.dart | 37 + ...eek_weight_measurement_result_average.dart | 21 + .../weight/weight_measurement_result.dart | 77 ++ ...ear_weight_measurement_result_average.dart | 29 + .../water_monitor_view_model.dart | 29 +- lib/main.dart | 4 + .../health_calculator_detailed_page.dart | 49 +- .../health_calculator_view_model.dart | 25 +- .../add_health_tracker_entry_page.dart | 567 +++++++++ .../health_tracker_detail_page.dart | 1126 +++++++++++++++++ .../health_trackers/health_trackers_page.dart | 118 ++ .../health_trackers_view_model.dart | 1012 +++++++++++++++ .../widgets/tracker_last_value_card.dart | 271 ++++ .../hmg_services/services_page.dart | 28 +- lib/presentation/home/landing_page.dart | 2 +- .../organ_selector_screen.dart | 2 +- .../possible_conditions_screen.dart | 4 +- .../symptoms_checker/risk_factors_screen.dart | 2 +- .../symptoms_checker/suggestions_screen.dart | 2 +- .../symptoms_selector_screen.dart | 10 +- .../symptoms_checker/triage_screen.dart | 10 +- .../symptoms_checker/user_info_selection.dart | 61 +- ...creen.dart => water_consumption_page.dart} | 60 +- ....dart => water_monitor_settings_page.dart} | 9 +- .../widgets/water_action_buttons_widget.dart | 2 +- lib/routes/app_routes.dart | 62 +- lib/theme/colors.dart | 179 +-- lib/widgets/graph/custom_graph.dart | 33 +- lib/widgets/input_widget.dart | 56 +- lib/widgets/time_picker_widget.dart | 348 +++++ .../time_picker_widget_usage_example.dart | 165 +++ 52 files changed, 5396 insertions(+), 277 deletions(-) create mode 100644 assets/images/svg/blood_pressure_icon.svg create mode 100644 assets/images/svg/blood_sugar_only_icon.svg create mode 100644 assets/images/svg/low_indicator_icon.svg create mode 100644 assets/images/svg/normal_status_green_icon.svg create mode 100644 assets/images/svg/send_email_icon.svg create mode 100644 assets/images/svg/weight_icon.svg create mode 100644 lib/features/health_trackers/health_trackers_repo.dart create mode 100644 lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart create mode 100644 lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart create mode 100644 lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart create mode 100644 lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart create mode 100644 lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart create mode 100644 lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart create mode 100644 lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart create mode 100644 lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart create mode 100644 lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart create mode 100644 lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart create mode 100644 lib/features/health_trackers/models/weight/weight_measurement_result.dart create mode 100644 lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart create mode 100644 lib/presentation/health_trackers/add_health_tracker_entry_page.dart create mode 100644 lib/presentation/health_trackers/health_tracker_detail_page.dart create mode 100644 lib/presentation/health_trackers/health_trackers_page.dart create mode 100644 lib/presentation/health_trackers/health_trackers_view_model.dart create mode 100644 lib/presentation/health_trackers/widgets/tracker_last_value_card.dart rename lib/presentation/water_monitor/{water_consumption_screen.dart => water_consumption_page.dart} (95%) rename lib/presentation/water_monitor/{water_monitor_settings_screen.dart => water_monitor_settings_page.dart} (97%) create mode 100644 lib/widgets/time_picker_widget.dart create mode 100644 lib/widgets/time_picker_widget_usage_example.dart diff --git a/assets/images/svg/blood_pressure_icon.svg b/assets/images/svg/blood_pressure_icon.svg new file mode 100644 index 00000000..0b027ad6 --- /dev/null +++ b/assets/images/svg/blood_pressure_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/blood_sugar_only_icon.svg b/assets/images/svg/blood_sugar_only_icon.svg new file mode 100644 index 00000000..f81cee8a --- /dev/null +++ b/assets/images/svg/blood_sugar_only_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/low_indicator_icon.svg b/assets/images/svg/low_indicator_icon.svg new file mode 100644 index 00000000..f2ca09fc --- /dev/null +++ b/assets/images/svg/low_indicator_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/normal_status_green_icon.svg b/assets/images/svg/normal_status_green_icon.svg new file mode 100644 index 00000000..b3f2619c --- /dev/null +++ b/assets/images/svg/normal_status_green_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/send_email_icon.svg b/assets/images/svg/send_email_icon.svg new file mode 100644 index 00000000..eb8684ab --- /dev/null +++ b/assets/images/svg/send_email_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight_icon.svg b/assets/images/svg/weight_icon.svg new file mode 100644 index 00000000..f93c6626 --- /dev/null +++ b/assets/images/svg/weight_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f3663294..039787b8 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient { final int statusCode = response.statusCode; log("uri: ${Uri.parse(url.trim())}"); log("body: ${json.encode(body)}"); - // log("response.body: ${response.body}"); + log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 65ac1ef9..246a8015 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -398,19 +398,6 @@ var GET_COVID_DRIVETHRU_PROCEDURES_LIST = 'Services/Doctors.svc/REST/COVID19_Get var GET_PATIENT_LAST_RECORD = 'Services/Patients.svc/REST/Med_GetPatientLastRecord'; var INSERT_PATIENT_HEALTH_DATA = 'Services/Patients.svc/REST/Med_InsertTransactions'; -///My Trackers -var GET_DIABETIC_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_GetDiabeticResultAverage'; -var GET_DIABTEC_RESULT = 'Services/Patients.svc/REST/Patient_GetDiabtecResults'; -var ADD_DIABTEC_RESULT = 'Services/Patients.svc/REST/Patient_AddDiabtecResult'; - -var GET_BLOOD_PRESSURE_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_GetBloodPressureResultAverage'; -var GET_BLOOD_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_GetBloodPressureResult'; -var ADD_BLOOD_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_AddBloodPressureResult'; - -var GET_WEIGHT_PRESSURE_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResultAverage'; -var GET_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResult'; -var ADD_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_AddWeightMeasurementResult'; - var ADD_ACTIVE_PRESCRIPTIONS_REPORT_BY_PATIENT_ID = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; var GET_CALL_INFO_HOURS_RESULT = 'Services/Doctors.svc/REST/GetCallInfoHoursResult'; @@ -420,7 +407,6 @@ var GET_QUESTION_TYPES = 'Services/OUTPs.svc/REST/getQuestionsTypes'; var UPDATE_DIABETIC_RESULT = 'Services/Patients.svc/REST/Patient_UpdateDiabeticResult'; -var SEND_AVERAGE_BLOOD_SUGAR_REPORT = 'Services/Notifications.svc/REST/SendAverageBloodSugarReport'; var DEACTIVATE_DIABETIC_STATUS = 'services/Patients.svc/REST/Patient_DeactivateDiabeticStatus'; var DEACTIVATE_BLOOD_PRESSURES_STATUS = 'services/Patients.svc/REST/Patient_DeactivateBloodPressureStatus'; @@ -826,7 +812,6 @@ class ApiConsts { static final String addHHCOrder = 'api/HHC/add'; // SYMPTOMS CHECKER API - static final String symptomsUserLogin = '$symptomsCheckerApi/user_login'; static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName'; static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors'; @@ -850,6 +835,31 @@ class ApiConsts { static String h2oUpdateUserDetail = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New"; static String h2oUndoUserActivity = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity"; + // HEALTH TRACKERS + // Blood Sugar (Diabetic) + static String getDiabeticResultAverage = 'Services/Patients.svc/REST/Patient_GetDiabeticResultAverage'; + static String getDiabeticResult = 'Services/Patients.svc/REST/Patient_GetDiabtecResults'; + static String addDiabeticResult = 'Services/Patients.svc/REST/Patient_AddDiabtecResult'; + static String updateDiabeticResult = 'Services/Patients.svc/REST/Patient_UpdateDiabtecResult'; + static String deactivateDiabeticStatus = 'Services/Patients.svc/REST/Patient_DeactivateDiabeticStatus'; + static String sendAverageBloodSugarReport = 'Services/Notifications.svc/REST/SendAverageBloodSugarReport'; + + // Blood Pressure + static String getBloodPressureResultAverage = 'Services/Patients.svc/REST/Patient_GetBloodPressureResultAverage'; + static String getBloodPressureResult = 'Services/Patients.svc/REST/Patient_GetBloodPressureResult'; + static String addBloodPressureResult = 'Services/Patients.svc/REST/Patient_AddBloodPressureResult'; + static String updateBloodPressureResult = 'Services/Patients.svc/REST/Patient_UpdateBloodPressureResult'; + static String deactivateBloodPressureStatus = 'Services/Patients.svc/REST/Patient_DeactivateBloodPressureStatus'; + static String sendAverageBloodPressureReport = 'Services/Notifications.svc/REST/SendAverageBloodPressureReport'; + + // Weight Measurement + static String getWeightMeasurementResultAverage = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResultAverage'; + static String getWeightMeasurementResult = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResult'; + static String addWeightMeasurementResult = 'Services/Patients.svc/REST/Patient_AddWeightMeasurementResult'; + static String updateWeightMeasurementResult = 'Services/Patients.svc/REST/Patient_UpdateWeightMeasurementResult'; + static String deactivateWeightMeasurementStatus = 'Services/Patients.svc/REST/Patient_DeactivateWeightMeasurementStatus'; + static String sendAverageBodyWeightReport = 'Services/Notifications.svc/REST/SendAverageBodyWeightReport'; + // ************ static values for Api **************** static final double appVersionID = 50.3; static final int appChannelId = 3; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 2fdc3891..741cb6b6 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -267,16 +267,21 @@ class AppAssets { static const String yellowArrowDownIcon = '$svgBasePath/yellow_arrow_down_icon.svg'; static const String greenTickIcon = '$svgBasePath/green_tick_icon.svg'; - // PNGS - - static const String bloodSugar = '$svgBasePath/bloodsugar.svg'; static const String bloodCholestrol = '$svgBasePath/bloodcholestrol.svg'; static const String triglycerides = '$svgBasePath/triglycerides.svg'; static const String bulb = '$svgBasePath/bulb.svg'; static const String switchBtn = '$svgBasePath/switch.svg'; + //Health Trackers + static const String bloodPressureIcon = '$svgBasePath/blood_pressure_icon.svg'; + static const String bloodSugarOnlyIcon = '$svgBasePath/blood_sugar_only_icon.svg'; + static const String weightIcon = '$svgBasePath/weight_icon.svg'; + static const String normalStatusGreenIcon = '$svgBasePath/normal_status_green_icon.svg'; + static const String sendEmailIcon = '$svgBasePath/send_email_icon.svg'; + static const String lowIndicatorIcon = '$svgBasePath/low_indicator_icon.svg'; + // Health Calculators static const String ovulationAccordion = '$svgBasePath/approximate_ovulation_accordion.svg'; static const String nextPeriodAccordion = '$svgBasePath/next_period_accordion.svg'; static const String fertileAccordion = '$svgBasePath/fertile_window_accordion.svg'; @@ -285,9 +290,7 @@ 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'; @@ -296,8 +299,6 @@ class AppAssets { 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'; @@ -324,8 +325,6 @@ 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'; - - } class AppAnimations { @@ -346,6 +345,4 @@ class AppAnimations { static const String ambulance = '$lottieBasePath/ambulance.json'; static const String ambulanceAlert = '$lottieBasePath/ambulance_alert.json'; static const String rrtAmbulance = '$lottieBasePath/rrt_ambulance.json'; - - } diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index c6c5554b..f284b39b 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -18,6 +18,7 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_repo.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_repo.dart'; @@ -50,6 +51,7 @@ import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -141,6 +143,7 @@ class AppDependencies { getIt.registerLazySingleton(() => BloodDonationRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => WaterMonitorRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => MyInvoicesRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => HealthTrackersRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton @@ -167,10 +170,7 @@ class AppDependencies { ); getIt.registerLazySingleton( - () => HabibWalletViewModel( - habibWalletRepo: getIt(), - errorHandlerService: getIt() - ), + () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()), ); getIt.registerLazySingleton( @@ -273,8 +273,13 @@ class AppDependencies { getIt.registerLazySingleton(() => HealthProvider()); - getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt())); + getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + getIt.registerLazySingleton(() => MyInvoicesViewModel( + myInvoicesRepo: getIt(), + errorHandlerService: getIt(), + navServices: getIt(), + )); + getIt.registerLazySingleton(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt())); } } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 8fd4818c..6dc3bf68 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -16,7 +16,7 @@ enum CountryEnum { saudiArabia, unitedArabEmirates } enum CalenderEnum { gregorian, hijri } -enum SelectionTypeEnum { dropdown, calendar, search } +enum SelectionTypeEnum { dropdown, calendar, search, time } enum GenderTypeEnum { male, female } @@ -38,7 +38,19 @@ enum HealthCalConEnum { calculator, converter } enum HealthCalculatorEnum { general, women } -enum HealthCalculatorsTypeEnum { bmi, calories, bmr, idealBodyWeight, bodyFat, crabsProteinFat, ovulation, deliveryDueDate, bloodSugar, bloodCholesterol, triglycerides } +enum HealthCalculatorsTypeEnum { + bmi, + calories, + bmr, + idealBodyWeight, + bodyFat, + crabsProteinFat, + ovulation, + deliveryDueDate, + bloodSugar, + bloodCholesterol, + triglycerides +} extension HealthCalculatorExtenshion on HealthCalculatorsTypeEnum { String get displayName { @@ -297,3 +309,5 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { // SymptomsChecker enum PossibleConditionsSeverityEnum { seekMedicalAdvice, monitorOnly, emergency } + +enum HealthTrackerTypeEnum { bloodSugar, bloodPressure, weightTracker } diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index 4fdc09cf..02b81957 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -7,16 +7,14 @@ import 'package:flutter/material.dart'; // These are the Viewport values of your const num figmaDesignWidth = 375; // iPhone X / 12 base width const num figmaDesignHeight = 812; // iPhone X / 12 base height - extension ConstrainedResponsive on num { /// Width with max cap for tablets - double get wCapped => isTablet ? math.min( w, this * 1.3) : w; + double get wCapped => isTablet ? math.min(w, this * 1.3) : w; /// Height with max cap for tablets double get hCapped => isTablet ? math.min(h, this * 1.3) : h; } - extension ResponsiveExtension on num { double get _screenWidth => SizeUtils.width; diff --git a/lib/features/health_trackers/health_trackers_repo.dart b/lib/features/health_trackers/health_trackers_repo.dart new file mode 100644 index 00000000..e6930f9c --- /dev/null +++ b/lib/features/health_trackers/health_trackers_repo.dart @@ -0,0 +1,752 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Progress types to request different ranges from the progress API. +enum ProgressType { today, week, month } + +abstract class HealthTrackersRepo { + // ==================== BLOOD SUGAR (DIABETIC) ==================== + /// Get blood sugar result averages (week, month, year). + Future>> getDiabeticResultAverage(); + + /// Get blood sugar results (week, month, year). + Future>> getDiabeticResults(); + + /// Add new blood sugar result. + Future>> addDiabeticResult({ + required String bloodSugarDateChart, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + }); + + /// Update existing blood sugar result. + Future>> updateDiabeticResult({ + required DateTime month, + required DateTime hour, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + required int lineItemNo, + }); + + /// Deactivate blood sugar record. + Future>> deactivateDiabeticStatus({ + required int lineItemNo, + }); + + // ==================== BLOOD PRESSURE ==================== + /// Get blood pressure result averages (week, month, year). + Future>> getBloodPressureResultAverage(); + + /// Get blood pressure results (week, month, year). + Future>> getBloodPressureResults(); + + /// Add new blood pressure result. + Future>> addBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + }); + + /// Update existing blood pressure result. + Future>> updateBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + required int lineItemNo, + }); + + /// Deactivate blood pressure record. + Future>> deactivateBloodPressureStatus({ + required int lineItemNo, + }); + + // ==================== WEIGHT MEASUREMENT ==================== + /// Get weight measurement result averages (week, month, year). + Future>> getWeightMeasurementResultAverage(); + + /// Get weight measurement results (week, month, year). + Future>> getWeightMeasurementResults(); + + /// Add new weight measurement result. + Future>> addWeightMeasurementResult({ + required String weightDate, + required String weightMeasured, + required int weightUnit, + }); + + /// Update existing weight measurement result. + Future>> updateWeightMeasurementResult({ + required int lineItemNo, + required int weightUnit, + required String weightMeasured, + required String weightDate, + }); + + /// Deactivate weight measurement record. + Future>> deactivateWeightMeasurementStatus({ + required int lineItemNo, + }); +} + +class HealthTrackersRepoImp implements HealthTrackersRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + HealthTrackersRepoImp({required this.loggerService, required this.apiClient}); + + // ==================== BLOOD SUGAR (DIABETIC) METHODS ==================== + + @override + Future>> getDiabeticResultAverage() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getDiabeticResultAverage, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract average lists + extracted = { + 'monthAverageList': response['List_MonthDiabtectResultAverage'] ?? [], + 'weekAverageList': response['List_WeekDiabtectResultAverage'] ?? [], + 'yearAverageList': response['List_YearDiabtecResultAverage'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getDiabeticResults() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getDiabeticResult, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract patient result lists + extracted = { + 'monthResultList': response['List_MonthDiabtecPatientResult'] ?? [], + 'weekResultList': response['List_WeekDiabtecPatientResult'] ?? [], + 'yearResultList': response['List_YearDiabtecPatientResult'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addDiabeticResult({ + required String bloodSugarDateChart, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'BloodSugerDateChart': bloodSugarDateChart, + 'BloodSugerResult': bloodSugarResult, + 'DiabtecUnit': diabeticUnit, + 'MeasuredTime': measuredTime + 1, // Add 1 as per old service + }; + + await apiClient.post( + ApiConsts.addDiabeticResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateDiabeticResult({ + required DateTime month, + required DateTime hour, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + // Format: 'YYYY-MM-DD HH:MM:SS' as per old service + String formattedDate = '${month.year}-${month.month}-${month.day} ${hour.hour}:${hour.minute}:00'; + + Map body = { + 'BloodSugerDateChart': formattedDate, + 'BloodSugerResult': bloodSugarResult, + 'DiabtecUnit': diabeticUnit, + 'MeasuredTime': measuredTime + 1, // Add 1 as per old service + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.updateDiabeticResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> deactivateDiabeticStatus({ + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.deactivateDiabeticStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + // ==================== BLOOD PRESSURE METHODS ==================== + + @override + Future>> getBloodPressureResultAverage() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBloodPressureResultAverage, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract all three list types + extracted = { + 'monthList': response['List_MonthBloodPressureResultAverage'] ?? [], + 'weekList': response['List_WeekBloodPressureResultAverage'] ?? [], + 'yearList': response['List_YearBloodPressureResultAverage'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getBloodPressureResults() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBloodPressureResult, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract all three list types + extracted = { + 'weekList': response['List_WeekBloodPressureResult'] ?? [], + 'monthList': response['List_MonthBloodPressureResult'] ?? [], + 'yearList': response['List_YearBloodPressureResult'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'BloodPressureDate': bloodPressureDate, + 'DiastolicPressure': diastolicPressure, + 'SystolicePressure': systolicePressure, + 'MeasuredArm': measuredArm, + }; + + await apiClient.post( + ApiConsts.addBloodPressureResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'BloodPressureDate': bloodPressureDate, + 'DiastolicPressure': diastolicPressure, + 'SystolicePressure': systolicePressure, + 'MeasuredArm': measuredArm, + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.updateBloodPressureResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> deactivateBloodPressureStatus({ + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.deactivateBloodPressureStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + // ==================== WEIGHT MEASUREMENT METHODS ==================== + + @override + Future>> getWeightMeasurementResultAverage() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getWeightMeasurementResultAverage, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract average lists + extracted = { + 'monthAverageList': response['List_MonthWeightMeasurementResultAverage'] ?? [], + 'weekAverageList': response['List_WeekWeightMeasurementResultAverage'] ?? [], + 'yearAverageList': response['List_YearWeightMeasurementResultAverage'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getWeightMeasurementResults() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getWeightMeasurementResult, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract result lists + extracted = { + 'weekResultList': response['List_WeekWeightMeasurementResult'] ?? [], + 'monthResultList': response['List_MonthWeightMeasurementResult'] ?? [], + 'yearResultList': response['List_YearWeightMeasurementResult'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addWeightMeasurementResult({ + required String weightDate, + required String weightMeasured, + required int weightUnit, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'WeightDate': weightDate, + 'WeightMeasured': weightMeasured, + 'weightUnit': weightUnit, + }; + + await apiClient.post( + ApiConsts.addWeightMeasurementResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateWeightMeasurementResult({ + required int lineItemNo, + required int weightUnit, + required String weightMeasured, + required String weightDate, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + 'weightUnit': '$weightUnit', // Convert to string as per old service + 'WeightMeasured': weightMeasured, + 'WeightDate': weightDate, + }; + + await apiClient.post( + ApiConsts.updateWeightMeasurementResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> deactivateWeightMeasurementStatus({ + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.deactivateWeightMeasurementStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart b/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart new file mode 100644 index 00000000..f024d8e1 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart @@ -0,0 +1,87 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class BloodPressureResult { + int? patientID; + int? lineItemNo; + DateTime? bloodPressureDate; + int? measuredArm; + int? systolicePressure; + int? diastolicPressure; + dynamic remark; + bool? isActive; + int? chartYear; + String? chartMonth; + dynamic yearSystolicePressureAverageResult; + dynamic monthSystolicePressureResult; + dynamic weekSystolicePressureResult; + int? yearDiastolicPressureAverageResult; + dynamic monthDiastolicPressureResult; + dynamic weekDiastolicPressureResult; + String? measuredArmDesc; + dynamic weekDesc; + + BloodPressureResult( + {this.patientID, + this.lineItemNo, + this.bloodPressureDate, + this.measuredArm, + this.systolicePressure, + this.diastolicPressure, + this.remark, + this.isActive, + this.chartYear, + this.chartMonth, + this.yearSystolicePressureAverageResult, + this.monthSystolicePressureResult, + this.weekSystolicePressureResult, + this.yearDiastolicPressureAverageResult, + this.monthDiastolicPressureResult, + this.weekDiastolicPressureResult, + this.measuredArmDesc, + this.weekDesc}); + + BloodPressureResult.fromJson(Map json) { + patientID = json['PatientID']; + lineItemNo = json['LineItemNo']; + bloodPressureDate = DateUtil.convertStringToDate(json['BloodPressureDate']); + measuredArm = json['MeasuredArm']; + systolicePressure = json['SystolicePressure']; + diastolicPressure = json['DiastolicPressure']; + remark = json['Remark']; + isActive = json['IsActive']; + chartYear = json['ChartYear']; + chartMonth = json['ChartMonth']; + yearSystolicePressureAverageResult = json['YearSystolicePressureAverageResult']; + monthSystolicePressureResult = json['MonthSystolicePressureResult']; + weekSystolicePressureResult = json['WeekSystolicePressureResult']; + yearDiastolicPressureAverageResult = json['YearDiastolicPressureAverageResult']; + monthDiastolicPressureResult = json['MonthDiastolicPressureResult']; + weekDiastolicPressureResult = json['WeekDiastolicPressureResult']; + measuredArmDesc = json['MeasuredArmDesc']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['PatientID'] = patientID; + data['LineItemNo'] = lineItemNo; + data['BloodPressureDate'] = bloodPressureDate; + data['MeasuredArm'] = measuredArm; + data['SystolicePressure'] = systolicePressure; + data['DiastolicPressure'] = diastolicPressure; + data['Remark'] = remark; + data['IsActive'] = isActive; + data['ChartYear'] = chartYear; + data['ChartMonth'] = chartMonth; + data['YearSystolicePressureAverageResult'] = yearSystolicePressureAverageResult; + data['MonthSystolicePressureResult'] = monthSystolicePressureResult; + data['WeekSystolicePressureResult'] = weekSystolicePressureResult; + data['YearDiastolicPressureAverageResult'] = yearDiastolicPressureAverageResult; + data['MonthDiastolicPressureResult'] = monthDiastolicPressureResult; + data['WeekDiastolicPressureResult'] = weekDiastolicPressureResult; + data['MeasuredArmDesc'] = measuredArmDesc; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart new file mode 100644 index 00000000..3975805f --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart @@ -0,0 +1,58 @@ +class MonthBloodPressureResultAverage { + dynamic weekfourSystolicePressureAverageResult; + dynamic weekfourDiastolicPressureAverageResult; + dynamic weekthreeSystolicePressureAverageResult; + dynamic weekthreeDiastolicPressureAverageResult; + dynamic weektwoSystolicePressureAverageResult; + dynamic weektwoDiastolicPressureAverageResult; + dynamic weekoneSystolicePressureAverageResult; + dynamic weekoneDiastolicPressureAverageResult; + String? weekDesc; + int? weekDiastolicPressureAverageResult; + int? weekSystolicePressureAverageResult; + + MonthBloodPressureResultAverage({ + this.weekfourSystolicePressureAverageResult, + this.weekfourDiastolicPressureAverageResult, + this.weekthreeSystolicePressureAverageResult, + this.weekthreeDiastolicPressureAverageResult, + this.weektwoSystolicePressureAverageResult, + this.weektwoDiastolicPressureAverageResult, + this.weekoneSystolicePressureAverageResult, + this.weekoneDiastolicPressureAverageResult, + this.weekDesc, + this.weekDiastolicPressureAverageResult, + this.weekSystolicePressureAverageResult, + }); + + MonthBloodPressureResultAverage.fromJson(Map json) { + weekfourSystolicePressureAverageResult = json['weekfourSystolicePressureAverageResult']; + weekfourDiastolicPressureAverageResult = json['weekfourDiastolicPressureAverageResult']; + weekthreeSystolicePressureAverageResult = json['weekthreeSystolicePressureAverageResult']; + weekthreeDiastolicPressureAverageResult = json['weekthreeDiastolicPressureAverageResult']; + weektwoSystolicePressureAverageResult = json['weektwoSystolicePressureAverageResult']; + weektwoDiastolicPressureAverageResult = json['weektwoDiastolicPressureAverageResult']; + weekoneSystolicePressureAverageResult = json['weekoneSystolicePressureAverageResult']; + weekoneDiastolicPressureAverageResult = json['weekoneDiastolicPressureAverageResult']; + weekDesc = json['WeekDesc']; + weekDiastolicPressureAverageResult = json['WeekDiastolicPressureAverageResult']; + weekSystolicePressureAverageResult = json['WeekSystolicePressureAverageResult']; + } + + Map toJson() { + final Map data = {}; + data['weekfourSystolicePressureAverageResult'] = weekfourSystolicePressureAverageResult; + data['weekfourDiastolicPressureAverageResult'] = weekfourDiastolicPressureAverageResult; + data['weekthreeSystolicePressureAverageResult'] = weekthreeSystolicePressureAverageResult; + data['weekthreeDiastolicPressureAverageResult'] = weekthreeDiastolicPressureAverageResult; + data['weektwoSystolicePressureAverageResult'] = weektwoSystolicePressureAverageResult; + data['weektwoDiastolicPressureAverageResult'] = weektwoDiastolicPressureAverageResult; + data['weekoneSystolicePressureAverageResult'] = weekoneSystolicePressureAverageResult; + data['weekoneDiastolicPressureAverageResult'] = weekoneDiastolicPressureAverageResult; + data['WeekDesc'] = weekDesc; + data['WeekDiastolicPressureAverageResult'] = weekDiastolicPressureAverageResult; + data['WeekSystolicePressureAverageResult'] = weekSystolicePressureAverageResult; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart new file mode 100644 index 00000000..381d514f --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart @@ -0,0 +1,24 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekBloodPressureResultAverage { + int? dailySystolicePressureAverageResult; + int? dailyDiastolicPressureAverageResult; + DateTime? bloodPressureDate; + + WeekBloodPressureResultAverage({this.dailySystolicePressureAverageResult, this.dailyDiastolicPressureAverageResult, this.bloodPressureDate}); + + WeekBloodPressureResultAverage.fromJson(Map json) { + dailySystolicePressureAverageResult = json['DailySystolicePressureAverageResult']; + dailyDiastolicPressureAverageResult = json['DailyDiastolicPressureAverageResult']; + bloodPressureDate = DateUtil.convertStringToDate(json['BloodPressureDate']); + } + + Map toJson() { + final Map data = {}; + data['DailySystolicePressureAverageResult'] = dailySystolicePressureAverageResult; + data['DailyDiastolicPressureAverageResult'] = dailyDiastolicPressureAverageResult; + data['BloodPressureDate'] = bloodPressureDate; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart new file mode 100644 index 00000000..6f342461 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart @@ -0,0 +1,37 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearBloodPressureResultAverage { + int? monthSystolicePressureAverageResult; + int? monthDiastolicPressureAverageResult; + dynamic monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearBloodPressureResultAverage({ + this.monthSystolicePressureAverageResult, + this.monthDiastolicPressureAverageResult, + this.monthNumber, + this.monthName, + this.yearName, + }); + + YearBloodPressureResultAverage.fromJson(Map json) { + monthSystolicePressureAverageResult = json['monthSystolicePressureAverageResult']; + monthDiastolicPressureAverageResult = json['monthDiastolicPressureAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } + + Map toJson() { + final Map data = {}; + data['monthSystolicePressureAverageResult'] = monthSystolicePressureAverageResult; + data['monthDiastolicPressureAverageResult'] = monthDiastolicPressureAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} diff --git a/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart b/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart new file mode 100644 index 00000000..066df3ca --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart @@ -0,0 +1,99 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class DiabeticPatientResult { + String? chartMonth; + var chartYear; + DateTime? dateChart; + var description; + var descriptionN; + int? diabtecAvarage; + bool? isActive; + int? lineItemNo; + var listMonth; + var listWeek; + int? measured; + String? measuredDesc; + var monthAverageResult; + int? patientID; + var remark; + var resultDesc; + dynamic resultValue; + String? unit; + var weekAverageResult; + String? weekDesc; + var yearAverageResult; + + DiabeticPatientResult( + {this.chartMonth, + this.chartYear, + this.dateChart, + this.description, + this.descriptionN, + this.diabtecAvarage, + this.isActive, + this.lineItemNo, + this.listMonth, + this.listWeek, + this.measured, + this.measuredDesc, + this.monthAverageResult, + this.patientID, + this.remark, + this.resultDesc, + this.resultValue, + this.unit, + this.weekAverageResult, + this.weekDesc, + this.yearAverageResult}); + + DiabeticPatientResult.fromJson(Map json) { + chartMonth = json['ChartMonth']; + chartYear = json['ChartYear']; + dateChart = DateUtil.convertStringToDate(json['DateChart']); + description = json['Description']; + descriptionN = json['DescriptionN']; + diabtecAvarage = json['DiabtecAvarage']; + isActive = json['IsActive']; + lineItemNo = json['LineItemNo']; + listMonth = json['List_Month']; + listWeek = json['List_Week']; + measured = json['Measured']; + measuredDesc = json['MeasuredDesc']; + monthAverageResult = json['MonthAverageResult']; + patientID = json['PatientID']; + remark = json['Remark']; + resultDesc = json['ResultDesc']; + resultValue = json['ResultValue']; + unit = json['Unit']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + yearAverageResult = json['YearAverageResult']; + } + + Map toJson() { + final Map data = {}; + data['ChartMonth'] = chartMonth; + data['ChartYear'] = chartYear; + data['DateChart'] = DateUtil.convertDateToString(dateChart!); + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['DiabtecAvarage'] = diabtecAvarage; + data['IsActive'] = isActive; + data['LineItemNo'] = lineItemNo; + data['List_Month'] = listMonth; + data['List_Week'] = listWeek; + data['Measured'] = measured; + data['MeasuredDesc'] = measuredDesc; + data['MonthAverageResult'] = monthAverageResult; + data['PatientID'] = patientID; + data['Remark'] = remark; + data['ResultDesc'] = resultDesc; + data['ResultValue'] = resultValue; + data['Unit'] = unit; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + data['YearAverageResult'] = yearAverageResult; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart new file mode 100644 index 00000000..77b06f11 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart @@ -0,0 +1,37 @@ +class MonthDiabeticResultAverage { + var weekfourAverageResult; + var weekthreeAverageResult; + var weektwoAverageResult; + var weekoneAverageResult; + dynamic weekAverageResult; + String? weekDesc; + + MonthDiabeticResultAverage( + {this.weekfourAverageResult, + this.weekthreeAverageResult, + this.weektwoAverageResult, + this.weekoneAverageResult, + this.weekAverageResult, + this.weekDesc}); + + MonthDiabeticResultAverage.fromJson(Map json) { + weekfourAverageResult = json['weekfourAverageResult']; + weekthreeAverageResult = json['weekthreeAverageResult']; + weektwoAverageResult = json['weektwoAverageResult']; + weekoneAverageResult = json['weekoneAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['weekfourAverageResult'] = weekfourAverageResult; + data['weekthreeAverageResult'] = weekthreeAverageResult; + data['weektwoAverageResult'] = weektwoAverageResult; + data['weekoneAverageResult'] = weekoneAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart new file mode 100644 index 00000000..3b35fe79 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart @@ -0,0 +1,21 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekDiabeticResultAverage { + dynamic dailyAverageResult; + DateTime? dateChart; + + WeekDiabeticResultAverage({this.dailyAverageResult, this.dateChart}); + + WeekDiabeticResultAverage.fromJson(Map json) { + dailyAverageResult = json['DailyAverageResult']; + dateChart = DateUtil.convertStringToDate(json['DateChart']); + } + + Map toJson() { + final Map data = {}; + data['DailyAverageResult'] = dailyAverageResult; + data['DateChart'] = DateUtil.convertDateToString(dateChart!); + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart new file mode 100644 index 00000000..fb340564 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart @@ -0,0 +1,35 @@ +import 'dart:developer'; + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearDiabeticResultAverage { + dynamic monthAverageResult; + var monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearDiabeticResultAverage({this.monthAverageResult, this.monthNumber, this.monthName, this.yearName}); + + YearDiabeticResultAverage.fromJson(Map json) { + try { + monthAverageResult = json['monthAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } catch (e) { + log(e.toString()); + } + } + + Map toJson() { + final Map data = {}; + data['monthAverageResult'] = monthAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart new file mode 100644 index 00000000..f95470c5 --- /dev/null +++ b/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart @@ -0,0 +1,37 @@ +class MonthWeightMeasurementResultAverage { + dynamic weekfourAverageResult; + dynamic weekthreeAverageResult; + dynamic weektwoAverageResult; + dynamic weekoneAverageResult; + dynamic weekAverageResult; + String? weekDesc; + + MonthWeightMeasurementResultAverage( + {this.weekfourAverageResult, + this.weekthreeAverageResult, + this.weektwoAverageResult, + this.weekoneAverageResult, + this.weekAverageResult, + this.weekDesc}); + + MonthWeightMeasurementResultAverage.fromJson(Map json) { + weekfourAverageResult = json['weekfourAverageResult']; + weekthreeAverageResult = json['weekthreeAverageResult']; + weektwoAverageResult = json['weektwoAverageResult']; + weekoneAverageResult = json['weekoneAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['weekfourAverageResult'] = weekfourAverageResult; + data['weekthreeAverageResult'] = weekthreeAverageResult; + data['weektwoAverageResult'] = weektwoAverageResult; + data['weekoneAverageResult'] = weekoneAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart new file mode 100644 index 00000000..ad53325e --- /dev/null +++ b/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart @@ -0,0 +1,21 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekWeightMeasurementResultAverage { + dynamic dailyAverageResult; + DateTime? weightDate; + + WeekWeightMeasurementResultAverage({this.dailyAverageResult, this.weightDate}); + + WeekWeightMeasurementResultAverage.fromJson(Map json) { + dailyAverageResult = json['DailyAverageResult']; + weightDate = DateUtil.convertStringToDate(json['WeightDate']); + } + + Map toJson() { + final Map data = {}; + data['DailyAverageResult'] = dailyAverageResult; + data['WeightDate'] = weightDate; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/weight_measurement_result.dart b/lib/features/health_trackers/models/weight/weight_measurement_result.dart new file mode 100644 index 00000000..896d33f6 --- /dev/null +++ b/lib/features/health_trackers/models/weight/weight_measurement_result.dart @@ -0,0 +1,77 @@ +import 'dart:developer'; + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeightMeasurementResult { + int? patientID; + int? lineItemNo; + int? weightMeasured; + DateTime? weightDate; + dynamic remark; + bool? isActive; + int? measured; + dynamic unit; + int? chartYear; + dynamic chartMonth; + double? yearAverageResult; + dynamic monthAverageResult; + dynamic weekAverageResult; + dynamic weekDesc; + + WeightMeasurementResult( + {this.patientID, + this.lineItemNo, + this.weightMeasured, + this.weightDate, + this.remark, + this.isActive, + this.measured, + this.unit, + this.chartYear, + this.chartMonth, + this.yearAverageResult, + this.monthAverageResult, + this.weekAverageResult, + this.weekDesc}); + + WeightMeasurementResult.fromJson(Map json) { + try { + patientID = json['PatientID']; + lineItemNo = json['LineItemNo']; + weightMeasured = json['WeightMeasured']; + weightDate = DateUtil.convertStringToDate(json['WeightDate']); + remark = json['Remark']; + isActive = json['IsActive']; + measured = json['Measured']; + unit = json['Unit']; + chartYear = json['ChartYear']; + chartMonth = json['ChartMonth']; + // Convert to double safely since API may return int + yearAverageResult = json['YearAverageResult'] != null ? (json['YearAverageResult'] as num).toDouble() : null; + monthAverageResult = json['MonthAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } catch (e) { + log(e.toString()); + } + } + + Map toJson() { + final Map data = {}; + data['PatientID'] = patientID; + data['LineItemNo'] = lineItemNo; + data['WeightMeasured'] = weightMeasured; + data['WeightDate'] = weightDate; + data['Remark'] = remark; + data['IsActive'] = isActive; + data['Measured'] = measured; + data['Unit'] = unit; + data['ChartYear'] = chartYear; + data['ChartMonth'] = chartMonth; + data['YearAverageResult'] = yearAverageResult; + data['MonthAverageResult'] = monthAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} diff --git a/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart new file mode 100644 index 00000000..7dc4b246 --- /dev/null +++ b/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart @@ -0,0 +1,29 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearWeightMeasurementResultAverage { + dynamic monthAverageResult; + int? monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearWeightMeasurementResultAverage({this.monthAverageResult, this.monthNumber, this.monthName, this.yearName}); + + YearWeightMeasurementResultAverage.fromJson(Map json) { + monthAverageResult = json['monthAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } + + Map toJson() { + final Map data = {}; + data['monthAverageResult'] = monthAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} + diff --git a/lib/features/water_monitor/water_monitor_view_model.dart b/lib/features/water_monitor/water_monitor_view_model.dart index 18ffdddc..d82712a1 100644 --- a/lib/features/water_monitor/water_monitor_view_model.dart +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -16,14 +16,19 @@ import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_mode import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class WaterMonitorViewModel extends ChangeNotifier { WaterMonitorRepo waterMonitorRepo; + ErrorHandlerService errorHandlerService; - WaterMonitorViewModel({required this.waterMonitorRepo}); + WaterMonitorViewModel({ + required this.waterMonitorRepo, + required this.errorHandlerService, + }); // Controllers final TextEditingController nameController = TextEditingController(); @@ -190,9 +195,9 @@ class WaterMonitorViewModel extends ChangeNotifier { identificationNo: identification, ); - result.fold((failure) { - log('Error fetching user progress: ${failure.message}'); - }, (apiModel) { + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { log("User Progress Data ($_selectedDuration): ${apiModel.data.toString()}"); // Parse the response based on progress type @@ -300,10 +305,13 @@ class WaterMonitorViewModel extends ChangeNotifier { identificationNo: identification, ); - result.fold((failure) { - _userDetailData = null; - if (onError != null) onError(failure.message); - }, (apiModel) { + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + _userDetailData = null; + if (onError != null) onError(failure.message); + }, + (apiModel) { _userDetailData = apiModel.data; // Populate form fields from the fetched data @@ -322,7 +330,7 @@ class WaterMonitorViewModel extends ChangeNotifier { if (_userDetailData == null) { try { - _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsScreen); + _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsPage); } catch (navErr) { log('Navigation to water monitor settings failed: $navErr'); } @@ -686,6 +694,7 @@ class WaterMonitorViewModel extends ChangeNotifier { return result.fold( (failure) { + errorHandlerService.handleError(failure: failure); _validationError = failure.message; _isLoading = false; notifyListeners(); @@ -1060,6 +1069,7 @@ class WaterMonitorViewModel extends ChangeNotifier { return result.fold( (failure) { + errorHandlerService.handleError(failure: failure); log('Error inserting user activity: ${failure.message}'); _isLoading = false; notifyListeners(); @@ -1134,6 +1144,7 @@ class WaterMonitorViewModel extends ChangeNotifier { return result.fold( (failure) { + errorHandlerService.handleError(failure: failure); log('Error undoing user activity: ${failure.message}'); _isLoading = false; notifyListeners(); diff --git a/lib/main.dart b/lib/main.dart index a9d16dc8..f0537b31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,6 +35,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_v import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -173,6 +174,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart index eb22ab65..42cba5df 100644 --- a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart +++ b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bf.dart'; @@ -12,17 +17,12 @@ import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ibw.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ovulation.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/triglycerides.dart'; -import 'package:provider/provider.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; class HealthCalculatorDetailedPage extends StatefulWidget { HealthCalculatorsTypeEnum calculatorType; @@ -50,8 +50,8 @@ class _HealthCalculatorDetailedPageState extends State months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const List months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; return months[month - 1]; } @@ -828,7 +846,8 @@ class HealthCalcualtorViewModel extends ChangeNotifier { // expose map-like results for widgets to forward to parent Map? get bmiResultMap => bmiResult == null ? null : {'bmiResult': bmiResult, 'bmiCategory': bmiCategory}; - Map? get caloriesResultMap => caloriesResult == null ? null : {'calories': caloriesResult, 'bmr': bmrResult, 'calorieRange': calorieRange, 'bmrRange': bmrRange}; + Map? get caloriesResultMap => + caloriesResult == null ? null : {'calories': caloriesResult, 'bmr': bmrResult, 'calorieRange': calorieRange, 'bmrRange': bmrRange}; Map? get ibwResultMap => ibwResult == null ? null : {'ibw': ibwResult, 'difference': weightDifference, 'status': weightStatus}; diff --git a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart new file mode 100644 index 00000000..56f82fc2 --- /dev/null +++ b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart @@ -0,0 +1,567 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class AddHealthTrackerEntryPage extends StatefulWidget { + final HealthTrackerTypeEnum trackerType; + + const AddHealthTrackerEntryPage({ + super.key, + required this.trackerType, + }); + + @override + State createState() => _AddHealthTrackerEntryPageState(); +} + +class _AddHealthTrackerEntryPageState extends State { + late DialogService dialogService; + + // Controllers for date and time + final TextEditingController dateController = TextEditingController(); + final TextEditingController timeController = TextEditingController(); + + @override + void initState() { + super.initState(); + dialogService = getIt.get(); + } + + @override + void dispose() { + dateController.dispose(); + timeController.dispose(); + super.dispose(); + } + + /// Get page title based on tracker type + String _getPageTitle() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Add Blood Sugar".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Add Blood Pressure".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Add Weight".needTranslation; + } + } + + /// Get success message based on tracker type + String _getSuccessMessage() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Blood Sugar Data saved successfully".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Blood Pressure Data saved successfully".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Weight Data saved successfully".needTranslation; + } + } + + /// Save entry based on tracker type + Future _saveEntry(HealthTrackersViewModel viewModel) async { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + await _saveBloodSugarEntry(viewModel); + break; + case HealthTrackerTypeEnum.bloodPressure: + await _saveBloodPressureEntry(viewModel); + break; + case HealthTrackerTypeEnum.weightTracker: + await _saveWeightEntry(viewModel); + break; + } + } + + // Save Blood Sugar entry + Future _saveBloodSugarEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveBloodSugarEntry( + dateTime: dateTime, + measureTime: viewModel.selectedBloodSugarMeasureTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Save Weight entry + Future _saveWeightEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveWeightEntry( + dateTime: dateTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Save Blood Pressure entry + Future _saveBloodPressureEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveBloodPressureEntry( + dateTime: dateTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Show success message and pop back + void _showSuccessAndPop() { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: _getSuccessMessage(), + ), + callBackFunc: () { + Navigator.pop(context); + }, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + // Reusable method to show selection bottom sheet + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + // Blood Sugar unit selection + void _showBloodSugarUnitSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.bloodSugarUnit, + selectedValue: viewModel.selectedBloodSugarUnit, + onSelected: viewModel.setBloodSugarUnit, + useUpperCase: false, + ); + } + + // Blood Sugar measure time selection + void _showBloodSugarEntryTimeBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Measure Time".needTranslation, + items: viewModel.bloodSugarMeasureTimeEnList, + selectedValue: viewModel.selectedBloodSugarMeasureTime, + onSelected: viewModel.setBloodSugarMeasureTime, + useUpperCase: false, + ); + } + + // Weight unit selection + void _showWeightUnitSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.weightUnits, + selectedValue: viewModel.selectedWeightUnit, + onSelected: viewModel.setWeightUnit, + useUpperCase: false, + ); + } + + // Blood Pressure measured arm selection + void _showMeasuredArmSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Arm".needTranslation, + items: viewModel.measuredArmList, + selectedValue: viewModel.selectedMeasuredArm, + onSelected: viewModel.setMeasuredArm, + useUpperCase: false, + ); + } + + // Reusable method to build text field + Widget _buildTextField(TextEditingController controller, String hintText, {TextInputType keyboardType = TextInputType.name}) { + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: 1, + cursorHeight: 14.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hintText, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ); + } + + // Reusable method to build settings row + Widget _buildSettingsRow({ + required String icon, + required String label, + String? value, + Widget? inputField, + String? unit, + VoidCallback? onUnitTap, + VoidCallback? onRowTap, + Color? iconColor, + bool showDivider = true, + }) { + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 40.w, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.greyColor, + borderRadius: 10.r, + hasShadow: false, + ), + child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w, iconColor: iconColor)), + ), + SizedBox(width: 12.w), + Expanded( + flex: unit != null ? 2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + if (inputField != null) + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: inputField, + ) + else if (value != null && value.isNotEmpty) + value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ), + if (unit != null) ...[ + Container( + width: 1.w, + height: 30.w, + color: AppColors.dividerColor, + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(onUnitTap ?? () {}), + ), + ] else if (onRowTap != null) ...[ + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 8.w), + ], + ], + ).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}), + if (showDivider) Divider(height: 1, color: AppColors.dividerColor), + ], + ); + } + + /// Build form fields based on tracker type + Widget _buildFormFields(HealthTrackersViewModel viewModel) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return _buildBloodSugarForm(viewModel); + case HealthTrackerTypeEnum.bloodPressure: + return _buildBloodPressureForm(viewModel); + case HealthTrackerTypeEnum.weightTracker: + return _buildWeightForm(viewModel); + } + } + + /// Blood Sugar form fields + Widget _buildBloodSugarForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.heightIcon, + label: "Enter Blood Sugar".needTranslation, + inputField: _buildTextField(viewModel.bloodSugarController, '', keyboardType: TextInputType.number), + unit: viewModel.selectedBloodSugarUnit, + onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + Divider(height: 1, color: AppColors.dividerColor), + _buildSettingsRow( + icon: AppAssets.weight_tracker_icon, + label: "Select Measure Time".needTranslation, + value: viewModel.selectedBloodSugarMeasureTime, + onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel), + ), + ], + ); + } + + /// Blood Pressure form fields + Widget _buildBloodPressureForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.bloodPressureIcon, + iconColor: AppColors.greyTextColor, + label: "Enter Systolic Value".needTranslation, + inputField: _buildTextField(viewModel.systolicController, '', keyboardType: TextInputType.number), + ), + _buildSettingsRow( + icon: AppAssets.bloodPressureIcon, + iconColor: AppColors.greyTextColor, + label: "Enter Diastolic Value".needTranslation, + inputField: _buildTextField(viewModel.diastolicController, '', keyboardType: TextInputType.number), + ), + _buildSettingsRow( + icon: AppAssets.bodyIcon, + iconColor: AppColors.greyTextColor, + label: "Select Arm".needTranslation, + value: viewModel.selectedMeasuredArm, + onRowTap: () => _showMeasuredArmSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + ], + ); + } + + /// Weight form fields + Widget _buildWeightForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.weightScale, + label: "Enter Weight".needTranslation, + inputField: _buildTextField(viewModel.weightController, '', keyboardType: TextInputType.number), + unit: viewModel.selectedWeightUnit, + onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + ], + ); + } + + /// Common date and time fields + Widget _buildDateTimeFields() { + return Column( + children: [ + TextInputWidget( + controller: dateController, + isReadOnly: true, + isArrowTrailing: true, + labelText: "Date", + hintText: "Select date".needTranslation, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.calendarGrey, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + try { + final parsedDate = DateTime.parse(val); + final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate); + dateController.text = formattedDate; + log("date: $formattedDate"); + } catch (e) { + dateController.text = val; + log("date: $val"); + } + }, + ), + TextInputWidget( + controller: timeController, + isReadOnly: true, + isArrowTrailing: true, + labelText: "Time", + hintText: "Select time".needTranslation, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.calendarGrey, + selectionType: SelectionTypeEnum.time, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + timeController.text = val; + log("time: $val"); + }, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: _getPageTitle(), + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Save".needTranslation, + onPressed: () async => await _saveEntry(viewModel), + borderRadius: 12.r, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: _buildFormFields(viewModel), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/health_trackers/health_tracker_detail_page.dart b/lib/presentation/health_trackers/health_tracker_detail_page.dart new file mode 100644 index 00000000..a2b82ecc --- /dev/null +++ b/lib/presentation/health_trackers/health_tracker_detail_page.dart @@ -0,0 +1,1126 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/week_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/year_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/widgets/tracker_last_value_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.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/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class HealthTrackerDetailPage extends StatefulWidget { + final HealthTrackerTypeEnum trackerType; + + const HealthTrackerDetailPage({super.key, required this.trackerType}); + + @override + State createState() => _HealthTrackerDetailPageState(); +} + +class _HealthTrackerDetailPageState extends State { + @override + void initState() { + super.initState(); + // Load data based on tracker type + WidgetsBinding.instance.addPostFrameCallback((_) async { + final viewModel = context.read(); + await _loadTrackerData(viewModel); + }); + } + + /// Load data based on tracker type + Future _loadTrackerData(HealthTrackersViewModel viewModel) async { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + await viewModel.getBloodSugar(); + break; + case HealthTrackerTypeEnum.bloodPressure: + await viewModel.getBloodPressure(); + break; + case HealthTrackerTypeEnum.weightTracker: + await viewModel.getWeight(); + break; + } + } + + /// Get page title based on tracker type + String _getPageTitle() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Blood Sugar".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Blood Pressure".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Weight".needTranslation; + } + } + + /// Get unit based on tracker type + String _getUnit() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return 'mg/dL'; + case HealthTrackerTypeEnum.bloodPressure: + return 'mmHg'; + case HealthTrackerTypeEnum.weightTracker: + return 'kg'; + } + } + + /// Get empty state message based on tracker type + String _getEmptyStateMessage() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Please add data to track your Blood Sugar"; + case HealthTrackerTypeEnum.bloodPressure: + return "Please add data to track your Blood Pressure"; + case HealthTrackerTypeEnum.weightTracker: + return "Please add data to track your Weight"; + } + } + + /// Check if data is empty based on tracker type + bool _hasNoData(HealthTrackersViewModel viewModel) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return viewModel.weekDiabeticPatientResult.isEmpty && + viewModel.monthDiabeticPatientResult.isEmpty && + viewModel.yearDiabeticPatientResult.isEmpty; + case HealthTrackerTypeEnum.bloodPressure: + return viewModel.weekBloodPressureResult.isEmpty && viewModel.monthBloodPressureResult.isEmpty && viewModel.yearBloodPressureResult.isEmpty; + case HealthTrackerTypeEnum.weightTracker: + return viewModel.weekWeightMeasurementResult.isEmpty && + viewModel.monthWeightMeasurementResult.isEmpty && + viewModel.yearWeightMeasurementResult.isEmpty; + } + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + final dialogService = getIt.get(); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + void _showHistoryDurationBottomsheet(BuildContext context, HealthTrackersViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Duration".needTranslation, + items: viewModel.durationFilters, + selectedValue: viewModel.selectedDurationFilter, + onSelected: viewModel.setFilterDuration, + ); + } + + Widget buildHistoryListTile({required String title, required String subTitle, required String measureDesc, double? value}) { + // Get status color and rotation based on value and tracker type + Color statusColor = AppColors.successColor; + double rotation = 0; + + if (value != null) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + if (value < 70) { + statusColor = AppColors.errorColor; + rotation = 0; // pointing down + } else if (value <= 100) { + statusColor = AppColors.successColor; + rotation = -3.14159 / 2; // pointing right + } else if (value <= 125) { + statusColor = AppColors.ratingColorYellow; + rotation = 3.14159; // pointing up + } else { + statusColor = AppColors.errorColor; + rotation = 3.14159; // pointing up + } + break; + case HealthTrackerTypeEnum.bloodPressure: + // Systolic pressure ranges + if (value < 90) { + statusColor = AppColors.errorColor; + rotation = 0; // Low - pointing down + } else if (value <= 120) { + statusColor = AppColors.successColor; + rotation = -3.14159 / 2; // Normal - pointing right + } else if (value <= 140) { + statusColor = AppColors.ratingColorYellow; + rotation = 3.14159; // Elevated - pointing up + } else { + statusColor = AppColors.errorColor; + rotation = 3.14159; // High - pointing up + } + break; + case HealthTrackerTypeEnum.weightTracker: + // Weight doesn't have good/bad indicators, just show neutral + statusColor = AppColors.transparent; + rotation = -3.14159 / 2; // pointing right (neutral) + break; + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AppCustomChipWidget(labelText: title), + if (measureDesc.isNotEmpty) ...[ + SizedBox(width: 8.w), + AppCustomChipWidget(labelText: measureDesc), + ], + ], + ), + SizedBox(height: 4.h), + subTitle.toText16(weight: FontWeight.w600, color: AppColors.textColor), + ], + ), + Transform.rotate( + angle: rotation, + child: Utils.buildSvgWithAssets( + icon: AppAssets.lowIndicatorIcon, + iconColor: statusColor, + height: 20.h, + width: 20.h, + ), + ), + ], + ).paddingSymmetrical(0, 8.h); + } + + Widget _buildHistoryGraphOrList() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Consumer(builder: (BuildContext context, HealthTrackersViewModel viewModel, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + "History".needTranslation.toText16(isBold: true), + if (viewModel.isGraphView) ...[ + SizedBox(width: 12.w), + InkWell( + onTap: () => _showHistoryDurationBottomsheet(context, viewModel), + child: Container( + padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + backgroundColor: AppColors.greyColor, + borderRadius: 8.r, + hasShadow: true, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + viewModel.selectedDurationFilter.toText12(fontWeight: FontWeight.w500), + SizedBox(width: 4.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 16.h), + ], + ), + ), + ), + ], + ], + ), + InkWell( + onTap: () => viewModel.setGraphView(!viewModel.isGraphView), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(viewModel.isGraphView), + child: Utils.buildSvgWithAssets( + icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, + height: 24.h, + width: 24.h, + ), + ), + ), + ), + ], + ), + if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else ...[SizedBox(height: 16.h), _buildHistoryGraph()] + ], + ); + }), + ); + } + + String _formatTime(DateTime time) { + final hour = time.hour; + final minute = time.minute; + final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period'; + } + + String _getDayName(DateTime date) { + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return days[date.weekday - 1]; + } + + String _getMonthName(int monthNumber) { + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + if (monthNumber < 1 || monthNumber > 12) return 'Unknown'; + return months[monthNumber - 1]; + } + + Widget _buildHistoryListView(HealthTrackersViewModel viewModel) { + List listItems = []; + final unit = _getUnit(); + + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + listItems = _buildBloodSugarListItems(viewModel, unit); + break; + case HealthTrackerTypeEnum.bloodPressure: + listItems = _buildBloodPressureListItems(viewModel); + break; + case HealthTrackerTypeEnum.weightTracker: + listItems = _buildWeightListItems(viewModel, unit); + break; + } + + if (viewModel.isLoading) { + return _buildLoadingShimmer().paddingOnly(top: 16.h); + } + + if (listItems.isEmpty) { + return _buildEmptyStateWidget(); + } + + return ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: listItems.length, + separatorBuilder: (context, index) => SizedBox.shrink(), + itemBuilder: (context, index) => listItems[index], + ); + } + + /// Build list items for Blood Sugar + List _buildBloodSugarListItems(HealthTrackersViewModel viewModel, String unit) { + List listItems = []; + final allResults = []; + + allResults.addAll(viewModel.weekDiabeticPatientResult); + allResults.addAll(viewModel.monthDiabeticPatientResult); + allResults.addAll(viewModel.yearDiabeticPatientResult); + + final seenIds = {}; + final uniqueResults = allResults.where((result) { + final id = '${result.lineItemNo}_${result.dateChart?.millisecondsSinceEpoch ?? 0}'; + if (seenIds.contains(id)) return false; + seenIds.add(id); + return true; + }).toList(); + + uniqueResults.sort((a, b) { + final dateA = a.dateChart ?? DateTime(1900); + final dateB = b.dateChart ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + + for (var result in uniqueResults) { + final resultValue = result.resultValue?.toDouble() ?? 0.0; + final value = result.resultValue?.toString() ?? '0'; + final resultUnit = result.unit ?? unit; + final measuredDesc = result.measuredDesc ?? ''; + final date = result.dateChart; + final dateLabel = date != null ? '${_getDayName(date)} ${date.day} ${_getMonthName(date.month).substring(0, 3)}, ${date.year}' : ''; + final timeLabel = date != null ? _formatTime(date) : 'Unknown'; + final displayLabel = date != null ? '$dateLabel, $timeLabel' : 'Unknown'; + final subTitleText = '$value $resultUnit'; + + listItems.add( + Column( + children: [ + buildHistoryListTile( + title: displayLabel, + subTitle: subTitleText, + value: resultValue, + measureDesc: measuredDesc, + ), + Divider(height: 1, color: AppColors.dividerColor).paddingOnly(bottom: 8.h), + ], + ), + ); + } + return listItems; + } + + /// Build list items for Blood Pressure + List _buildBloodPressureListItems(HealthTrackersViewModel viewModel) { + List listItems = []; + final allResults = []; + + allResults.addAll(viewModel.weekBloodPressureResult); + allResults.addAll(viewModel.monthBloodPressureResult); + allResults.addAll(viewModel.yearBloodPressureResult); + + final seenIds = {}; + final uniqueResults = allResults.where((result) { + final id = '${result.lineItemNo}_${result.bloodPressureDate?.millisecondsSinceEpoch ?? 0}'; + if (seenIds.contains(id)) return false; + seenIds.add(id); + return true; + }).toList(); + + uniqueResults.sort((a, b) { + final dateA = a.bloodPressureDate ?? DateTime(1900); + final dateB = b.bloodPressureDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + + for (var result in uniqueResults) { + final systolic = result.systolicePressure ?? 0; + final diastolic = result.diastolicPressure ?? 0; + final measuredArmDesc = result.measuredArmDesc ?? ''; + final date = result.bloodPressureDate; + final dateLabel = date != null ? '${_getDayName(date)} ${date.day} ${_getMonthName(date.month).substring(0, 3)}, ${date.year}' : ''; + final timeLabel = date != null ? _formatTime(date) : 'Unknown'; + final displayLabel = date != null ? '$dateLabel, $timeLabel' : 'Unknown'; + final subTitleText = '$systolic/$diastolic mmHg'; + + listItems.add( + Column( + children: [ + buildHistoryListTile( + title: displayLabel, + subTitle: subTitleText, + value: systolic.toDouble(), + measureDesc: measuredArmDesc, + ), + Divider(height: 1, color: AppColors.dividerColor).paddingOnly(bottom: 8.h), + ], + ), + ); + } + return listItems; + } + + /// Build list items for Weight + List _buildWeightListItems(HealthTrackersViewModel viewModel, String unit) { + List listItems = []; + final allResults = []; + + allResults.addAll(viewModel.weekWeightMeasurementResult); + allResults.addAll(viewModel.monthWeightMeasurementResult); + allResults.addAll(viewModel.yearWeightMeasurementResult); + + final seenIds = {}; + final uniqueResults = allResults.where((result) { + final id = '${result.lineItemNo}_${result.weightDate?.millisecondsSinceEpoch ?? 0}'; + if (seenIds.contains(id)) return false; + seenIds.add(id); + return true; + }).toList(); + + uniqueResults.sort((a, b) { + final dateA = a.weightDate ?? DateTime(1900); + final dateB = b.weightDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + + for (var result in uniqueResults) { + final weightValue = result.weightMeasured?.toDouble() ?? 0.0; + final weightUnit = result.unit ?? unit; + final date = result.weightDate; + final dateLabel = date != null ? '${_getDayName(date)} ${date.day} ${_getMonthName(date.month).substring(0, 3)}, ${date.year}' : ''; + final timeLabel = date != null ? _formatTime(date) : 'Unknown'; + final displayLabel = date != null ? '$dateLabel, $timeLabel' : 'Unknown'; + final subTitleText = '${weightValue.toInt()} $weightUnit'; + + listItems.add( + Column( + children: [ + buildHistoryListTile( + title: displayLabel, + subTitle: subTitleText, + value: weightValue, + measureDesc: '', + ), + Divider(height: 1, color: AppColors.dividerColor).paddingOnly(bottom: 8.h), + ], + ), + ); + } + return listItems; + } + + Widget _buildLoadingShimmer({bool isForHistory = true}) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(0.w), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: isForHistory ? 60.h : 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + Widget _buildEmptyStateWidget() { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.calendar, + iconColor: AppColors.textColor, + height: 48.w, + width: 48.w, + ), + SizedBox(height: 16.h), + "You do not have any data available.".toText14( + weight: FontWeight.w500, + color: AppColors.textColor, + isCenter: true, + ), + SizedBox(height: 8.h), + _getEmptyStateMessage().toText12( + color: AppColors.greyTextColor, + isCenter: true, + ), + ], + ), + ), + ); + } + + Widget _buildHistoryGraph() { + return Consumer( + builder: (context, viewModel, _) { + final selectedDuration = viewModel.selectedDurationFilter; + List dataPoints = []; + List? secondaryDataPoints; + + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + dataPoints = _buildBloodSugarGraphData(viewModel, selectedDuration); + break; + case HealthTrackerTypeEnum.bloodPressure: + final (systolicData, diastolicData) = _buildBloodPressureGraphData(viewModel, selectedDuration); + dataPoints = systolicData; // Systolic (primary line) + secondaryDataPoints = diastolicData; // Diastolic (secondary line) + break; + case HealthTrackerTypeEnum.weightTracker: + dataPoints = _buildWeightGraphData(viewModel, selectedDuration); + break; + } + + if (dataPoints.isEmpty) { + return _buildEmptyStateWidget(); + } + + if (viewModel.isLoading) { + return Container( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: _buildLoadingShimmer(), + ); + } + + // Calculate max value from both lines for blood pressure + double maxDataValue = dataPoints.isNotEmpty ? dataPoints.map((p) => p.value).reduce((a, b) => a > b ? a : b) : 0.0; + if (secondaryDataPoints != null && secondaryDataPoints.isNotEmpty) { + final secondaryMax = secondaryDataPoints.map((p) => p.value).reduce((a, b) => a > b ? a : b); + if (secondaryMax > maxDataValue) maxDataValue = secondaryMax; + } + + double maxY = maxDataValue > 200 ? (maxDataValue * 1.2) : 250; + double minY = 0; + double horizontalInterval = maxY / 4; + double leftLabelInterval = horizontalInterval; + + // Set colors based on tracker type + Color graphColor = AppColors.successColor; + Color? secondaryGraphColor; + + if (widget.trackerType == HealthTrackerTypeEnum.bloodPressure) { + graphColor = AppColors.errorColor; // Red for Systolic + secondaryGraphColor = AppColors.blueColor; // Blue for Diastolic + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Legend for blood pressure + if (widget.trackerType == HealthTrackerTypeEnum.bloodPressure) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendItem(AppColors.errorColor, "Systolic".needTranslation), + SizedBox(width: 24.w), + _buildLegendItem(AppColors.blueColor, "Diastolic".needTranslation), + ], + ), + SizedBox(height: 12.h), + ], + CustomGraph( + bottomLabelReservedSize: 30, + dataPoints: dataPoints, + secondaryDataPoints: secondaryDataPoints, + makeGraphBasedOnActualValue: false, + leftLabelReservedSize: 50.h, + showGridLines: true, + maxY: maxY, + minY: minY, + showLinePoints: true, + maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0, + horizontalInterval: horizontalInterval, + leftLabelInterval: leftLabelInterval, + showShadow: widget.trackerType != HealthTrackerTypeEnum.bloodPressure, + graphColor: graphColor, + secondaryGraphColor: secondaryGraphColor, + graphShadowColor: graphColor.withValues(alpha: 0.15), + getDrawingHorizontalLine: (value) { + if (value % horizontalInterval == 0 && value > 0) { + return FlLine( + color: AppColors.greyTextColor.withValues(alpha: 0.3), + strokeWidth: 1.5, + dashArray: [8, 4], + ); + } + return FlLine(color: AppColors.transparent, strokeWidth: 0); + }, + leftLabelFormatter: (value) { + final interval = maxY / 4; + final positions = [0.0, interval, interval * 2, interval * 3, maxY]; + for (var position in positions) { + if ((value - position).abs() < 1) { + return '${value.toInt()}'.toText10(weight: FontWeight.w600); + } + } + return SizedBox.shrink(); + }, + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return SizedBox.shrink(); + if ((value - value.round()).abs() > 0.01) return SizedBox.shrink(); + int index = value.round(); + if (index < 0 || index >= data.length) return SizedBox.shrink(); + + if (selectedDuration == 'Week' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10(weight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + if (selectedDuration == 'Month' && index < 6) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10(weight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + if (selectedDuration == 'Year' && index < 12) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText8(fontWeight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + return SizedBox.shrink(); + }, + scrollDirection: selectedDuration == 'Year' ? Axis.horizontal : Axis.vertical, + height: 250.h, + spotColor: graphColor, + ), + ], + ); + }, + ); + } + + /// Build legend item for graph + Widget _buildLegendItem(Color color, String label) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2.r), + ), + ), + SizedBox(width: 6.w), + label.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ); + } + + /// Build graph data for Blood Sugar + List _buildBloodSugarGraphData(HealthTrackersViewModel viewModel, String selectedDuration) { + List dataPoints = []; + final unit = _getUnit(); + + if (selectedDuration == 'Week') { + final weekResults = viewModel.weekDiabeticResultAverage; + if (weekResults.isNotEmpty) { + final sortedResults = List.from(weekResults); + sortedResults.sort((a, b) => (a.dateChart ?? DateTime.now()).compareTo(b.dateChart ?? DateTime.now())); + final last7Days = sortedResults.length > 7 ? sortedResults.sublist(sortedResults.length - 7) : sortedResults; + + for (var result in last7Days) { + final value = result.dailyAverageResult?.toDouble() ?? 0.0; + final date = result.dateChart ?? DateTime.now(); + final label = _getDayName(date).substring(0, 3); + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Month') { + final monthResults = viewModel.monthDiabeticResultAverage; + if (monthResults.isNotEmpty) { + for (int i = 0; i < monthResults.length; i++) { + final weekData = monthResults[i]; + final value = (weekData.weekAverageResult is num) + ? (weekData.weekAverageResult as num).toDouble() + : double.tryParse(weekData.weekAverageResult?.toString() ?? '0') ?? 0.0; + final weekLabel = weekData.weekDesc ?? 'Week ${i + 1}'; + dataPoints.add(DataPoint( + value: value, + label: 'W${i + 1}', + actualValue: value.toStringAsFixed(1), + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Year') { + final yearResults = viewModel.yearDiabeticResultAverage; + if (yearResults.isNotEmpty) { + for (int targetMonth = 1; targetMonth <= 12; targetMonth++) { + final monthData = yearResults.firstWhere((m) => m.monthNumber == targetMonth, + orElse: () => YearDiabeticResultAverage(monthAverageResult: 0.0, monthNumber: targetMonth, monthName: _getMonthName(targetMonth))); + final value = monthData.monthAverageResult?.toDouble() ?? 0.0; + final monthName = monthData.monthName ?? _getMonthName(targetMonth); + final label = monthName.length >= 3 ? monthName.substring(0, 3) : monthName; + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + } + } + } + return dataPoints; + } + + /// Build graph data for Blood Pressure - returns (systolicData, diastolicData) + (List, List) _buildBloodPressureGraphData(HealthTrackersViewModel viewModel, String selectedDuration) { + List systolicDataPoints = []; + List diastolicDataPoints = []; + const unit = 'mmHg'; + + if (selectedDuration == 'Week') { + final weekResults = viewModel.weekBloodPressureResult; + if (weekResults.isNotEmpty) { + final sortedResults = List.from(weekResults); + sortedResults.sort((a, b) => (a.bloodPressureDate ?? DateTime.now()).compareTo(b.bloodPressureDate ?? DateTime.now())); + final last7Days = sortedResults.length > 7 ? sortedResults.sublist(sortedResults.length - 7) : sortedResults; + + for (var result in last7Days) { + final systolic = (result.systolicePressure ?? 0).toDouble(); + final diastolic = (result.diastolicPressure ?? 0).toDouble(); + final date = result.bloodPressureDate ?? DateTime.now(); + final label = _getDayName(date).substring(0, 3); + + systolicDataPoints.add(DataPoint( + value: systolic, + label: label, + actualValue: '${systolic.toInt()}/${diastolic.toInt()}', + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + + diastolicDataPoints.add(DataPoint( + value: diastolic, + label: label, + actualValue: diastolic.toStringAsFixed(0), + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Month') { + final monthResults = viewModel.monthBloodPressureResult; + if (monthResults.isNotEmpty) { + // Group by week and calculate averages + final Map> weekGroups = {}; + for (var result in monthResults) { + final weekDesc = result.weekDesc ?? 'Week 1'; + weekGroups.putIfAbsent(weekDesc, () => []); + weekGroups[weekDesc]!.add(result); + } + + int weekIndex = 0; + for (var entry in weekGroups.entries) { + final weekData = entry.value; + double avgSystolic = 0; + double avgDiastolic = 0; + + for (var result in weekData) { + avgSystolic += (result.systolicePressure ?? 0); + avgDiastolic += (result.diastolicPressure ?? 0); + } + + if (weekData.isNotEmpty) { + avgSystolic = avgSystolic / weekData.length; + avgDiastolic = avgDiastolic / weekData.length; + } + + final weekLabel = entry.key; + systolicDataPoints.add(DataPoint( + value: avgSystolic, + label: 'W${weekIndex + 1}', + actualValue: '${avgSystolic.toInt()}/${avgDiastolic.toInt()}', + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + + diastolicDataPoints.add(DataPoint( + value: avgDiastolic, + label: 'W${weekIndex + 1}', + actualValue: avgDiastolic.toStringAsFixed(0), + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + + weekIndex++; + } + } + } else if (selectedDuration == 'Year') { + final yearResults = viewModel.yearBloodPressureResult; + if (yearResults.isNotEmpty) { + // Group by month and calculate averages + final Map> monthGroups = {}; + for (var result in yearResults) { + final chartMonth = result.chartMonth; + final monthNum = _getMonthNumber(chartMonth ?? 'January'); + monthGroups.putIfAbsent(monthNum, () => []); + monthGroups[monthNum]!.add(result); + } + + for (int targetMonth = 1; targetMonth <= 12; targetMonth++) { + double avgSystolic = 0; + double avgDiastolic = 0; + + if (monthGroups.containsKey(targetMonth) && monthGroups[targetMonth]!.isNotEmpty) { + final monthData = monthGroups[targetMonth]!; + for (var result in monthData) { + avgSystolic += (result.systolicePressure ?? 0); + avgDiastolic += (result.diastolicPressure ?? 0); + } + avgSystolic = avgSystolic / monthData.length; + avgDiastolic = avgDiastolic / monthData.length; + } + + final monthName = _getMonthName(targetMonth); + final label = monthName.length >= 3 ? monthName.substring(0, 3) : monthName; + + systolicDataPoints.add(DataPoint( + value: avgSystolic, + label: label, + actualValue: '${avgSystolic.toInt()}/${avgDiastolic.toInt()}', + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + + diastolicDataPoints.add(DataPoint( + value: avgDiastolic, + label: label, + actualValue: avgDiastolic.toStringAsFixed(0), + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + } + } + } + return (systolicDataPoints, diastolicDataPoints); + } + + /// Helper to get month number from name + int _getMonthNumber(String monthName) { + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + final index = months.indexWhere((m) => m.toLowerCase() == monthName.toLowerCase()); + return index >= 0 ? index + 1 : 1; + } + + /// Build graph data for Weight + List _buildWeightGraphData(HealthTrackersViewModel viewModel, String selectedDuration) { + List dataPoints = []; + final unit = _getUnit(); + + if (selectedDuration == 'Week') { + final weekResults = viewModel.weekWeightMeasurementResultAverage; + if (weekResults.isNotEmpty) { + final sortedResults = List.from(weekResults); + sortedResults.sort((a, b) => (a.weightDate ?? DateTime.now()).compareTo(b.weightDate ?? DateTime.now())); + final last7Days = sortedResults.length > 7 ? sortedResults.sublist(sortedResults.length - 7) : sortedResults; + + for (var result in last7Days) { + final value = result.dailyAverageResult?.toDouble() ?? 0.0; + final date = result.weightDate ?? DateTime.now(); + final label = _getDayName(date).substring(0, 3); + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Month') { + final monthResults = viewModel.monthWeightMeasurementResultAverage; + if (monthResults.isNotEmpty) { + for (int i = 0; i < monthResults.length; i++) { + final weekData = monthResults[i]; + final value = weekData.weekAverageResult?.toDouble() ?? 0.0; + final weekLabel = weekData.weekDesc ?? 'Week ${i + 1}'; + dataPoints.add(DataPoint( + value: value, + label: 'W${i + 1}', + actualValue: value.toStringAsFixed(1), + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Year') { + final yearResults = viewModel.yearWeightMeasurementResultAverage; + if (yearResults.isNotEmpty) { + for (int targetMonth = 1; targetMonth <= 12; targetMonth++) { + final monthData = yearResults.firstWhere((m) => m.monthNumber == targetMonth, + orElse: () => + YearWeightMeasurementResultAverage(monthAverageResult: 0.0, monthNumber: targetMonth, monthName: _getMonthName(targetMonth))); + final value = monthData.monthAverageResult?.toDouble() ?? 0.0; + final monthName = monthData.monthName ?? _getMonthName(targetMonth); + final label = monthName.length >= 3 ? monthName.substring(0, 3) : monthName; + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + } + } + } + return dataPoints; + } + + void onSendEmailPressed(BuildContext context) async { + // TODO: Implement send email functionality + } + + Widget _buildPageShimmer() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + SizedBox(height: 16.h), + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 120.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + ), + ), + SizedBox(height: 16.h), + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 300.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + ), + ), + SizedBox(height: 16.h), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + sendEmail: () async => onSendEmailPressed(context), + title: _getPageTitle(), + bottomChild: Consumer( + builder: (context, viewModel, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Add new Record".needTranslation, + onPressed: () { + if (!viewModel.isLoading) { + context.navigateWithName(AppRoutes.addHealthTrackerEntryPage, arguments: widget.trackerType); + } + }, + icon: AppAssets.add_icon, + borderRadius: 12.r, + borderColor: AppColors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + }, + ), + child: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading) { + return _buildPageShimmer(); + } + + if (_hasNoData(viewModel)) { + return _buildEmptyStateWidget(); + } + + return Column( + children: [ + SizedBox(height: 16.h), + TrackerLastValueCard(trackerType: widget.trackerType), + SizedBox(height: 16.h), + _buildHistoryGraphOrList(), + SizedBox(height: 16.h), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/health_trackers/health_trackers_page.dart b/lib/presentation/health_trackers/health_trackers_page.dart new file mode 100644 index 00000000..c55e8b46 --- /dev/null +++ b/lib/presentation/health_trackers/health_trackers_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; + +class HealthTrackersPage extends StatefulWidget { + const HealthTrackersPage({super.key}); + + @override + State createState() => _HealthTrackersPageState(); +} + +Widget buildHealthTrackerCard({ + required String icon, + required String title, + required String description, + required Color iconBgColor, + required VoidCallback onTap, +}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: iconBgColor, borderRadius: 10.r), + height: 40.w, + width: 40.w, + child: Utils.buildSvgWithAssets( + icon: icon, + fit: BoxFit.none, + height: 22.w, + width: 22.w, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + title.toText16(weight: FontWeight.w600), + description.toText12( + fontWeight: FontWeight.w500, + color: Color(0xFF8F9AA3), + ), + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets( + icon: AppAssets.arrowRight, + width: 24.w, + height: 24.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor, + ), + ], + ).paddingAll(16.w), + ).onPress(onTap); +} + +class _HealthTrackersPageState extends State { + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "Health Trackers".needTranslation, + child: Column( + children: [ + buildHealthTrackerCard( + iconBgColor: AppColors.primaryRedColor, + icon: AppAssets.bloodSugarOnlyIcon, + title: "Blood Sugar".needTranslation, + description: "Track your glucose levels, understand trends, and get personalized insights for better health.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.bloodSugar, + ); + }, + ), + SizedBox(height: 16.h), + buildHealthTrackerCard( + iconBgColor: AppColors.infoColor, + icon: AppAssets.bloodPressureIcon, + title: "Blood Pressure".needTranslation, + description: "Monitor your blood pressure levels, track systolic and diastolic readings, and maintain a healthy heart.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.bloodPressure, + ); + }, + ), + SizedBox(height: 16.h), + buildHealthTrackerCard( + iconBgColor: AppColors.successColor, + icon: AppAssets.weightIcon, + title: "Weight".needTranslation, + description: "Track your weight progress, set goals, and maintain a healthy body mass for overall wellness.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.weightTracker, + ); + }, + ), + ], + ).paddingSymmetrical(20.w, 24.h), + ); + } +} diff --git a/lib/presentation/health_trackers/health_trackers_view_model.dart b/lib/presentation/health_trackers/health_trackers_view_model.dart new file mode 100644 index 00000000..059c11ec --- /dev/null +++ b/lib/presentation/health_trackers/health_trackers_view_model.dart @@ -0,0 +1,1012 @@ +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/blood_pressure_result.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/month_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/week_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/weight_measurement_result.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/year_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class HealthTrackersViewModel extends ChangeNotifier { + HealthTrackersRepo healthTrackersRepo; + ErrorHandlerService errorHandlerService; + + HealthTrackersViewModel({required this.healthTrackersRepo, required this.errorHandlerService}); + + // ==================== STATE MANAGEMENT ==================== + bool isLoading = false; + String? _errorMessage; + + String? get errorMessage => _errorMessage; + + List get durationFilters => ["Week", "Month", "Year"]; + + String _selectedDuration = "Week"; + bool _isGraphView = true; + + String get selectedDurationFilter => _selectedDuration; + + bool get isGraphView => _isGraphView; + + final List bloodSugarUnit = ['mg/dlt', 'mol/L']; + + String _selectedBloodSugarUnit = 'mg/dlt'; + + String get selectedBloodSugarUnit => _selectedBloodSugarUnit; + + String _selectedBloodSugarMeasureTime = ''; + + String get selectedBloodSugarMeasureTime => _selectedBloodSugarMeasureTime; + + final List bloodSugarMeasureTimeEnList = [ + 'Before Breakfast', + 'After Breakfast', + 'Before Lunch', + 'After Lunch', + 'Before Dinner', + 'After Dinner', + 'Before Sleep', + 'After Sleep', + 'Fasting', + 'Other', + ]; + final List bloodSugarMeasureTimeArList = [ + "قبل الإفطار", + "بعد الإفطار", + "قبل الغداء", + "بعد الغداء", + "قبل العشاء", + "بعد العشاء", + "قبل النوم", + "بعد النوم", + "صائم", + "آخر", + ]; + + // Setters with notification + void setBloodSugarMeasureTime(String duration) async { + _selectedBloodSugarMeasureTime = duration; + notifyListeners(); + } + + // Setters with notification + void setFilterDuration(String duration) async { + _selectedDuration = duration; + notifyListeners(); + } + + // Setters with notification + void setGraphView(bool value) { + _isGraphView = value; + notifyListeners(); + } + + void setBloodSugarUnit(String unit) { + _selectedBloodSugarUnit = unit; + notifyListeners(); + } + + // ==================== WEIGHT FORM FIELDS ==================== + final List weightUnits = ['kg', 'lb']; + String _selectedWeightUnit = 'kg'; + + String get selectedWeightUnit => _selectedWeightUnit; + + void setWeightUnit(String unit) { + _selectedWeightUnit = unit; + notifyListeners(); + } + + // ==================== BLOOD PRESSURE FORM FIELDS ==================== + final List measuredArmList = ['Left Arm', 'Right Arm']; + String _selectedMeasuredArm = ''; + + String get selectedMeasuredArm => _selectedMeasuredArm; + + void setMeasuredArm(String arm) { + _selectedMeasuredArm = arm; + notifyListeners(); + } + + // Text Controllers + TextEditingController weightController = TextEditingController(); + TextEditingController bloodSugarController = TextEditingController(); + TextEditingController systolicController = TextEditingController(); + TextEditingController diastolicController = TextEditingController(); + + // Get current progress list based on selected duration + // dynamic get currentProgressData { + // switch (_selectedDuration) { + // case 'Daily': + // return _todayProgressList; + // case 'Weekly': + // return _weekProgressList; + // case 'Monthly': + // return _monthProgressList; + // default: + // return _todayProgressList; + // } + // } + + // ==================== WEIGHT TRACKING DATA ==================== + final List _monthWeightMeasurementResultAverage = []; + final List _weekWeightMeasurementResultAverage = []; + final List _yearWeightMeasurementResultAverage = []; + + final List _monthWeightMeasurementResult = []; + final List _weekWeightMeasurementResult = []; + final List _yearWeightMeasurementResult = []; + + // Getters for weight data + List get monthWeightMeasurementResultAverage => _monthWeightMeasurementResultAverage; + + List get weekWeightMeasurementResultAverage => _weekWeightMeasurementResultAverage; + + List get yearWeightMeasurementResultAverage => _yearWeightMeasurementResultAverage; + + List get monthWeightMeasurementResult => _monthWeightMeasurementResult; + + List get weekWeightMeasurementResult => _weekWeightMeasurementResult; + + List get yearWeightMeasurementResult => _yearWeightMeasurementResult; + + // ==================== BLOOD PRESSURE TRACKING DATA ==================== + final List _monthBloodPressureResultAverage = []; + final List _weekBloodPressureResultAverage = []; + final List _yearBloodPressureResultAverage = []; + + final List _monthBloodPressureResult = []; + final List _weekBloodPressureResult = []; + final List _yearBloodPressureResult = []; + + // Getters for blood pressure data + List get monthBloodPressureResultAverage => _monthBloodPressureResultAverage; + + List get weekBloodPressureResultAverage => _weekBloodPressureResultAverage; + + List get yearBloodPressureResultAverage => _yearBloodPressureResultAverage; + + List get monthBloodPressureResult => _monthBloodPressureResult; + + List get weekBloodPressureResult => _weekBloodPressureResult; + + List get yearBloodPressureResult => _yearBloodPressureResult; + + // ==================== BLOOD SUGAR (DIABETIC) TRACKING DATA ==================== + final List _monthDiabeticResultAverage = []; + final List _weekDiabeticResultAverage = []; + final List _yearDiabeticResultAverage = []; + + final List _monthDiabeticPatientResult = []; + final List _weekDiabeticPatientResult = []; + final List _yearDiabeticPatientResult = []; + + // Getters for blood sugar data + List get monthDiabeticResultAverage => _monthDiabeticResultAverage; + + List get weekDiabeticResultAverage => _weekDiabeticResultAverage; + + List get yearDiabeticResultAverage => _yearDiabeticResultAverage; + + List get monthDiabeticPatientResult => _monthDiabeticPatientResult; + + List get weekDiabeticPatientResult => _weekDiabeticPatientResult; + + List get yearDiabeticPatientResult => _yearDiabeticPatientResult; + + // ==================== WEIGHT TRACKING METHODS ==================== + + /// Fetch weight averages and results + Future getWeight() async { + isLoading = true; + notifyListeners(); + + try { + // Fetch weight averages + final averageResult = await healthTrackersRepo.getWeightMeasurementResultAverage(); + + averageResult.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthWeightMeasurementResultAverage.clear(); + _weekWeightMeasurementResultAverage.clear(); + _yearWeightMeasurementResultAverage.clear(); + + // Parse month averages + if (data['monthAverageList'] != null) { + for (var item in (data['monthAverageList'] as List)) { + _monthWeightMeasurementResultAverage.add( + MonthWeightMeasurementResultAverage.fromJson(item), + ); + } + } + + // Parse week averages + if (data['weekAverageList'] != null) { + for (var item in (data['weekAverageList'] as List)) { + _weekWeightMeasurementResultAverage.add( + WeekWeightMeasurementResultAverage.fromJson(item), + ); + } + } + + // Parse year averages + if (data['yearAverageList'] != null) { + for (var item in (data['yearAverageList'] as List)) { + _yearWeightMeasurementResultAverage.add( + YearWeightMeasurementResultAverage.fromJson(item), + ); + } + } + } + }, + ); + + // Fetch weight results + final resultsResponse = await healthTrackersRepo.getWeightMeasurementResults(); + + resultsResponse.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthWeightMeasurementResult.clear(); + _weekWeightMeasurementResult.clear(); + _yearWeightMeasurementResult.clear(); + + // Parse week results + if (data['weekResultList'] != null) { + for (var item in (data['weekResultList'] as List)) { + _weekWeightMeasurementResult.add(WeightMeasurementResult.fromJson(item)); + } + } + + // Parse month results + if (data['monthResultList'] != null) { + for (var item in (data['monthResultList'] as List)) { + _monthWeightMeasurementResult.add(WeightMeasurementResult.fromJson(item)); + } + } + + // Parse year results + if (data['yearResultList'] != null) { + for (var item in (data['yearResultList'] as List)) { + _yearWeightMeasurementResult.add(WeightMeasurementResult.fromJson(item)); + } + } + } + }, + ); + } catch (e) { + log('Error in getWeight: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Add new weight result + Future addWeightResult({ + required String weightDate, + required String weightMeasured, + required int weightUnit, + }) async { + try { + final result = await healthTrackersRepo.addWeightMeasurementResult( + weightDate: weightDate, + weightMeasured: weightMeasured, + weightUnit: weightUnit, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful add + await getWeight(); + }, + ); + + return success; + } catch (e) { + log('Error in addWeightResult: $e'); + return false; + } + } + + /// Update existing weight result + Future updateWeightResult({ + required int lineItemNo, + required int weightUnit, + required String weightMeasured, + required String weightDate, + }) async { + try { + final result = await healthTrackersRepo.updateWeightMeasurementResult( + lineItemNo: lineItemNo, + weightUnit: weightUnit, + weightMeasured: weightMeasured, + weightDate: weightDate, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful update + await getWeight(); + }, + ); + + return success; + } catch (e) { + log('Error in updateWeightResult: $e'); + return false; + } + } + + /// Delete weight result + Future deleteWeightResult({ + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.deactivateWeightMeasurementStatus( + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful delete + await getWeight(); + }, + ); + + return success; + } catch (e) { + return false; + } + } + + // ==================== BLOOD PRESSURE TRACKING METHODS ==================== + + /// Fetch blood pressure averages and results + Future getBloodPressure() async { + isLoading = true; + notifyListeners(); + + try { + // Fetch blood pressure averages + final averageResult = await healthTrackersRepo.getBloodPressureResultAverage(); + + averageResult.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + + if (data is Map) { + // Clear existing data + _monthBloodPressureResultAverage.clear(); + _weekBloodPressureResultAverage.clear(); + _yearBloodPressureResultAverage.clear(); + + // Parse month averages + if (data['monthList'] != null) { + for (var item in (data['monthList'] as List)) { + _monthBloodPressureResultAverage.add( + MonthBloodPressureResultAverage.fromJson(item), + ); + } + } + + // Parse week averages + if (data['weekList'] != null) { + for (var item in (data['weekList'] as List)) { + _weekBloodPressureResultAverage.add( + WeekBloodPressureResultAverage.fromJson(item), + ); + } + } + + // Parse year averages + if (data['yearList'] != null) { + for (var item in (data['yearList'] as List)) { + _yearBloodPressureResultAverage.add( + YearBloodPressureResultAverage.fromJson(item), + ); + } + } + } + }, + ); + + // Fetch blood pressure results + final resultsResponse = await healthTrackersRepo.getBloodPressureResults(); + + resultsResponse.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthBloodPressureResult.clear(); + _weekBloodPressureResult.clear(); + _yearBloodPressureResult.clear(); + + // Parse week results + if (data['weekList'] != null) { + for (var item in (data['weekList'] as List)) { + _weekBloodPressureResult.add(BloodPressureResult.fromJson(item)); + } + } + + // Parse month results + if (data['monthList'] != null) { + for (var item in (data['monthList'] as List)) { + _monthBloodPressureResult.add(BloodPressureResult.fromJson(item)); + } + } + + // Parse year results + if (data['yearList'] != null) { + for (var item in (data['yearList'] as List)) { + _yearBloodPressureResult.add(BloodPressureResult.fromJson(item)); + } + } + } + }, + ); + } catch (e) { + log('Error in getBloodPressure: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Add or Update blood pressure result + Future addOrUpdateBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + int? lineItemNo, + bool isUpdate = false, + }) async { + try { + final result = isUpdate + ? await healthTrackersRepo.updateBloodPressureResult( + bloodPressureDate: bloodPressureDate, + diastolicPressure: diastolicPressure, + systolicePressure: systolicePressure, + measuredArm: measuredArm, + lineItemNo: lineItemNo!, + ) + : await healthTrackersRepo.addBloodPressureResult( + bloodPressureDate: bloodPressureDate, + diastolicPressure: diastolicPressure, + systolicePressure: systolicePressure, + measuredArm: measuredArm, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful add/update + await getBloodPressure(); + }, + ); + + return success; + } catch (e) { + log('Error in addOrUpdateBloodPressureResult: $e'); + return false; + } + } + + /// Delete blood pressure result + Future deleteBloodPressureResult({ + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.deactivateBloodPressureStatus( + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful delete + await getBloodPressure(); + }, + ); + + return success; + } catch (e) { + log('Error in deleteBloodPressureResult: $e'); + return false; + } + } + + // ==================== BLOOD SUGAR (DIABETIC) TRACKING METHODS ==================== + + /// Fetch blood sugar averages and results + Future getBloodSugar() async { + isLoading = true; + notifyListeners(); + + try { + // Fetch blood sugar averages + final averageResult = await healthTrackersRepo.getDiabeticResultAverage(); + + averageResult.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthDiabeticResultAverage.clear(); + _weekDiabeticResultAverage.clear(); + _yearDiabeticResultAverage.clear(); + + // Parse month averages + if (data['monthAverageList'] != null) { + for (var item in (data['monthAverageList'] as List)) { + _monthDiabeticResultAverage.add( + MonthDiabeticResultAverage.fromJson(item), + ); + } + } + + // Parse week averages + if (data['weekAverageList'] != null) { + for (var item in (data['weekAverageList'] as List)) { + _weekDiabeticResultAverage.add( + WeekDiabeticResultAverage.fromJson(item), + ); + } + } + + // Parse year averages + if (data['yearAverageList'] != null) { + for (var item in (data['yearAverageList'] as List)) { + _yearDiabeticResultAverage.add( + YearDiabeticResultAverage.fromJson(item), + ); + } + } + } + }, + ); + + // Fetch blood sugar results + final resultsResponse = await healthTrackersRepo.getDiabeticResults(); + + resultsResponse.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthDiabeticPatientResult.clear(); + _weekDiabeticPatientResult.clear(); + _yearDiabeticPatientResult.clear(); + + // Parse week results + if (data['weekResultList'] != null) { + for (var item in (data['weekResultList'] as List)) { + _weekDiabeticPatientResult.add(DiabeticPatientResult.fromJson(item)); + } + } + + // Parse month results + if (data['monthResultList'] != null) { + for (var item in (data['monthResultList'] as List)) { + _monthDiabeticPatientResult.add(DiabeticPatientResult.fromJson(item)); + } + } + + // Parse year results + if (data['yearResultList'] != null) { + for (var item in (data['yearResultList'] as List)) { + _yearDiabeticPatientResult.add(DiabeticPatientResult.fromJson(item)); + } + } + } + }, + ); + } catch (e) { + log('Error in getBloodSugar: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Add new blood sugar result + Future addBloodSugarResult({ + required String bloodSugarDateChart, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + }) async { + try { + final result = await healthTrackersRepo.addDiabeticResult( + bloodSugarDateChart: bloodSugarDateChart, + bloodSugarResult: bloodSugarResult, + diabeticUnit: diabeticUnit, + measuredTime: measuredTime, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful add + await getBloodSugar(); + }, + ); + + return success; + } catch (e) { + log('Error in addBloodSugarResult: $e'); + return false; + } + } + + /// Update existing blood sugar result + Future updateBloodSugarResult({ + required DateTime month, + required DateTime hour, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.updateDiabeticResult( + month: month, + hour: hour, + bloodSugarResult: bloodSugarResult, + diabeticUnit: diabeticUnit, + measuredTime: measuredTime, + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful update + await getBloodSugar(); + }, + ); + + return success; + } catch (e) { + log('Error in updateBloodSugarResult: $e'); + return false; + } + } + + /// Delete blood sugar result + Future deleteBloodSugarResult({ + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.deactivateDiabeticStatus( + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful delete + await getBloodSugar(); + }, + ); + + return success; + } catch (e) { + log('Error in deleteBloodSugarResult: $e'); + return false; + } + } + + // Validation method + String? _validateBloodSugarEntry(String dateTime) { + // Validate blood sugar value + if (bloodSugarController.text.trim().isEmpty) { + return "Please enter blood sugar value"; + } + + final bloodSugarValue = double.tryParse(bloodSugarController.text.trim()); + if (bloodSugarValue == null) { + return "Please enter a valid number"; + } + + if (bloodSugarValue <= 0) { + return "Blood sugar value must be greater than 0"; + } + + // Validate reasonable range (typical ranges) + if (bloodSugarValue > 1000) { + return "Blood sugar value seems too high. Please check and enter again"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + // Validate measure time + if (_selectedBloodSugarMeasureTime.isEmpty) { + return "Please select when the measurement was taken"; + } + + return null; // No errors + } + + // Save blood sugar entry with validation + Future saveBloodSugarEntry({ + required String dateTime, + required String measureTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateBloodSugarEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get measure time index (0-based, but API expects 1-based) + final measureTimeIndex = bloodSugarMeasureTimeEnList.indexOf(measureTime); + + // Call API + final success = await addBloodSugarResult( + bloodSugarDateChart: dateTime, + bloodSugarResult: bloodSugarController.text.trim(), + diabeticUnit: _selectedBloodSugarUnit, + measuredTime: measureTimeIndex >= 0 ? measureTimeIndex : 0, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + bloodSugarController.clear(); + _selectedBloodSugarMeasureTime = ''; + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save blood sugar entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveBloodSugarEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + // ==================== WEIGHT ENTRY METHODS ==================== + + // Validate weight entry before saving + String? _validateWeightEntry(String dateTime) { + // Validate weight value + final weightValue = weightController.text.trim(); + if (weightValue.isEmpty) { + return "Please enter weight value"; + } + + // Check if it's a valid number + final parsedValue = double.tryParse(weightValue); + if (parsedValue == null || parsedValue <= 0) { + return "Please enter a valid weight value"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + return null; // No errors + } + + // Save weight entry with validation + Future saveWeightEntry({ + required String dateTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateWeightEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get weight unit index (0 = kg, 1 = lb) + final weightUnitIndex = weightUnits.indexOf(_selectedWeightUnit); + + // Call API + final success = await addWeightResult( + weightDate: dateTime, + weightMeasured: weightController.text.trim(), + weightUnit: weightUnitIndex >= 0 ? weightUnitIndex : 0, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + weightController.clear(); + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save weight entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveWeightEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + // ==================== BLOOD PRESSURE ENTRY METHODS ==================== + + // Validate blood pressure entry before saving + String? _validateBloodPressureEntry(String dateTime) { + // Validate systolic value + final systolicValue = systolicController.text.trim(); + if (systolicValue.isEmpty) { + return "Please enter systolic value"; + } + final parsedSystolic = int.tryParse(systolicValue); + if (parsedSystolic == null || parsedSystolic <= 0) { + return "Please enter a valid systolic value"; + } + + // Validate diastolic value + final diastolicValue = diastolicController.text.trim(); + if (diastolicValue.isEmpty) { + return "Please enter diastolic value"; + } + final parsedDiastolic = int.tryParse(diastolicValue); + if (parsedDiastolic == null || parsedDiastolic <= 0) { + return "Please enter a valid diastolic value"; + } + + // Validate arm selection + if (_selectedMeasuredArm.isEmpty) { + return "Please select measured arm"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + return null; // No errors + } + + // Save blood pressure entry with validation + Future saveBloodPressureEntry({ + required String dateTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateBloodPressureEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get measured arm index (0 = Left Arm, 1 = Right Arm) + final measuredArmIndex = measuredArmList.indexOf(_selectedMeasuredArm); + + // Call API + final success = await addOrUpdateBloodPressureResult( + bloodPressureDate: dateTime, + systolicePressure: systolicController.text.trim(), + diastolicPressure: diastolicController.text.trim(), + measuredArm: measuredArmIndex >= 0 ? measuredArmIndex : 0, + isUpdate: false, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + systolicController.clear(); + diastolicController.clear(); + _selectedMeasuredArm = ''; + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save blood pressure entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveBloodPressureEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + @override + void dispose() { + bloodSugarController.dispose(); + weightController.dispose(); + systolicController.dispose(); + diastolicController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart new file mode 100644 index 00000000..542baa58 --- /dev/null +++ b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart @@ -0,0 +1,271 @@ +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_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class TrackerLastValueCard extends StatelessWidget { + final HealthTrackerTypeEnum trackerType; + + const TrackerLastValueCard({super.key, required this.trackerType}); + + /// Get status text and color based on blood sugar value + (String status, Color color, Color bgColor) _getBloodSugarStatus(double value) { + if (value < 70) { + return ('Low'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.5)); + } else if (value <= 100) { + return ('Normal'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } else if (value <= 125) { + return ('Pre-diabetic'.needTranslation, AppColors.ratingColorYellow, AppColors.errorColor.withValues(alpha: 0.4)); + } else { + return ('High'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.4)); + } + } + + /// Get status text and color based on blood pressure value (systolic) + (String status, Color color, Color bgColor) _getBloodPressureStatus(int systolic) { + if (systolic < 90) { + return ('Low'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.5)); + } else if (systolic <= 120) { + return ('Normal'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } else if (systolic <= 140) { + return ('Elevated'.needTranslation, AppColors.ratingColorYellow, AppColors.errorColor.withValues(alpha: 0.4)); + } else { + return ('High'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.4)); + } + } + + /// Get status for weight (neutral - no good/bad status) + (String status, Color color, Color bgColor) _getWeightStatus() { + return ('Recorded'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } + + /// Get default unit based on tracker type + String _getDefaultUnit() { + switch (trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return 'mg/dL'; + case HealthTrackerTypeEnum.bloodPressure: + return 'mmHg'; + case HealthTrackerTypeEnum.weightTracker: + return 'kg'; + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, viewModel, child) { + // Get the last record based on tracker type + dynamic lastRecord; + String displayValue = '--'; + String unit = _getDefaultUnit(); + DateTime? lastDate; + String status = ''; + Color statusColor = AppColors.greyTextColor; + + switch (trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + final allResults = [ + ...viewModel.weekDiabeticPatientResult, + ...viewModel.monthDiabeticPatientResult, + ...viewModel.yearDiabeticPatientResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.dateChart ?? DateTime(1900); + final dateB = b.dateChart ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final lastValue = lastRecord.resultValue?.toDouble() ?? 0.0; + displayValue = lastValue.toStringAsFixed(0); + unit = lastRecord.unit ?? 'mg/dL'; + lastDate = lastRecord.dateChart; + final (s, c, _) = _getBloodSugarStatus(lastValue); + status = s; + statusColor = c; + } + break; + + case HealthTrackerTypeEnum.bloodPressure: + final allResults = [ + ...viewModel.weekBloodPressureResult, + ...viewModel.monthBloodPressureResult, + ...viewModel.yearBloodPressureResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.bloodPressureDate ?? DateTime(1900); + final dateB = b.bloodPressureDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final systolic = lastRecord.systolicePressure ?? 0; + final diastolic = lastRecord.diastolicPressure ?? 0; + displayValue = '$systolic/$diastolic'; + unit = 'mmHg'; + lastDate = lastRecord.bloodPressureDate; + final (s, c, _) = _getBloodPressureStatus(systolic); + status = s; + statusColor = c; + } + break; + + case HealthTrackerTypeEnum.weightTracker: + final allResults = [ + ...viewModel.weekWeightMeasurementResult, + ...viewModel.monthWeightMeasurementResult, + ...viewModel.yearWeightMeasurementResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.weightDate ?? DateTime(1900); + final dateB = b.weightDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final weightValue = lastRecord.weightMeasured?.toDouble() ?? 0.0; + displayValue = weightValue.toStringAsFixed(0); + unit = lastRecord.unit ?? 'kg'; + lastDate = lastRecord.weightDate; + final (s, c, _) = _getWeightStatus(); + status = s; + statusColor = c; + } + break; + } + + final formattedDate = lastDate != null ? DateFormat('EEE DD MMM, yy').format(lastDate) : DateFormat('EEE DD MMM, yy').format(DateTime.now()); + + // Show shimmer while loading + if (viewModel.isLoading) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40.h, + width: 120.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + SizedBox(height: 8.h), + Row( + children: [ + Container( + height: 32.h, + width: 150.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + ), + SizedBox(width: 8.w), + Container( + height: 32.h, + width: 80.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + ), + ], + ), + ], + ), + ), + ); + } + + // Show empty state if no records + if (lastRecord == null) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "--".toText32(isBold: true, color: AppColors.greyTextColor), + SizedBox(width: 6.w), + unit.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500).paddingOnly(top: 8.h), + ], + ), + SizedBox(height: 8.h), + AppCustomChipWidget( + labelText: "No records yet".needTranslation, + icon: AppAssets.doctor_calendar_icon, + ), + ], + ), + ); + } + + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + displayValue.toText32(isBold: true, color: statusColor), + SizedBox(width: 6.w), + unit.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500).paddingOnly(top: 8.h), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + AppCustomChipWidget( + labelText: "${"Last Record".needTranslation}: $formattedDate", + icon: AppAssets.doctor_calendar_icon, + ), + SizedBox(width: 8.w), + if (trackerType != HealthTrackerTypeEnum.weightTracker) ...[ + AppCustomChipWidget( + labelText: status.needTranslation, + icon: AppAssets.normalStatusGreenIcon, + iconColor: statusColor, + ), + ] + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 3b31d38e..a5903db4 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -45,12 +45,13 @@ class ServicesPage extends StatelessWidget { late final List hmgServices = [ HmgServicesComponentModel( - 11, - "Emergency Services".needTranslation, - "".needTranslation, - AppAssets.emergency_services_icon, - bgColor: AppColors.primaryRedColor, - true, route: null, onTap: () { + 11, + "Emergency Services".needTranslation, + "".needTranslation, + AppAssets.emergency_services_icon, + bgColor: AppColors.primaryRedColor, + true, + route: null, onTap: () { getIt.get().flushData(); getIt.get().getTransportationOrders( showLoader: false, @@ -162,6 +163,15 @@ class ServicesPage extends StatelessWidget { ]; late final List hmgHealthToolServices = [ + HmgServicesComponentModel( + 11, + "Health Trackers".needTranslation, + "".needTranslation, + AppAssets.general_health, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.healthTrackersPage, + ), HmgServicesComponentModel( 11, "Daily Water Monitor".needTranslation, @@ -179,14 +189,14 @@ class ServicesPage extends StatelessWidget { LoaderBottomSheet.hideLoader(); if (userDetail == null) { waterMonitorVM.populateFromAuthenticatedUser(); - context.navigateWithName(AppRoutes.waterMonitorSettingsScreen); + context.navigateWithName(AppRoutes.waterMonitorSettingsPage); } else { - context.navigateWithName(AppRoutes.waterConsumptionScreen); + context.navigateWithName(AppRoutes.waterConsumptionPage); } }, onError: (error) { LoaderBottomSheet.hideLoader(); - context.navigateWithName(AppRoutes.waterConsumptionScreen); + context.navigateWithName(AppRoutes.waterConsumptionPage); }, ); }, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 95884d46..138865d0 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -537,7 +537,7 @@ class _LandingPageState extends State { width: double.infinity, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: Padding( - padding: EdgeInsets.all(12.h), + padding: EdgeInsets.all(16.h), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index 4e0ec10e..1786dece 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -52,7 +52,7 @@ class _OrganSelectorPageState extends State { password: password, onSuccess: () { LoaderBottomSheet.hideLoader(); - context.navigateWithName(AppRoutes.symptomsSelectorScreen); + context.navigateWithName(AppRoutes.symptomsSelectorPage); }, onError: (String error) { LoaderBottomSheet.hideLoader(); diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 188d07bf..a63d1e2d 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -19,8 +19,8 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; -class PossibleConditionsScreen extends StatelessWidget { - const PossibleConditionsScreen({super.key}); +class PossibleConditionsPage extends StatelessWidget { + const PossibleConditionsPage({super.key}); Widget _buildLoadingShimmer() { return ListView.separated( diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index aef7ce79..8669c3ca 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -41,7 +41,7 @@ class _RiskFactorsScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedRiskFactors) { - context.navigateWithName(AppRoutes.suggestionsScreen); + context.navigateWithName(AppRoutes.suggestionsPage); } else { dialogService.showErrorBottomSheet( message: 'Please select at least one risk before proceeding'.needTranslation, diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index b5b43880..d0d5b2f4 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -43,7 +43,7 @@ class _SuggestionsScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedSuggestions) { // Navigate to triage screen - context.navigateWithName(AppRoutes.triageScreen); + context.navigateWithName(AppRoutes.triagePage); } else { dialogService.showErrorBottomSheet( message: 'Please select at least one option before proceeding'.needTranslation, diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 39509743..d6036c62 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -17,14 +17,14 @@ import 'package:hmg_patient_app_new/widgets/chip/custom_selectable_chip.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; -class SymptomsSelectorScreen extends StatefulWidget { - const SymptomsSelectorScreen({super.key}); +class SymptomsSelectorPage extends StatefulWidget { + const SymptomsSelectorPage({super.key}); @override - State createState() => _SymptomsSelectorScreenState(); + State createState() => _SymptomsSelectorPageState(); } -class _SymptomsSelectorScreenState extends State { +class _SymptomsSelectorPageState extends State { late DialogService dialogService; @override @@ -41,7 +41,7 @@ class _SymptomsSelectorScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedSymptoms) { // Navigate to triage screen - context.navigateWithName(AppRoutes.riskFactorsScreen); + context.navigateWithName(AppRoutes.riskFactorsPage); } else { dialogService.showErrorBottomSheet( message: 'Please select at least one symptom before proceeding'.needTranslation, diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index ba159bdf..9d5d884b 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -21,14 +21,14 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; -class TriageScreen extends StatefulWidget { - const TriageScreen({super.key}); +class TriagePage extends StatefulWidget { + const TriagePage({super.key}); @override - State createState() => _TriageScreenState(); + State createState() => _TriagePageState(); } -class _TriageScreenState extends State { +class _TriagePageState extends State { late SymptomsCheckerViewModel viewModel; late DialogService dialogService; @@ -78,7 +78,7 @@ class _TriageScreenState extends State { // Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 7) { // Navigate to results/possible conditions screen - context.navigateWithName(AppRoutes.possibleConditionsScreen); + context.navigateWithName(AppRoutes.possibleConditionsPage); return; } diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index c3ce68b0..b4384203 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.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/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -90,8 +91,8 @@ class _UserInfoSelectionScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - title.toText16(weight: FontWeight.w500), - subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + title.toText14(weight: FontWeight.w500), + subTitle.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), ], ), ], @@ -106,7 +107,7 @@ class _UserInfoSelectionScreenState extends State { Widget _getDivider() { return Divider( color: AppColors.dividerColor, - ).paddingSymmetrical(0, 16.h); + ).paddingSymmetrical(0, 8.h); } @override @@ -114,8 +115,25 @@ class _UserInfoSelectionScreenState extends State { AppState appState = getIt.get(); String name = ""; + int? userAgeFromDOB; if (appState.isAuthenticated) { - name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} "; + final user = appState.getAuthenticatedUser(); + name = "${user!.firstName!} ${user.lastName!} "; + + // Calculate age from authenticated user's DOB if available + if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { + try { + DateTime dob = DateUtil.convertStringToDate(user.dateofBirth!); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || (now.month == dob.month && now.day < dob.day)) { + age--; + } + userAgeFromDOB = age; + } catch (e) { + // If date parsing fails, ignore + } + } } else { name = "Guest"; } @@ -132,8 +150,9 @@ class _UserInfoSelectionScreenState extends State { // Get display values String genderText = viewModel.selectedGender ?? "Not set"; - // Show age calculated from DOB, not the DOB itself - String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set"; + // Show age calculated from DOB (prefer viewModel's age, fallback to calculated from user's DOB) + int? displayAge = viewModel.selectedAge ?? userAgeFromDOB; + String ageText = displayAge != null ? "$displayAge Years" : "Not set"; String heightText = viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set"; String weightText = @@ -154,11 +173,11 @@ class _UserInfoSelectionScreenState extends State { padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), child: Column( children: [ - "Hello $name, Is your information up to date?".needTranslation.toText18( + "Hello $name, Is your information up to date?".needTranslation.toText16( weight: FontWeight.w600, color: AppColors.textColor, ), - SizedBox(height: 24.h), + SizedBox(height: 32.h), _buildEditInfoTile( context: context, leadingIcon: AppAssets.genderIcon, @@ -240,13 +259,10 @@ class _UserInfoSelectionScreenState extends State { icon: AppAssets.edit_icon, iconColor: AppColors.primaryRedColor, onPressed: () { - context - .read() - .setUserInfoPage(0, isSinglePageEdit: false); + context.read().setUserInfoPage(0, isSinglePageEdit: false); context.navigateWithName(AppRoutes.userInfoFlowManager); }, - backgroundColor: - AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, @@ -257,22 +273,13 @@ class _UserInfoSelectionScreenState extends State { child: CustomButton( text: "Yes, It is".needTranslation, icon: AppAssets.tickIcon, - iconColor: hasEmptyFields - ? AppColors.greyTextColor - : AppColors.whiteColor, + iconColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, onPressed: hasEmptyFields ? () {} // Empty function for disabled state - : () => context - .navigateWithName(AppRoutes.organSelectorPage), - backgroundColor: hasEmptyFields - ? AppColors.greyLightColor - : AppColors.primaryRedColor, - borderColor: hasEmptyFields - ? AppColors.greyLightColor - : AppColors.primaryRedColor, - textColor: hasEmptyFields - ? AppColors.greyTextColor - : AppColors.whiteColor, + : () => context.navigateWithName(AppRoutes.organSelectorPage), + backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, fontSize: 16.f, ), ), diff --git a/lib/presentation/water_monitor/water_consumption_screen.dart b/lib/presentation/water_monitor/water_consumption_page.dart similarity index 95% rename from lib/presentation/water_monitor/water_consumption_screen.dart rename to lib/presentation/water_monitor/water_consumption_page.dart index 5d08bcd6..2bd429c0 100644 --- a/lib/presentation/water_monitor/water_consumption_screen.dart +++ b/lib/presentation/water_monitor/water_consumption_page.dart @@ -21,14 +21,14 @@ import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; -class WaterConsumptionScreen extends StatefulWidget { - const WaterConsumptionScreen({super.key}); +class WaterConsumptionPage extends StatefulWidget { + const WaterConsumptionPage({super.key}); @override - State createState() => _WaterConsumptionScreenState(); + State createState() => _WaterConsumptionPageState(); } -class _WaterConsumptionScreenState extends State { +class _WaterConsumptionPageState extends State { @override void initState() { super.initState(); @@ -98,37 +98,14 @@ class _WaterConsumptionScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "History".needTranslation.toText16(isBold: true), Row( children: [ - InkWell( - onTap: () => viewModel.setGraphView(!viewModel.isGraphView), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: ScaleTransition( - scale: animation, - child: child, - ), - ); - }, - child: Container( - key: ValueKey(viewModel.isGraphView), - child: Utils.buildSvgWithAssets( - icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, - height: 24.h, - width: 24.h, - ), - ), - ), - ), + "History".needTranslation.toText16(isBold: true), SizedBox(width: 8.w), InkWell( onTap: () => _showHistoryDurationBottomsheet(context, viewModel), child: Container( - padding: EdgeInsets.symmetric(vertical: 6.h, horizontal: 6.h), + padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 6.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( backgroundColor: AppColors.greyColor, borderRadius: 8.r, @@ -137,13 +114,36 @@ class _WaterConsumptionScreenState extends State { child: Row( children: [ viewModel.selectedDurationFilter.toText12(fontWeight: FontWeight.w500), - Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 16.h), ], ), ), ) ], ), + InkWell( + onTap: () => viewModel.setGraphView(!viewModel.isGraphView), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(viewModel.isGraphView), + child: Utils.buildSvgWithAssets( + icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, + height: 24.h, + width: 24.h, + ), + ), + ), + ), ], ), if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else ...[SizedBox(height: 16.h), _buildHistoryGraph()] diff --git a/lib/presentation/water_monitor/water_monitor_settings_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_page.dart similarity index 97% rename from lib/presentation/water_monitor/water_monitor_settings_screen.dart rename to lib/presentation/water_monitor/water_monitor_settings_page.dart index 3470344b..302940c9 100644 --- a/lib/presentation/water_monitor/water_monitor_settings_screen.dart +++ b/lib/presentation/water_monitor/water_monitor_settings_page.dart @@ -13,14 +13,14 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; -class WaterMonitorSettingsScreen extends StatefulWidget { - const WaterMonitorSettingsScreen({super.key}); +class WaterMonitorSettingsPage extends StatefulWidget { + const WaterMonitorSettingsPage({super.key}); @override - State createState() => _WaterMonitorSettingsScreenState(); + State createState() => _WaterMonitorSettingsPageState(); } -class _WaterMonitorSettingsScreenState extends State { +class _WaterMonitorSettingsPageState extends State { late DialogService dialogService; @override @@ -68,7 +68,6 @@ class _WaterMonitorSettingsScreenState extends State required Function(String) onSelected, bool useUpperCase = false, }) { - dialogService.showFamilyBottomSheetWithoutHWithChild( label: title.needTranslation, message: "", diff --git a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart index 97795629..2359904b 100644 --- a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart +++ b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart @@ -94,7 +94,7 @@ class WaterActionButtonsWidget extends StatelessWidget { ), _buildActionButton( context: context, - onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen), + onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsPage), title: "Settings".needTranslation, icon: Icon( Icons.settings, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 78045b53..c57ffb90 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -6,8 +6,12 @@ import 'package:hmg_patient_app_new/presentation/authentication/register_step2.d import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart'; +import 'package:hmg_patient_app_new/presentation/covid19test/covid19_landing_page.dart'; import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculators_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/add_health_tracker_entry_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_tracker_detail_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; @@ -23,12 +27,10 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_sele import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart'; import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; -import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption_screen.dart'; -import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_screen.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption_page.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_page.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; -import '../presentation/covid19test/covid19_landing_page.dart'; - class AppRoutes { static const String initialRoute = '/initialRoute'; static const String loginScreen = '/loginScreen'; @@ -47,25 +49,29 @@ class AppRoutes { static const String huaweiHealthExample = '/huaweiHealthExample'; static const String covid19Test = '/covid19Test'; static const String vitalSign = '/vitalSign'; + //appointments static const String bookAppointmentPage = '/bookAppointmentPage'; // Water Monitor - static const String waterConsumptionScreen = '/waterConsumptionScreen'; - static const String waterMonitorSettingsScreen = '/waterMonitorSettingsScreen'; + static const String waterConsumptionPage = '/waterConsumptionScreen'; + static const String waterMonitorSettingsPage = '/waterMonitorSettingsScreen'; // Symptoms Checker static const String organSelectorPage = '/organSelectorPage'; - static const String symptomsSelectorScreen = '/symptomsCheckerScreen'; - static const String suggestionsScreen = '/suggestionsScreen'; - static const String riskFactorsScreen = '/riskFactorsScreen'; - static const String possibleConditionsScreen = '/possibleConditionsScreen'; - static const String triageScreen = '/triageProgressScreen'; - - //UserInfoSelection + static const String symptomsSelectorPage = '/symptomsCheckerScreen'; + static const String suggestionsPage = '/suggestionsScreen'; + static const String riskFactorsPage = '/riskFactorsScreen'; + static const String possibleConditionsPage = '/possibleConditionsScreen'; + static const String triagePage = '/triageProgressScreen'; static const String userInfoSelection = '/userInfoSelection'; static const String userInfoFlowManager = '/userInfoFlowManager'; + // Health Trackers + static const String healthTrackersPage = '/healthTrackersListScreen'; + static const String addHealthTrackerEntryPage = '/addHealthTrackerEntryPage'; + static const String healthTrackerDetailPage = '/healthTrackerDetailPage'; + static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -78,11 +84,11 @@ class AppRoutes { comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), homeHealthCarePage: (context) => HhcProceduresPage(), organSelectorPage: (context) => OrganSelectorPage(), - symptomsSelectorScreen: (context) => SymptomsSelectorScreen(), - riskFactorsScreen: (context) => RiskFactorsScreen(), - suggestionsScreen: (context) => SuggestionsScreen(), - possibleConditionsScreen: (context) => PossibleConditionsScreen(), - triageScreen: (context) => TriageScreen(), + symptomsSelectorPage: (context) => SymptomsSelectorPage(), + riskFactorsPage: (context) => RiskFactorsScreen(), + suggestionsPage: (context) => SuggestionsScreen(), + possibleConditionsPage: (context) => PossibleConditionsPage(), + triagePage: (context) => TriagePage(), bloodDonationPage: (context) => BloodDonationPage(), bookAppointmentPage: (context) => BookAppointmentPage(), userInfoSelection: (context) => UserInfoSelectionScreen(), @@ -90,11 +96,23 @@ class AppRoutes { smartWatches: (context) => SmartwatchInstructionsPage(), huaweiHealthExample: (context) => HuaweiHealthExample(), covid19Test: (context) => Covid19LandingPage(), - // - waterConsumptionScreen: (context) => WaterConsumptionScreen(), - waterMonitorSettingsScreen: (context) => WaterMonitorSettingsScreen(), + waterConsumptionPage: (context) => WaterConsumptionPage(), + waterMonitorSettingsPage: (context) => WaterMonitorSettingsPage(), healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), - vitalSign: (context) => VitalSignPage() + healthTrackersPage: (context) => HealthTrackersPage(), + vitalSign: (context) => VitalSignPage(), + addHealthTrackerEntryPage: (context) { + final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?; + return AddHealthTrackerEntryPage( + trackerType: args ?? HealthTrackerTypeEnum.bloodSugar, + ); + }, + healthTrackerDetailPage: (context) { + final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?; + return HealthTrackerDetailPage( + trackerType: args ?? HealthTrackerTypeEnum.bloodSugar, + ); + }, }; } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index c631f5b8..aee425ce 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -2,111 +2,112 @@ import 'package:flutter/material.dart'; class AppColors { static const transparent = Colors.transparent; - static const mainPurple = Color(0xFF7954F7); + static const mainPurple = Color(0xFF7954F7); // #7954F7 - static const scaffoldBgColor = Color(0xFFF8F8F8); - static const bottomSheetBgColor = Color(0xFFF8F8FA); - static const lightGreyEFColor = Color(0xffeaeaff); - static const greyF7Color = Color(0xffF7F7F7); - static const greyInfoTextColor = Color(0xff777777); - static const lightGrayColor = Color(0xff808080); - static const greyTextColorLight = Color(0xFFA2A2A2); + static const scaffoldBgColor = Color(0xFFF8F8F8); // #F8F8F8 + static const bottomSheetBgColor = Color(0xFFF8F8FA); // #F8F8FA + static const lightGreyEFColor = Color(0xffeaeaff); // #EAEAFF + static const greyF7Color = Color(0xffF7F7F7); // #F7F7F7 + static const greyInfoTextColor = Color(0xff777777); // #777777 + static const lightGrayColor = Color(0xff808080); // #808080 + static const greyTextColorLight = Color(0xFFA2A2A2); // #A2A2A2 // New UI Colors - static const whiteColor = Color(0xFFffffff); - static const Color bgScaffoldColor = Color(0xffF8F8F8); - static const Color primaryRedColor = Color(0xFFED1C2B); - static const Color primaryRedBorderColor = Color(0xFFED1C2B); - static const Color secondaryLightRedColor = Color(0xFFFEE9EA); - static const Color secondaryLightRedBorderColor = Color(0xFFFEE9EA); - static const Color bgRedLightColor = Color(0xFFFEE9EA); - static const Color bgGreenColor = Color(0xFF18C273); - static const Color textColor = Color(0xFF2E3039); - static const Color borderGrayColor = Color(0x332E3039); - static const Color textColorLight = Color(0xFF5E5E5E); - static const Color borderOnlyColor = Color(0xFF2E3039); - static const Color chipBorderColorOpacity20 = Color(0x332E3039); - static const Color dividerColor = Color(0x40D2D2D2); - static const Color warningColorYellow = Color(0xFFF4A308); - static const Color blackBgColor = Color(0xFF2E3039); - static const blackColor = textColor; - static const Color inputLabelTextColor = Color(0xff898A8D); - static const Color greyTextColor = Color(0xFF8F9AA3); - static const Color lightGrayBGColor = Color(0x142E3039); - static const Color checkBoxBorderColor = Color(0xffD2D2D2); - - static const Color pharmacyBGColor = Color(0xFF359846); - - static const lightGreenColor = Color(0xFF0ccedde); - static const textGreenColor = Color(0xFF18C273); - static const Color ratingColorYellow = Color(0xFFFFAF15); - static const Color spacerLineColor = Color(0x2E30391A); + static const whiteColor = Color(0xFFffffff); // #FFFFFF + static const Color bgScaffoldColor = Color(0xffF8F8F8); // #F8F8F8 + static const Color primaryRedColor = Color(0xFFED1C2B); // #ED1C2B + static const Color primaryRedBorderColor = Color(0xFFED1C2B); // #ED1C2B + static const Color secondaryLightRedColor = Color(0xFFFEE9EA); // #FEE9EA + static const Color secondaryLightRedBorderColor = Color(0xFFFEE9EA); // #FEE9EA + static const Color bgRedLightColor = Color(0xFFFEE9EA); // #FEE9EA + static const Color bgGreenColor = Color(0xFF18C273); // #18C273 + static const Color textColor = Color(0xFF2E3039); // #2E3039 + static const Color borderGrayColor = Color(0x332E3039); // #2E3039 (20% opacity) + static const Color textColorLight = Color(0xFF5E5E5E); // #5E5E5E + static const Color borderOnlyColor = Color(0xFF2E3039); // #2E3039 + static const Color chipBorderColorOpacity20 = Color(0x332E3039); // #2E3039 (20% opacity) + static const Color dividerColor = Color(0x40D2D2D2); // #D2D2D2 (25% opacity) + static const Color warningColorYellow = Color(0xFFF4A308); // #F4A308 + static const Color blackBgColor = Color(0xFF2E3039); // #2E3039 + static const blackColor = textColor; // #2E3039 + static const Color inputLabelTextColor = Color(0xff898A8D); // #898A8D + static const Color greyTextColor = Color(0xFF8F9AA3); // #8F9AA3 + static const Color lightGrayBGColor = Color(0x142E3039); // #2E3039 (8% opacity) + static const Color checkBoxBorderColor = Color(0xffD2D2D2); // #D2D2D2 + + static const Color pharmacyBGColor = Color(0xFF359846); // #359846 + + static const lightGreenColor = Color(0xFF0ccedde); // #0CCEDDE + static const textGreenColor = Color(0xFF18C273); // #18C273 + static const Color ratingColorYellow = Color(0xFFFFAF15); // #FFAF15 + static const Color spacerLineColor = Color(0x2E30391A); // #2E3039 (10% opacity) //Chips - static const Color successColor = Color(0xff18C273); - static const Color successLightBgColor = Color(0xffDDF6EA); - static const Color errorColor = Color(0xFFED1C2B); - static const Color alertColor = Color(0xFFD48D05); - static const Color infoColor = Color(0xFF0B85F7); - static const Color warningColor = Color(0xFFFFCC00); - static const Color greyColor = Color(0xFFEFEFF0); - static const Color chipPrimaryRedBorderColor = Color(0xFFED1C2B); - static const Color chipSecondaryLightRedColor = Color(0xFFFEE9EA); - - static const Color successLightColor = Color(0xFF18C273); - static const Color errorLightColor = Color(0xFFED1C2B); - static const Color alertLightColor = Color(0xFFD48D05); - static const Color infoLightColor = Color(0xFF0B85F7); - static const Color warningLightColor = Color(0xFFFFCC00); - static const Color greyLightColor = Color(0xFFEFEFF0); - static const Color thumbColor = Color(0xFF18C273); - static const Color switchBackgroundColor = Color(0x2618C273); - - static const Color bottomNAVBorder = Color(0xFFEEEEEE); - - static const Color quickLoginColor = Color(0xFF666666); - - static const Color tooltipTextColor = Color(0xFF414D55); - static const Color graphGridColor = Color(0x4D18C273); - static const Color criticalLowAndHigh = Color(0xFFED1C2B); - static const Color highAndLow = Color(0xFFFFAF15); - static const Color labelTextColor = Color(0xFF838383); - static const Color calenderTextColor = Color(0xFFD0D0D0); - static const Color lightGreenButtonColor = Color(0x2618C273); - - static const Color lightRedButtonColor = Color(0x1AED1C2B); + static const Color successColor = Color(0xff18C273); // #18C273 + static const Color successLightBgColor = Color(0xffDDF6EA); // #DDF6EA + static const Color errorColor = Color(0xFFED1C2B); // #ED1C2B + static const Color alertColor = Color(0xFFD48D05); // #D48D05 + static const Color infoColor = Color(0xFF0B85F7); // #0B85F7 + static const Color warningColor = Color(0xFFFFCC00); // #FFCC00 + static const Color greyColor = Color(0xFFEFEFF0); // #EFEFF0 + static const Color chipPrimaryRedBorderColor = Color(0xFFED1C2B); // #ED1C2B + static const Color chipSecondaryLightRedColor = Color(0xFFFEE9EA); // #FEE9EA + // static const Color chipSecondaryLightRedColor = Color(0xFFFF9E15); // #FEE9EA + + static const Color successLightColor = Color(0xFF18C273); // #18C273 + static const Color errorLightColor = Color(0xFFED1C2B); // #ED1C2B + static const Color alertLightColor = Color(0xFFD48D05); // #D48D05 + static const Color infoLightColor = Color(0xFF0B85F7); // #0B85F7 + static const Color warningLightColor = Color(0xFFFFCC00); // #FFCC00 + static const Color greyLightColor = Color(0xFFEFEFF0); // #EFEFF0 + static const Color thumbColor = Color(0xFF18C273); // #18C273 + static const Color switchBackgroundColor = Color(0x2618C273); // #18C273 (15% opacity) + + static const Color bottomNAVBorder = Color(0xFFEEEEEE); // #EEEEEE + + static const Color quickLoginColor = Color(0xFF666666); // #666666 + + static const Color tooltipTextColor = Color(0xFF414D55); // #414D55 + static const Color graphGridColor = Color(0x4D18C273); // #18C273 (30% opacity) + static const Color criticalLowAndHigh = Color(0xFFED1C2B); // #ED1C2B + static const Color highAndLow = Color(0xFFFFAF15); // #FFAF15 + static const Color labelTextColor = Color(0xFF838383); // #838383 + static const Color calenderTextColor = Color(0xFFD0D0D0); // #D0D0D0 + static const Color lightGreenButtonColor = Color(0x2618C273); // #18C273 (15% opacity) + + static const Color lightRedButtonColor = Color(0x1AED1C2B); // #ED1C2B (10% opacity) // Status Colors - static const Color statusPendingColor = Color(0xffCC9B14); - static const Color statusProcessingColor = Color(0xff2E303A); - static const Color statusCompletedColor = Color(0xff359846); - static const Color statusRejectedColor = Color(0xffD02127); + static const Color statusPendingColor = Color(0xffCC9B14); // #CC9B14 + static const Color statusProcessingColor = Color(0xff2E303A); // #2E303A + static const Color statusCompletedColor = Color(0xff359846); // #359846 + static const Color statusRejectedColor = Color(0xffD02127); // #D02127 // Info Banner Colors - static const Color infoBannerBgColor = Color(0xFFFFF4E6); - static const Color infoBannerBorderColor = Color(0xFFFFE5B4); - static const Color infoBannerIconColor = Color(0xFFCC9B14); - static const Color infoBannerTextColor = Color(0xFF856404); + static const Color infoBannerBgColor = Color(0xFFFFF4E6); // #FFF4E6 + static const Color infoBannerBorderColor = Color(0xFFFFE5B4); // #FFE5B4 + static const Color infoBannerIconColor = Color(0xFFCC9B14); // #CC9B14 + static const Color infoBannerTextColor = Color(0xFF856404); // #856404 // SymptomsChecker - static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); - static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103); - static const Color chipColorMonitor = Color(0xFF18C273); - static const Color chipColorEmergency = Color(0xFFED1C2B); + static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); // #FFAF15 + static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103); // #AB7103 + static const Color chipColorMonitor = Color(0xFF18C273); // #18C273 + static const Color chipColorEmergency = Color(0xFFED1C2B); // #ED1C2B // Services Page Colors - static const Color eReferralCardColor = Color(0xFFFF8012); - static const Color bloodDonationCardColor = Color(0xFFFF5662); - static const Color bookAppointment = Color(0xFF415364); + static const Color eReferralCardColor = Color(0xFFFF8012); // #FF8012 + static const Color bloodDonationCardColor = Color(0xFFFF5662); // #FF5662 + static const Color bookAppointment = Color(0xFF415364); // #415364 // Water Monitor - static const Color blueColor = Color(0xFF4EB5FF); - static const Color blueGradientColorOne = Color(0xFFF1F7FD); - static const Color blueGradientColorTwo = Color(0xFFD9EFFF); + static const Color blueColor = Color(0xFF4EB5FF); // #4EB5FF + static const Color blueGradientColorOne = Color(0xFFF1F7FD); // #F1F7FD + static const Color blueGradientColorTwo = Color(0xFFD9EFFF); // #D9EFFF // Shimmer - static const Color shimmerBaseColor = Color(0xFFE0E0E0); - static const Color shimmerHighlightColor = Color(0xFFF5F5F5); - static const Color covid29Color = Color(0xff2563EB); + static const Color shimmerBaseColor = Color(0xFFE0E0E0); // #E0E0E0 + static const Color shimmerHighlightColor = Color(0xFFF5F5F5); // #F5F5F5 + static const Color covid29Color = Color(0xff2563EB); // #2563EB } diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index 1f5d8060..ad47cd2b 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// /// **Parameters:** /// - [dataPoints]: List of `DataPoint` objects to plot. +/// - [secondaryDataPoints]: Optional list for a second line (e.g., diastolic in blood pressure). /// - [leftLabelFormatter]: Function to build left axis labels. /// - [bottomLabelFormatter]: Function to build bottom axis labels. /// - [width]: Optional width of the chart. @@ -18,6 +19,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// - [maxY], [maxX], [minX]: Axis bounds. /// - [spotColor]: Color of the touched spot marker. /// - [graphColor]: Color of the line. +/// - [secondaryGraphColor]: Color of the secondary line. /// - [graphShadowColor]: Color of the area below the line. /// - [graphGridColor]: Color of the grid lines. /// - [bottomLabelColor]: Color of bottom axis labels. @@ -43,6 +45,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// ) class CustomGraph extends StatelessWidget { final List dataPoints; + final List? secondaryDataPoints; // For dual-line graphs (e.g., blood pressure) final double? width; final double height; final double? maxY; @@ -50,6 +53,7 @@ class CustomGraph extends StatelessWidget { final double? minX; final Color spotColor; final Color graphColor; + final Color? secondaryGraphColor; // Color for secondary line final Color graphShadowColor; final Color graphGridColor; final Color bottomLabelColor; @@ -79,6 +83,7 @@ class CustomGraph extends StatelessWidget { const CustomGraph( {super.key, required this.dataPoints, + this.secondaryDataPoints, required this.leftLabelFormatter, this.width, required this.scrollDirection, @@ -89,6 +94,7 @@ class CustomGraph extends StatelessWidget { this.isFullScreeGraph = false, this.spotColor = AppColors.bgGreenColor, this.graphColor = AppColors.bgGreenColor, + this.secondaryGraphColor, this.graphShadowColor = AppColors.graphGridColor, this.graphGridColor = AppColors.graphGridColor, this.bottomLabelColor = AppColors.textColor, @@ -225,7 +231,7 @@ class CustomGraph extends StatelessWidget { return FlSpot(entry.key.toDouble(), value); }).toList(); - var data = [ + var data = [ LineChartBarData( spots: allSpots, isCurved: true, @@ -254,6 +260,31 @@ class CustomGraph extends StatelessWidget { ) ]; + // Add secondary line if provided (for dual-line graphs like blood pressure) + if (secondaryDataPoints != null && secondaryDataPoints!.isNotEmpty) { + final List secondarySpots = secondaryDataPoints!.asMap().entries.map((entry) { + double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; + return FlSpot(entry.key.toDouble(), value); + }).toList(); + + data.add( + LineChartBarData( + spots: secondarySpots, + isCurved: true, + isStrokeCapRound: true, + isStrokeJoinRound: true, + barWidth: 2, + gradient: LinearGradient( + colors: [secondaryGraphColor ?? AppColors.blueColor, secondaryGraphColor ?? AppColors.blueColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + dotData: FlDotData(show: showLinePoints), + belowBarData: BarAreaData(show: false), + ), + ); + } + return data; } } diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 94ed9f8b..9f101d47 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -3,17 +3,14 @@ import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.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/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; - -import '../core/dependencies.dart'; - -// TODO: Import AppColors if bgRedColor is defined there -// import 'package:hmg_patient_app_new/core/ui_utils/app_colors.dart'; +import 'package:hmg_patient_app_new/widgets/time_picker_widget.dart'; class TextInputWidget extends StatelessWidget { final String labelText; @@ -49,6 +46,7 @@ class TextInputWidget extends StatelessWidget { final int maxLines; final Color? hintColor; final bool? isHideSwitcher; + final bool? isArrowTrailing; // final List countryList; // final Function(Country)? onCountryChange; @@ -87,6 +85,7 @@ class TextInputWidget extends StatelessWidget { this.minLines = 3, this.maxLines = 6, this.isHideSwitcher, + this.isArrowTrailing, // this.countryList = const [], // this.onCountryChange, }); @@ -167,7 +166,8 @@ class TextInputWidget extends StatelessWidget { ], ), ), - if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context), + if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context, isArrowTrailing: isArrowTrailing ?? false), + if (selectionType == SelectionTypeEnum.time) _buildTimePickerIcon(context, isArrowTrailing: isArrowTrailing ?? false), if (selectionType == SelectionTypeEnum.search) _buildTrailingIconForSearch(context), ], ), @@ -200,7 +200,7 @@ class TextInputWidget extends StatelessWidget { child: Utils.buildSvgWithAssets(icon: leadingIcon!)); } - Widget _buildTrailingIcon(BuildContext context) { + Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) { final AppState appState = getIt.get(); return Container( height: 40.h, @@ -233,7 +233,47 @@ class TextInputWidget extends StatelessWidget { onChange!(picked.toString()); } }, - child: Utils.buildSvgWithAssets(icon: AppAssets.calendar), + child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.calendar), + ), + ); + } + + Widget _buildTimePickerIcon(BuildContext context, {bool isArrowTrailing = false}) { + return Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.zero, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.whiteColor, + ), + child: GestureDetector( + onTap: () async { + // Parse existing time if available + TimeOfDay? initialTime; + if (controller?.text.isNotEmpty ?? false) { + initialTime = TimePickerWidget.parseTime(controller!.text); + } + + final picked = await TimePickerWidget.show( + context, + initialTime: initialTime, + use24HourFormat: false, // You can make this configurable if needed + onTimeSelected: (time) { + if (onChange != null) { + final formattedTime = TimePickerWidget.formatTime(time); + onChange!(formattedTime); + } + }, + ); + + // Update controller if time was picked + if (picked != null && controller != null) { + controller!.text = TimePickerWidget.formatTime(picked); + } + }, + child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.alarm_clock_icon), ), ); } diff --git a/lib/widgets/time_picker_widget.dart b/lib/widgets/time_picker_widget.dart new file mode 100644 index 00000000..71d9ab2e --- /dev/null +++ b/lib/widgets/time_picker_widget.dart @@ -0,0 +1,348 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.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/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +/// A reusable time picker widget that can be used anywhere in the app +/// Shows a bottom sheet with iOS-style time picker +class TimePickerWidget { + /// Shows a time picker bottom sheet + /// + /// [context] - BuildContext for showing the bottom sheet + /// [initialTime] - Initial time to display (defaults to current time) + /// [use24HourFormat] - Whether to use 24-hour format (defaults to false) + /// [onTimeSelected] - Callback when time is selected + /// + /// Returns the selected TimeOfDay or null if cancelled + static Future show( + BuildContext context, { + TimeOfDay? initialTime, + bool use24HourFormat = false, + bool displaySelectedTime = false, + Function(TimeOfDay)? onTimeSelected, + }) async { + final selectedTime = initialTime ?? TimeOfDay.now(); + + final result = await showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (BuildContext context) { + return _TimePickerBottomSheet( + initialTime: selectedTime, + use24HourFormat: use24HourFormat, + displaySelectedTime: displaySelectedTime, + onTimeChanged: (time) { + // Time is being changed in real-time + }, + ); + }, + ); + + if (result != null && onTimeSelected != null) { + onTimeSelected(result); + } + + return result; + } + + /// Formats TimeOfDay to string (HH:mm format) + static String formatTime(TimeOfDay time, {bool use24HourFormat = false}) { + if (use24HourFormat) { + return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } else { + final hour = time.hourOfPeriod == 0 ? 12 : time.hourOfPeriod; + final period = time.period == DayPeriod.am ? 'AM' : 'PM'; + return '${hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')} $period'; + } + } + + /// Parses time string to TimeOfDay + static TimeOfDay? parseTime(String timeString) { + try { + final parts = timeString.split(':'); + if (parts.length == 2) { + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1].split(' ')[0]); + return TimeOfDay(hour: hour, minute: minute); + } + } catch (e) { + return null; + } + return null; + } +} + +class _TimePickerBottomSheet extends StatefulWidget { + final TimeOfDay initialTime; + final bool use24HourFormat; + final bool displaySelectedTime; + final Function(TimeOfDay) onTimeChanged; + + const _TimePickerBottomSheet({ + required this.initialTime, + required this.use24HourFormat, + required this.displaySelectedTime, + required this.onTimeChanged, + }); + + @override + State<_TimePickerBottomSheet> createState() => _TimePickerBottomSheetState(); +} + +class _TimePickerBottomSheetState extends State<_TimePickerBottomSheet> { + late int selectedHour; + late int selectedMinute; + late DayPeriod selectedPeriod; + + @override + void initState() { + super.initState(); + selectedHour = widget.use24HourFormat ? widget.initialTime.hour : widget.initialTime.hourOfPeriod; + if (selectedHour == 0 && !widget.use24HourFormat) selectedHour = 12; + selectedMinute = widget.initialTime.minute; + selectedPeriod = widget.initialTime.period; + } + + TimeOfDay _getCurrentTime() { + if (widget.use24HourFormat) { + return TimeOfDay(hour: selectedHour, minute: selectedMinute); + } else { + int hour = selectedHour; + if (selectedPeriod == DayPeriod.pm && hour != 12) { + hour += 12; + } else if (selectedPeriod == DayPeriod.am && hour == 12) { + hour = 0; + } + return TimeOfDay(hour: hour, minute: selectedMinute); + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.dividerColor, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Select Time".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Utils.buildSvgWithAssets( + icon: AppAssets.cancel, + width: 24.h, + height: 24.h, + iconColor: AppColors.textColor, + ), + ), + ], + ), + ), + + // Time Picker + SizedBox( + height: 250.h, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Hour Picker + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: widget.use24HourFormat ? selectedHour : (selectedHour - 1), + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + if (widget.use24HourFormat) { + selectedHour = index; + } else { + selectedHour = index + 1; + } + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: List.generate( + widget.use24HourFormat ? 24 : 12, + (index) { + final hour = widget.use24HourFormat ? index : index + 1; + return Center( + child: Text( + hour.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ); + }, + ), + ), + ), + + // Separator + Text( + ':', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + + // Minute Picker + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: selectedMinute, + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + selectedMinute = index; + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: List.generate( + 60, + (index) => Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + ), + ), + ), + + // AM/PM Picker (only for 12-hour format) + if (!widget.use24HourFormat) + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: selectedPeriod == DayPeriod.am ? 0 : 1, + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + selectedPeriod = index == 0 ? DayPeriod.am : DayPeriod.pm; + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: [ + Center( + child: Text( + 'AM', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + Center( + child: Text( + 'PM', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + if (widget.displaySelectedTime) + // Current Time Display + Container( + margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + padding: EdgeInsets.symmetric(vertical: 12.h), + decoration: BoxDecoration( + color: AppColors.lightGrayBGColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Center( + child: TimePickerWidget.formatTime( + _getCurrentTime(), + use24HourFormat: widget.use24HourFormat, + ).toText20( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ), + + // Action Buttons + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + child: Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: "Cancel".needTranslation, + onPressed: () => Navigator.pop(context), + textColor: AppColors.textColor, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + height: 56.h, + text: "Confirm".needTranslation, + onPressed: () { + Navigator.pop(context, _getCurrentTime()); + }, + textColor: AppColors.whiteColor, + backgroundColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/time_picker_widget_usage_example.dart b/lib/widgets/time_picker_widget_usage_example.dart new file mode 100644 index 00000000..9ff6c356 --- /dev/null +++ b/lib/widgets/time_picker_widget_usage_example.dart @@ -0,0 +1,165 @@ +// Example usage of TimePickerWidget +// +// This file demonstrates how to use the TimePickerWidget in your Flutter app. +// The TimePickerWidget is a reusable component that shows a bottom sheet with +// an iOS-style time picker. + +// ============================================================================ +// EXAMPLE 1: Using with TextInputWidget +// ============================================================================ +/* +TextInputWidget( + labelText: "Appointment Time", + hintText: "Select time", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + print("Selected time: $value"); + }, +) +*/ + +// ============================================================================ +// EXAMPLE 2: Direct usage with custom button +// ============================================================================ +/* +ElevatedButton( + onPressed: () async { + final selectedTime = await TimePickerWidget.show( + context, + initialTime: TimeOfDay.now(), + use24HourFormat: false, + onTimeSelected: (time) { + print("Time selected: ${TimePickerWidget.formatTime(time)}"); + }, + ); + + if (selectedTime != null) { + print("Final time: ${TimePickerWidget.formatTime(selectedTime)}"); + } + }, + child: Text("Pick Time"), +) +*/ + +// ============================================================================ +// EXAMPLE 3: Using with 24-hour format +// ============================================================================ +/* +TextInputWidget( + labelText: "Meeting Time", + hintText: "Select time (24h)", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + // The value will be formatted as "14:30" for 2:30 PM in 24h format + print("Selected time (24h): $value"); + }, +) + +// Or programmatically: +final time = await TimePickerWidget.show( + context, + use24HourFormat: true, // Enable 24-hour format +); +*/ + +// ============================================================================ +// EXAMPLE 4: Parsing and formatting times +// ============================================================================ +/* +// Parse time string to TimeOfDay +String timeString = "02:30 PM"; +TimeOfDay? parsedTime = TimePickerWidget.parseTime(timeString); + +// Format TimeOfDay to string +TimeOfDay time = TimeOfDay(hour: 14, minute: 30); +String formatted12h = TimePickerWidget.formatTime(time); // "02:30 PM" +String formatted24h = TimePickerWidget.formatTime(time, use24HourFormat: true); // "14:30" +*/ + +// ============================================================================ +// EXAMPLE 5: Complete form example with date and time +// ============================================================================ +/* +class AppointmentForm extends StatefulWidget { + @override + _AppointmentFormState createState() => _AppointmentFormState(); +} + +class _AppointmentFormState extends State { + final TextEditingController dateController = TextEditingController(); + final TextEditingController timeController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Date picker + TextInputWidget( + labelText: "Appointment Date", + hintText: "Select date", + controller: dateController, + selectionType: SelectionTypeEnum.calendar, + isReadOnly: true, + onChange: (value) { + print("Date selected: $value"); + }, + ), + + SizedBox(height: 16), + + // Time picker + TextInputWidget( + labelText: "Appointment Time", + hintText: "Select time", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + print("Time selected: $value"); + }, + ), + ], + ); + } +} +*/ + +// ============================================================================ +// Features: +// ============================================================================ +// ✅ iOS-style cupertino picker (works on both iOS and Android) +// ✅ Support for 12-hour format (with AM/PM) +// ✅ Support for 24-hour format +// ✅ Beautiful bottom sheet UI matching app design +// ✅ Real-time preview of selected time +// ✅ Confirm/Cancel buttons +// ✅ Easy integration with TextInputWidget +// ✅ Parse and format time utilities +// ✅ Fully customizable and reusable + +// ============================================================================ +// API Reference: +// ============================================================================ +// TimePickerWidget.show() - Shows the time picker bottom sheet +// Parameters: +// - context: BuildContext (required) +// - initialTime: TimeOfDay? (optional, defaults to current time) +// - use24HourFormat: bool (optional, defaults to false) +// - onTimeSelected: Function(TimeOfDay)? (optional callback) +// Returns: Future +// +// TimePickerWidget.formatTime() - Formats TimeOfDay to string +// Parameters: +// - time: TimeOfDay (required) +// - use24HourFormat: bool (optional, defaults to false) +// Returns: String (e.g., "02:30 PM" or "14:30") +// +// TimePickerWidget.parseTime() - Parses time string to TimeOfDay +// Parameters: +// - timeString: String (required, e.g., "02:30 PM") +// Returns: TimeOfDay? + From 001808488c9ee29d431f421ca256c53cbb7e6e45 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Mon, 12 Jan 2026 11:30:01 +0300 Subject: [PATCH 19/21] Added 'send report to email' feature --- .../health_trackers/health_trackers_repo.dart | 127 ++++- lib/main.dart | 1 + .../health_tracker_detail_page.dart | 183 ++++++- .../health_trackers_view_model.dart | 104 ++++ .../medical_file/medical_file_page.dart | 456 +++++++++--------- 5 files changed, 644 insertions(+), 227 deletions(-) diff --git a/lib/features/health_trackers/health_trackers_repo.dart b/lib/features/health_trackers/health_trackers_repo.dart index e6930f9c..2a64fc46 100644 --- a/lib/features/health_trackers/health_trackers_repo.dart +++ b/lib/features/health_trackers/health_trackers_repo.dart @@ -39,6 +39,11 @@ abstract class HealthTrackersRepo { required int lineItemNo, }); + /// Send blood sugar report by email. + Future>> sendBloodSugarReportByEmail({ + required String email, + }); + // ==================== BLOOD PRESSURE ==================== /// Get blood pressure result averages (week, month, year). Future>> getBloodPressureResultAverage(); @@ -68,6 +73,11 @@ abstract class HealthTrackersRepo { required int lineItemNo, }); + /// Send blood pressure report by email. + Future>> sendBloodPressureReportByEmail({ + required String email, + }); + // ==================== WEIGHT MEASUREMENT ==================== /// Get weight measurement result averages (week, month, year). Future>> getWeightMeasurementResultAverage(); @@ -94,6 +104,11 @@ abstract class HealthTrackersRepo { Future>> deactivateWeightMeasurementStatus({ required int lineItemNo, }); + + /// Send weight report by email. + Future>> sendWeightReportByEmail({ + required String email, + }); } class HealthTrackersRepoImp implements HealthTrackersRepo { @@ -322,6 +337,42 @@ class HealthTrackersRepoImp implements HealthTrackersRepo { } } + @override + Future>> sendBloodSugarReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBloodSugarReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + // ==================== BLOOD PRESSURE METHODS ==================== @override @@ -538,6 +589,42 @@ class HealthTrackersRepoImp implements HealthTrackersRepo { } } + @override + Future>> sendBloodPressureReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBloodPressureReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + // ==================== WEIGHT MEASUREMENT METHODS ==================== @override @@ -715,9 +802,7 @@ class HealthTrackersRepoImp implements HealthTrackersRepo { } @override - Future>> deactivateWeightMeasurementStatus({ - required int lineItemNo, - }) async { + Future>> deactivateWeightMeasurementStatus({required int lineItemNo}) async { try { GenericApiModel? apiResponse; Failure? failure; @@ -749,4 +834,40 @@ class HealthTrackersRepoImp implements HealthTrackersRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> sendWeightReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBodyWeightReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/main.dart b/lib/main.dart index f0537b31..50915726 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -219,3 +219,4 @@ class MyApp extends StatelessWidget { } } // flutter pub run easy_localization:generate -S assets/langs -f keys -o locale_keys.g.dart + diff --git a/lib/presentation/health_trackers/health_tracker_detail_page.dart b/lib/presentation/health_trackers/health_tracker_detail_page.dart index a2b82ecc..9443f122 100644 --- a/lib/presentation/health_trackers/health_tracker_detail_page.dart +++ b/lib/presentation/health_trackers/health_tracker_detail_page.dart @@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; @@ -9,8 +10,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart'; -import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart'; import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart'; import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart'; import 'package:hmg_patient_app_new/features/health_trackers/models/weight/week_weight_measurement_result_average.dart'; @@ -22,7 +21,10 @@ 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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; @@ -1028,7 +1030,182 @@ class _HealthTrackerDetailPageState extends State { } void onSendEmailPressed(BuildContext context) async { - // TODO: Implement send email functionality + _showEmailInputBottomSheet(context); + } + + /// Show email input bottom sheet + void _showEmailInputBottomSheet(BuildContext context) { + final viewModel = context.read(); + final appState = getIt.get(); + final dialogService = getIt.get(); + + // Get user's email from authenticated user + final userEmail = appState.getAuthenticatedUser()?.emailAddress ?? ''; + + // Create email controller and pre-fill if available + final emailController = TextEditingController(text: userEmail); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Send Report by Email".needTranslation, + message: "", + child: _buildEmailInputContent( + context: context, + emailController: emailController, + viewModel: viewModel, + dialogService: dialogService, + ), + onOkPressed: () {}, + ); + } + + /// Build email input content + Widget _buildEmailInputContent({ + required BuildContext context, + required TextEditingController emailController, + required HealthTrackersViewModel viewModel, + required DialogService dialogService, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Enter your email address to receive the report".needTranslation.toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + SizedBox(height: 16.h), + + // Email Input Field using TextInputWidget + TextInputWidget( + padding: EdgeInsets.symmetric(horizontal: 8.w), + labelText: "Email Address".needTranslation, + hintText: "Enter email address".needTranslation, + controller: emailController, + keyboardType: TextInputType.emailAddress, + isEnable: true, + isBorderAllowed: true, + isAllowRadius: true, + ), + + SizedBox(height: 24.h), + + // Send Button + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: "Send Report".needTranslation, + onPressed: () { + _sendEmailReport( + context: context, + email: emailController.text.trim(), + viewModel: viewModel, + dialogService: dialogService, + ); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ); + } + + /// Send email report based on tracker type + Future _sendEmailReport({ + required BuildContext context, + required String email, + required HealthTrackersViewModel viewModel, + required DialogService dialogService, + }) async { + // Validate email + if (email.isEmpty) { + dialogService.showErrorBottomSheet( + message: "Please enter your email address".needTranslation, + ); + return; + } + + // Basic email validation + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(email)) { + dialogService.showErrorBottomSheet( + message: "Please enter a valid email address".needTranslation, + ); + return; + } + + // Close the email input bottom sheet + Navigator.of(context).pop(); + + // Call appropriate email function based on tracker type + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + await viewModel.sendBloodSugarReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + + case HealthTrackerTypeEnum.bloodPressure: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + + await viewModel.sendBloodPressureReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + + case HealthTrackerTypeEnum.weightTracker: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + await viewModel.sendWeightReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + } + } + + /// Show success message + void _showSuccessMessage(BuildContext context, DialogService dialogService) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: "Report has been sent to your email successfully".needTranslation, + ), + callBackFunc: () {}, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); } Widget _buildPageShimmer() { diff --git a/lib/presentation/health_trackers/health_trackers_view_model.dart b/lib/presentation/health_trackers/health_trackers_view_model.dart index 059c11ec..ce1bdf26 100644 --- a/lib/presentation/health_trackers/health_trackers_view_model.dart +++ b/lib/presentation/health_trackers/health_trackers_view_model.dart @@ -388,6 +388,41 @@ class HealthTrackersViewModel extends ChangeNotifier { } } + /// Send weight report by email + Future sendWeightReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendWeightReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendWeightReportByEmail: $e'); + + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + // ==================== BLOOD PRESSURE TRACKING METHODS ==================== /// Fetch blood pressure averages and results @@ -554,6 +589,41 @@ class HealthTrackersViewModel extends ChangeNotifier { } } + /// Send blood pressure report by email + Future sendBloodPressureReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendBloodPressureReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendBloodPressureReportByEmail: $e'); + + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + // ==================== BLOOD SUGAR (DIABETIC) TRACKING METHODS ==================== /// Fetch blood sugar averages and results @@ -746,6 +816,40 @@ class HealthTrackersViewModel extends ChangeNotifier { } } + /// Send blood sugar report by email + Future sendBloodSugarReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendBloodSugarReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendBloodSugarReportByEmail: $e'); + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + // Validation method String? _validateBloodSugarEntry(String dateTime) { // Validate blood sugar value diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index f7059936..554131a0 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -7,10 +7,11 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.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/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_config.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/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; @@ -234,9 +235,9 @@ class _MedicalFilePageState extends State { labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), onChipTap: () { navigationService.pushPage( - page: FamilyMedicalScreen( - profiles: medicalFileViewModel.patientFamilyFiles, - onSelect: (FamilyFileResponseModelLists p1) {}, + page: FamilyMedicalScreen( + profiles: medicalFileViewModel.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists p1) {}, ), ); }, @@ -279,7 +280,8 @@ class _MedicalFilePageState extends State { iconColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, textColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 12.w, - backgroundColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + backgroundColor: + insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), ); }), @@ -381,9 +383,7 @@ class _MedicalFilePageState extends State { width: hmgServicesVM.vitalSignCurrentPage == index ? 24.w : 8.w, height: 8.h, decoration: BoxDecoration( - color: hmgServicesVM.vitalSignCurrentPage == index - ? AppColors.primaryRedColor - : AppColors.dividerColor, + color: hmgServicesVM.vitalSignCurrentPage == index ? AppColors.primaryRedColor : AppColors.dividerColor, borderRadius: BorderRadius.circular(4.r), ), ), @@ -587,7 +587,8 @@ class _MedicalFilePageState extends State { ? Container( padding: EdgeInsets.all(12.w), width: MediaQuery.of(context).size.width, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), @@ -624,57 +625,58 @@ class _MedicalFilePageState extends State { itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - 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) { + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + 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 { 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"); + } + }, + onError: (_) { LoaderBottomSheet.hideLoader(); - print("Doctor is not available"); - } - }, - onError: (_) { - LoaderBottomSheet.hideLoader(); - }, - ); - }, + }, + ); + }, + ), ), ), - ), - )); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), ), @@ -733,116 +735,125 @@ class _MedicalFilePageState extends State { child: Column( children: [ ListView.separated( - itemCount: prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, + itemCount: + prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, shrinkWrap: true, padding: EdgeInsets.only(left: 0, right: 8.w), physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Row( - children: [ - Image.network( - prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ).circle(100.r), - SizedBox(width: 16.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true), - SizedBox(height: 4.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.w, - runSpacing: 4.w, - children: [ - AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate( - DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate), - false, + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: Row( + children: [ + Image.network( + prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).circle(100.r), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true), + SizedBox(height: 4.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.w, + runSpacing: 4.w, + children: [ + AppCustomChipWidget( + labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), + AppCustomChipWidget( + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate( + DateUtil.convertStringToDate( + prescriptionVM.patientPrescriptionOrders[index].appointmentDate), + false, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - // SizedBox(width: 40.h), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), - ], - ).onPress(() { - prescriptionVM.setPrescriptionsDetailsLoading(); - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionDetailPage(isFromAppointments: false, prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), - ), - ); - }), - ), - )); - }, - 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: 40.h), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + width: 15.w, + height: 15.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor)), + ], + ).onPress(() { + prescriptionVM.setPrescriptionsDetailsLoading(); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDetailPage( + isFromAppointments: false, + prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), + ), + ); + }), + ), + )); + }, + 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) @@ -896,7 +907,10 @@ class _MedicalFilePageState extends State { fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), + ("Dr. John Smith Smith Smith") + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: true), ], ) : myAppointmentsVM.patientMyDoctorsList.isEmpty @@ -923,58 +937,58 @@ class _MedicalFilePageState extends State { shrinkWrap: true, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SizedBox( - // width: 80.w, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.w, - height: 64.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: false, radius: 50.r), - SizedBox(height: 8.h), - Expanded( - child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: false), - ), - ], - ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, - projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, - doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, - )); - 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, - ); - }); - }), - ), - )); + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + // width: 80.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + Expanded( + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, + projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, + doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, + )); + 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, + ); + }); + }), + ), + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), @@ -1083,9 +1097,14 @@ class _MedicalFilePageState extends State { text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", onPressed: () { insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); - insuranceViewModel.getPatientInsuranceDetailsForUpdate( - appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + insuranceViewModel.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(), + appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, + child: PatientInsuranceCardUpdateCard(), + callBackFunc: () {}, + title: "", + isCloseButtonVisible: false, + isFullScreen: false); }, backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), borderColor: AppColors.bgGreenColor.withOpacity(0.0), @@ -1282,7 +1301,7 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.blood_sugar_icon, isLargeText: true, iconSize: 36.w, - ).onPress(() {}), + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.bloodSugar)), MedicalFileCard( label: "Blood Pressure".needTranslation, textColor: AppColors.blackColor, @@ -1290,7 +1309,7 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.lab_result_icon, isLargeText: true, iconSize: 36.w, - ).onPress(() {}), + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.bloodPressure)), MedicalFileCard( label: "Weight Tracker".needTranslation, textColor: AppColors.blackColor, @@ -1298,7 +1317,7 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.weight_tracker_icon, isLargeText: true, iconSize: 36.w, - ).onPress(() {}), + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.weightTracker)), ], ).paddingSymmetrical(0.w, 0.0), SizedBox(height: 16.h), @@ -1547,7 +1566,6 @@ class _MedicalFilePageState extends State { ], ), SizedBox(height: 14.h), - Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), decoration: BoxDecoration( @@ -1585,7 +1603,6 @@ class _MedicalFilePageState extends State { ), ), SizedBox(height: 8.h), - Align( alignment: AlignmentDirectional.centerEnd, child: Utils.buildSvgWithAssets( @@ -1603,6 +1620,3 @@ class _MedicalFilePageState extends State { ); } } - - - From ac0d72b3ff8b94d8e72dc6daeb33c5c74f356079 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 12 Jan 2026 11:41:35 +0300 Subject: [PATCH 20/21] vital sign finalized. --- .../medical_file/medical_file_page.dart | 287 +++++++++--------- .../vital_sign/vital_sign_details_page.dart | 274 +++++++++-------- .../vital_sign/vital_sign_page.dart | 46 +-- 3 files changed, 329 insertions(+), 278 deletions(-) diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 6dde9580..3d8d1159 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -305,55 +305,64 @@ class _MedicalFilePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), - Row( - children: [ - LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), - ], - ), - ], - ).paddingSymmetrical(0.w, 0.h).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: VitalSignPage(), - ), - ); - }), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), + Row( + children: [ + LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(0.w, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }), + ), SizedBox(height: 16.h), // Make this section dynamic-height (no fixed 160.h) LayoutBuilder( builder: (context, constraints) { if (hmgServicesVM.isVitalSignLoading) { - return _buildVitalSignShimmer(); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildVitalSignShimmer(), + ); } if (hmgServicesVM.vitalSignList.isEmpty) { - return Container( - padding: EdgeInsets.all(16.w), - width: MediaQuery.of(context).size.width, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.r, - hasShadow: false, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), - SizedBox(height: 12.h), - "No vital signs recorded yet".needTranslation.toText12(isCenter: true), - ], + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Container( + padding: EdgeInsets.all(16.w), + width: MediaQuery.of(context).size.width, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "No vital signs recorded yet".needTranslation.toText12(isCenter: true), + ], + ), ), ); } // The cards define their own height; measure the first rendered page once _scheduleVitalSignMeasure(); - final double hostHeight = _vitalSignMeasuredHeight ?? (160.h); + final double hostHeight = _vitalSignMeasuredHeight ?? (135.h); return SizedBox( height: hostHeight, @@ -400,7 +409,7 @@ class _MedicalFilePageState extends State { ), ], ], - ).paddingSymmetrical(24.w, 0.0); + ); }), SizedBox(height: 16.h), @@ -1394,23 +1403,23 @@ class _MedicalFilePageState extends State { ).toShimmer(), SizedBox(height: 16.h), // Label shimmer - Container( - width: 70.w, - height: 12.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.r), - ), - ).toShimmer(), - SizedBox(height: 8.h), + // Container( + // width: 70.w, + // height: 12.h, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4.r), + // ), + // ).toShimmer(), + // SizedBox(height: 8.h), // Value shimmer (larger) - Container( - width: 60.w, - height: 32.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.r), - ), - ).toShimmer(), - SizedBox(height: 12.h), + // Container( + // width: 60.w, + // height: 32.h, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4.r), + // ), + // ).toShimmer(), + // SizedBox(height: 12.h), // Bottom row with chip and arrow Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1446,62 +1455,66 @@ class _MedicalFilePageState extends State { }) { return [ // Page 1: BMI + Height - Row( - children: [ - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.bmiVital, - label: "BMI", - value: vitalSign.bodyMassIndex?.toString() ?? '--', - unit: '', - status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, - onTap: onTap, + Padding( + padding: EdgeInsets.only(left: 24.w), + child: Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: "BMI", + value: vitalSign.bodyMassIndex?.toString() ?? '--', + unit: '', + status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, + onTap: onTap, + ), ), - ), - SizedBox(width: 12.w), - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.heightVital, - label: "Height", - value: vitalSign.heightCm?.toString() ?? '--', - unit: 'cm', - status: null, - onTap: onTap, + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.heightVital, + label: "Height", + value: vitalSign.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: onTap, + ), ), - ), - ], + ], + ), ), // Page 2: Weight + Blood Pressure - Row( - children: [ - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.weightVital, - label: "Weight", - value: vitalSign.weightKg?.toString() ?? '--', - unit: 'kg', - status: vitalSign.weightKg != null ? "Normal" : null, - onTap: onTap, + Padding(padding: EdgeInsets.symmetric(horizontal: 12.w),child: Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: "Weight", + value: vitalSign.weightKg?.toString() ?? '--', + unit: 'kg', + status: vitalSign.weightKg != null ? "Normal" : null, + onTap: onTap, + ), ), - ), - SizedBox(width: 12.w), - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.bloodPressure, - label: "Blood Pressure", - value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null - ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" - : '--', - unit: '', - status: _getBloodPressureStatus( - systolic: vitalSign.bloodPressureHigher, - diastolic: vitalSign.bloodPressureLower, + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: "Blood Pressure", + value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ), + onTap: onTap, ), - onTap: onTap, ), - ), - ], - ), + ], + )), + ]; } @@ -1526,7 +1539,6 @@ class _MedicalFilePageState extends State { return GestureDetector( onTap: onTap, child: Container( - // Same styling used originally for vitals in MedicalFilePage decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 16.r, @@ -1540,15 +1552,15 @@ class _MedicalFilePageState extends State { Row( children: [ Container( - padding: EdgeInsets.all(10.h), + padding: EdgeInsets.all(8.h), decoration: BoxDecoration( color: scheme.iconBg, borderRadius: BorderRadius.circular(12.r), ), child: Utils.buildSvgWithAssets( icon: icon, - width: 20.w, - height: 20.h, + width: 22.w, + height: 22.h, iconColor: scheme.iconFg, fit: BoxFit.contain, ), @@ -1563,55 +1575,56 @@ class _MedicalFilePageState extends State { ], ), SizedBox(height: 14.h), - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), - decoration: BoxDecoration( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bgScaffoldColor, - borderRadius: BorderRadius.circular(10.r), + borderRadius: 10.r, + hasShadow: false, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - value.toText17( - isBold: true, - color: AppColors.textColor, - ), - if (unit.isNotEmpty) ...[ - SizedBox(width: 3.w), - unit.toText12( - color: AppColors.textColor, - fontWeight: FontWeight.w500, + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: value.toText17( + isBold: true, + color: AppColors.textColor, + ), ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], ], - ], + ), ), - if (status != null) + if (status != null) ...[ + SizedBox(width: 4.w), AppCustomChipWidget( labelText: status, backgroundColor: scheme.chipBg, textColor: scheme.chipFg, - ) + ), + ] else - const SizedBox.shrink(), + AppCustomChipWidget( + labelText: "", + backgroundColor: AppColors.bgScaffoldColor, + textColor:null, + ) + ], ), ), - SizedBox(height: 8.h), - Align( - alignment: AlignmentDirectional.centerEnd, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - width: 18.w, - height: 18.h, - iconColor: AppColors.textColorLight, - fit: BoxFit.contain, - ), - ), ], ), ), diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index fbbea64e..f75a71bc 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -74,7 +74,7 @@ class _VitalSignDetailsPageState extends State { return SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, children: [ _headerCard( context, @@ -85,16 +85,8 @@ class _VitalSignDetailsPageState extends State { scheme: scheme, latestDate: latest?.vitalSignDate, ), - SizedBox(height: 16.h), - _whatIsThisResultCard(context), - SizedBox(height: 16.h), - _historyCard(context, history: history), - SizedBox(height: 16.h), - - _nextStepsCard(context), - SizedBox(height: 32.h), ], ).paddingAll(24.h), ); @@ -121,65 +113,71 @@ class _VitalSignDetailsPageState extends State { padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - padding: EdgeInsets.all(10.h), - decoration: BoxDecoration( - color: scheme.iconBg, - borderRadius: BorderRadius.circular(12.r), - ), - child: Utils.buildSvgWithAssets( - icon: icon, - width: 20.w, - height: 20.h, - iconColor: scheme.iconFg, - fit: BoxFit.contain, - ), - ), - SizedBox(width: 10.w), - title.toText18(isBold: true, weight: FontWeight.w600), + title.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -1), + + ], ), - if (status != null) - Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), - decoration: BoxDecoration( - color: scheme.chipBg, - borderRadius: BorderRadius.circular(100.r), - ), - child: status.toText11( - color: scheme.chipFg, - weight: FontWeight.w500, - ), + SizedBox(height: 8.h), + (latestDate != null + ? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation) + : ('Result of --'.needTranslation)) + .toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: valueText.toText28( + isBold: true, + color: scheme.iconFg, + letterSpacing: -2, + ), + ), + SizedBox(width: 4.h), + if (status != null) + Column( + spacing: 6.h, + children: [ + status.toText10(weight: FontWeight.w500, color: AppColors.greyTextColor), + Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: scheme.iconFg, + ), + ], + ), + ], ), + ), ], ), - SizedBox(height: 10.h), - ( - latestDate != null - ? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation) - : ('Result of --'.needTranslation) - ).toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), - SizedBox(height: 12.h), - - valueText.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -2), - - if (args.low != null || args.high != null) ...[ - SizedBox(height: 8.h), + if (args.low != null || args.high != null) Text( _referenceText(context), style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w500, + fontFamily: 'Poppins', color: AppColors.greyTextColor, ), - ) - ] + softWrap: true, + ), ], ), ); @@ -208,21 +206,10 @@ class _VitalSignDetailsPageState extends State { padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, children: [ - 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600), - SizedBox(height: 8.h), - _descriptionText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10), - SizedBox(height: 12.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.bulb, width: 16.w, height: 16.h, iconColor: AppColors.greyTextColor), - SizedBox(width: 6.w), - Expanded( - child: 'This information is for monitoring and not a diagnosis.'.needTranslation - .toText11(color: AppColors.greyTextColor, weight: FontWeight.w500, maxLine: 3), - ), - ], - ) + 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor), + _descriptionText(context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), ], ), ); @@ -235,34 +222,63 @@ class _VitalSignDetailsPageState extends State { borderRadius: 24.h, hasShadow: true, ), - padding: EdgeInsets.all(16.h), + height: _isGraphVisible + ? 260.h + : (history.length < 3) + ? (history.length * 64) + 80.h + : 260.h, + padding: EdgeInsets.all(15.h), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - 'History flowchart'.needTranslation.toText16(weight: FontWeight.w600), + Text( + _isGraphVisible ? 'History flowchart'.needTranslation : 'History'.needTranslation, + style: TextStyle( + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), Row( + mainAxisSize: MainAxisSize.min, children: [ - // toggle graph/list similar to lab result details - Utils.buildSvgWithAssets( - icon: _isGraphVisible ? AppAssets.graphIcon : AppAssets.listIcon, - width: 18.w, - height: 18.h, - iconColor: AppColors.greyTextColor, - ).onPress(() { - setState(() { - _isGraphVisible = !_isGraphVisible; - }); - }), - SizedBox(width: 10.w), - Utils.buildSvgWithAssets(icon: AppAssets.calendarGrey, width: 18.w, height: 18.h, iconColor: AppColors.greyTextColor), + Container( + width: 24.h, + height: 24.h, + alignment: Alignment.center, + child: InkWell( + onTap: () { + setState(() { + _isGraphVisible = !_isGraphVisible; + }); + }, + child: Utils.buildSvgWithAssets( + icon: _isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph, + width: 24.h, + height: 24.h, + ), + ), + ), + // SizedBox(width: 16.h), + // Container( + // width: 24.h, + // height: 24.h, + // alignment: Alignment.center, + // child: Utils.buildSvgWithAssets( + // icon: AppAssets.ic_date_filter, + // width: 24.h, + // height: 24.h, + // ), + // ), ], ), ], - ), - SizedBox(height: 12.h), + ).paddingOnly(bottom: _isGraphVisible ? 16.h : 24.h), + if (history.isEmpty) Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) else if (_isGraphVisible) @@ -277,47 +293,63 @@ class _VitalSignDetailsPageState extends State { Widget _buildHistoryGraph(List history) { final minY = _minY(history); final maxY = _maxY(history); + final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); + return CustomGraph( dataPoints: history, makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, showGridLines: true, + showShadow: true, leftLabelInterval: _leftInterval(history), maxY: maxY, minY: minY, maxX: history.length.toDouble() - .75, - horizontalInterval: .1, + horizontalInterval: _leftInterval(history), leftLabelFormatter: (value) { - // Match the lab screen behavior: only show High/Low labels. - final v = double.parse(value.toStringAsFixed(1)); - if (args.high != null && v == args.high) { - return _axisLabel('High'.needTranslation); + // Show labels at interval points + if (args.high != null && (value - args.high!).abs() < 0.1) { + return _axisLabel('High'); } - if (args.low != null && v == args.low) { - return _axisLabel('Low'.needTranslation); + if (args.low != null && (value - args.low!).abs() < 0.1) { + return _axisLabel('Low'); } - return const SizedBox.shrink(); + // Show numeric labels at regular intervals + return _axisLabel(value.toStringAsFixed(0)); }, getDrawingHorizontalLine: (value) { - value = double.parse(value.toStringAsFixed(1)); - if ((args.high != null && value == args.high) || (args.low != null && value == args.low)) { + // Draw reference lines for high/low bounds + if (args.high != null && (value - args.high!).abs() < 0.1) { return FlLine( - color: AppColors.bgGreenColor.withValues(alpha: 0.6), + color: AppColors.bgGreenColor.withOpacity(0.2), strokeWidth: 1, + dashArray: [5, 5], ); } - return const FlLine(color: Colors.transparent, strokeWidth: 1); - }, - graphColor: AppColors.blackColor, - graphShadowColor: Colors.transparent, - graphGridColor: AppColors.graphGridColor.withValues(alpha: .4), - bottomLabelFormatter: (value, data) { - if (data.isEmpty) return const SizedBox.shrink(); - if (value == 0) return _bottomLabel(data[value.toInt()].label); - if (value == data.length - 1) return _bottomLabel(data[value.toInt()].label); - if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); - return const SizedBox.shrink(); + if (args.low != null && (value - args.low!).abs() < 0.1) { + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.2), + strokeWidth: 1, + dashArray: [5, 5], + ); + } + // Draw grid lines at intervals + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.2), + strokeWidth: 1, + dashArray: [5, 5], + ); }, + graphColor: AppColors.bgGreenColor, + graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), + graphGridColor: scheme.iconFg, + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return const SizedBox.shrink(); + if (value == 0) return _bottomLabel(data[value.toInt()].label); + if (value == data.length - 1) return _bottomLabel(data[value.toInt()].label, isLast: true); + if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); + return const SizedBox.shrink(); + }, rangeAnnotations: _rangeAnnotations(history), minX: (history.length == 1) ? null : -.2, scrollDirection: Axis.horizontal, @@ -325,6 +357,7 @@ class _VitalSignDetailsPageState extends State { ); } + Widget _buildHistoryList(BuildContext context, List history) { final items = history.reversed.toList(); final height = items.length < 3 ? items.length * 64.0 : 180.h; @@ -393,7 +426,7 @@ class _VitalSignDetailsPageState extends State { HorizontalRangeAnnotation( y1: minY, y2: args.low!, - color: AppColors.highAndLow.withValues(alpha: 0.05), + color: AppColors.highAndLow.withOpacity(0.05), ), ); } @@ -403,7 +436,7 @@ class _VitalSignDetailsPageState extends State { HorizontalRangeAnnotation( y1: args.low!, y2: args.high!, - color: AppColors.bgGreenColor.withValues(alpha: 0.05), + color: AppColors.bgGreenColor.withOpacity(0.05), ), ); } @@ -413,7 +446,7 @@ class _VitalSignDetailsPageState extends State { HorizontalRangeAnnotation( y1: args.high!, y2: maxY, - color: AppColors.criticalLowAndHigh.withValues(alpha: 0.05), + color: AppColors.criticalLowAndHigh.withOpacity(0.05), ), ); } @@ -447,11 +480,14 @@ class _VitalSignDetailsPageState extends State { case VitalSignMetric.respiratoryRate: return _toDouble(v.respirationBeatPerMinute); case VitalSignMetric.bloodPressure: - // Graph only systolic for now (simple single-series). + // Graph only systolic for now (simple single-series). return _toDouble(v.bloodPressureHigher); } } + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + double index = 0; for (final v in sorted) { final mv = metricValue(v); @@ -459,7 +495,7 @@ class _VitalSignDetailsPageState extends State { if (mv == 0) continue; final dt = v.vitalSignDate ?? DateTime.now(); - final label = '${dt.day}/${dt.month}'; + final label = '${monthNames[dt.month - 1]}, ${dt.year}'; points.add( DataPoint( @@ -603,18 +639,14 @@ class _VitalSignDetailsPageState extends State { ); } - Widget _bottomLabel(String label) { + Widget _bottomLabel(String label, {bool isLast = false}) { return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - label, - style: TextStyle( - fontSize: 8.f, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - color: AppColors.labelTextColor, - ), + padding: EdgeInsets.only( + top: 8.0, + right: isLast ? 16.h : 0, ), + child: label.toText8(fontWeight: FontWeight.w500), ); } + } diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index d8b6d7e3..95ff3e53 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -178,7 +178,7 @@ class _VitalSignPageState extends State { children: [ // Body anatomy image with Heart Rate card overlaid at bottom SizedBox( - height: 480.h, + height: 420.h, width: double.infinity, child: Stack( clipBehavior: Clip.none, @@ -196,7 +196,7 @@ class _VitalSignPageState extends State { Align( alignment: Alignment.bottomCenter, child: SizedBox( - height: 420.h, + height: 480.h, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), child: Container( @@ -245,7 +245,7 @@ class _VitalSignPageState extends State { ], ), ), - SizedBox(height: 12.h), + SizedBox(height: 12.h), // Respiratory rate Card _buildVitalSignCard( @@ -308,15 +308,15 @@ class _VitalSignPageState extends State { Row( children: [ Container( - padding: EdgeInsets.all(10.h), + padding: EdgeInsets.all(8.h), decoration: BoxDecoration( color: scheme.iconBg, borderRadius: BorderRadius.circular(12.r), ), child: Utils.buildSvgWithAssets( icon: icon, - width: 20.w, - height: 20.h, + width: 22.w, + height: 22.h, iconColor: scheme.iconFg, fit: BoxFit.contain, ), @@ -332,10 +332,15 @@ class _VitalSignPageState extends State { ), SizedBox(height: 14.h), Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), - decoration: BoxDecoration( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h), + // decoration: BoxDecoration( + // color: AppColors.bgScaffoldColor, + // borderRadius: BorderRadius.circular(10.r), + // ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bgScaffoldColor, - borderRadius: BorderRadius.circular(10.r), + borderRadius: 10.r, + hasShadow: false, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -361,23 +366,24 @@ class _VitalSignPageState extends State { labelText: status, backgroundColor: scheme.chipBg, textColor: scheme.chipFg, + ) else const SizedBox.shrink(), ], ), ), - SizedBox(height: 8.h), - Align( - alignment: AlignmentDirectional.centerEnd, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - width: 18.w, - height: 18.h, - iconColor: AppColors.textColorLight, - fit: BoxFit.contain, - ), - ), + // SizedBox(height: 8.h), + // Align( + // alignment: AlignmentDirectional.centerEnd, + // child: Utils.buildSvgWithAssets( + // icon: AppAssets.arrow_forward, + // width: 18.w, + // height: 18.h, + // iconColor: AppColors.textColorLight, + // fit: BoxFit.contain, + // ), + // ), ], ), ), From 0514716910eab59caa0f5fcba914f9a608d76333 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 12 Jan 2026 11:48:05 +0300 Subject: [PATCH 21/21] no message --- lib/presentation/vital_sign/vital_sign_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index 95ff3e53..fbf9fc6d 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -196,7 +196,7 @@ class _VitalSignPageState extends State { Align( alignment: Alignment.bottomCenter, child: SizedBox( - height: 480.h, + height: 460.h, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), child: Container(