From 3f5bd0a759bcbda7da91e9745a72d22427cfb1bc Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 11 Jan 2026 17:01:07 +0300 Subject: [PATCH] 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 0000000..0b027ad --- /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 0000000..f81cee8 --- /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 0000000..f2ca09f --- /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 0000000..b3f2619 --- /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 0000000..eb8684a --- /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 0000000..f93c662 --- /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 f366329..039787b 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 65ac1ef..246a801 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 2fdc389..741cb6b 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 c6c5554..f284b39 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 8fd4818..6dc3bf6 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 4fdc09c..02b8195 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 0000000..e6930f9 --- /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 0000000..f024d8e --- /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 0000000..3975805 --- /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 0000000..381d514 --- /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 0000000..6f34246 --- /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 0000000..066df3c --- /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 0000000..77b06f1 --- /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 0000000..3b35fe7 --- /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 0000000..fb34056 --- /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 0000000..f95470c --- /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 0000000..ad53325 --- /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 0000000..896d33f --- /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 0000000..7dc4b24 --- /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 18ffddd..d82712a 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 a9d16dc..f0537b3 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 eb22ab6..42cba5d 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 0000000..56f82fc --- /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 0000000..a2b82ec --- /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 0000000..c55e8b4 --- /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 0000000..059c11e --- /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 0000000..542baa5 --- /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 3b31d38..a5903db 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 95884d4..138865d 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 4e0ec10..1786dec 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 188d07b..a63d1e2 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 aef7ce7..8669c3c 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 b5b4388..d0d5b2f 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 3950974..d6036c6 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 ba159bd..9d5d884 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 c3ce68b..b438420 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 5d08bcd..2bd429c 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 3470344..302940c 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 9779562..2359904 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 78045b5..c57ffb9 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 c631f5b..aee425c 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 1f5d806..ad47cd2 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 94ed9f8..9f101d4 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 0000000..71d9ab2 --- /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 0000000..9ff6c35 --- /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? +