diff --git a/assets/images/jpg/report.jpg b/assets/images/jpg/report.jpg new file mode 100644 index 00000000..5846cd53 Binary files /dev/null and b/assets/images/jpg/report.jpg differ diff --git a/assets/images/svg/blood_pressure_icon.svg b/assets/images/svg/blood_pressure_icon.svg new file mode 100644 index 00000000..0b027ad6 --- /dev/null +++ b/assets/images/svg/blood_pressure_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/blood_sugar_only_icon.svg b/assets/images/svg/blood_sugar_only_icon.svg new file mode 100644 index 00000000..f81cee8a --- /dev/null +++ b/assets/images/svg/blood_sugar_only_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/low_indicator_icon.svg b/assets/images/svg/low_indicator_icon.svg new file mode 100644 index 00000000..f2ca09fc --- /dev/null +++ b/assets/images/svg/low_indicator_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/normal_status_green_icon.svg b/assets/images/svg/normal_status_green_icon.svg new file mode 100644 index 00000000..b3f2619c --- /dev/null +++ b/assets/images/svg/normal_status_green_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/send_email_icon.svg b/assets/images/svg/send_email_icon.svg new file mode 100644 index 00000000..eb8684ab --- /dev/null +++ b/assets/images/svg/send_email_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight_icon.svg b/assets/images/svg/weight_icon.svg new file mode 100644 index 00000000..f93c6626 --- /dev/null +++ b/assets/images/svg/weight_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f3663294..039787b8 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient { final int statusCode = response.statusCode; log("uri: ${Uri.parse(url.trim())}"); log("body: ${json.encode(body)}"); - // log("response.body: ${response.body}"); + log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index c103291a..fe0b4d58 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -207,7 +207,7 @@ var GET_APPOINTMENT_DETAILS_BY_NO = 'Services/MobileNotifications.svc/REST/GetAp var NEW_RATE_APPOINTMENT_URL = "Services/Doctors.svc/REST/AppointmentsRating_InsertAppointmentRate"; var NEW_RATE_DOCTOR_URL = "Services/Doctors.svc/REST/DoctorsRating_InsertDoctorRate"; -var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; +//var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; //URL to get clinic list var GET_CLINICS_LIST_URL = "Services/lists.svc/REST/GetClinicCentralized"; @@ -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'; @@ -799,6 +785,11 @@ class ApiConsts { static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; + static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; + static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; + static final String getMonthlyReports = 'Services/Patients.svc/Rest/UpdatePateintHealthSummaryReport'; + static final String updatePatientEmail = 'Services/Patients.svc/Rest/UpdatePateintEmail'; + static final String getQrParkingDetails = 'Services/SWP.svc/REST/GetQRParkingByID'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; @@ -825,11 +816,12 @@ class ApiConsts { static final String updateHHCOrder = 'api/hhc/update'; static final String addHHCOrder = 'api/HHC/add'; - // SYMPTOMS CHECKER + // SYMPTOMS CHECKER API + static final String symptomsUserLogin = '$symptomsCheckerApi/user_login'; static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName'; static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors'; static final String getSuggestions = '$symptomsCheckerApi/GetSuggestion'; - static final String diagnosis = '$symptomsCheckerApi/diagnosis'; + static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis'; static final String explain = '$symptomsCheckerApi/explain'; //E-REFERRAL SERVICES @@ -848,6 +840,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'; + //Blood Donation static String bloodGroupUpdate = "Services/PatientVarification.svc/REST/BloodDonation_RegisterBloodType"; static String userAgreementForBloodGroupUpdate = "Services/PatientVarification.svc/REST/AddUserAgreementForBloodDonation"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 97637589..d378c78f 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -270,16 +270,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'; @@ -288,9 +293,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'; @@ -299,8 +302,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'; @@ -327,8 +328,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 { @@ -349,6 +348,5 @@ 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 7db5d414..e8d30713 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -10,6 +10,8 @@ import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_repo. import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/common/common_repo.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_repo.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; @@ -18,6 +20,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'; @@ -43,6 +46,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; @@ -52,7 +56,9 @@ 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/presentation/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -66,8 +72,13 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import '../features/active_prescriptions/active_prescriptions_repo.dart'; +import '../features/monthly_reports/terms_conditions_repo.dart'; +import '../features/monthly_reports/terms_conditions_view_model.dart'; + GetIt getIt = GetIt.instance; class AppDependencies { @@ -143,7 +154,21 @@ 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())); getIt.registerLazySingleton(() => MonthlyReportRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), + ),); + getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => QrParkingRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); // ViewModels // Global/shared VMs → LazySingleton @@ -260,10 +285,30 @@ 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(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); + getIt.registerLazySingleton(() => MyInvoicesViewModel( + myInvoicesRepo: getIt(), + errorHandlerService: getIt(), + navServices: getIt(), + )); + getIt.registerLazySingleton(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => ActivePrescriptionsViewModel( + errorHandlerService: getIt(), + activePrescriptionsRepo: getIt() + ), + ); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + } } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 8fd4818c..6dc3bf68 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -16,7 +16,7 @@ enum CountryEnum { saudiArabia, unitedArabEmirates } enum CalenderEnum { gregorian, hijri } -enum SelectionTypeEnum { dropdown, calendar, search } +enum SelectionTypeEnum { dropdown, calendar, search, time } enum GenderTypeEnum { male, female } @@ -38,7 +38,19 @@ enum HealthCalConEnum { calculator, converter } enum HealthCalculatorEnum { general, women } -enum HealthCalculatorsTypeEnum { bmi, calories, bmr, idealBodyWeight, bodyFat, crabsProteinFat, ovulation, deliveryDueDate, bloodSugar, bloodCholesterol, triglycerides } +enum HealthCalculatorsTypeEnum { + bmi, + calories, + bmr, + idealBodyWeight, + bodyFat, + crabsProteinFat, + ovulation, + deliveryDueDate, + bloodSugar, + bloodCholesterol, + triglycerides +} extension HealthCalculatorExtenshion on HealthCalculatorsTypeEnum { String get displayName { @@ -297,3 +309,5 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { // SymptomsChecker enum PossibleConditionsSeverityEnum { seekMedicalAdvice, monitorOnly, emergency } + +enum HealthTrackerTypeEnum { bloodSugar, bloodPressure, weightTracker } diff --git a/lib/core/utils/calendar_utils.dart b/lib/core/utils/calendar_utils.dart new file mode 100644 index 00000000..8c0db187 --- /dev/null +++ b/lib/core/utils/calendar_utils.dart @@ -0,0 +1,316 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:device_calendar/device_calendar.dart'; +import 'package:flutter/widgets.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/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_reminder_view.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/services/permission_service.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:manage_calendar_events/manage_calendar_events.dart' as ios; +import 'package:permission_handler/permission_handler.dart'; +import 'package:timezone/data/latest.dart' as tzl; + +final DeviceCalendarPlugin deviceCalendarPlugin = DeviceCalendarPlugin(); +final ios.CalendarPlugin _myPlugin = ios.CalendarPlugin(); + +class CalendarUtils { + static Completer? _completer; + + dynamic get writableCalendars => calendars.firstWhere((c) => !c.isReadOnly!); + dynamic calendars; + + CalendarUtils._(this.calendars); + + // static Future getInstance() async { + // if (_completer == null) { + // _completer = Completer(); + // print(_completer!.isCompleted); + // try { + // final dynamic calendarsResult; + // if (Platform.isIOS) { + // calendarsResult = await _myPlugin.getCalendars(); + // if (!_completer!.isCompleted) { + // _completer?.complete(CalendarUtils._(await calendarsResult!)); + // } + // } else { + // calendarsResult = await deviceCalendarPlugin.retrieveCalendars(); + // if (!_completer!.isCompleted) { + // _completer?.complete(CalendarUtils._(await calendarsResult.data!)); + // } + // } + // } on Exception catch (e) { + // if (!_completer!.isCompleted) { + // _completer!.completeError(e); + // } + // } + // } + // return _completer!.future; + // } + + static Future getInstance() async { + tzl.initializeTimeZones(); + if (_completer != null) { + return _completer!.future; + } + _completer = Completer(); + try { + final dynamic calendarsResult; + if (Platform.isIOS) { + calendarsResult = await _myPlugin.getCalendars(); + _completer!.complete(CalendarUtils._(calendarsResult)); + } else { + calendarsResult = await deviceCalendarPlugin.retrieveCalendars(); + _completer!.complete(CalendarUtils._(calendarsResult.data)); + } + } catch (e) { + _completer!.completeError(e); + } + + return _completer!.future; + } + + Future createOrUpdateEvents({List? scheduleList, String? title, String? description, List? scheduleDateTime, List? daysOfWeek}) async { + tzl.initializeTimeZones(); + List events = []; + Location _currentLocation; + if (DateTime.now().timeZoneName == "+04") + _currentLocation = getLocation('Asia/Dubai'); + else + _currentLocation = getLocation('Asia/Riyadh'); + + scheduleDateTime!.forEach((element) { + RecurrenceRule recurrenceRule = RecurrenceRule( + // RecurrenceFrequency.Daily, + // daysOfWeek: daysOfWeek, + // endDate: element, + until: element, frequency: Frequency.daily, + ); + //added byAamir Tz Time + Event event = Event(writableCalendars!.id, + recurrenceRule: recurrenceRule, + start: TZDateTime.from(element, _currentLocation), + end: TZDateTime.from(element.add(Duration(minutes: 30)), _currentLocation), + title: title, + description: description); + events.add(event); + }); + + events.forEach((element) { + deviceCalendarPlugin.createOrUpdateEvent(element); + }); + } + + Future createOrUpdateEvent({required String title, required String description, required String location, DateTime? scheduleDateTime, String? eventId}) async { + RecurrenceRule recurrenceRule = RecurrenceRule( + // RecurrenceFrequency.Daily, + // daysOfWeek: daysOfWeek, + // endDate: scheduleDateTime, + until: scheduleDateTime, frequency: Frequency.daily, + ); + + Location _currentLocation; + // if (DateTime.now().timeZoneName == "+04") + // _currentLocation = getLocation('Asia/Dubai'); + // else + _currentLocation = getLocation('Asia/Riyadh'); + + TZDateTime scheduleDateTimeUTZ = TZDateTime.from(scheduleDateTime!, _currentLocation); + + print("writableCalendars-name: " + writableCalendars.name); + print("writableCalendars-Id: " + writableCalendars.id); + print("writableCalendarsToString: " + writableCalendars.toString()); + print("writableCalendarsToString: " + writableCalendars!.id!); + Event event = Event( + writableCalendars!.id, + start: scheduleDateTimeUTZ, + end: scheduleDateTimeUTZ.add(Duration(minutes: 30)), + title: title, + description: description, + ); + + ios.CalendarEvent iosCalEvent = + ios.CalendarEvent(location: location, startDate: scheduleDateTimeUTZ, endDate: scheduleDateTimeUTZ.add(Duration(minutes: 30)), title: title, description: description, isAllDay: false); + + if (Platform.isAndroid) { + Result result = await deviceCalendarPlugin.hasPermissions(); + print(result); + await deviceCalendarPlugin.createOrUpdateEvent(event).catchError((e) { + print("catchError " + e.toString()); + }).whenComplete(() { + print("whenComplete Calender ID " + eventId!); + }); + } else { + await _myPlugin.createEvent(calendarId: writableCalendars.id!, event: iosCalEvent).catchError((e) { + print("catchError " + e.toString()); + }).whenComplete(() { + print("whenComplete Calender ID iOS " + eventId!); + }); + } + } + + deleteEvent(String _calendarId, String _eventId) async { + if (Platform.isIOS) { + await _myPlugin.deleteEvent(calendarId: _calendarId, eventId: _eventId); + } else { + await deviceCalendarPlugin.deleteEvent(_calendarId, _eventId); + } + } + + Future retrieveEvents( + String calendarId, + RetrieveEventsParams retrieveEventsParams, + ) async { + if (Platform.isIOS) { + return await _myPlugin.getEvents(calendarId: calendarId); + } else { + return await deviceCalendarPlugin.retrieveEvents(calendarId, retrieveEventsParams); + } + } + + Future createCalendar( + String calendarName, { + Color? calendarColor, + String? localAccountName, + }) async { + return await deviceCalendarPlugin.createCalendar(calendarName, calendarColor: calendarColor, localAccountName: localAccountName); + } +} + +Future> requestPermissions() async { + var permissionResults = [Permission.calendarFullAccess].request(); + return permissionResults; +} + +showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async { + if (Platform.isAndroid) { + if (await PermissionService.isCalendarPermissionEnabled()) { + _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + } else { + // Utils.showPermissionConsentDialog(context, TranslationBase.of(context).calendarPermission, () async { + // if (await Permission.calendarFullAccess.request().isGranted) { + // _showReminderDialog(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + // onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + // } + // }); + } + } else { + if (await Permission.calendarWriteOnly.request().isGranted) { + if (await Permission.calendarFullAccess.request().isGranted) { + _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + } + } + } +} + +Future _showReminderBottomSheet(BuildContext providedContext, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + {required Function onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool? isMultiAllowed}) async { + showCommonBottomSheetWithoutHeight(providedContext, title: "Set the timer of reminder".needTranslation, child: PrescriptionReminderView( + setReminder: (int value) async { + if (!isMultiAllowed!) { + if (onMultiDateSuccess == null) { + CalendarUtils calendarUtils = await CalendarUtils.getInstance(); + await calendarUtils.createOrUpdateEvent( + title: title ?? "You have appointment with Dr. ".needTranslation + doctorName, + description: description ?? "At " + appoDateFormatted + " " + appoTimeFormatted, + scheduleDateTime: dateTime, + eventId: eventId, + location: ''); + onSuccess(); + } + } else { + onMultiDateSuccess!(value); + } + }, + ), callBackFunc: () {}, isFullScreen: false); +} + +setCalender(BuildContext context, + {required String eventId, required int selectedMinutes, int? frequencyNumber, required int days, required String orderDate, required String itemDescriptionN, required String route}) async { + DateTime actualDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); + frequencyNumber ??= 2; //Some time frequency number is null so by default will be 2 + + int remainingDays = days - (Jiffy.parseFromDateTime(DateTime.now()).diff(Jiffy.parseFromDateTime(DateUtil.convertStringToDate(orderDate)), unit: Unit.day) as int); + if (remainingDays.isNegative) { + getIt.get().showErrorBottomSheet(message: "Prescription date has been already passed you can not add a reminder for this prescription."); + return; + } + CalendarUtils calendarUtils = await CalendarUtils.getInstance(); + + try { + for (int i = 0; i < remainingDays; i++) { + //event for number of days. + for (int j = 0; j < frequencyNumber; j++) { + // event for number of times per day. + if (j != 0) { + actualDate.add(new Duration(hours: 8)); // 8 hours addition for daily dose. + } + //Time subtraction from actual reminder time. like before 30, or 1 hour. + + actualDate = Jiffy.parseFromDateTime(actualDate).subtract(minutes: selectedMinutes).dateTime; + + calendarUtils.createOrUpdateEvent( + title: "$itemDescriptionN} Prescription Reminder", + description: "$itemDescriptionN $frequencyNumber $route ", + scheduleDateTime: actualDate, + eventId: eventId + (i.toString() + j.toString()), + location: '', //event id with varitions + ); + print("Creating event #$j for day $i → $actualDate"); + actualDate = DateTime(actualDate.year, actualDate.month, actualDate.day, 8, 0); + } + actualDate = Jiffy.parseFromDateTime(actualDate).add(days: 1).dateTime; + } + } catch (ex) { + getIt.get().showErrorBottomSheet(message: "catch:$ex"); + } +} + +Future checkAndRemove(hasReminder, {bool delete = false, String itemDescriptionN = ""}) async { + final ios.CalendarPlugin _myPlugin = ios.CalendarPlugin(); + CalendarUtils calendarUtils = await CalendarUtils.getInstance(); + DateTime startEventsDate = Jiffy.parseFromDateTime(DateTime.now()).subtract(days: 30).dateTime; + DateTime endEventsDate = Jiffy.parseFromDateTime(DateTime.now()).add(days: 120).dateTime; + RetrieveEventsParams params = RetrieveEventsParams(startDate: startEventsDate, endDate: endEventsDate); + + if (calendarUtils.calendars != null) { + if (Platform.isAndroid) { + await processEvents(calendarUtils.calendars, calendarUtils, params, delete, itemDescriptionN, hasReminder); + } else { + List? iosCalendars = await _myPlugin.getCalendars(); + if (iosCalendars != null) { + await processEvents(iosCalendars.map((cal) => Calendar(id: cal.id, name: cal.name, accountName: cal.accountName)).toList(), calendarUtils, params, delete, itemDescriptionN, hasReminder); + } + } + } +} + +Future processEvents(List calendars, calendarUtils, params, delete, String itemDescriptionN, hasReminder) async { + for (var calendar in calendars) { + Result> events = await calendarUtils.retrieveEvents(calendar.id!, params); + for (var event in events.data!) { + if (event.title!.contains(itemDescriptionN)) { + if (delete) { + await calendarUtils.deleteEvent(calendar, event); + // AppToast.showSuccessToast(message: TranslationBase.of(context).reminderCancelSuccess); + hasReminder = false; + } else { + hasReminder = false; + // setState(() { + // hasReminder = true; + // }); + } + } + } + } +} diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 5a43d788..5e9e91b2 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:device_calendar_plus/device_calendar_plus.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:jiffy/jiffy.dart' show Jiffy; +import 'package:manage_calendar_events/manage_calendar_events.dart' hide Calendar; class CalenderUtilsNew { final DeviceCalendar calender = DeviceCalendar.instance; diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index 4fdc09cf..02b81957 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -7,16 +7,14 @@ import 'package:flutter/material.dart'; // These are the Viewport values of your const num figmaDesignWidth = 375; // iPhone X / 12 base width const num figmaDesignHeight = 812; // iPhone X / 12 base height - extension ConstrainedResponsive on num { /// Width with max cap for tablets - double get wCapped => isTablet ? math.min( w, this * 1.3) : w; + double get wCapped => isTablet ? math.min(w, this * 1.3) : w; /// Height with max cap for tablets double get hCapped => isTablet ? math.min(h, this * 1.3) : h; } - extension ResponsiveExtension on num { double get _screenWidth => SizeUtils.width; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 03ff6b59..f5ffd367 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -262,16 +262,6 @@ class Utils { return await prefs.remove(key); } - static void showLoading({bool isNeedBinding = true}) { - if (isNeedBinding) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showLoadingDialog(); - }); - } else { - showLoadingDialog(); - } - } - static void showLoadingDialog() { _isLoadingVisible = true; showDialog( @@ -288,18 +278,6 @@ class Utils { ); } - static void hideLoading() { - try { - if (_isLoadingVisible) { - _isLoadingVisible = false; - Navigator.of(navigationService.navigatorKey.currentContext!).pop(); - } - _isLoadingVisible = false; - } catch (e) { - log("errr: ${e.toString()}"); - } - } - static List uniqueBy(List list, K Function(T) keySelector) { final seenKeys = {}; return list.where((item) => seenKeys.add(keySelector(item))).toList(); diff --git a/lib/features/active_prescriptions/active_prescriptions_repo.dart b/lib/features/active_prescriptions/active_prescriptions_repo.dart new file mode 100644 index 00000000..437f364b --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_repo.dart @@ -0,0 +1,65 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class ActivePrescriptionsRepo { + + Future>>> getActivePrescriptionsDetails(); + +} + +class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + ActivePrescriptionsRepoImp({required this.loggerService, required this.apiClient}); + + @override + + Future>>> getActivePrescriptionsDetails() async + { + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getActivePrescriptionsDetails, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + var list = response['List_ActiveGetPrescriptionReportByPatientID']; + var res = list + .map( + (item) => ActivePrescriptionsResponseModel.fromJson(item)) + .toList(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + // data: response, + data: res + ); + return apiResponse; + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + +} \ No newline at end of file diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart new file mode 100644 index 00000000..e4da04dd --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -0,0 +1,101 @@ + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class ActivePrescriptionsViewModel extends ChangeNotifier { + late ActivePrescriptionsRepo activePrescriptionsRepo; + late ErrorHandlerService errorHandlerService; + List activePrescriptionsDetailsList = []; + + ActivePrescriptionsViewModel({ + required this.activePrescriptionsRepo, + required this.errorHandlerService, + }); + + Future getActiveMedications({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = + await activePrescriptionsRepo.getActivePrescriptionsDetails(); + result.fold( + (failure) async => + await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 1) { + activePrescriptionsDetailsList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data); + } + }, + ); + } + + DateTime parseDate(String? date) { + if (date == null) return DateTime.now(); + final regex = RegExp(r"\/Date\((\d+)([+-]\d+)?\)\/"); + final match = regex.firstMatch(date); + if (match != null) { + final millis = int.parse(match.group(1)!); + return DateTime.fromMillisecondsSinceEpoch(millis); + } + return DateTime.tryParse(date) ?? DateTime.now(); + } + + List generateMedicationDays(ActivePrescriptionsResponseModel med) { + final start = parseDate(med.startDate); + final duration = med.days ?? 0; + if (duration <= 0) return []; + final f = (med.frequency ?? "").toLowerCase().trim(); + int intervalDays = 1; + + if (f.contains("every six hours") || + f.contains("every 6 hours") || + f.contains("every four hours") || + f.contains("every 4 hours") || + f.contains("every eight hours") || + f.contains("every 8 hours") || + f.contains("every 12 hours") || + f.contains("every twelve hours") || + f.contains("every 24 hours") || + f.contains("3 times a day") || + f.contains("once a day")) { + intervalDays = 1; + } + else if (f.contains("once a week")) { + intervalDays = 7; + } + else if (f.contains("every 3 days")) { + intervalDays = 3; + } + else if (f.contains("every other day")) { + intervalDays = 2; + } + + List result = []; + for (int offset = 0; offset < duration; offset += intervalDays) { + result.add(start.add(Duration(days: offset))); + } + + return result; + } + + bool sameYMD(DateTime a, DateTime b) => + a.year == b.year && a.month == b.month && a.day == b.day; + + List getMedsForSelectedDay( + DateTime selectedDate) { + final clean = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + + return activePrescriptionsDetailsList.where((med) { + final days = generateMedicationDays(med); + return days.any((d) => sameYMD(d, clean)); + }).toList(); + } +} + + + + diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart new file mode 100644 index 00000000..dc859a96 --- /dev/null +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -0,0 +1,78 @@ + +class ActivePrescriptionsResponseModel { + String? itemId; + String? itemDescription; + String? route; + String? frequency; + int? frequencyNumber; + int? doseDailyQuantity; + int? days; + String? startDate; + String? endDate; + String? orderDate; + String? productImageString; + bool isReminderOn; + List selectedDoseTimes = []; + + ActivePrescriptionsResponseModel({ + this.itemId, + this.itemDescription, + this.route, + this.frequency, + this.frequencyNumber, + this.doseDailyQuantity, + this.days, + this.startDate, + this.endDate, + this.orderDate, + this.productImageString, + this.isReminderOn = false, + List? selectedDoseTimes, + }) { + this.selectedDoseTimes = selectedDoseTimes ?? []; + } + + /// ========== JSON FROM ========== + factory ActivePrescriptionsResponseModel.fromJson(Map json) { + return ActivePrescriptionsResponseModel( + itemId: json["ItemID"]?.toString() ?? "", + itemDescription: json["ItemDescription"] ?? "", + route: json["Route"] ?? "", + frequency: json["Frequency"] ?? "", + frequencyNumber: json["FrequencyNumber"], + doseDailyQuantity: json["DoseDailyQuantity"] ?? 1, + days: json["Days"] ?? 0, + startDate: json["StartDate"] ?? "", + endDate: json["EndDate"] ?? "", + orderDate: json["OrderDate"] ?? "", + productImageString: json["ProductImageString"] ?? "", + isReminderOn: json["IsReminderOn"] == true, + selectedDoseTimes: + (json["SelectedDoseTimes"] as List?) + ?.map((e) => e?.toString()) + .toList() ?? + [], + ); + } + + + + /// ========== JSON TO ========== + Map toJson() { + return { + "ItemID": itemId, + "ItemDescription": itemDescription, + "Route": route, + "Frequency": frequency, + "FrequencyNumber": frequencyNumber, + "DoseDailyQuantity": doseDailyQuantity, + "Days": days, + "StartDate": startDate, + "EndDate": endDate, + "OrderDate": orderDate, + "ProductImageString": productImageString, + "IsReminderOn": isReminderOn, + "SelectedDoseTimes": selectedDoseTimes, + }; + } +} diff --git a/lib/features/health_trackers/health_trackers_repo.dart b/lib/features/health_trackers/health_trackers_repo.dart new file mode 100644 index 00000000..2a64fc46 --- /dev/null +++ b/lib/features/health_trackers/health_trackers_repo.dart @@ -0,0 +1,873 @@ +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, + }); + + /// Send blood sugar report by email. + Future>> sendBloodSugarReportByEmail({ + required String email, + }); + + // ==================== 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, + }); + + /// Send blood pressure report by email. + Future>> sendBloodPressureReportByEmail({ + required String email, + }); + + // ==================== 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, + }); + + /// Send weight report by email. + Future>> sendWeightReportByEmail({ + required String email, + }); +} + +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())); + } + } + + @override + Future>> sendBloodSugarReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBloodSugarReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + // ==================== BLOOD PRESSURE METHODS ==================== + + @override + 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())); + } + } + + @override + Future>> sendBloodPressureReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBloodPressureReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + // ==================== WEIGHT MEASUREMENT METHODS ==================== + + @override + 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())); + } + } + + @override + Future>> sendWeightReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBodyWeightReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart b/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart new file mode 100644 index 00000000..f024d8e1 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart @@ -0,0 +1,87 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class BloodPressureResult { + int? patientID; + int? lineItemNo; + DateTime? bloodPressureDate; + int? measuredArm; + int? systolicePressure; + int? diastolicPressure; + dynamic remark; + bool? isActive; + int? chartYear; + String? chartMonth; + dynamic yearSystolicePressureAverageResult; + dynamic monthSystolicePressureResult; + dynamic weekSystolicePressureResult; + int? yearDiastolicPressureAverageResult; + dynamic monthDiastolicPressureResult; + dynamic weekDiastolicPressureResult; + String? measuredArmDesc; + dynamic weekDesc; + + BloodPressureResult( + {this.patientID, + this.lineItemNo, + this.bloodPressureDate, + this.measuredArm, + this.systolicePressure, + this.diastolicPressure, + this.remark, + this.isActive, + this.chartYear, + this.chartMonth, + this.yearSystolicePressureAverageResult, + this.monthSystolicePressureResult, + this.weekSystolicePressureResult, + this.yearDiastolicPressureAverageResult, + this.monthDiastolicPressureResult, + this.weekDiastolicPressureResult, + this.measuredArmDesc, + this.weekDesc}); + + BloodPressureResult.fromJson(Map json) { + patientID = json['PatientID']; + lineItemNo = json['LineItemNo']; + bloodPressureDate = DateUtil.convertStringToDate(json['BloodPressureDate']); + measuredArm = json['MeasuredArm']; + systolicePressure = json['SystolicePressure']; + diastolicPressure = json['DiastolicPressure']; + remark = json['Remark']; + isActive = json['IsActive']; + chartYear = json['ChartYear']; + chartMonth = json['ChartMonth']; + yearSystolicePressureAverageResult = json['YearSystolicePressureAverageResult']; + monthSystolicePressureResult = json['MonthSystolicePressureResult']; + weekSystolicePressureResult = json['WeekSystolicePressureResult']; + yearDiastolicPressureAverageResult = json['YearDiastolicPressureAverageResult']; + monthDiastolicPressureResult = json['MonthDiastolicPressureResult']; + weekDiastolicPressureResult = json['WeekDiastolicPressureResult']; + measuredArmDesc = json['MeasuredArmDesc']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['PatientID'] = patientID; + data['LineItemNo'] = lineItemNo; + data['BloodPressureDate'] = bloodPressureDate; + data['MeasuredArm'] = measuredArm; + data['SystolicePressure'] = systolicePressure; + data['DiastolicPressure'] = diastolicPressure; + data['Remark'] = remark; + data['IsActive'] = isActive; + data['ChartYear'] = chartYear; + data['ChartMonth'] = chartMonth; + data['YearSystolicePressureAverageResult'] = yearSystolicePressureAverageResult; + data['MonthSystolicePressureResult'] = monthSystolicePressureResult; + data['WeekSystolicePressureResult'] = weekSystolicePressureResult; + data['YearDiastolicPressureAverageResult'] = yearDiastolicPressureAverageResult; + data['MonthDiastolicPressureResult'] = monthDiastolicPressureResult; + data['WeekDiastolicPressureResult'] = weekDiastolicPressureResult; + data['MeasuredArmDesc'] = measuredArmDesc; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart new file mode 100644 index 00000000..3975805f --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart @@ -0,0 +1,58 @@ +class MonthBloodPressureResultAverage { + dynamic weekfourSystolicePressureAverageResult; + dynamic weekfourDiastolicPressureAverageResult; + dynamic weekthreeSystolicePressureAverageResult; + dynamic weekthreeDiastolicPressureAverageResult; + dynamic weektwoSystolicePressureAverageResult; + dynamic weektwoDiastolicPressureAverageResult; + dynamic weekoneSystolicePressureAverageResult; + dynamic weekoneDiastolicPressureAverageResult; + String? weekDesc; + int? weekDiastolicPressureAverageResult; + int? weekSystolicePressureAverageResult; + + MonthBloodPressureResultAverage({ + this.weekfourSystolicePressureAverageResult, + this.weekfourDiastolicPressureAverageResult, + this.weekthreeSystolicePressureAverageResult, + this.weekthreeDiastolicPressureAverageResult, + this.weektwoSystolicePressureAverageResult, + this.weektwoDiastolicPressureAverageResult, + this.weekoneSystolicePressureAverageResult, + this.weekoneDiastolicPressureAverageResult, + this.weekDesc, + this.weekDiastolicPressureAverageResult, + this.weekSystolicePressureAverageResult, + }); + + MonthBloodPressureResultAverage.fromJson(Map json) { + weekfourSystolicePressureAverageResult = json['weekfourSystolicePressureAverageResult']; + weekfourDiastolicPressureAverageResult = json['weekfourDiastolicPressureAverageResult']; + weekthreeSystolicePressureAverageResult = json['weekthreeSystolicePressureAverageResult']; + weekthreeDiastolicPressureAverageResult = json['weekthreeDiastolicPressureAverageResult']; + weektwoSystolicePressureAverageResult = json['weektwoSystolicePressureAverageResult']; + weektwoDiastolicPressureAverageResult = json['weektwoDiastolicPressureAverageResult']; + weekoneSystolicePressureAverageResult = json['weekoneSystolicePressureAverageResult']; + weekoneDiastolicPressureAverageResult = json['weekoneDiastolicPressureAverageResult']; + weekDesc = json['WeekDesc']; + weekDiastolicPressureAverageResult = json['WeekDiastolicPressureAverageResult']; + weekSystolicePressureAverageResult = json['WeekSystolicePressureAverageResult']; + } + + Map toJson() { + final Map data = {}; + data['weekfourSystolicePressureAverageResult'] = weekfourSystolicePressureAverageResult; + data['weekfourDiastolicPressureAverageResult'] = weekfourDiastolicPressureAverageResult; + data['weekthreeSystolicePressureAverageResult'] = weekthreeSystolicePressureAverageResult; + data['weekthreeDiastolicPressureAverageResult'] = weekthreeDiastolicPressureAverageResult; + data['weektwoSystolicePressureAverageResult'] = weektwoSystolicePressureAverageResult; + data['weektwoDiastolicPressureAverageResult'] = weektwoDiastolicPressureAverageResult; + data['weekoneSystolicePressureAverageResult'] = weekoneSystolicePressureAverageResult; + data['weekoneDiastolicPressureAverageResult'] = weekoneDiastolicPressureAverageResult; + data['WeekDesc'] = weekDesc; + data['WeekDiastolicPressureAverageResult'] = weekDiastolicPressureAverageResult; + data['WeekSystolicePressureAverageResult'] = weekSystolicePressureAverageResult; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart new file mode 100644 index 00000000..381d514f --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart @@ -0,0 +1,24 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekBloodPressureResultAverage { + int? dailySystolicePressureAverageResult; + int? dailyDiastolicPressureAverageResult; + DateTime? bloodPressureDate; + + WeekBloodPressureResultAverage({this.dailySystolicePressureAverageResult, this.dailyDiastolicPressureAverageResult, this.bloodPressureDate}); + + WeekBloodPressureResultAverage.fromJson(Map json) { + dailySystolicePressureAverageResult = json['DailySystolicePressureAverageResult']; + dailyDiastolicPressureAverageResult = json['DailyDiastolicPressureAverageResult']; + bloodPressureDate = DateUtil.convertStringToDate(json['BloodPressureDate']); + } + + Map toJson() { + final Map data = {}; + data['DailySystolicePressureAverageResult'] = dailySystolicePressureAverageResult; + data['DailyDiastolicPressureAverageResult'] = dailyDiastolicPressureAverageResult; + data['BloodPressureDate'] = bloodPressureDate; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart new file mode 100644 index 00000000..6f342461 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart @@ -0,0 +1,37 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearBloodPressureResultAverage { + int? monthSystolicePressureAverageResult; + int? monthDiastolicPressureAverageResult; + dynamic monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearBloodPressureResultAverage({ + this.monthSystolicePressureAverageResult, + this.monthDiastolicPressureAverageResult, + this.monthNumber, + this.monthName, + this.yearName, + }); + + YearBloodPressureResultAverage.fromJson(Map json) { + monthSystolicePressureAverageResult = json['monthSystolicePressureAverageResult']; + monthDiastolicPressureAverageResult = json['monthDiastolicPressureAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } + + Map toJson() { + final Map data = {}; + data['monthSystolicePressureAverageResult'] = monthSystolicePressureAverageResult; + data['monthDiastolicPressureAverageResult'] = monthDiastolicPressureAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} diff --git a/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart b/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart new file mode 100644 index 00000000..066df3ca --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart @@ -0,0 +1,99 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class DiabeticPatientResult { + String? chartMonth; + var chartYear; + DateTime? dateChart; + var description; + var descriptionN; + int? diabtecAvarage; + bool? isActive; + int? lineItemNo; + var listMonth; + var listWeek; + int? measured; + String? measuredDesc; + var monthAverageResult; + int? patientID; + var remark; + var resultDesc; + dynamic resultValue; + String? unit; + var weekAverageResult; + String? weekDesc; + var yearAverageResult; + + DiabeticPatientResult( + {this.chartMonth, + this.chartYear, + this.dateChart, + this.description, + this.descriptionN, + this.diabtecAvarage, + this.isActive, + this.lineItemNo, + this.listMonth, + this.listWeek, + this.measured, + this.measuredDesc, + this.monthAverageResult, + this.patientID, + this.remark, + this.resultDesc, + this.resultValue, + this.unit, + this.weekAverageResult, + this.weekDesc, + this.yearAverageResult}); + + DiabeticPatientResult.fromJson(Map json) { + chartMonth = json['ChartMonth']; + chartYear = json['ChartYear']; + dateChart = DateUtil.convertStringToDate(json['DateChart']); + description = json['Description']; + descriptionN = json['DescriptionN']; + diabtecAvarage = json['DiabtecAvarage']; + isActive = json['IsActive']; + lineItemNo = json['LineItemNo']; + listMonth = json['List_Month']; + listWeek = json['List_Week']; + measured = json['Measured']; + measuredDesc = json['MeasuredDesc']; + monthAverageResult = json['MonthAverageResult']; + patientID = json['PatientID']; + remark = json['Remark']; + resultDesc = json['ResultDesc']; + resultValue = json['ResultValue']; + unit = json['Unit']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + yearAverageResult = json['YearAverageResult']; + } + + Map toJson() { + final Map data = {}; + data['ChartMonth'] = chartMonth; + data['ChartYear'] = chartYear; + data['DateChart'] = DateUtil.convertDateToString(dateChart!); + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['DiabtecAvarage'] = diabtecAvarage; + data['IsActive'] = isActive; + data['LineItemNo'] = lineItemNo; + data['List_Month'] = listMonth; + data['List_Week'] = listWeek; + data['Measured'] = measured; + data['MeasuredDesc'] = measuredDesc; + data['MonthAverageResult'] = monthAverageResult; + data['PatientID'] = patientID; + data['Remark'] = remark; + data['ResultDesc'] = resultDesc; + data['ResultValue'] = resultValue; + data['Unit'] = unit; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + data['YearAverageResult'] = yearAverageResult; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart new file mode 100644 index 00000000..77b06f11 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart @@ -0,0 +1,37 @@ +class MonthDiabeticResultAverage { + var weekfourAverageResult; + var weekthreeAverageResult; + var weektwoAverageResult; + var weekoneAverageResult; + dynamic weekAverageResult; + String? weekDesc; + + MonthDiabeticResultAverage( + {this.weekfourAverageResult, + this.weekthreeAverageResult, + this.weektwoAverageResult, + this.weekoneAverageResult, + this.weekAverageResult, + this.weekDesc}); + + MonthDiabeticResultAverage.fromJson(Map json) { + weekfourAverageResult = json['weekfourAverageResult']; + weekthreeAverageResult = json['weekthreeAverageResult']; + weektwoAverageResult = json['weektwoAverageResult']; + weekoneAverageResult = json['weekoneAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['weekfourAverageResult'] = weekfourAverageResult; + data['weekthreeAverageResult'] = weekthreeAverageResult; + data['weektwoAverageResult'] = weektwoAverageResult; + data['weekoneAverageResult'] = weekoneAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart new file mode 100644 index 00000000..3b35fe79 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart @@ -0,0 +1,21 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekDiabeticResultAverage { + dynamic dailyAverageResult; + DateTime? dateChart; + + WeekDiabeticResultAverage({this.dailyAverageResult, this.dateChart}); + + WeekDiabeticResultAverage.fromJson(Map json) { + dailyAverageResult = json['DailyAverageResult']; + dateChart = DateUtil.convertStringToDate(json['DateChart']); + } + + Map toJson() { + final Map data = {}; + data['DailyAverageResult'] = dailyAverageResult; + data['DateChart'] = DateUtil.convertDateToString(dateChart!); + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart new file mode 100644 index 00000000..fb340564 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart @@ -0,0 +1,35 @@ +import 'dart:developer'; + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearDiabeticResultAverage { + dynamic monthAverageResult; + var monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearDiabeticResultAverage({this.monthAverageResult, this.monthNumber, this.monthName, this.yearName}); + + YearDiabeticResultAverage.fromJson(Map json) { + try { + monthAverageResult = json['monthAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } catch (e) { + log(e.toString()); + } + } + + Map toJson() { + final Map data = {}; + data['monthAverageResult'] = monthAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart new file mode 100644 index 00000000..f95470c5 --- /dev/null +++ b/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart @@ -0,0 +1,37 @@ +class MonthWeightMeasurementResultAverage { + dynamic weekfourAverageResult; + dynamic weekthreeAverageResult; + dynamic weektwoAverageResult; + dynamic weekoneAverageResult; + dynamic weekAverageResult; + String? weekDesc; + + MonthWeightMeasurementResultAverage( + {this.weekfourAverageResult, + this.weekthreeAverageResult, + this.weektwoAverageResult, + this.weekoneAverageResult, + this.weekAverageResult, + this.weekDesc}); + + MonthWeightMeasurementResultAverage.fromJson(Map json) { + weekfourAverageResult = json['weekfourAverageResult']; + weekthreeAverageResult = json['weekthreeAverageResult']; + weektwoAverageResult = json['weektwoAverageResult']; + weekoneAverageResult = json['weekoneAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['weekfourAverageResult'] = weekfourAverageResult; + data['weekthreeAverageResult'] = weekthreeAverageResult; + data['weektwoAverageResult'] = weektwoAverageResult; + data['weekoneAverageResult'] = weekoneAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart new file mode 100644 index 00000000..ad53325e --- /dev/null +++ b/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart @@ -0,0 +1,21 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekWeightMeasurementResultAverage { + dynamic dailyAverageResult; + DateTime? weightDate; + + WeekWeightMeasurementResultAverage({this.dailyAverageResult, this.weightDate}); + + WeekWeightMeasurementResultAverage.fromJson(Map json) { + dailyAverageResult = json['DailyAverageResult']; + weightDate = DateUtil.convertStringToDate(json['WeightDate']); + } + + Map toJson() { + final Map data = {}; + data['DailyAverageResult'] = dailyAverageResult; + data['WeightDate'] = weightDate; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/weight_measurement_result.dart b/lib/features/health_trackers/models/weight/weight_measurement_result.dart new file mode 100644 index 00000000..896d33f6 --- /dev/null +++ b/lib/features/health_trackers/models/weight/weight_measurement_result.dart @@ -0,0 +1,77 @@ +import 'dart:developer'; + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeightMeasurementResult { + int? patientID; + int? lineItemNo; + int? weightMeasured; + DateTime? weightDate; + dynamic remark; + bool? isActive; + int? measured; + dynamic unit; + int? chartYear; + dynamic chartMonth; + double? yearAverageResult; + dynamic monthAverageResult; + dynamic weekAverageResult; + dynamic weekDesc; + + WeightMeasurementResult( + {this.patientID, + this.lineItemNo, + this.weightMeasured, + this.weightDate, + this.remark, + this.isActive, + this.measured, + this.unit, + this.chartYear, + this.chartMonth, + this.yearAverageResult, + this.monthAverageResult, + this.weekAverageResult, + this.weekDesc}); + + WeightMeasurementResult.fromJson(Map json) { + try { + patientID = json['PatientID']; + lineItemNo = json['LineItemNo']; + weightMeasured = json['WeightMeasured']; + weightDate = DateUtil.convertStringToDate(json['WeightDate']); + remark = json['Remark']; + isActive = json['IsActive']; + measured = json['Measured']; + unit = json['Unit']; + chartYear = json['ChartYear']; + chartMonth = json['ChartMonth']; + // Convert to double safely since API may return int + yearAverageResult = json['YearAverageResult'] != null ? (json['YearAverageResult'] as num).toDouble() : null; + monthAverageResult = json['MonthAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } catch (e) { + log(e.toString()); + } + } + + Map toJson() { + final Map data = {}; + data['PatientID'] = patientID; + data['LineItemNo'] = lineItemNo; + data['WeightMeasured'] = weightMeasured; + data['WeightDate'] = weightDate; + data['Remark'] = remark; + data['IsActive'] = isActive; + data['Measured'] = measured; + data['Unit'] = unit; + data['ChartYear'] = chartYear; + data['ChartMonth'] = chartMonth; + data['YearAverageResult'] = yearAverageResult; + data['MonthAverageResult'] = monthAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} diff --git a/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart new file mode 100644 index 00000000..7dc4b246 --- /dev/null +++ b/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart @@ -0,0 +1,29 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearWeightMeasurementResultAverage { + dynamic monthAverageResult; + int? monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearWeightMeasurementResultAverage({this.monthAverageResult, this.monthNumber, this.monthName, this.yearName}); + + YearWeightMeasurementResultAverage.fromJson(Map json) { + monthAverageResult = json['monthAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } + + Map toJson() { + final Map data = {}; + data['monthAverageResult'] = monthAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} + diff --git a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart index 7a921988..ebc95117 100644 --- a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart +++ b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart @@ -8,8 +8,10 @@ class HmgServicesComponentModel { Color? iconColor; bool isLogin; bool isLocked; + Color textColor; Color bgColor; String? route; + bool isExternalLink; Function? onTap; HmgServicesComponentModel( @@ -23,6 +25,8 @@ class HmgServicesComponentModel { this.bgColor = Colors.white, this.iconColor = Colors.white, this.route, - this.onTap + this.onTap, + this.textColor = Colors.black, + this.isExternalLink = false, }); } diff --git a/lib/features/monthly_reports/monthly_reports_repo.dart b/lib/features/monthly_reports/monthly_reports_repo.dart new file mode 100644 index 00000000..4ace6ec1 --- /dev/null +++ b/lib/features/monthly_reports/monthly_reports_repo.dart @@ -0,0 +1,96 @@ +import 'package:dartz/dartz.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class MonthlyReportsRepo { + Future>> saveMonthlyReport({ + String? email, + }); +} + +class MonthlyReportsRepoImp implements MonthlyReportsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MonthlyReportsRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>> saveMonthlyReport({ + String? email, + }) async { + try { + Failure? failure; + + GenericApiModel? reportApiResponse; + + await apiClient.post( + ApiConsts.getMonthlyReports, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + reportApiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (reportApiResponse == null) return Left(ServerFailure("Unknown error")); + + if ((reportApiResponse!.messageStatus ?? 0) != 1) { + return Right(reportApiResponse!); + } + + GenericApiModel? emailApiResponse; + + final Map emailRequest = {}; + + if (email != null && email.trim().isNotEmpty) { + emailRequest["Email"] = email.trim(); + } + + await apiClient.post( + ApiConsts.updatePatientEmail, + body: emailRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + emailApiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (emailApiResponse == null) return Left(ServerFailure("Unknown error")); + + return Right(emailApiResponse!); + } catch (e) { + loggerService.logError("MonthlyReportsRepo.saveMonthlyReport error: $e"); + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/monthly_reports/monthly_reports_view_model.dart b/lib/features/monthly_reports/monthly_reports_view_model.dart new file mode 100644 index 00000000..4fd82da1 --- /dev/null +++ b/lib/features/monthly_reports/monthly_reports_view_model.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'monthly_reports_repo.dart'; +class MonthlyReportsViewModel extends ChangeNotifier { + final MonthlyReportsRepo monthlyReportsRepo; + final ErrorHandlerService errorHandlerService; + + bool isLoading = false; + + MonthlyReportsViewModel({ + required this.monthlyReportsRepo, + required this.errorHandlerService, + }); + + Future saveMonthlyReport({String? email}) async { + isLoading = true; + notifyListeners(); + + final result = await monthlyReportsRepo.saveMonthlyReport(email: email); + + final success = result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + return false; + }, + (apiResponse) => (apiResponse.messageStatus ?? 0) == 1, + ); + + isLoading = false; + notifyListeners(); + return success; + } +} diff --git a/lib/features/monthly_reports/terms_conditions_repo.dart b/lib/features/monthly_reports/terms_conditions_repo.dart new file mode 100644 index 00000000..a5d3f95b --- /dev/null +++ b/lib/features/monthly_reports/terms_conditions_repo.dart @@ -0,0 +1,60 @@ +import 'package:dartz/dartz.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class TermsConditionsRepo { + Future> getTermsConditions(); +} + +class TermsConditionsRepoImp implements TermsConditionsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + TermsConditionsRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future> getTermsConditions() async { + Failure? failure; + String? html; + + try { + await apiClient.post( + ApiConsts.getTermsConditions, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType ?? ServerFailure(error.toString()); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + final content = response['UserAgreementContent']; + + if (content is String && content.isNotEmpty) { + html = content; + } else { + failure = DataParsingFailure( + 'UserAgreementContent is null or not String'); + } + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + } catch (e) { + failure = UnknownFailure(e.toString()); + } + + if (failure != null) return Left(failure!); + if (html == null || html!.isEmpty) { + return Left(ServerFailure('No terms and conditions returned')); + } + + return Right(html!); + } +} + diff --git a/lib/features/monthly_reports/terms_conditions_view_model.dart b/lib/features/monthly_reports/terms_conditions_view_model.dart new file mode 100644 index 00000000..bd70b873 --- /dev/null +++ b/lib/features/monthly_reports/terms_conditions_view_model.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/terms_conditions_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class TermsConditionsViewModel extends ChangeNotifier { + final TermsConditionsRepo termsConditionsRepo; + final ErrorHandlerService errorHandlerService; + + String? termsConditionsHtml; + bool isLoading = false; + + TermsConditionsViewModel({ + required this.termsConditionsRepo, + required this.errorHandlerService, + }); + + Future getTermsConditions({ + Function()? onSuccess, + Function(String)? onError, + }) async { + isLoading = true; + notifyListeners(); + + final result = await termsConditionsRepo.getTermsConditions(); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + isLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message ?? 'Something went wrong'); + } + }, + (html) { + termsConditionsHtml = html; + isLoading = false; + notifyListeners(); + if (onSuccess != null) onSuccess(); + }, + ); + } +} + + diff --git a/lib/features/qr_parking/models/qr_parking_response_model.dart b/lib/features/qr_parking/models/qr_parking_response_model.dart new file mode 100644 index 00000000..2e90da17 --- /dev/null +++ b/lib/features/qr_parking/models/qr_parking_response_model.dart @@ -0,0 +1,183 @@ + + +class QrParkingResponseModel { + dynamic totalRecords; + dynamic nRowID; + int? qRParkingID; + String? description; + String? descriptionN; + dynamic qRCompare; + dynamic qRValue; + String? imagePath; + bool? isActive; + int? parkingID; + int? branchID; + int? companyID; + int? buildingID; + int? rowID; + int? gateID; + int? floorID; + dynamic imagePath1; + int? createdBy; + String? createdOn; + dynamic editedBy; + dynamic editedOn; + String? parkingDescription; + String? parkingDescriptionN; + String? gateDescription; + String? gateDescriptionN; + String? branchDescription; + String? branchDescriptionN; + String? companyDescription; + String? companyDescriptionN; + String? rowDescription; + String? rowDescriptionN; + String? floorDescription; + String? floorDescriptionN; + String? buildingDescription; + String? buildingDescriptionN; + String? qRParkingCode; + String? parkingCode; + double? latitude; + double? longitude; + String? qRImageStr; + + QrParkingResponseModel({ + this.totalRecords, + this.nRowID, + this.qRParkingID, + this.description, + this.descriptionN, + this.qRCompare, + this.qRValue, + this.imagePath, + this.isActive, + this.parkingID, + this.branchID, + this.companyID, + this.buildingID, + this.rowID, + this.gateID, + this.floorID, + this.imagePath1, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.parkingDescription, + this.parkingDescriptionN, + this.gateDescription, + this.gateDescriptionN, + this.branchDescription, + this.branchDescriptionN, + this.companyDescription, + this.companyDescriptionN, + this.rowDescription, + this.rowDescriptionN, + this.floorDescription, + this.floorDescriptionN, + this.buildingDescription, + this.buildingDescriptionN, + this.qRParkingCode, + this.parkingCode, + this.latitude, + this.longitude, + this.qRImageStr, + }); + + QrParkingResponseModel.fromJson(Map json) { + totalRecords = json['TotalRecords']; + nRowID = json['nRowID']; + qRParkingID = json['QRParkingID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + qRCompare = json['QRCompare']; + qRValue = json['QRValue']; + imagePath = json['ImagePath']; + isActive = json['IsActive']; + parkingID = json['ParkingID']; + branchID = json['BranchID']; + companyID = json['CompanyID']; + buildingID = json['BuildingID']; + rowID = json['RowID']; + gateID = json['GateID']; + floorID = json['FloorID']; + imagePath1 = json['ImagePath1']; + createdBy = json['CreatedBy']; + createdOn = json['CreatedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + parkingDescription = json['ParkingDescription']; + parkingDescriptionN = json['ParkingDescriptionN']; + gateDescription = json['GateDescription']; + gateDescriptionN = json['GateDescriptionN']; + branchDescription = json['BranchDescription']; + branchDescriptionN = json['BranchDescriptionN']; + companyDescription = json['CompanyDescription']; + companyDescriptionN = json['CompanyDescriptionN']; + rowDescription = json['RowDescription']; + rowDescriptionN = json['RowDescriptionN']; + floorDescription = json['FloorDescription']; + floorDescriptionN = json['FloorDescriptionN']; + buildingDescription = json['BuildingDescription']; + buildingDescriptionN = json['BuildingDescriptionN']; + qRParkingCode = json['QRParkingCode']; + parkingCode = json['ParkingCode']; + latitude = _toDouble(json['Latitude']); + longitude = _toDouble(json['Longitude']); + qRImageStr = json['QRImageStr']; + } + + Map toJson() { + final Map data = {}; + data['TotalRecords'] = totalRecords; + data['nRowID'] = nRowID; + data['QRParkingID'] = qRParkingID; + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['QRCompare'] = qRCompare; + data['QRValue'] = qRValue; + data['ImagePath'] = imagePath; + data['IsActive'] = isActive; + data['ParkingID'] = parkingID; + data['BranchID'] = branchID; + data['CompanyID'] = companyID; + data['BuildingID'] = buildingID; + data['RowID'] = rowID; + data['GateID'] = gateID; + data['FloorID'] = floorID; + data['ImagePath1'] = imagePath1; + data['CreatedBy'] = createdBy; + data['CreatedOn'] = createdOn; + data['EditedBy'] = editedBy; + data['EditedOn'] = editedOn; + data['ParkingDescription'] = parkingDescription; + data['ParkingDescriptionN'] = parkingDescriptionN; + data['GateDescription'] = gateDescription; + data['GateDescriptionN'] = gateDescriptionN; + data['BranchDescription'] = branchDescription; + data['BranchDescriptionN'] = branchDescriptionN; + data['CompanyDescription'] = companyDescription; + data['CompanyDescriptionN'] = companyDescriptionN; + data['RowDescription'] = rowDescription; + data['RowDescriptionN'] = rowDescriptionN; + data['FloorDescription'] = floorDescription; + data['FloorDescriptionN'] = floorDescriptionN; + data['BuildingDescription'] = buildingDescription; + data['BuildingDescriptionN'] = buildingDescriptionN; + data['QRParkingCode'] = qRParkingCode; + data['ParkingCode'] = parkingCode; + data['Latitude'] = latitude; + data['Longitude'] = longitude; + data['QRImageStr'] = qRImageStr; + return data; + } + + static double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is double) return v; + if (v is int) return v.toDouble(); + return double.tryParse(v.toString()); + } +} + diff --git a/lib/features/qr_parking/qr_parking_repo.dart b/lib/features/qr_parking/qr_parking_repo.dart new file mode 100644 index 00000000..1ec905f1 --- /dev/null +++ b/lib/features/qr_parking/qr_parking_repo.dart @@ -0,0 +1,74 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + + +abstract class QrParkingRepo { + Future>>> + getQrParking({ + required int qrParkingId, + }); +} + +class QrParkingRepoImp implements QrParkingRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + QrParkingRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>>> + getQrParking({required int qrParkingId}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getQrParkingDetails, // GetQRParkingByID + body: {'QRParkingID': qrParkingId}, + onFailure: (error, statusCode, + {messageStatus, failureType}) { + failure = failureType ?? + StatusCodeFailure("$error ($statusCode)"); + }, + onSuccess: (response, statusCode, + {messageStatus, errorMessage}) { + final list = + (response['List_SWP_QRParkingModel'] as List?) ?? []; + + final res = list + .map((e) => QrParkingResponseModel.fromJson( + Map.from(e), + )) + .toList(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: res, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) { + return Left(ServerFailure("Unknown error")); + } + + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} + diff --git a/lib/features/qr_parking/qr_parking_view_model.dart b/lib/features/qr_parking/qr_parking_view_model.dart new file mode 100644 index 00000000..b0b688fd --- /dev/null +++ b/lib/features/qr_parking/qr_parking_view_model.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; + +import '../../services/cache_service.dart'; +import '../../services/error_handler_service.dart'; +import 'models/qr_parking_response_model.dart'; + + +class QrParkingViewModel extends ChangeNotifier { + final QrParkingRepo qrParkingRepo; + final ErrorHandlerService errorHandlerService; + final CacheService cacheService; + String IS_GO_TO_PARKING = 'IS_GO_TO_PARKING'; + + bool isLoading = false; + String? error; + + bool isSavePark = false; + QrParkingResponseModel? qrParkingModel; + List qrParkingList = []; + + QrParkingViewModel({ + required this.qrParkingRepo, + required this.errorHandlerService, + required this.cacheService, + }); + + + Future scanAndGetParking() async { + try { + error = null; + isLoading = true; + notifyListeners(); + + final result = await BarcodeScanner.scan(); + + if (result.type != ResultType.Barcode) { + isLoading = false; + notifyListeners(); + return null; + } + + final raw = result.rawContent.trim(); + if (raw.isEmpty) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final qrParkingId = _extractQrParkingId(raw); + if (qrParkingId == null) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final apiResult = + await qrParkingRepo.getQrParking(qrParkingId: qrParkingId); + + final model = apiResult.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + error = failure.toString(); + return null; + }, + (apiResponse) { + qrParkingList = apiResponse.data ?? []; + if (qrParkingList.isNotEmpty) { + return qrParkingList.first; + } + error = "Invalid Qr Code"; + return null; + }, + ); + + if (model != null) { + qrParkingModel = model; + isSavePark = true; + + await cacheService.saveObject( + key: IS_GO_TO_PARKING, + value: model.toJson(), + ); + } + + isLoading = false; + notifyListeners(); + return model; + } catch (e) { + error = "Scan error"; + isLoading = false; + notifyListeners(); + return null; + } + } + + /// Load saved parking + Future getIsSaveParking() async { + isLoading = true; + notifyListeners(); + + final parking = + await cacheService.getObject(key: IS_GO_TO_PARKING); + + if (parking != null) { + isSavePark = true; + qrParkingModel = QrParkingResponseModel.fromJson( + Map.from(parking), + ); + } else { + isSavePark = false; + qrParkingModel = null; + } + + isLoading = false; + notifyListeners(); + } + + /// Reset parking + Future clearParking() async { + await cacheService.remove(key: IS_GO_TO_PARKING); + isSavePark = false; + qrParkingModel = null; + notifyListeners(); + } + + int? _extractQrParkingId(String raw) { + try { + if (raw.startsWith("{")) { + final data = jsonDecode(raw); + return int.tryParse(data['QRParkingID'].toString()); + } + return int.tryParse(raw); + } catch (_) { + return null; + } + } +} + + diff --git a/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart new file mode 100644 index 00000000..c0466c4e --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart @@ -0,0 +1,188 @@ +class SymptomsUserDetailsResponseModel { + final TokenDetails? tokenDetails; + final UserDetails? userDetails; + final String? sessionId; + + SymptomsUserDetailsResponseModel({ + this.tokenDetails, + this.userDetails, + this.sessionId, + }); + + factory SymptomsUserDetailsResponseModel.fromJson(Map json) { + return SymptomsUserDetailsResponseModel( + tokenDetails: json['tokenDetails'] != null ? TokenDetails.fromJson(json['tokenDetails']) : null, + userDetails: json['userDetails'] != null ? UserDetails.fromJson(json['userDetails']) : null, + sessionId: json['sessionId'], + ); + } + + Map toJson() { + return { + 'tokenDetails': tokenDetails?.toJson(), + 'userDetails': userDetails?.toJson(), + 'sessionId': sessionId, + }; + } +} + +class TokenDetails { + final String? id; + final String? authToken; + final int? expiresIn; + + TokenDetails({ + this.id, + this.authToken, + this.expiresIn, + }); + + factory TokenDetails.fromJson(Map json) { + return TokenDetails( + id: json['id'], + authToken: json['auth_token'], + expiresIn: json['expires_in'], + ); + } + + Map toJson() { + return { + 'id': id, + 'auth_token': authToken, + 'expires_in': expiresIn, + }; + } +} + +class UserDetails { + final String? fileNo; + final String? nationalId; + final String? email; + final String? dateOfBirth; + final String? dateOfBirthHijri; + final int? age; + final UserName? name; + final int? maritalStatus; + final String? maritalStatusCode; + final String? nationality; + final String? nationalityIsoCode; + final String? occupation; + final int? idType; + final int? gender; + final String? jwtToken; + final String? countryDialCode; + final String? phoneNo; + + UserDetails({ + this.fileNo, + this.nationalId, + this.email, + this.dateOfBirth, + this.dateOfBirthHijri, + this.age, + this.name, + this.maritalStatus, + this.maritalStatusCode, + this.nationality, + this.nationalityIsoCode, + this.occupation, + this.idType, + this.gender, + this.jwtToken, + this.countryDialCode, + this.phoneNo, + }); + + factory UserDetails.fromJson(Map json) { + return UserDetails( + fileNo: json['FileNo'], + nationalId: json['national_id'], + email: json['email'], + dateOfBirth: json['date_of_birth'], + dateOfBirthHijri: json['date_of_birth_hijri'], + age: json['age'], + name: json['name'] != null ? UserName.fromJson(json['name']) : null, + maritalStatus: json['marital_status'], + maritalStatusCode: json['marital_status_code'], + nationality: json['nationality'], + nationalityIsoCode: json['nationality_iso_code'], + occupation: json['occupation'], + idType: json['id_type'], + gender: json['gender'], + jwtToken: json['jwt_token'], + countryDialCode: json['country_dial_code'], + phoneNo: json['phone_no'], + ); + } + + Map toJson() { + return { + 'FileNo': fileNo, + 'national_id': nationalId, + 'email': email, + 'date_of_birth': dateOfBirth, + 'date_of_birth_hijri': dateOfBirthHijri, + 'age': age, + 'name': name?.toJson(), + 'marital_status': maritalStatus, + 'marital_status_code': maritalStatusCode, + 'nationality': nationality, + 'nationality_iso_code': nationalityIsoCode, + 'occupation': occupation, + 'id_type': idType, + 'gender': gender, + 'jwt_token': jwtToken, + 'country_dial_code': countryDialCode, + 'phone_no': phoneNo, + }; + } + + // Helper method to get full name + String getFullName(bool isArabic) { + if (name == null) return ''; + if (isArabic) { + return '${name!.firstNameAr ?? ''} ${name!.middleNameAr ?? ''} ${name!.lastNameAr ?? ''}'.trim(); + } + return '${name!.firstName ?? ''} ${name!.middleName ?? ''} ${name!.lastName ?? ''}'.trim(); + } +} + +class UserName { + final String? firstName; + final String? middleName; + final String? lastName; + final String? firstNameAr; + final String? middleNameAr; + final String? lastNameAr; + + UserName({ + this.firstName, + this.middleName, + this.lastName, + this.firstNameAr, + this.middleNameAr, + this.lastNameAr, + }); + + factory UserName.fromJson(Map json) { + return UserName( + firstName: json['first_name'], + middleName: json['middle_name'], + lastName: json['last_name'], + firstNameAr: json['first_name_ar'], + middleNameAr: json['middle_name_ar'], + lastNameAr: json['last_name_ar'], + ); + } + + Map toJson() { + return { + 'first_name': firstName, + 'middle_name': middleName, + 'last_name': lastName, + 'first_name_ar': firstNameAr, + 'middle_name_ar': middleNameAr, + 'last_name_ar': lastNameAr, + }; + } +} diff --git a/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart new file mode 100644 index 00000000..f0d7e904 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart @@ -0,0 +1,209 @@ +class TriageDataDetails { + final TriageQuestion? question; + final List? conditions; + final bool? hasEmergencyEvidence; + final bool? shouldStop; + final String? interviewToken; + final String? message; + final List? errorList; + final int? id; + final String? language; + final String? generalId; + final String? createDate; + final String? lastEditDate; + final String? createdBy; + final String? lastEditBy; + final bool? active; + final int? sortOrder; + final int? userType; + final String? userId; + + TriageDataDetails({ + this.question, + this.conditions, + this.hasEmergencyEvidence, + this.shouldStop, + this.interviewToken, + this.message, + this.errorList, + this.id, + this.language, + this.generalId, + this.createDate, + this.lastEditDate, + this.createdBy, + this.lastEditBy, + this.active, + this.sortOrder, + this.userType, + this.userId, + }); + + factory TriageDataDetails.fromJson(Map json) { + return TriageDataDetails( + question: json['question'] != null ? TriageQuestion.fromJson(json['question']) : null, + conditions: json['conditions'] != null ? (json['conditions'] as List).map((item) => TriageCondition.fromJson(item)).toList() : null, + hasEmergencyEvidence: json['has_emergency_evidence'], + shouldStop: json['should_stop'], + interviewToken: json['interview_token'], + message: json['Message'], + errorList: json['ErrorList'] != null ? List.from(json['ErrorList']) : null, + id: json['Id'], + language: json['language'], + generalId: json['generalId'], + createDate: json['CreateDate'], + lastEditDate: json['LastEditDate'], + createdBy: json['CreatedBy'], + lastEditBy: json['LastEditBy'], + active: json['Active'], + sortOrder: json['SortOrder'], + userType: json['userType'], + userId: json['userId'], + ); + } + + Map toJson() { + return { + 'question': question?.toJson(), + 'conditions': conditions?.map((item) => item.toJson()).toList(), + 'has_emergency_evidence': hasEmergencyEvidence, + 'should_stop': shouldStop, + 'interview_token': interviewToken, + 'Message': message, + 'ErrorList': errorList, + 'Id': id, + 'language': language, + 'generalId': generalId, + 'CreateDate': createDate, + 'LastEditDate': lastEditDate, + 'CreatedBy': createdBy, + 'LastEditBy': lastEditBy, + 'Active': active, + 'SortOrder': sortOrder, + 'userType': userType, + 'userId': userId, + }; + } +} + +class TriageQuestion { + final int? type; + final String? text; + final List? items; + + TriageQuestion({ + this.type, + this.text, + this.items, + }); + + factory TriageQuestion.fromJson(Map json) { + return TriageQuestion( + type: json['type'], + text: json['text'], + items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'type': type, + 'text': text, + 'items': items?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageQuestionItem { + final String? id; + final String? name; + final List? choices; + + TriageQuestionItem({ + this.id, + this.name, + this.choices, + }); + + factory TriageQuestionItem.fromJson(Map json) { + return TriageQuestionItem( + id: json['id'], + name: json['name'], + choices: json['choices'] != null ? (json['choices'] as List).map((item) => TriageChoice.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'choices': choices?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageChoice { + final String? id; + final String? label; + + TriageChoice({ + this.id, + this.label, + }); + + factory TriageChoice.fromJson(Map json) { + return TriageChoice( + id: json['id'], + label: json['label'], + ); + } + + Map toJson() { + return { + 'id': id, + 'label': label, + }; + } +} + +class TriageCondition { + final String? id; + final String? name; + final String? commonName; + final double? probability; + final dynamic conditionDetails; + + TriageCondition({ + this.id, + this.name, + this.commonName, + this.probability, + this.conditionDetails, + }); + + factory TriageCondition.fromJson(Map json) { + return TriageCondition( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + probability: json['probability']?.toDouble(), + conditionDetails: json['condition_details'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'common_name': commonName, + 'probability': probability, + 'condition_details': conditionDetails, + }; + } + + /// Get probability as percentage + String getProbabilityPercentage() { + if (probability == null) return '0%'; + return '${(probability! * 100).toStringAsFixed(1)}%'; + } +} diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 954d414a..c55c2f0c 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; @@ -8,25 +7,56 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class SymptomsCheckerRepo { - Future>> getBodySymptomsByName({ + Future>> + getUserDetails({ + required String userName, + required String password, + }); + + Future>> + getBodySymptomsByName({ required List organNames, + required String userSessionToken, + required int gender, + }); + + Future>> + getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, }); - Future>> getRiskFactors({ + Future>> + getSuggestions({ required int age, required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required String sessionId, + required int gender, }); - Future>> getSuggestions({ + Future>> + getDiagnosisForTriage({ required int age, required String sex, required List evidenceIds, + List>? triageEvidence, required String language, + required String userSessionToken, + required int gender, + required String sessionId, }); } @@ -34,14 +64,79 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { final ApiClient apiClient; final LoggerService loggerService; - SymptomsCheckerRepoImp({required this.apiClient, required this.loggerService}); + SymptomsCheckerRepoImp( + {required this.apiClient, required this.loggerService}); @override - Future>> getBodySymptomsByName({required List organNames}) async { - log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}"); - log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}"); + Future>> + getUserDetails({ + required String userName, + required String password, + }) async { + Map body = {"userName": userName, "password": password}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.symptomsUserLogin, + body: body, + isExternal: true, + isAllowAny: true, + isBodyPlainText: false, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("getUserDetails API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = + response is String ? jsonDecode(response) : response; - Map headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'}; + SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = + SymptomsUserDetailsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: symptomsUserDetailsResponseModel, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing getUserDetails response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getUserDetails: $e"); + loggerService.logError("StackTrace: $stackTrace"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> + getBodySymptomsByName({ + required List organNames, + required String userSessionToken, + required int gender, + }) async { + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + Map body = { + 'bodyPartName': organNames, + 'gender': gender, + }; try { GenericApiModel? apiResponse; @@ -50,22 +145,17 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { await apiClient.post( ApiConsts.getBodySymptomsByName, apiHeaders: headers, - body: jsonEncode(organNames), + body: body, isExternal: true, isAllowAny: true, - isBodyPlainText: true, + isBodyPlainText: false, onFailure: (error, statusCode, {messageStatus, failureType}) { - loggerService.logError("GetBodySymptomsByName API Failed: $error"); - log("GetBodySymptomsByName Failed: $error, Status: $statusCode"); failure = failureType ?? ServerFailure(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - log("GetBodySymptomsByName Response Status: $statusCode"); - loggerService.logInfo("GetBodySymptomsByName API Success: $response"); - log("GetBodySymptomsByName Response: $response"); - - BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(response); + BodySymptomResponseModel bodySymptomResponse = + BodySymptomResponseModel.fromJson(response); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -74,9 +164,9 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { data: bodySymptomResponse, ); } catch (e, stackTrace) { - loggerService.logError("Error parsing GetBodySymptomsByName response: $e"); + loggerService + .logError("Error parsing GetBodySymptomsByName response: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); failure = DataParsingFailure(e.toString()); } }, @@ -88,17 +178,20 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Exception in getBodySymptomsByName: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); return Left(UnknownFailure(e.toString())); } } @override - Future>> getRiskFactors({ + Future>> + getRiskFactors({ required int age, required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required int gender, + required String sessionId, }) async { final Map body = { "age": { @@ -107,6 +200,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { "sex": sex, "evidence": evidenceIds.map((id) => {"id": id}).toList(), "language": language, + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', }; try { @@ -115,21 +214,22 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { await apiClient.post( ApiConsts.getRiskFactors, + apiHeaders: headers, body: body, isExternal: true, isAllowAny: true, onFailure: (error, statusCode, {messageStatus, failureType}) { - log("GetRiskFactors Failed: $error, Status: $statusCode"); + loggerService.logError("GetRiskFactors API Failed: $error"); failure = failureType ?? ServerFailure(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - log("GetRiskFactors Response: $response"); - // Parse response if it's a string - final Map responseData = response is String ? jsonDecode(response) : response; + final Map responseData = + response is String ? jsonDecode(response) : response; - RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); + RiskAndSuggestionsResponseModel riskFactorsResponse = + RiskAndSuggestionsResponseModel.fromJson(responseData); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -140,7 +240,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Error parsing GetRiskFactors response: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); failure = DataParsingFailure(e.toString()); } }, @@ -152,17 +251,111 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Exception in getRiskFactors: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + Future>> + getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + List>? + triageEvidence, // Additional triage-specific evidence + required String language, + required String userSessionToken, + required int gender, + required String sessionId, + }) async { + // Build evidence list: combine initial symptoms with triage evidence + List> evidenceList = []; + + // Add initial evidence as simple IDs + for (var id in evidenceIds) { + evidenceList.add({"id": id}); + } + + // Add triage evidence with proper format (id, choice_id, source) + if (triageEvidence != null && triageEvidence.isNotEmpty) { + evidenceList.addAll(triageEvidence); + } + + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceList, + "language": language, + "suggest_method": "diagnosis", + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.diagnosis, + apiHeaders: headers, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("getDiagnosisForTriage API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = + response is String ? jsonDecode(response) : response; + + final updatedResponseData = responseData['dataDetails']; + + TriageDataDetails riskFactorsResponse = + TriageDataDetails.fromJson(updatedResponseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService + .logError("Error parsing getDiagnosisForTriage response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getDiagnosisForTriage: $e"); + loggerService.logError("StackTrace: $stackTrace"); return Left(UnknownFailure(e.toString())); } } @override - Future>> getSuggestions({ + Future>> + getSuggestions({ required int age, required String sex, required List evidenceIds, required String language, + required String userSessionToken, + required String sessionId, + required int gender, }) async { final Map body = { "age": { @@ -171,6 +364,12 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { "sex": sex, "evidence": evidenceIds.map((id) => {"id": id}).toList(), "language": language, + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', }; try { @@ -179,21 +378,22 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { await apiClient.post( ApiConsts.getSuggestions, + apiHeaders: headers, body: body, isExternal: true, isAllowAny: true, onFailure: (error, statusCode, {messageStatus, failureType}) { - log("getSuggestions Failed: $error, Status: $statusCode"); + loggerService.logError("GetSuggestions API Failed: $error"); failure = failureType ?? ServerFailure(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - log("getSuggestions Response: $response"); - // Parse response if it's a string - final Map responseData = response is String ? jsonDecode(response) : response; + final Map responseData = + response is String ? jsonDecode(response) : response; - RiskAndSuggestionsResponseModel riskFactorsResponse = RiskAndSuggestionsResponseModel.fromJson(responseData); + RiskAndSuggestionsResponseModel riskFactorsResponse = + RiskAndSuggestionsResponseModel.fromJson(responseData); apiResponse = GenericApiModel( messageStatus: messageStatus ?? 1, @@ -202,9 +402,8 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { data: riskFactorsResponse, ); } catch (e, stackTrace) { - loggerService.logError("Error parsing getSuggestions response: $e"); + loggerService.logError("Error parsing GetSuggestions response: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); failure = DataParsingFailure(e.toString()); } }, @@ -216,7 +415,6 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { } catch (e, stackTrace) { loggerService.logError("Exception in getSuggestions: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); return Left(UnknownFailure(e.toString())); } } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index da439c6c..73768fe0 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -7,6 +7,8 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -35,11 +37,23 @@ class SymptomsCheckerViewModel extends ChangeNotifier { bool isBodySymptomsLoading = false; bool isRiskFactorsLoading = false; bool isSuggestionsLoading = false; + bool isTriageDiagnosisLoading = false; // API data storage - using API models directly + SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel; BodySymptomResponseModel? bodySymptomResponse; RiskAndSuggestionsResponseModel? riskFactorsResponse; RiskAndSuggestionsResponseModel? suggestionsResponse; + TriageDataDetails? triageDataDetails; + + // Triage state + int? + _selectedTriageChoiceIndex; // Deprecated - keeping for backward compatibility + final Map _selectedTriageChoicesByItemId = + {}; // Map of itemId -> choiceIndex for multi-item questions + final List> _triageEvidenceList = + []; // Store triage evidence with proper format + int _triageQuestionCount = 0; // Track number of triage questions answered // Selected risk factors tracking final Set _selectedRiskFactorIds = {}; @@ -52,7 +66,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // User Info Flow State int _userInfoCurrentPage = 0; - bool _isSinglePageEditMode = false; // Track if editing single page or full flow + bool _isSinglePageEditMode = + false; // Track if editing single page or full flow String? _selectedGender; DateTime? _dateOfBirth; int? _selectedAge; @@ -92,8 +107,54 @@ class SymptomsCheckerViewModel extends ChangeNotifier { String? get tooltipOrganId => _tooltipOrganId; + String get currentSessionAuthToken => + symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? ""; + + String get currentSessionId => + symptomsUserDetailsResponseModel?.sessionId ?? ""; + + // Triage-related getters + bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false; + + bool get hasEmergencyEvidence => + triageDataDetails?.hasEmergencyEvidence ?? false; + + String? get currentInterviewToken => triageDataDetails?.interviewToken; + + TriageQuestion? get currentTriageQuestion => triageDataDetails?.question; + + List? get currentConditions => triageDataDetails?.conditions; + + int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex; + + /// Get the number of triage questions answered + int get triageQuestionCount => _triageQuestionCount; + + /// Get choice index for a specific item + int? getTriageChoiceForItem(String itemId) { + return _selectedTriageChoicesByItemId[itemId]; + } + + /// Check if all items in current question have been answered + bool get areAllTriageItemsAnswered { + if (currentTriageQuestion?.items == null || + currentTriageQuestion!.items!.isEmpty) { + return false; + } + + // Check if we have an answer for each item + for (var item in currentTriageQuestion!.items!) { + if (item.id != null && + !_selectedTriageChoicesByItemId.containsKey(item.id)) { + return false; + } + } + return true; + } + /// Get organs for current view - List get currentOrgans => OrganData.getOrgansForView(_currentView); + List get currentOrgans => + OrganData.getOrgansForView(_currentView); /// Get all selected organs from both views List get selectedOrgans { @@ -101,7 +162,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ...OrganData.frontViewOrgans, ...OrganData.backViewOrgans, ]; - return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList(); + return allOrgans + .where((organ) => _selectedOrganIds.contains(organ.id)) + .toList(); } /// Check if any organs are selected @@ -118,11 +181,13 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } int get totalSelectedSymptomsCount { - return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); + return _selectedSymptomsByOrgan.values + .fold(0, (sum, symptomIds) => sum + symptomIds.length); } bool get hasSelectedSymptoms { - return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty); + return _selectedSymptomsByOrgan.values + .any((symptomIds) => symptomIds.isNotEmpty); } /// Get risk factors list @@ -148,7 +213,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } void toggleView() { - _currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front; + _currentView = + _currentView == BodyView.front ? BodyView.back : BodyView.front; notifyListeners(); } @@ -251,7 +317,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { return; } - List organNames = selectedOrgans.map((organ) => organ.name).toList(); + List organNames = + selectedOrgans.map((organ) => organ.name).toList(); await getBodySymptomsByName( organNames: organNames, @@ -301,7 +368,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } } - if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { + if (matchingOrganId != null && + _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!; if (organResult.bodySymptoms != null) { @@ -352,7 +420,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Get all selected risk factors List getAllSelectedRiskFactors() { - return riskFactorsList.where((factor) => factor.id != null && _selectedRiskFactorIds.contains(factor.id)).toList(); + return riskFactorsList + .where((factor) => + factor.id != null && _selectedRiskFactorIds.contains(factor.id)) + .toList(); } /// Clear all risk factor selections @@ -385,12 +456,14 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } // Extract symptom IDs - List evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + List evidenceIds = + selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); await getRiskFactors( age: _selectedAge!, sex: _selectedGender!.toLowerCase(), evidenceIds: evidenceIds, + sessionId: currentSessionId, language: appState.isArabic() ? 'ar' : 'en', onSuccess: (response) { if (onSuccess != null) { @@ -409,6 +482,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier { Future getRiskFactors({ required int age, required String sex, + required String sessionId, required List evidenceIds, required String language, Function(RiskAndSuggestionsResponseModel)? onSuccess, @@ -422,6 +496,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { sex: sex, evidenceIds: evidenceIds, language: language, + sessionId: sessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( @@ -438,8 +515,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { if (apiResponse.messageStatus == 1 && apiResponse.data != null) { riskFactorsResponse = apiResponse.data; - if (riskFactorsResponse != null && riskFactorsResponse!.dataDetails != null) { - RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( + if (riskFactorsResponse != null && + riskFactorsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = + RiskAndSuggestionsItemModel( id: "not_applicable", commonName: "Not Applicable", name: "Not Applicable", @@ -493,7 +572,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { /// Get all selected risk factors List getAllSelectedSuggestions() { - return suggestionsList.where((factor) => factor.id != null && _selectedSuggestionsIds.contains(factor.id)).toList(); + return suggestionsList + .where((factor) => + factor.id != null && _selectedSuggestionsIds.contains(factor.id)) + .toList(); } /// Clear all risk factor selections @@ -502,6 +584,30 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } + /// Get all evidence IDs (symptoms + risk factors + suggestions) for triage/diagnosis + List getAllEvidenceIds() { + List evidenceIds = []; + + // Add selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + evidenceIds + .addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!)); + + // Add selected risk factors (excluding "not_applicable") + final selectedRiskFactors = getAllSelectedRiskFactors(); + evidenceIds.addAll(selectedRiskFactors + .where((rf) => rf.id != null && rf.id != "not_applicable") + .map((rf) => rf.id!)); + + // Add selected suggestions (excluding "not_applicable") + final selectedSuggestions = getAllSelectedSuggestions(); + evidenceIds.addAll(selectedSuggestions + .where((s) => s.id != null && s.id != "not_applicable") + .map((s) => s.id!)); + + return evidenceIds; + } + /// Fetch risk factors based on selected symptoms Future fetchSuggestions({ Function()? onSuccess, @@ -526,13 +632,17 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } // Extract symptom IDs - List evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + List evidenceIds = + selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); // Get all selected symptoms final selectedRisks = getAllSelectedRiskFactors(); if (selectedRisks.isNotEmpty) { - List evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList(); + List evidenceRisksIds = selectedRisks + .where((s) => s.id != null && s.id != "not_applicable") + .map((s) => s.id!) + .toList(); evidenceIds.addAll(evidenceRisksIds); } @@ -571,6 +681,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { sex: sex, evidenceIds: evidenceIds, language: language, + sessionId: currentSessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( @@ -587,8 +700,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { if (apiResponse.messageStatus == 1 && apiResponse.data != null) { suggestionsResponse = apiResponse.data; - if (suggestionsResponse != null && suggestionsResponse!.dataDetails != null) { - RiskAndSuggestionsItemModel riskFactorItem = RiskAndSuggestionsItemModel( + if (suggestionsResponse != null && + suggestionsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = + RiskAndSuggestionsItemModel( id: "not_applicable", commonName: "Not Applicable", name: "Not Applicable", @@ -612,15 +727,155 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ); } + /// Call Diagnosis API for Triage - This is called iteratively until shouldStop is true + Future getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + List>? triageEvidence, + required String language, + Function(TriageDataDetails)? onSuccess, + Function(String)? onError, + }) async { + isTriageDiagnosisLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getDiagnosisForTriage( + age: age, + sex: sex, + evidenceIds: evidenceIds, + triageEvidence: triageEvidence, + language: language, + sessionId: currentSessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, + ); + + result.fold( + (failure) async { + isTriageDiagnosisLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isTriageDiagnosisLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + triageDataDetails = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch diagnosis'); + } + } + }, + ); + } + + /// Convenience method to start or continue the triage process + /// This automatically uses all selected evidence (symptoms + risk factors + suggestions) + Future startOrContinueTriage({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Get all evidence IDs + final evidenceIds = getAllEvidenceIds(); + + if (evidenceIds.isEmpty) { + if (onError != null) { + onError('No evidence selected'); + } + return; + } + + await getDiagnosisForTriage( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Select a triage choice option (for backward compatibility with single-item questions) + void selectTriageChoice(int choiceIndex) { + _selectedTriageChoiceIndex = choiceIndex; + notifyListeners(); + } + + /// Select a choice for a specific item (for multi-item questions) + void selectTriageChoiceForItem(String itemId, int choiceIndex) { + _selectedTriageChoicesByItemId[itemId] = choiceIndex; + notifyListeners(); + } + + /// Reset triage choice selection and increment question count + void resetTriageChoice() { + _selectedTriageChoiceIndex = null; + _selectedTriageChoicesByItemId.clear(); + _triageQuestionCount++; // Increment question count + notifyListeners(); + } + + /// Add triage evidence in the proper format + void addTriageEvidence(String itemId, String choiceId) { + _triageEvidenceList.add({ + "id": itemId, + "choice_id": choiceId, + "source": "triage", + }); + notifyListeners(); + } + + /// Get all triage evidence + List> getTriageEvidence() { + return List.from(_triageEvidenceList); + } + + /// Clear triage evidence + void clearTriageEvidence() { + _triageEvidenceList.clear(); + notifyListeners(); + } + void reset() { _currentView = BodyView.front; _selectedOrganIds.clear(); _selectedSymptomsByOrgan.clear(); _selectedRiskFactorIds.clear(); _selectedSuggestionsIds.clear(); + _triageEvidenceList.clear(); + _selectedTriageChoicesByItemId.clear(); + _triageQuestionCount = 0; // Reset question count bodySymptomResponse = null; riskFactorsResponse = null; suggestionsResponse = null; + triageDataDetails = null; + isTriageDiagnosisLoading = false; + _selectedTriageChoiceIndex = null; _isBottomSheetExpanded = false; _tooltipTimer?.cancel(); _tooltipOrganId = null; @@ -686,7 +941,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Calculate age from date of birth final now = DateTime.now(); int age = now.year - dateOfBirth.year; - if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { + if (now.month < dateOfBirth.month || + (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { age--; } _selectedAge = age; @@ -725,6 +981,44 @@ class SymptomsCheckerViewModel extends ChangeNotifier { }; } + Future getSymptomsUserDetails({ + required String userName, + required String password, + Function()? onSuccess, + Function(String)? onError, + }) async { + isBodySymptomsLoading = true; + notifyListeners(); + final result = await symptomsCheckerRepo.getUserDetails( + userName: userName, password: password); + + result.fold( + (failure) async { + isBodySymptomsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isBodySymptomsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + symptomsUserDetailsResponseModel = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms'); + } + } + }, + ); + } + Future getBodySymptomsByName({ required List organNames, Function(BodySymptomResponseModel)? onSuccess, @@ -735,6 +1029,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { final result = await symptomsCheckerRepo.getBodySymptomsByName( organNames: organNames, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( diff --git a/lib/features/water_monitor/water_monitor_view_model.dart b/lib/features/water_monitor/water_monitor_view_model.dart index 01167fef..d82712a1 100644 --- a/lib/features/water_monitor/water_monitor_view_model.dart +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -16,14 +16,19 @@ import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_mode import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class WaterMonitorViewModel extends ChangeNotifier { WaterMonitorRepo waterMonitorRepo; + ErrorHandlerService errorHandlerService; - WaterMonitorViewModel({required this.waterMonitorRepo}); + WaterMonitorViewModel({ + required this.waterMonitorRepo, + required this.errorHandlerService, + }); // Controllers final TextEditingController nameController = TextEditingController(); @@ -190,9 +195,9 @@ class WaterMonitorViewModel extends ChangeNotifier { identificationNo: identification, ); - result.fold((failure) { - log('Error fetching user progress: ${failure.message}'); - }, (apiModel) { + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { log("User Progress Data ($_selectedDuration): ${apiModel.data.toString()}"); // Parse the response based on progress type @@ -300,10 +305,13 @@ class WaterMonitorViewModel extends ChangeNotifier { identificationNo: identification, ); - result.fold((failure) { - _userDetailData = null; - if (onError != null) onError(failure.message); - }, (apiModel) { + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + _userDetailData = null; + if (onError != null) onError(failure.message); + }, + (apiModel) { _userDetailData = apiModel.data; // Populate form fields from the fetched data @@ -322,7 +330,7 @@ class WaterMonitorViewModel extends ChangeNotifier { if (_userDetailData == null) { try { - _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsScreen); + _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsPage); } catch (navErr) { log('Navigation to water monitor settings failed: $navErr'); } @@ -686,6 +694,7 @@ class WaterMonitorViewModel extends ChangeNotifier { return result.fold( (failure) { + errorHandlerService.handleError(failure: failure); _validationError = failure.message; _isLoading = false; notifyListeners(); @@ -1060,14 +1069,14 @@ class WaterMonitorViewModel extends ChangeNotifier { return result.fold( (failure) { + errorHandlerService.handleError(failure: failure); log('Error inserting user activity: ${failure.message}'); _isLoading = false; notifyListeners(); return false; }, - (apiModel) { + (apiModel) async { log("Insert user activity success: ${apiModel.data.toString()}"); - // Update consumed amount and goal from the response if (apiModel.data != null && apiModel.data is List && (apiModel.data as List).isNotEmpty) { final progressData = (apiModel.data as List).first; if (progressData is Map) { @@ -1090,7 +1099,7 @@ class WaterMonitorViewModel extends ChangeNotifier { } // Refresh progress data to ensure consistency - fetchUserProgressForMonitoring(); + await fetchUserProgressForMonitoring(); } _isLoading = false; @@ -1135,12 +1144,13 @@ class WaterMonitorViewModel extends ChangeNotifier { return result.fold( (failure) { + errorHandlerService.handleError(failure: failure); log('Error undoing user activity: ${failure.message}'); _isLoading = false; notifyListeners(); return false; }, - (apiModel) { + (apiModel) async { log("Undo user activity success: ${apiModel.data.toString()}"); // Update consumed amount and goal from the response @@ -1164,8 +1174,8 @@ class WaterMonitorViewModel extends ChangeNotifier { } } } + await fetchUserProgressForMonitoring(); } - fetchUserProgressForMonitoring(); _isLoading = false; notifyListeners(); return true; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index ea0bcb8d..f550c81b 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -475,7 +475,7 @@ abstract class LocaleKeys { static const shareReview = 'shareReview'; static const review = 'review'; static const viewMedicalFile = 'viewMedicalFile'; - static const viewAllServices = 'viewAllServices'; + static String get viewAllServices => 'viewAllServices'; static const medicalFile = 'medicalFile'; static const verified = 'verified'; static const checkup = 'checkup'; diff --git a/lib/main.dart b/lib/main.dart index eb57c374..e730e078 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; @@ -36,6 +37,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'; @@ -45,6 +47,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'core/utils/size_utils.dart'; +import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; @pragma('vm:entry-point') @@ -175,8 +178,17 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), @@ -219,3 +231,4 @@ class MyApp extends StatelessWidget { } } // flutter pub run easy_localization:generate -S assets/langs -f keys -o locale_keys.g.dart + diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart new file mode 100644 index 00000000..d0720fb2 --- /dev/null +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -0,0 +1,1035 @@ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:flutter/cupertino.dart'; +import '../../core/app_assets.dart'; +import '../../core/utils/calendar_utils.dart'; +import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; +import '../../features/active_prescriptions/models/active_prescriptions_response_model.dart'; +import '../../generated/locale_keys.g.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import 'package:intl/intl.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import '../../widgets/buttons/custom_button.dart'; +import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting +import 'package:provider/provider.dart'; +import '../../widgets/loader/bottomsheet_loader.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + + +class ActiveMedicationPage extends StatefulWidget { + const ActiveMedicationPage({super.key}); + + @override + State createState() => _ActiveMedicationPageState(); +} + +class _ActiveMedicationPageState extends State { + late DateTime currentDate; + late DateTime selectedDate; + List selectedDayMeds = []; + ActivePrescriptionsViewModel? activePreVM; + + + Map medReminderStatus = {}; + + String _buildMedKey(ActivePrescriptionsResponseModel med) { + return "${med.itemId}_${med.startDate}_${med.days}_${med.frequency}"; + } + + int _getDosesCount(ActivePrescriptionsResponseModel med) { + return med.frequencyNumber ?? 1; + } + + @override + void initState() { + super.initState(); + currentDate = DateTime.now(); + selectedDate = currentDate; + + WidgetsBinding.instance.addPostFrameCallback((_) async { + activePreVM = + Provider.of(context, listen: false); + LoaderBottomSheet.showLoader(); + await activePreVM!.getActiveMedications( + onSuccess: (_) async { + LoaderBottomSheet.hideLoader(); + + final todayMeds = + activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = todayMeds); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await loadSavedReminders(); + }); + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); + + activePreVM!.addListener(() { + if (!mounted) return; + final medsForDay = + activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = medsForDay); + }); + }); + } + + Future loadSavedReminders() async { + final prefs = await SharedPreferences.getInstance(); + + for (final med in activePreVM!.activePrescriptionsDetailsList) { + final medKey = _buildMedKey(med); + final doses = _getDosesCount(med); + + med.selectedDoseTimes = + List.filled(doses, null, growable: false); + + for (int i = 0; i < doses; i++) { + final saved = prefs.getString("doseTime_${medKey}_$i"); + if (saved != null) { + med.selectedDoseTimes[i] = saved; + } + } + + final reminderOn = + prefs.getBool("reminderStatus_$medKey") ?? false; + med.isReminderOn = reminderOn; + medReminderStatus[medKey] = reminderOn; + } + + setState(() {}); + } + + Future saveReminderStatus(String medKey, bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool("reminderStatus_$medKey", value); + } + + Future saveDoseTime( + String medKey, int doseIndex, String time) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString("doseTime_${medKey}_$doseIndex", time); + } + + List getUpcomingDays() => + List.generate(7, (index) => currentDate.add(Duration(days: index))); + + @override + Widget build(BuildContext context) { + final days = getUpcomingDays(); + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Active Medications".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600)), + SizedBox(height: 16.h), + SizedBox( + height: 65.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + final day = days[index]; + final label = DateFormat('E').format(day); + return Padding( + padding: const EdgeInsets.only(right: 12), + child: buildDayCard(label, day), + ); + }, + ), + ), + SizedBox(height: 20.h), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${selectedDate.day}", + style: TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -4), + child: Text( + _getSuffix(selectedDate.day), + style: const TextStyle( + fontSize: 12, + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + TextSpan( + text: " ${DateFormat.MMMM().format(selectedDate)}", + style: const TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + Text("Medications".needTranslation, + style: TextStyle( + color: AppColors.primaryRedBorderColor, + fontSize: 12.f, + fontWeight: FontWeight.w500)), + SizedBox(height: 16.h), + Expanded( + child: SingleChildScrollView( + child: selectedDayMeds.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedDayMeds.length, + itemBuilder: (context, index) { + final med = selectedDayMeds[index]; + final doses = _getDosesCount(med); + if (med.selectedDoseTimes.length != doses) { + final old = med.selectedDoseTimes; + med.selectedDoseTimes = + List.filled(doses, null, + growable: false); + for (int i = 0; + i < old.length && i < doses; + i++) { + med.selectedDoseTimes[i] = old[i]; + } + } + + return Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + margin: EdgeInsets.all(10), + child: Column( + children: [ + _buildMedHeader(med), + Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + // Utils.buildSvgWithAssets( + // icon: AppAssets., + // height: 18.h, + // width: 18.h, + // iconColor: + // AppColors.lightGreyTextColor, + // ), + Icon( + Icons.info_outline, + color: AppColors + .lightGreyTextColor, + size: 18, + ), + SizedBox(width: 6.h), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Remarks: " + .needTranslation, + style: TextStyle( + color: + AppColors.textColor, + fontWeight: + FontWeight.w600, + fontSize: 10, + ), + ), + TextSpan( + text: + "some remarks about the prescription will be here" + .needTranslation, + style: TextStyle( + color: AppColors + .lightGreyTextColor, + fontWeight: + FontWeight.normal, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ).paddingOnly(left: 16, right: 16), + const Divider( + color: AppColors.greyColor), + GestureDetector( + onTap: () => showDoseDialog(med), + child: Row( + children: [ + Container( + width: 40.h, + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: + BorderRadius.circular(10), + ), + child: + Utils.buildSvgWithAssets( + icon: AppAssets.bell, + height: 24.h, + width: 24.h, + iconColor: + AppColors.greyTextColor, + ), + ), + SizedBox(width: 12.h), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Set Reminder" + .needTranslation, + style: TextStyle( + fontSize: 14.f, + fontWeight: + FontWeight.w600, + color: AppColors + .textColor)), + Text( + "Notify me before the consumption time" + .needTranslation, + style: TextStyle( + fontSize: 12.f, + color: AppColors + .textColorLight, + )), + ], + ), + ), + _buildToggle(med), + ], + ).paddingAll(16), + ), + const Divider( + color: AppColors.greyColor), + _buildButtons(), + ], + ), + ); + }, + ) + : Utils.getNoDataWidget( + context, + noDataText: + "No medications today".needTranslation, + ), + ), + ), + ], + ).paddingAll(16), + ); + } + + // medicine card + Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 59.h, + height: 59.h, + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.spacerLineColor, + borderRadius: 30.r, + hasShadow: false, + ), + child: Utils.buildImgWithNetwork( + url: med.productImageString ?? "", + iconColor: Colors.transparent) + .circle(52.h)), + ), + SizedBox(width: 12.h), + Expanded( + child: Text( + med.itemDescription ?? "", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor), + ), + ), + ]), + SizedBox(height: 12.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget( + labelText: + "Route: ${med.route}".needTranslation), + AppCustomChipWidget( + labelText: + "Frequency: ${med.frequency}".needTranslation), + AppCustomChipWidget( + labelText: + "Daily Dose: ${med.doseDailyQuantity}" + .needTranslation), + AppCustomChipWidget( + labelText: + "Duration: ${med.days}".needTranslation), + ], + ), + ], + ), + ); + + Widget _buildButtons() => Padding( + padding: EdgeInsets.all(16), + child: Row(children: [ + Expanded( + child: CustomButton( + text: "Check Availability".needTranslation, + fontSize: 13.f, + onPressed: () {}, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + SizedBox(width: 12.h), + Expanded( + child: CustomButton( + text: "Read Instructions".needTranslation, + fontSize: 13.f, + onPressed: () {})), + ]), + ); + + Widget _buildToggle(ActivePrescriptionsResponseModel med) { + final medKey = _buildMedKey(med); + final value = medReminderStatus[medKey] ?? false; + + return GestureDetector( + onTap: () async { + await showDoseDialog(med); + final hasTime = + (med.selectedDoseTimes).any((t) => t != null); + medReminderStatus[medKey] = hasTime; + await saveReminderStatus(medKey, hasTime); + + setState(() {}); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: value + ? AppColors.lightGreenColor + : AppColors.greyColor.withOpacity(0.3), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: + value ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: value + ? AppColors.textGreenColor + : AppColors.greyTextColor, + ), + ), + ), + ), + ), + ); + } + + + Future showDoseDialog(ActivePrescriptionsResponseModel med) { + final doses = _getDosesCount(med); + if (med.selectedDoseTimes.length != doses) { + final old = med.selectedDoseTimes; + med.selectedDoseTimes = + List.filled(doses, null, growable: false); + for (int i = 0; i < old.length && i < doses; i++) { + med.selectedDoseTimes[i] = old[i]; + } + } + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 520.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + customBorder: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + hasShadow: true, + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Reminders".needTranslation, + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + height: 24.h, + width: 24.h, + iconColor: + AppColors.blackBgColor, + ), + ), + ], + ), + SizedBox(height: 20.h), + Expanded( + child: ListView.builder( + itemCount: doses, + itemBuilder: (context, doseIndex) { + final badgeColor = [ + AppColors.textGreenColor, + AppColors.infoColor, + AppColors.labelColorYellow, + AppColors.mainPurple + ][doseIndex % 4]; + final doseLabel = + "${doseIndex + 1}${_getSuffix(doseIndex + 1)}"; + final time = + med.selectedDoseTimes[doseIndex] ?? + "Not set yet"; + return GestureDetector( + onTap: () { + Navigator.pop(context); + showTimePickerSheet(med, doseIndex); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 14), + decoration: BoxDecoration( + color: badgeColor, + borderRadius: BorderRadius.circular(12), + ), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${doseIndex + 1}", + style: TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -4), + child: Text( + _getSuffix(doseIndex + 1), + style: TextStyle( + color: AppColors.whiteColor, + fontSize: 10.f, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Set reminder for $doseLabel dose", + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + height: 24.h, + width: 24.h, + iconColor: + AppColors.textColor, + ), + ], + ), + SizedBox(height: 4.h), + Text( + time, + style: TextStyle( + fontSize: 12.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + + void showTimePickerSheet( + ActivePrescriptionsResponseModel med, int doseIndex) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 460.h, + decoration: const BoxDecoration( + color: AppColors.bottomSheetBgColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: ReminderTimerDialog( + med: med, + frequencyNumber: _getDosesCount(med), + doseIndex: doseIndex, + onTimeSelected: (String time) async { + final medKey = _buildMedKey(med); + med.selectedDoseTimes[doseIndex] = time; + await saveDoseTime(medKey, doseIndex, time); + medReminderStatus[medKey] = true; + await saveReminderStatus(medKey, true); + setState(() {}); + }, + ), + ), + ); + } + + Widget buildDayCard(String label, DateTime date) { + final isSelected = selectedDate.day == date.day && + selectedDate.month == date.month && + selectedDate.year == date.year; + return GestureDetector( + onTap: () { + final vm = + Provider.of(context, + listen: false); + setState(() { + selectedDate = date; + selectedDayMeds = vm.getMedsForSelectedDay(date); + }); + }, + child: Container( + width: 57.h, + height: 65.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isSelected + ? AppColors.secondaryLightRedBorderColor + : Colors.transparent, + border: Border.all( + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.spacerLineColor, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day == currentDate.day ? "Today" : label, + style: TextStyle( + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.greyTextColor, + fontSize: 11.f, + fontWeight: FontWeight.w500), + ), + SizedBox(height: 5.h), + Text("${date.day}", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.bold, + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.textColor)) + ]), + ), + ), + ); + } + + String _getSuffix(int day) { + if (day == 1 || day == 21 || day == 31) return "st"; + if (day == 2 || day == 22) return "nd"; + if (day == 3 || day == 23) return "rd"; + return "th"; + } + +} + + +class ReminderTimerDialog extends StatefulWidget { + final int frequencyNumber; + final int doseIndex; + final Function(String) onTimeSelected; + final ActivePrescriptionsResponseModel med; + + const ReminderTimerDialog({ + super.key, + required this.frequencyNumber, + required this.doseIndex, + required this.onTimeSelected, + required this.med, + }); + + @override + State createState() => _ReminderTimerDialogState(); +} + +class _ReminderTimerDialogState extends State { + TimeOfDay selectedTime = TimeOfDay.now(); + String? _selectedTime; + String bigTimeText = "00:00"; + bool showPicker = false; + + final List> presetTimes = [ + ["06:00 AM", "07:00 AM", "08:00 AM", "09:00 AM"], // Morning + ["12:00 PM", "01:00 PM", "02:00 PM", "03:00 PM"], // Noon + ["06:00 PM", "07:00 PM", "08:00 PM", "09:00 PM"], // Evening + ]; + + String _getSuffix(int number) { + if (number == 1 || number == 21 || number == 31) return "st"; + if (number == 2 || number == 22) return "nd"; + if (number == 3 || number == 23) return "rd"; + return "th"; + } + + @override + Widget build(BuildContext context) { + final int bucket = widget.doseIndex.clamp(0, 2); + final List times = presetTimes[bucket]; + + return Padding( + padding: const EdgeInsets.all(16), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + customBorder: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Time for ", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + TextSpan( + text: "${widget.doseIndex + 1}", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -6), + child: Text( + _getSuffix(widget.doseIndex + 1), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + ), + ), + TextSpan( + text: " reminder", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child:Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + height: 24.h, + width: 24.h, + iconColor: + AppColors.blackBgColor, + ),), + ], + ), + SizedBox(height: 12.h), + Wrap( + spacing: 8, + runSpacing: 8, + children: times.map((t) { + bool selected = _selectedTime == t; + return AppCustomChipWidget( + labelText: t, + backgroundColor: selected + ? AppColors.lightGreenButtonColor + : AppColors.transparent, + textColor: AppColors.textColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: selected + ? AppColors.successColor + : AppColors.spacerLineColor, + width: 1.2, + ), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + onChipTap: () { + setState(() { + _selectedTime = t; + selectedTime = _parseTime(t); + bigTimeText = selectedTime.format(context).split(" ")[0]; + showPicker = false; + }); + }, + ); + }).toList(), + ), + SizedBox(height: 25.h), + GestureDetector( + onTap: () { + setState(() => showPicker = !showPicker); + }, + child: Center( + child: Column( + children: [ + Text( + bigTimeText, + style: TextStyle( + fontSize: 48.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + Text( + selectedTime.period == DayPeriod.am ? "AM" : "PM", + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.bold, + color: AppColors.greyTextColor, + ), + ), + ], + ), + ), + ), + SizedBox(height: 15.h), + if (showPicker) + SizedBox( + height: 100.h, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: false, + initialDateTime: DateTime( + 2024, + 1, + 1, + selectedTime.hour, + selectedTime.minute, + ), + onDateTimeChanged: (newTime) { + setState(() { + _selectedTime = null; + selectedTime = TimeOfDay(hour: newTime.hour, minute: newTime.minute); + bigTimeText = selectedTime.format(context).split(" ")[0]; + }); + }, + ), + ), + SizedBox(height: 25.h), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () async { + final selectedFormattedTime = selectedTime.format(context); + widget.onTimeSelected(selectedFormattedTime); + try { + final parts = selectedFormattedTime.split(":"); + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1].split(" ")[0]); + bool isPM = selectedFormattedTime.contains("PM"); + if (isPM && hour != 12) hour += 12; + if (!isPM && hour == 12) hour = 0; + int totalMinutes = hour * 60 + minute; + await setCalender( + context, + eventId: widget.med.itemId.toString(), + selectedMinutes: totalMinutes, + frequencyNumber: widget.frequencyNumber, + days: widget.med.days ?? 1, + orderDate: widget.med.orderDate ?? "", + itemDescriptionN: widget.med.itemDescription ?? "", + route: widget.med.route ?? "", + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Reminder added to calendar ✅".needTranslation)), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error while setting calendar: $e".needTranslation)), + ); + } + Navigator.pop(context); + }, + child: Text( + LocaleKeys.save.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.f, + ), + ), + ), + ), + ], + ), + ], + ).paddingAll(16), + ), + ); + } + + TimeOfDay _parseTime(String t) { + try { + int hour = int.parse(t.split(":")[0]); + int minute = int.parse(t.split(":")[1].split(" ")[0]); + bool pm = t.contains("PM"); + if (pm && hour != 12) hour += 12; + if (!pm && hour == 12) hour = 0; + return TimeOfDay(hour: hour, minute: minute); + } catch (_) { + return TimeOfDay.now(); + } + } +} + + + + + + diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart index eb22ab65..42cba5df 100644 --- a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart +++ b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bf.dart'; @@ -12,17 +17,12 @@ import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ibw.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ovulation.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/triglycerides.dart'; -import 'package:provider/provider.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; class HealthCalculatorDetailedPage extends StatefulWidget { HealthCalculatorsTypeEnum calculatorType; @@ -50,8 +50,8 @@ class _HealthCalculatorDetailedPageState extends State months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const List months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; return months[month - 1]; } @@ -828,7 +846,8 @@ class HealthCalcualtorViewModel extends ChangeNotifier { // expose map-like results for widgets to forward to parent Map? get bmiResultMap => bmiResult == null ? null : {'bmiResult': bmiResult, 'bmiCategory': bmiCategory}; - Map? get caloriesResultMap => caloriesResult == null ? null : {'calories': caloriesResult, 'bmr': bmrResult, 'calorieRange': calorieRange, 'bmrRange': bmrRange}; + Map? get caloriesResultMap => + caloriesResult == null ? null : {'calories': caloriesResult, 'bmr': bmrResult, 'calorieRange': calorieRange, 'bmrRange': bmrRange}; Map? get ibwResultMap => ibwResult == null ? null : {'ibw': ibwResult, 'difference': weightDifference, 'status': weightStatus}; diff --git a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart new file mode 100644 index 00000000..56f82fc2 --- /dev/null +++ b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart @@ -0,0 +1,567 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class AddHealthTrackerEntryPage extends StatefulWidget { + final HealthTrackerTypeEnum trackerType; + + const AddHealthTrackerEntryPage({ + super.key, + required this.trackerType, + }); + + @override + State createState() => _AddHealthTrackerEntryPageState(); +} + +class _AddHealthTrackerEntryPageState extends State { + late DialogService dialogService; + + // Controllers for date and time + final TextEditingController dateController = TextEditingController(); + final TextEditingController timeController = TextEditingController(); + + @override + void initState() { + super.initState(); + dialogService = getIt.get(); + } + + @override + void dispose() { + dateController.dispose(); + timeController.dispose(); + super.dispose(); + } + + /// Get page title based on tracker type + String _getPageTitle() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Add Blood Sugar".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Add Blood Pressure".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Add Weight".needTranslation; + } + } + + /// Get success message based on tracker type + String _getSuccessMessage() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Blood Sugar Data saved successfully".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Blood Pressure Data saved successfully".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Weight Data saved successfully".needTranslation; + } + } + + /// Save entry based on tracker type + Future _saveEntry(HealthTrackersViewModel viewModel) async { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + await _saveBloodSugarEntry(viewModel); + break; + case HealthTrackerTypeEnum.bloodPressure: + await _saveBloodPressureEntry(viewModel); + break; + case HealthTrackerTypeEnum.weightTracker: + await _saveWeightEntry(viewModel); + break; + } + } + + // Save Blood Sugar entry + Future _saveBloodSugarEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveBloodSugarEntry( + dateTime: dateTime, + measureTime: viewModel.selectedBloodSugarMeasureTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Save Weight entry + Future _saveWeightEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveWeightEntry( + dateTime: dateTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Save Blood Pressure entry + Future _saveBloodPressureEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveBloodPressureEntry( + dateTime: dateTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Show success message and pop back + void _showSuccessAndPop() { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: _getSuccessMessage(), + ), + callBackFunc: () { + Navigator.pop(context); + }, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + // Reusable method to show selection bottom sheet + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + // Blood Sugar unit selection + void _showBloodSugarUnitSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.bloodSugarUnit, + selectedValue: viewModel.selectedBloodSugarUnit, + onSelected: viewModel.setBloodSugarUnit, + useUpperCase: false, + ); + } + + // Blood Sugar measure time selection + void _showBloodSugarEntryTimeBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Measure Time".needTranslation, + items: viewModel.bloodSugarMeasureTimeEnList, + selectedValue: viewModel.selectedBloodSugarMeasureTime, + onSelected: viewModel.setBloodSugarMeasureTime, + useUpperCase: false, + ); + } + + // Weight unit selection + void _showWeightUnitSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.weightUnits, + selectedValue: viewModel.selectedWeightUnit, + onSelected: viewModel.setWeightUnit, + useUpperCase: false, + ); + } + + // Blood Pressure measured arm selection + void _showMeasuredArmSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Arm".needTranslation, + items: viewModel.measuredArmList, + selectedValue: viewModel.selectedMeasuredArm, + onSelected: viewModel.setMeasuredArm, + useUpperCase: false, + ); + } + + // Reusable method to build text field + Widget _buildTextField(TextEditingController controller, String hintText, {TextInputType keyboardType = TextInputType.name}) { + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: 1, + cursorHeight: 14.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hintText, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ); + } + + // Reusable method to build settings row + Widget _buildSettingsRow({ + required String icon, + required String label, + String? value, + Widget? inputField, + String? unit, + VoidCallback? onUnitTap, + VoidCallback? onRowTap, + Color? iconColor, + bool showDivider = true, + }) { + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 40.w, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.greyColor, + borderRadius: 10.r, + hasShadow: false, + ), + child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w, iconColor: iconColor)), + ), + SizedBox(width: 12.w), + Expanded( + flex: unit != null ? 2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + if (inputField != null) + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: inputField, + ) + else if (value != null && value.isNotEmpty) + value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ), + if (unit != null) ...[ + Container( + width: 1.w, + height: 30.w, + color: AppColors.dividerColor, + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(onUnitTap ?? () {}), + ), + ] else if (onRowTap != null) ...[ + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 8.w), + ], + ], + ).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}), + if (showDivider) Divider(height: 1, color: AppColors.dividerColor), + ], + ); + } + + /// Build form fields based on tracker type + Widget _buildFormFields(HealthTrackersViewModel viewModel) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return _buildBloodSugarForm(viewModel); + case HealthTrackerTypeEnum.bloodPressure: + return _buildBloodPressureForm(viewModel); + case HealthTrackerTypeEnum.weightTracker: + return _buildWeightForm(viewModel); + } + } + + /// Blood Sugar form fields + Widget _buildBloodSugarForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.heightIcon, + label: "Enter Blood Sugar".needTranslation, + inputField: _buildTextField(viewModel.bloodSugarController, '', keyboardType: TextInputType.number), + unit: viewModel.selectedBloodSugarUnit, + onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + Divider(height: 1, color: AppColors.dividerColor), + _buildSettingsRow( + icon: AppAssets.weight_tracker_icon, + label: "Select Measure Time".needTranslation, + value: viewModel.selectedBloodSugarMeasureTime, + onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel), + ), + ], + ); + } + + /// Blood Pressure form fields + Widget _buildBloodPressureForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.bloodPressureIcon, + iconColor: AppColors.greyTextColor, + label: "Enter Systolic Value".needTranslation, + inputField: _buildTextField(viewModel.systolicController, '', keyboardType: TextInputType.number), + ), + _buildSettingsRow( + icon: AppAssets.bloodPressureIcon, + iconColor: AppColors.greyTextColor, + label: "Enter Diastolic Value".needTranslation, + inputField: _buildTextField(viewModel.diastolicController, '', keyboardType: TextInputType.number), + ), + _buildSettingsRow( + icon: AppAssets.bodyIcon, + iconColor: AppColors.greyTextColor, + label: "Select Arm".needTranslation, + value: viewModel.selectedMeasuredArm, + onRowTap: () => _showMeasuredArmSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + ], + ); + } + + /// Weight form fields + Widget _buildWeightForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.weightScale, + label: "Enter Weight".needTranslation, + inputField: _buildTextField(viewModel.weightController, '', keyboardType: TextInputType.number), + unit: viewModel.selectedWeightUnit, + onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + ], + ); + } + + /// Common date and time fields + Widget _buildDateTimeFields() { + return Column( + children: [ + TextInputWidget( + controller: dateController, + isReadOnly: true, + isArrowTrailing: true, + labelText: "Date", + hintText: "Select date".needTranslation, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.calendarGrey, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + try { + final parsedDate = DateTime.parse(val); + final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate); + dateController.text = formattedDate; + log("date: $formattedDate"); + } catch (e) { + dateController.text = val; + log("date: $val"); + } + }, + ), + TextInputWidget( + controller: timeController, + isReadOnly: true, + isArrowTrailing: true, + labelText: "Time", + hintText: "Select time".needTranslation, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.calendarGrey, + selectionType: SelectionTypeEnum.time, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + timeController.text = val; + log("time: $val"); + }, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: _getPageTitle(), + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Save".needTranslation, + onPressed: () async => await _saveEntry(viewModel), + borderRadius: 12.r, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: _buildFormFields(viewModel), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/health_trackers/health_tracker_detail_page.dart b/lib/presentation/health_trackers/health_tracker_detail_page.dart new file mode 100644 index 00000000..9443f122 --- /dev/null +++ b/lib/presentation/health_trackers/health_tracker_detail_page.dart @@ -0,0 +1,1303 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +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_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/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +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 { + _showEmailInputBottomSheet(context); + } + + /// Show email input bottom sheet + void _showEmailInputBottomSheet(BuildContext context) { + final viewModel = context.read(); + final appState = getIt.get(); + final dialogService = getIt.get(); + + // Get user's email from authenticated user + final userEmail = appState.getAuthenticatedUser()?.emailAddress ?? ''; + + // Create email controller and pre-fill if available + final emailController = TextEditingController(text: userEmail); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Send Report by Email".needTranslation, + message: "", + child: _buildEmailInputContent( + context: context, + emailController: emailController, + viewModel: viewModel, + dialogService: dialogService, + ), + onOkPressed: () {}, + ); + } + + /// Build email input content + Widget _buildEmailInputContent({ + required BuildContext context, + required TextEditingController emailController, + required HealthTrackersViewModel viewModel, + required DialogService dialogService, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Enter your email address to receive the report".needTranslation.toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + SizedBox(height: 16.h), + + // Email Input Field using TextInputWidget + TextInputWidget( + padding: EdgeInsets.symmetric(horizontal: 8.w), + labelText: "Email Address".needTranslation, + hintText: "Enter email address".needTranslation, + controller: emailController, + keyboardType: TextInputType.emailAddress, + isEnable: true, + isBorderAllowed: true, + isAllowRadius: true, + ), + + SizedBox(height: 24.h), + + // Send Button + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: "Send Report".needTranslation, + onPressed: () { + _sendEmailReport( + context: context, + email: emailController.text.trim(), + viewModel: viewModel, + dialogService: dialogService, + ); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ); + } + + /// Send email report based on tracker type + Future _sendEmailReport({ + required BuildContext context, + required String email, + required HealthTrackersViewModel viewModel, + required DialogService dialogService, + }) async { + // Validate email + if (email.isEmpty) { + dialogService.showErrorBottomSheet( + message: "Please enter your email address".needTranslation, + ); + return; + } + + // Basic email validation + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(email)) { + dialogService.showErrorBottomSheet( + message: "Please enter a valid email address".needTranslation, + ); + return; + } + + // Close the email input bottom sheet + Navigator.of(context).pop(); + + // Call appropriate email function based on tracker type + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + await viewModel.sendBloodSugarReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + + case HealthTrackerTypeEnum.bloodPressure: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + + await viewModel.sendBloodPressureReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + + case HealthTrackerTypeEnum.weightTracker: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + await viewModel.sendWeightReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + } + } + + /// Show success message + void _showSuccessMessage(BuildContext context, DialogService dialogService) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: "Report has been sent to your email successfully".needTranslation, + ), + callBackFunc: () {}, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); + } + + Widget _buildPageShimmer() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + SizedBox(height: 16.h), + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 120.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + ), + ), + SizedBox(height: 16.h), + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 300.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + ), + ), + SizedBox(height: 16.h), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + sendEmail: () async => onSendEmailPressed(context), + title: _getPageTitle(), + bottomChild: Consumer( + builder: (context, viewModel, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Add new Record".needTranslation, + onPressed: () { + if (!viewModel.isLoading) { + context.navigateWithName(AppRoutes.addHealthTrackerEntryPage, arguments: widget.trackerType); + } + }, + icon: AppAssets.add_icon, + borderRadius: 12.r, + borderColor: AppColors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + }, + ), + child: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading) { + return _buildPageShimmer(); + } + + if (_hasNoData(viewModel)) { + return _buildEmptyStateWidget(); + } + + return Column( + children: [ + SizedBox(height: 16.h), + TrackerLastValueCard(trackerType: widget.trackerType), + SizedBox(height: 16.h), + _buildHistoryGraphOrList(), + SizedBox(height: 16.h), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/health_trackers/health_trackers_page.dart b/lib/presentation/health_trackers/health_trackers_page.dart new file mode 100644 index 00000000..c55e8b46 --- /dev/null +++ b/lib/presentation/health_trackers/health_trackers_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; + +class HealthTrackersPage extends StatefulWidget { + const HealthTrackersPage({super.key}); + + @override + State createState() => _HealthTrackersPageState(); +} + +Widget buildHealthTrackerCard({ + required String icon, + required String title, + required String description, + required Color iconBgColor, + required VoidCallback onTap, +}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: iconBgColor, borderRadius: 10.r), + height: 40.w, + width: 40.w, + child: Utils.buildSvgWithAssets( + icon: icon, + fit: BoxFit.none, + height: 22.w, + width: 22.w, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + title.toText16(weight: FontWeight.w600), + description.toText12( + fontWeight: FontWeight.w500, + color: Color(0xFF8F9AA3), + ), + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets( + icon: AppAssets.arrowRight, + width: 24.w, + height: 24.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor, + ), + ], + ).paddingAll(16.w), + ).onPress(onTap); +} + +class _HealthTrackersPageState extends State { + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "Health Trackers".needTranslation, + child: Column( + children: [ + buildHealthTrackerCard( + iconBgColor: AppColors.primaryRedColor, + icon: AppAssets.bloodSugarOnlyIcon, + title: "Blood Sugar".needTranslation, + description: "Track your glucose levels, understand trends, and get personalized insights for better health.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.bloodSugar, + ); + }, + ), + SizedBox(height: 16.h), + buildHealthTrackerCard( + iconBgColor: AppColors.infoColor, + icon: AppAssets.bloodPressureIcon, + title: "Blood Pressure".needTranslation, + description: "Monitor your blood pressure levels, track systolic and diastolic readings, and maintain a healthy heart.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.bloodPressure, + ); + }, + ), + SizedBox(height: 16.h), + buildHealthTrackerCard( + iconBgColor: AppColors.successColor, + icon: AppAssets.weightIcon, + title: "Weight".needTranslation, + description: "Track your weight progress, set goals, and maintain a healthy body mass for overall wellness.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.weightTracker, + ); + }, + ), + ], + ).paddingSymmetrical(20.w, 24.h), + ); + } +} diff --git a/lib/presentation/health_trackers/health_trackers_view_model.dart b/lib/presentation/health_trackers/health_trackers_view_model.dart new file mode 100644 index 00000000..ce1bdf26 --- /dev/null +++ b/lib/presentation/health_trackers/health_trackers_view_model.dart @@ -0,0 +1,1116 @@ +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; + } + } + + /// Send weight report by email + Future sendWeightReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendWeightReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendWeightReportByEmail: $e'); + + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + + // ==================== BLOOD PRESSURE TRACKING METHODS ==================== + + /// Fetch blood pressure averages and results + 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; + } + } + + /// Send blood pressure report by email + Future sendBloodPressureReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendBloodPressureReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendBloodPressureReportByEmail: $e'); + + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + + // ==================== BLOOD SUGAR (DIABETIC) TRACKING METHODS ==================== + + /// Fetch blood sugar averages and results + 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; + } + } + + /// Send blood sugar report by email + Future sendBloodSugarReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendBloodSugarReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendBloodSugarReportByEmail: $e'); + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + + // Validation method + String? _validateBloodSugarEntry(String dateTime) { + // Validate blood sugar value + if (bloodSugarController.text.trim().isEmpty) { + return "Please enter blood sugar value"; + } + + final bloodSugarValue = double.tryParse(bloodSugarController.text.trim()); + if (bloodSugarValue == null) { + return "Please enter a valid number"; + } + + if (bloodSugarValue <= 0) { + return "Blood sugar value must be greater than 0"; + } + + // Validate reasonable range (typical ranges) + if (bloodSugarValue > 1000) { + return "Blood sugar value seems too high. Please check and enter again"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + // Validate measure time + if (_selectedBloodSugarMeasureTime.isEmpty) { + return "Please select when the measurement was taken"; + } + + return null; // No errors + } + + // Save blood sugar entry with validation + Future saveBloodSugarEntry({ + required String dateTime, + required String measureTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateBloodSugarEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get measure time index (0-based, but API expects 1-based) + final measureTimeIndex = bloodSugarMeasureTimeEnList.indexOf(measureTime); + + // Call API + final success = await addBloodSugarResult( + bloodSugarDateChart: dateTime, + bloodSugarResult: bloodSugarController.text.trim(), + diabeticUnit: _selectedBloodSugarUnit, + measuredTime: measureTimeIndex >= 0 ? measureTimeIndex : 0, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + bloodSugarController.clear(); + _selectedBloodSugarMeasureTime = ''; + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save blood sugar entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveBloodSugarEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + // ==================== WEIGHT ENTRY METHODS ==================== + + // Validate weight entry before saving + String? _validateWeightEntry(String dateTime) { + // Validate weight value + final weightValue = weightController.text.trim(); + if (weightValue.isEmpty) { + return "Please enter weight value"; + } + + // Check if it's a valid number + final parsedValue = double.tryParse(weightValue); + if (parsedValue == null || parsedValue <= 0) { + return "Please enter a valid weight value"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + return null; // No errors + } + + // Save weight entry with validation + Future saveWeightEntry({ + required String dateTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateWeightEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get weight unit index (0 = kg, 1 = lb) + final weightUnitIndex = weightUnits.indexOf(_selectedWeightUnit); + + // Call API + final success = await addWeightResult( + weightDate: dateTime, + weightMeasured: weightController.text.trim(), + weightUnit: weightUnitIndex >= 0 ? weightUnitIndex : 0, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + weightController.clear(); + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save weight entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveWeightEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + // ==================== BLOOD PRESSURE ENTRY METHODS ==================== + + // Validate blood pressure entry before saving + String? _validateBloodPressureEntry(String dateTime) { + // Validate systolic value + final systolicValue = systolicController.text.trim(); + if (systolicValue.isEmpty) { + return "Please enter systolic value"; + } + final parsedSystolic = int.tryParse(systolicValue); + if (parsedSystolic == null || parsedSystolic <= 0) { + return "Please enter a valid systolic value"; + } + + // Validate diastolic value + final diastolicValue = diastolicController.text.trim(); + if (diastolicValue.isEmpty) { + return "Please enter diastolic value"; + } + final parsedDiastolic = int.tryParse(diastolicValue); + if (parsedDiastolic == null || parsedDiastolic <= 0) { + return "Please enter a valid diastolic value"; + } + + // Validate arm selection + if (_selectedMeasuredArm.isEmpty) { + return "Please select measured arm"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + return null; // No errors + } + + // Save blood pressure entry with validation + Future saveBloodPressureEntry({ + required String dateTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateBloodPressureEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get measured arm index (0 = Left Arm, 1 = Right Arm) + final measuredArmIndex = measuredArmList.indexOf(_selectedMeasuredArm); + + // Call API + final success = await addOrUpdateBloodPressureResult( + bloodPressureDate: dateTime, + systolicePressure: systolicController.text.trim(), + diastolicPressure: diastolicController.text.trim(), + measuredArm: measuredArmIndex >= 0 ? measuredArmIndex : 0, + isUpdate: false, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + systolicController.clear(); + diastolicController.clear(); + _selectedMeasuredArm = ''; + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save blood pressure entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveBloodPressureEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + @override + void dispose() { + bloodSugarController.dispose(); + weightController.dispose(); + systolicController.dispose(); + diastolicController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart new file mode 100644 index 00000000..542baa58 --- /dev/null +++ b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart @@ -0,0 +1,271 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class TrackerLastValueCard extends StatelessWidget { + final HealthTrackerTypeEnum trackerType; + + const TrackerLastValueCard({super.key, required this.trackerType}); + + /// Get status text and color based on blood sugar value + (String status, Color color, Color bgColor) _getBloodSugarStatus(double value) { + if (value < 70) { + return ('Low'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.5)); + } else if (value <= 100) { + return ('Normal'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } else if (value <= 125) { + return ('Pre-diabetic'.needTranslation, AppColors.ratingColorYellow, AppColors.errorColor.withValues(alpha: 0.4)); + } else { + return ('High'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.4)); + } + } + + /// Get status text and color based on blood pressure value (systolic) + (String status, Color color, Color bgColor) _getBloodPressureStatus(int systolic) { + if (systolic < 90) { + return ('Low'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.5)); + } else if (systolic <= 120) { + return ('Normal'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } else if (systolic <= 140) { + return ('Elevated'.needTranslation, AppColors.ratingColorYellow, AppColors.errorColor.withValues(alpha: 0.4)); + } else { + return ('High'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.4)); + } + } + + /// Get status for weight (neutral - no good/bad status) + (String status, Color color, Color bgColor) _getWeightStatus() { + return ('Recorded'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } + + /// Get default unit based on tracker type + String _getDefaultUnit() { + switch (trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return 'mg/dL'; + case HealthTrackerTypeEnum.bloodPressure: + return 'mmHg'; + case HealthTrackerTypeEnum.weightTracker: + return 'kg'; + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, viewModel, child) { + // Get the last record based on tracker type + dynamic lastRecord; + String displayValue = '--'; + String unit = _getDefaultUnit(); + DateTime? lastDate; + String status = ''; + Color statusColor = AppColors.greyTextColor; + + switch (trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + final allResults = [ + ...viewModel.weekDiabeticPatientResult, + ...viewModel.monthDiabeticPatientResult, + ...viewModel.yearDiabeticPatientResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.dateChart ?? DateTime(1900); + final dateB = b.dateChart ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final lastValue = lastRecord.resultValue?.toDouble() ?? 0.0; + displayValue = lastValue.toStringAsFixed(0); + unit = lastRecord.unit ?? 'mg/dL'; + lastDate = lastRecord.dateChart; + final (s, c, _) = _getBloodSugarStatus(lastValue); + status = s; + statusColor = c; + } + break; + + case HealthTrackerTypeEnum.bloodPressure: + final allResults = [ + ...viewModel.weekBloodPressureResult, + ...viewModel.monthBloodPressureResult, + ...viewModel.yearBloodPressureResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.bloodPressureDate ?? DateTime(1900); + final dateB = b.bloodPressureDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final systolic = lastRecord.systolicePressure ?? 0; + final diastolic = lastRecord.diastolicPressure ?? 0; + displayValue = '$systolic/$diastolic'; + unit = 'mmHg'; + lastDate = lastRecord.bloodPressureDate; + final (s, c, _) = _getBloodPressureStatus(systolic); + status = s; + statusColor = c; + } + break; + + case HealthTrackerTypeEnum.weightTracker: + final allResults = [ + ...viewModel.weekWeightMeasurementResult, + ...viewModel.monthWeightMeasurementResult, + ...viewModel.yearWeightMeasurementResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.weightDate ?? DateTime(1900); + final dateB = b.weightDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final weightValue = lastRecord.weightMeasured?.toDouble() ?? 0.0; + displayValue = weightValue.toStringAsFixed(0); + unit = lastRecord.unit ?? 'kg'; + lastDate = lastRecord.weightDate; + final (s, c, _) = _getWeightStatus(); + status = s; + statusColor = c; + } + break; + } + + final formattedDate = lastDate != null ? DateFormat('EEE DD MMM, yy').format(lastDate) : DateFormat('EEE DD MMM, yy').format(DateTime.now()); + + // Show shimmer while loading + if (viewModel.isLoading) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40.h, + width: 120.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + SizedBox(height: 8.h), + Row( + children: [ + Container( + height: 32.h, + width: 150.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + ), + SizedBox(width: 8.w), + Container( + height: 32.h, + width: 80.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + ), + ], + ), + ], + ), + ), + ); + } + + // Show empty state if no records + if (lastRecord == null) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "--".toText32(isBold: true, color: AppColors.greyTextColor), + SizedBox(width: 6.w), + unit.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500).paddingOnly(top: 8.h), + ], + ), + SizedBox(height: 8.h), + AppCustomChipWidget( + labelText: "No records yet".needTranslation, + icon: AppAssets.doctor_calendar_icon, + ), + ], + ), + ); + } + + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + displayValue.toText32(isBold: true, color: statusColor), + SizedBox(width: 6.w), + unit.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500).paddingOnly(top: 8.h), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + AppCustomChipWidget( + labelText: "${"Last Record".needTranslation}: $formattedDate", + icon: AppAssets.doctor_calendar_icon, + ), + SizedBox(width: 8.w), + if (trackerType != HealthTrackerTypeEnum.weightTracker) ...[ + AppCustomChipWidget( + labelText: status.needTranslation, + icon: AppAssets.normalStatusGreenIcon, + iconColor: statusColor, + ), + ] + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index f2cab90b..e49a6216 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -25,6 +25,7 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -36,6 +37,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../core/dependencies.dart' show getIt; +import '../../features/qr_parking/qr_parking_view_model.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -45,12 +47,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, @@ -172,6 +175,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, @@ -189,14 +201,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); }, ); }, @@ -517,7 +529,18 @@ class ServicesPage extends StatelessWidget { SizedBox(width: 8.w), "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) ], - ), + ).onPress(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), + ), + ), + ); + + }), ), ), ), diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 3f0c44eb..59efb73f 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServiceGridViewItem extends StatelessWidget { final HmgServicesComponentModel hmgServiceComponentModel; @@ -16,18 +17,18 @@ class ServiceGridViewItem extends StatelessWidget { final bool isHealthToolIcon; final Function? onTap; - const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false, this.onTap, this.isHealthToolIcon = false}); + const ServiceGridViewItem( + this.hmgServiceComponentModel, this.index, this.isHomePage, + {super.key, this.isLocked = false, required this.isHealthToolIcon, this.onTap}); @override Widget build(BuildContext context) { return InkWell( - onTap: () { - hmgServiceComponentModel.route != null - ? getIt.get().pushPageRoute(hmgServiceComponentModel.route!) - : hmgServiceComponentModel.onTap != null - ? hmgServiceComponentModel.onTap!() - : null; - }, + onTap: () => hmgServiceComponentModel.isExternalLink + ? _openLink(hmgServiceComponentModel.route!) + : getIt + .get() + .pushPageRoute(hmgServiceComponentModel.route!), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -58,4 +59,14 @@ class ServiceGridViewItem extends StatelessWidget { ], )); } + + Future _openLink(String link) async { + final Uri url = Uri.parse(link); + + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + throw "Could not launch $url"; + } + } } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index b25ae823..6d7a91c6 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -59,6 +59,8 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dart'; import 'package:provider/provider.dart'; +import '../active_medication/active_medication_page.dart'; + class LandingPage extends StatefulWidget { const LandingPage({super.key}); @@ -537,7 +539,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/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 6dde9580..c6ff85f3 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -9,12 +9,14 @@ 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/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_config.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; @@ -31,6 +33,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/active_medication/active_medication_page.dart'; import 'package:hmg_patient_app_new/presentation/allergies/allergies_list_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; @@ -74,6 +77,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; import 'package:provider/provider.dart'; +import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; import '../prescriptions/prescription_detail_page.dart'; import 'widgets/medical_file_appointment_card.dart'; @@ -97,6 +101,7 @@ class _MedicalFilePageState extends State { late MonthlyReportViewModel monthlyReportViewModel; final CacheService cacheService = GetIt.instance(); + late ActivePrescriptionsViewModel activePrescriptionsViewModel; int currentIndex = 0; @@ -243,9 +248,9 @@ class _MedicalFilePageState extends State { labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), onChipTap: () { navigationService.pushPage( - page: FamilyMedicalScreen( - profiles: medicalFileViewModel.patientFamilyFiles, - onSelect: (FamilyFileResponseModelLists p1) {}, + page: FamilyMedicalScreen( + profiles: medicalFileViewModel.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists p1) {}, ), ); }, @@ -288,7 +293,8 @@ class _MedicalFilePageState extends State { iconColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, textColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 12.w, - backgroundColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + backgroundColor: + insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), ); }), @@ -305,55 +311,64 @@ class _MedicalFilePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), - Row( - children: [ - LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), - ], - ), - ], - ).paddingSymmetrical(0.w, 0.h).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: VitalSignPage(), - ), - ); - }), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), + Row( + children: [ + LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(0.w, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }), + ), SizedBox(height: 16.h), // Make this section dynamic-height (no fixed 160.h) LayoutBuilder( builder: (context, constraints) { if (hmgServicesVM.isVitalSignLoading) { - return _buildVitalSignShimmer(); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildVitalSignShimmer(), + ); } if (hmgServicesVM.vitalSignList.isEmpty) { - return Container( - padding: EdgeInsets.all(16.w), - width: MediaQuery.of(context).size.width, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.r, - hasShadow: false, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), - SizedBox(height: 12.h), - "No vital signs recorded yet".needTranslation.toText12(isCenter: true), - ], + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Container( + padding: EdgeInsets.all(16.w), + width: MediaQuery.of(context).size.width, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "No vital signs recorded yet".needTranslation.toText12(isCenter: true), + ], + ), ), ); } // The cards define their own height; measure the first rendered page once _scheduleVitalSignMeasure(); - final double hostHeight = _vitalSignMeasuredHeight ?? (160.h); + final double hostHeight = _vitalSignMeasuredHeight ?? (135.h); return SizedBox( height: hostHeight, @@ -390,9 +405,7 @@ class _MedicalFilePageState extends State { width: hmgServicesVM.vitalSignCurrentPage == index ? 24.w : 8.w, height: 8.h, decoration: BoxDecoration( - color: hmgServicesVM.vitalSignCurrentPage == index - ? AppColors.primaryRedColor - : AppColors.dividerColor, + color: hmgServicesVM.vitalSignCurrentPage == index ? AppColors.primaryRedColor : AppColors.dividerColor, borderRadius: BorderRadius.circular(4.r), ), ), @@ -400,7 +413,7 @@ class _MedicalFilePageState extends State { ), ], ], - ).paddingSymmetrical(24.w, 0.0); + ); }), SizedBox(height: 16.h), @@ -596,7 +609,8 @@ class _MedicalFilePageState extends State { ? Container( padding: EdgeInsets.all(12.w), width: MediaQuery.of(context).size.width, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), @@ -633,57 +647,58 @@ class _MedicalFilePageState extends State { itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: MedicalFileAppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - onRescheduleTap: () { - openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); - }, - onAskDoctorTap: () async { - LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); - await myAppointmentsViewModel.isDoctorAvailable( - projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID, - doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID, - clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID, - onSuccess: (value) async { - if (value) { - await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: MedicalFileAppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], + myAppointmentsViewModel: myAppointmentsViewModel, + onRescheduleTap: () { + openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); + }, + onAskDoctorTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); + await myAppointmentsViewModel.isDoctorAvailable( + projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID, + doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID, + clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID, + onSuccess: (value) async { + if (value) { + await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.askDoctor.tr(context: context), + child: AskDoctorRequestTypeSelect( + askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, + myAppointmentsViewModel: myAppointmentsViewModel, + patientAppointmentHistoryResponseModel: + myAppointmentsVM.patientAppointmentsHistoryList[index], + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.askDoctor.tr(context: context), - child: AskDoctorRequestTypeSelect( - askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, - myAppointmentsViewModel: myAppointmentsViewModel, - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - } else { + print("Doctor is not available"); + } + }, + onError: (_) { LoaderBottomSheet.hideLoader(); - print("Doctor is not available"); - } - }, - onError: (_) { - LoaderBottomSheet.hideLoader(); - }, - ); - }, + }, + ); + }, + ), ), ), - ), - )); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), ), @@ -742,116 +757,125 @@ class _MedicalFilePageState extends State { child: Column( children: [ ListView.separated( - itemCount: prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, + itemCount: + prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, shrinkWrap: true, padding: EdgeInsets.only(left: 0, right: 8.w), physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Row( - children: [ - Image.network( - prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ).circle(100.r), - SizedBox(width: 16.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true), - SizedBox(height: 4.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.w, - runSpacing: 4.w, - children: [ - AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate( - DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate), - false, + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: Row( + children: [ + Image.network( + prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).circle(100.r), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true), + SizedBox(height: 4.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.w, + runSpacing: 4.w, + children: [ + AppCustomChipWidget( + labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), + AppCustomChipWidget( + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate( + DateUtil.convertStringToDate( + prescriptionVM.patientPrescriptionOrders[index].appointmentDate), + false, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - // SizedBox(width: 40.h), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), - ], - ).onPress(() { - prescriptionVM.setPrescriptionsDetailsLoading(); - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionDetailPage(isFromAppointments: false, prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), - ), - ); - }), - ), - )); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), - SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: "All Prescriptions".needTranslation, - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionsListPage(), - ), - ); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - height: 40.h, - icon: AppAssets.requests, - iconColor: AppColors.primaryRedColor, - iconSize: 16.w, - ), + // SizedBox(width: 40.h), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + width: 15.w, + height: 15.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor)), + ], + ).onPress(() { + prescriptionVM.setPrescriptionsDetailsLoading(); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDetailPage( + isFromAppointments: false, + prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), + ), + ); + }), + ), + )); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 16.h), + const Divider(color: AppColors.dividerColor), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "All Prescriptions".needTranslation, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionsListPage(), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + height: 40.h, + icon: AppAssets.requests, + iconColor: AppColors.primaryRedColor, + iconSize: 16.w, ), - SizedBox(width: 6.w), - Expanded( - child: CustomButton( - text: "All Medications".needTranslation, - onPressed: () {}, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.h, - height: 40.h, - icon: AppAssets.all_medications_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - ), + ), + SizedBox(width: 6.w), + Expanded( + child: CustomButton( + text: "All Medications".needTranslation, + onPressed: () {}, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.h, + height: 40.h, + icon: AppAssets.all_medications_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, ), - ], - ), - ], + ), + ], + ), + ], ), ), ).paddingSymmetrical(0.w, 0.h) @@ -905,7 +929,10 @@ class _MedicalFilePageState extends State { fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), + ("Dr. John Smith Smith Smith") + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: true), ], ) : myAppointmentsVM.patientMyDoctorsList.isEmpty @@ -932,58 +959,58 @@ class _MedicalFilePageState extends State { shrinkWrap: true, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SizedBox( - // width: 80.w, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.w, - height: 64.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: false, radius: 50.r), - SizedBox(height: 8.h), - Expanded( - child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: false), - ), - ], - ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, - projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, - doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, - )); - LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), - ), - )); + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + // width: 80.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + Expanded( + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, + projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, + doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), @@ -1092,9 +1119,14 @@ class _MedicalFilePageState extends State { text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", onPressed: () { insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); - insuranceViewModel.getPatientInsuranceDetailsForUpdate( - appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + insuranceViewModel.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(), + appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, + child: PatientInsuranceCardUpdateCard(), + callBackFunc: () {}, + title: "", + isCloseButtonVisible: false, + isFullScreen: false); }, backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), borderColor: AppColors.bgGreenColor.withOpacity(0.0), @@ -1298,7 +1330,7 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.blood_sugar_icon, isLargeText: true, iconSize: 36.w, - ).onPress(() {}), + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.bloodSugar)), MedicalFileCard( label: "Blood Pressure".needTranslation, textColor: AppColors.blackColor, @@ -1306,7 +1338,7 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.lab_result_icon, isLargeText: true, iconSize: 36.w, - ).onPress(() {}), + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.bloodPressure)), MedicalFileCard( label: "Weight Tracker".needTranslation, textColor: AppColors.blackColor, @@ -1314,7 +1346,7 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.weight_tracker_icon, isLargeText: true, iconSize: 36.w, - ).onPress(() {}), + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.weightTracker)), ], ).paddingSymmetrical(0.w, 0.0), SizedBox(height: 16.h), @@ -1394,23 +1426,23 @@ class _MedicalFilePageState extends State { ).toShimmer(), SizedBox(height: 16.h), // Label shimmer - Container( - width: 70.w, - height: 12.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.r), - ), - ).toShimmer(), - SizedBox(height: 8.h), + // Container( + // width: 70.w, + // height: 12.h, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4.r), + // ), + // ).toShimmer(), + // SizedBox(height: 8.h), // Value shimmer (larger) - Container( - width: 60.w, - height: 32.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.r), - ), - ).toShimmer(), - SizedBox(height: 12.h), + // Container( + // width: 60.w, + // height: 32.h, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4.r), + // ), + // ).toShimmer(), + // SizedBox(height: 12.h), // Bottom row with chip and arrow Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1446,62 +1478,66 @@ class _MedicalFilePageState extends State { }) { return [ // Page 1: BMI + Height - Row( - children: [ - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.bmiVital, - label: "BMI", - value: vitalSign.bodyMassIndex?.toString() ?? '--', - unit: '', - status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, - onTap: onTap, + Padding( + padding: EdgeInsets.only(left: 24.w), + child: Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: "BMI", + value: vitalSign.bodyMassIndex?.toString() ?? '--', + unit: '', + status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, + onTap: onTap, + ), ), - ), - SizedBox(width: 12.w), - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.heightVital, - label: "Height", - value: vitalSign.heightCm?.toString() ?? '--', - unit: 'cm', - status: null, - onTap: onTap, + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.heightVital, + label: "Height", + value: vitalSign.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: onTap, + ), ), - ), - ], + ], + ), ), // Page 2: Weight + Blood Pressure - Row( - children: [ - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.weightVital, - label: "Weight", - value: vitalSign.weightKg?.toString() ?? '--', - unit: 'kg', - status: vitalSign.weightKg != null ? "Normal" : null, - onTap: onTap, + Padding(padding: EdgeInsets.symmetric(horizontal: 12.w),child: Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: "Weight", + value: vitalSign.weightKg?.toString() ?? '--', + unit: 'kg', + status: vitalSign.weightKg != null ? "Normal" : null, + onTap: onTap, + ), ), - ), - SizedBox(width: 12.w), - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.bloodPressure, - label: "Blood Pressure", - value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null - ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" - : '--', - unit: '', - status: _getBloodPressureStatus( - systolic: vitalSign.bloodPressureHigher, - diastolic: vitalSign.bloodPressureLower, + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: "Blood Pressure", + value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ), + onTap: onTap, ), - onTap: onTap, ), - ), - ], - ), + ], + )), + ]; } @@ -1526,7 +1562,6 @@ class _MedicalFilePageState extends State { return GestureDetector( onTap: onTap, child: Container( - // Same styling used originally for vitals in MedicalFilePage decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 16.r, @@ -1540,15 +1575,15 @@ class _MedicalFilePageState extends State { Row( children: [ Container( - padding: EdgeInsets.all(10.h), + padding: EdgeInsets.all(8.h), decoration: BoxDecoration( color: scheme.iconBg, borderRadius: BorderRadius.circular(12.r), ), child: Utils.buildSvgWithAssets( icon: icon, - width: 20.w, - height: 20.h, + width: 22.w, + height: 22.h, iconColor: scheme.iconFg, fit: BoxFit.contain, ), @@ -1563,53 +1598,53 @@ class _MedicalFilePageState extends State { ], ), SizedBox(height: 14.h), - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), - decoration: BoxDecoration( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bgScaffoldColor, - borderRadius: BorderRadius.circular(10.r), + borderRadius: 10.r, + hasShadow: false, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - value.toText17( - isBold: true, - color: AppColors.textColor, - ), - if (unit.isNotEmpty) ...[ - SizedBox(width: 3.w), - unit.toText12( - color: AppColors.textColor, - fontWeight: FontWeight.w500, + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: value.toText17( + isBold: true, + color: AppColors.textColor, + ), ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], ], - ], + ), ), - if (status != null) + if (status != null) ...[ + SizedBox(width: 4.w), AppCustomChipWidget( labelText: status, backgroundColor: scheme.chipBg, textColor: scheme.chipFg, - ) + ), + ] else - const SizedBox.shrink(), - ], - ), - ), - SizedBox(height: 8.h), + AppCustomChipWidget( + labelText: "", + backgroundColor: AppColors.bgScaffoldColor, + textColor:null, + ) - Align( - alignment: AlignmentDirectional.centerEnd, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - width: 18.w, - height: 18.h, - iconColor: AppColors.textColorLight, - fit: BoxFit.contain, + ], ), ), ], @@ -1619,6 +1654,3 @@ class _MedicalFilePageState extends State { ); } } - - - diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart new file mode 100644 index 00000000..d1a4d0ce --- /dev/null +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -0,0 +1,310 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/monthly_reports_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_reports/user_agreement_page.dart'; +import 'package:provider/provider.dart'; + +import '../../generated/locale_keys.g.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/input_widget.dart'; +import '../../widgets/loader/bottomsheet_loader.dart'; + +class MonthlyReportsPage extends StatefulWidget { + const MonthlyReportsPage({super.key}); + + @override + State createState() => _MonthlyReportsPageState(); +} + +class _MonthlyReportsPageState extends State { + bool isHealthSummaryEnabled = false; + bool isTermsAccepted = false; + + final TextEditingController emailController = TextEditingController(); + + @override + void dispose() { + emailController.dispose(); + super.dispose(); + } + + void _showError(String message) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + + void _showSuccessSnackBar() { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Successfully updated".needTranslation, + style: const TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.w600, + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: AppColors.textGreenColor, + duration: const Duration(seconds: 2), + ), + ); + } + + Future _onSavePressed() async { + if (!isTermsAccepted) { + _showError("Please accept the terms and conditions".needTranslation); + return; + } + + final email = emailController.text.trim(); + if (email.isEmpty) { + _showError("Please enter your email".needTranslation); + return; + } + + final vm = context.read(); + + // LoaderBottomSheet.showLoader(); + final ok = await vm.saveMonthlyReport(email: email); + // LoaderBottomSheet.hideLoader(); + + if (ok) { + setState(() => isHealthSummaryEnabled = true); + _showSuccessSnackBar(); + } else { + // _showError("Failed to update".needTranslation); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Monthly Reports".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + height: 54.h, + alignment: Alignment.center, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: (12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Patient Health Summary Report".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + _buildToggle(), + ], + ), + ), + + SizedBox(height: 16.h), + + TextInputWidget( + controller: emailController, + labelText: "Eamil*".needTranslation, + hintText: "email@email.com", + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + keyboardType: TextInputType.emailAddress, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + onChange: (value) { + setState(() {}); + }, + ).paddingOnly(top: 8.h, bottom: 8.h), + + Row( + children: [ + Text( + "To View The Terms and Conditions".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + InkWell( + child: Text( + "Click here".needTranslation, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const UserAgreementPage(), + ), + ); + }, + ), + ], + ), + + SizedBox(height: 12.h), + + GestureDetector( + onTap: () => setState(() => isTermsAccepted = !isTermsAccepted), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 24.h, + width: 24.h, + decoration: BoxDecoration( + color: isTermsAccepted + ? AppColors.textGreenColor + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isTermsAccepted + ? AppColors.lightGreenColor + : AppColors.greyColor, + width: 2.h, + ), + ), + child: isTermsAccepted + ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor,) + : null, + ), + SizedBox(width: 12.h), + Text( + "I agree to the terms and conditions".needTranslation, + style: context.dynamicTextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ], + ), + ), + + SizedBox(height: 12.h), + + Text( + "This monthly Health Summary Report reflects the health indicators and analysis results of the latest visits. Please note that this will be sent automatically from the system and it's not considered as an official report so no medical decisions should be taken based on it" + .needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 10.f, + fontWeight: FontWeight.w600, + ), + ), + + SizedBox(height: 12.h), + + Image.asset('assets/images/jpg/report.jpg'), + + SizedBox(height: 16.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _onSavePressed, + child: Text( + LocaleKeys.save.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.f, + ), + ), + ), + ), + ], + ), + ], + ), + ).paddingAll(16), + ); + } + + Widget _buildToggle() { + final value = isHealthSummaryEnabled; + + return AbsorbPointer( + absorbing: true, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: value + ? AppColors.lightGreenColor + : AppColors.greyColor.withOpacity(0.3), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: value + ? AppColors.textGreenColor + : AppColors.greyTextColor, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/monthly_reports/user_agreement_page.dart b/lib/presentation/monthly_reports/user_agreement_page.dart new file mode 100644 index 00000000..73ea5643 --- /dev/null +++ b/lib/presentation/monthly_reports/user_agreement_page.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/terms_conditions_view_model.dart'; +import 'package:provider/provider.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; + +class UserAgreementPage extends StatefulWidget { + const UserAgreementPage({super.key}); + + @override + State createState() => _UserAgreementPageState(); +} + +class _UserAgreementPageState extends State { + late final WebViewController _webViewController; + bool _isLoading = true; + String? _errorMessage; + + @override + void initState() { + super.initState(); + + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onPageStarted: (_) { + setState(() { + _isLoading = true; + }); + }, + onPageFinished: (_) { + setState(() { + _isLoading = false; + }); + }, + onWebResourceError: (error) { + }, + ), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final vm = + Provider.of(context, listen: false); + + vm.getTermsConditions( + onSuccess: () { + final htmlString = vm.termsConditionsHtml ?? ''; + + if (htmlString.isNotEmpty) { + setState(() { + _errorMessage = null; + _isLoading = true; + }); + _webViewController.loadHtmlString(htmlString); + } else { + setState(() { + _isLoading = false; + _errorMessage = 'لا توجد شروط متاحة حالياً'.needTranslation; + }); + } + }, + onError: (msg) { + setState(() { + _isLoading = false; + _errorMessage = msg; + }); + }, + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Stack( + children: [ + WebViewWidget(controller: _webViewController), + + if (_errorMessage != null) + Center( + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _errorMessage!, + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart new file mode 100644 index 00000000..cd1e8bcb --- /dev/null +++ b/lib/presentation/parking/paking_page.dart @@ -0,0 +1,147 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/parking/parking_slot.dart'; +import 'package:provider/provider.dart'; + +import '../../features/qr_parking/qr_parking_view_model.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/routes/custom_page_route.dart'; + + +class ParkingPage extends StatefulWidget { + const ParkingPage({super.key}); + + @override + State createState() => _ParkingPageState(); +} + +class _ParkingPageState extends State { + Future _readQR(BuildContext context) async { + final vm = context.read(); + + final model = await vm.scanAndGetParking(); + + if (model == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(vm.error ?? "Invalid Qr Code")), + ); + return; + } + + Navigator.of(context).push( + CustomPageRoute( + page: ParkingSlot(model: model), + ), + ); + } + + @override + Widget build(BuildContext context) { + final vm = context.watch(); // عشان loading + + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Parking".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Text( + "Dr. Sulaiman Al Habib hospital are conduction a test for the emerging corona" + " virus and issuing travel certificates 24/7 in a short time and with high accuracy." + " Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches " + "to conduct a corona test within few minutes. Dr. Sulaiman Al Habib hospital are conduction" + " a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. " + "Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes.", + style: TextStyle( + color: AppColors.textColor, + fontSize: 12, + height: 1.4, + fontWeight: FontWeight.w500, + ), + ), + ), + ).paddingOnly(top: 16, bottom: 16), + ], + ), + ), + ), + + /// Bottom button + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryRedColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: vm.isLoading ? null : () => _readQR(context), + child: vm.isLoading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + "Read Barcodes", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} + diff --git a/lib/presentation/parking/parking_slot.dart b/lib/presentation/parking/parking_slot.dart new file mode 100644 index 00000000..013bb6f2 --- /dev/null +++ b/lib/presentation/parking/parking_slot.dart @@ -0,0 +1,239 @@ + + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; + +import '../../features/qr_parking/qr_parking_view_model.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/chip/app_custom_chip_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:provider/provider.dart'; + + +class ParkingSlot extends StatefulWidget { + final QrParkingResponseModel model; + + const ParkingSlot({ + super.key, + required this.model, + }); + + @override + State createState() => _ParkingSlotState(); +} + +class _ParkingSlotState extends State { + + void _openDirection() { + final lat = widget.model.latitude; + final lng = widget.model.longitude; + + final valid = lat != null && + lng != null && + !(lat == 0.0 && lng == 0.0) && + lat >= -90 && lat <= 90 && + lng >= -180 && lng <= 180; + + if (!valid) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Parking location not available")), + ); + return; + } + + MapsLauncher.launchCoordinates(lat, lng); + } + + Future _resetDirection() async { + final vm = context.read(); + await vm.clearParking(); + Navigator.of(context).popUntil((route) => route.isFirst); + } + + DateTime? _parseDotNetDate(String? value) { + if (value == null || value.isEmpty) return null; + + final regExp = RegExp(r'Date\((\d+)([+-]\d+)?\)'); + final match = regExp.firstMatch(value); + if (match == null) return null; + + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds == null) return null; + + return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true) + .toLocal(); + } + + + String _formatPrettyDate(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + + final day = date.day; + final month = months[date.month - 1]; + final year = date.year; + + return "$day $month $year"; + } + + + String _formatPrettyTime(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + int hour = date.hour; + final minute = date.minute.toString().padLeft(2, '0'); + + final isPM = hour >= 12; + final period = isPM ? 'PM' : 'AM'; + + hour = hour % 12; + if (hour == 0) hour = 12; + + return "${hour.toString().padLeft(2, '0')}:$minute $period"; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: LayoutBuilder( + builder: (context, constraints) { + final maxW = constraints.maxWidth; + final contentW = maxW > 600 ? 600.0 : maxW; + + return Align( + alignment: Alignment.topCenter, + child: SizedBox( + width: contentW, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + + Container( + width: double.infinity, + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Parking Slot Details".needTranslation, + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 16.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget( + labelText: + "Slot: ${widget.model.qRParkingCode ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Basement: ${widget.model.floorDescription ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Date: ${_formatPrettyDate(widget.model.createdOn)}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Parked Since: ${_formatPrettyTime(widget.model.createdOn)}" + .needTranslation, + ), + ], + ), + ], + ), + ), + ), + + SizedBox(height: 24.h), + + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryRedColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: _openDirection, + child: Text( + "Get Direction".needTranslation, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.whiteColor, + ), + ), + ), + ), + + // const Spacer(), + // SizedBox( + // width: double.infinity, + // height: 48.h, + // child: OutlinedButton( + // style: OutlinedButton.styleFrom( + // side: BorderSide(color: AppColors.primaryRedColor), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // ), + // onPressed: _resetDirection, + // child: Text( + // "Reset Direction".needTranslation, + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.w600, + // color: AppColors.primaryRedColor, + // ), + // ), + // ), + // ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} + + diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index c23b956f..1786dece 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -10,9 +10,11 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; class OrganSelectorPage extends StatefulWidget { @@ -24,25 +26,38 @@ class OrganSelectorPage extends StatefulWidget { class _OrganSelectorPageState extends State { late final AppState _appState; + late final DialogService dialogService; @override void initState() { super.initState(); _appState = getIt.get(); + dialogService = getIt(); } - void _onNextPressed(SymptomsCheckerViewModel viewModel) { + void _onNextPressed(SymptomsCheckerViewModel viewModel) async { if (!viewModel.validateSelection()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one organ'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one organ'.needTranslation, ); return; } + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + + final String userName = 'guest_user'; + final String password = '123456'; - context.navigateWithName(AppRoutes.symptomsSelectorScreen); + await viewModel.getSymptomsUserDetails( + userName: userName, + password: password, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + context.navigateWithName(AppRoutes.symptomsSelectorPage); + }, + onError: (String error) { + LoaderBottomSheet.hideLoader(); + }, + ); } @override diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 2c995156..a63d1e2d 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/models/conditions_ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -18,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( @@ -44,7 +45,7 @@ class PossibleConditionsScreen extends StatelessWidget { ); } - Widget _buildPredictionsList(List conditions) { + Widget _buildPredictionsList(BuildContext context, List conditions) { if (conditions.isEmpty) { return Center( child: Padding( @@ -60,6 +61,8 @@ class PossibleConditionsScreen extends StatelessWidget { ); } + final dialogService = getIt(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -82,11 +85,8 @@ class PossibleConditionsScreen extends StatelessWidget { description: conditionModel.description, possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum, onActionPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('We are not available for a week. May you Rest In Peace :('), - backgroundColor: AppColors.primaryRedColor, - ), + dialogService.showErrorBottomSheet( + message: 'We are not available for a week. May you Rest In Peace :(', ); }, ); @@ -168,7 +168,7 @@ class PossibleConditionsScreen extends StatelessWidget { if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) { return _buildLoadingShimmer(); } - return _buildPredictionsList(dummyConditions); + return _buildPredictionsList(context, dummyConditions); }, ), ), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index d4ff2cb5..8669c3ca 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -2,11 +2,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -20,9 +22,12 @@ class RiskFactorsScreen extends StatefulWidget { } class _RiskFactorsScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Fetch risk factors based on selected symptoms WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -36,13 +41,10 @@ class _RiskFactorsScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedRiskFactors) { - context.navigateWithName(AppRoutes.suggestionsScreen); + context.navigateWithName(AppRoutes.suggestionsPage); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one risk before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one risk before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index f2aa71ec..d0d5b2f4 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -19,9 +21,12 @@ class SuggestionsScreen extends StatefulWidget { } class _SuggestionsScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -38,13 +43,10 @@ class _SuggestionsScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedSuggestions) { // Navigate to triage screen - context.navigateWithName(AppRoutes.triageScreen); + context.navigateWithName(AppRoutes.triagePage); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one option before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 522c5f82..d6036c62 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -8,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -15,17 +17,20 @@ 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 void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -36,13 +41,10 @@ class _SymptomsSelectorScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedSymptoms) { // Navigate to triage screen - context.navigateWithName(AppRoutes.riskFactorsScreen); + context.navigateWithName(AppRoutes.riskFactorsPage); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one symptom before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one symptom before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index ff0482e1..9d5d884b 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -1,89 +1,213 @@ +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart'; -import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package: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 { - late List triageQuestions; - int currentQuestionIndex = 0; +class _TriagePageState extends State { + late SymptomsCheckerViewModel viewModel; + late DialogService dialogService; @override void initState() { super.initState(); - triageQuestions = TriageQuestionsData.getSampleTriageQuestions(); + viewModel = context.read(); + dialogService = getIt.get(); + + // Start triage process when screen loads + WidgetsBinding.instance.addPostFrameCallback((_) { + _startTriage(); + }); } - TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex]; + void _startTriage() { + viewModel.startOrContinueTriage( + onSuccess: () { + _handleTriageResponse(); + }, + onError: (error) { + dialogService.showErrorBottomSheet( + message: error, + onOkPressed: () => context.pop(), + ); + }, + ); + } - bool get isFirstQuestion => currentQuestionIndex == 0; + void _handleTriageResponse() { + // Case 1: Emergency evidence detected + if (viewModel.hasEmergencyEvidence) { + _showEmergencyDialog(); + return; + } - bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1; + // Get the highest probability condition + final conditions = viewModel.currentConditions ?? []; + double highestProbability = 0.0; - void _onOptionSelected(int optionIndex) { - setState(() { - currentQuestion.selectOption(optionIndex); - }); + if (conditions.isNotEmpty) { + final sortedConditions = List.from(conditions); + sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); + highestProbability = (sortedConditions.first.probability ?? 0.0) * 100; + } + + // Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered + if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 7) { + // Navigate to results/possible conditions screen + context.navigateWithName(AppRoutes.possibleConditionsPage); + return; + } + + // Continue triage - question is loaded, reset selection for new question + viewModel.resetTriageChoice(); + } + + void _showEmergencyDialog() { + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.primaryRedColor, + borderRadius: 24.h, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: AppColors.whiteColor, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + "Emergency".needTranslation.toText28(color: AppColors.whiteColor, isBold: true), + SizedBox(height: 8.h), + "Emergency evidence detected. Please seek medical attention." + .needTranslation + .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async => Navigator.of(context).pop(), + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.whiteColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), + ), + ), + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () {}, + ); + } + + bool get isFirstQuestion => viewModel.getTriageEvidence().isEmpty; + + void _onOptionSelectedForItem(String itemId, int choiceIndex) { + viewModel.selectTriageChoiceForItem(itemId, choiceIndex); } void _onPreviousPressed() { - if (!isFirstQuestion) { - setState(() { - currentQuestionIndex--; - }); - } + context.pop(); } void _onNextPressed() { - if (currentQuestion.isAnswered) { - currentQuestion.confirmSelection(); - if (isLastQuestion) { - context.navigateWithName(AppRoutes.possibleConditionsScreen); - } else { - setState(() { - currentQuestionIndex++; - }); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select an option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + final currentQuestion = viewModel.currentTriageQuestion; + if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) { + dialogService.showErrorBottomSheet( + message: 'No question items available'.needTranslation, ); + return; } - } - _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { - return showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: "Are you sure you want to restart the organ selection?".needTranslation, - isShowActionButtons: true, - onCancelTap: () => Navigator.pop(context), - onConfirmTap: () => onConfirm(), - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, + // Check if all items have been answered + if (!viewModel.areAllTriageItemsAnswered) { + dialogService.showErrorBottomSheet(message: 'Please answer all questions before proceeding'.needTranslation); + return; + } + + // Collect all evidence from all items + for (var item in currentQuestion.items!) { + final itemId = item.id ?? ""; + if (itemId.isEmpty) continue; + + final selectedChoiceIndex = viewModel.getTriageChoiceForItem(itemId); + if (selectedChoiceIndex == null) continue; + + if (item.choices != null && selectedChoiceIndex < item.choices!.length) { + final selectedChoice = item.choices![selectedChoiceIndex]; + final choiceId = selectedChoice.id ?? ""; + + if (choiceId.isNotEmpty) { + viewModel.addTriageEvidence(itemId, choiceId); + } + } + } + + // Get all evidence: initial symptoms + risk factors + suggestions + triage evidence + List initialEvidenceIds = viewModel.getAllEvidenceIds(); + List> triageEvidence = viewModel.getTriageEvidence(); + + log("initialEvidenceIds: ${initialEvidenceIds.toString()}"); + log("triageEvidence: ${triageEvidence.toString()}"); + + // Call API with updated evidence + viewModel.getDiagnosisForTriage( + age: viewModel.selectedAge!, + sex: viewModel.selectedGender!.toLowerCase(), + evidenceIds: initialEvidenceIds, + triageEvidence: triageEvidence, + language: viewModel.appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + _handleTriageResponse(); + }, + onError: (error) { + dialogService.showErrorBottomSheet(message: error); + }, ); } @@ -91,45 +215,137 @@ class _TriageScreenState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: "Triage".needTranslation, - // onLeadingTapped: () => _buildConfirmationBottomSheet( - // context: context, - // onConfirm: () => { - // context.pop(), - // context.pop(), - // }), - - leadingCallback: () => context.pop(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - _buildQuestionCard(), - ], + body: Consumer( + builder: (context, viewModel, child) { + // Show normal question UI + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Triage".needTranslation, + leadingCallback: () => _showConfirmationBeforeExit(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildQuestionCard(viewModel), + ], + ), + ), ), + _buildStickyBottomCard(context, viewModel), + ], + ); + }, + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(1, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Suggestion', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); + }), ), - _buildStickyBottomCard(context), ], ), ); } - Widget _buildQuestionCard() { + void _showConfirmationBeforeExit(BuildContext context) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to exit? Your progress will be lost.".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () { + Navigator.pop(context); + context.pop(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + Widget _buildQuestionCard(SymptomsCheckerViewModel viewModel) { + if (viewModel.isTriageDiagnosisLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.currentTriageQuestion == null) { + return Center( + child: "No question available".needTranslation.toText16(weight: FontWeight.w500), + ); + } + + final question = viewModel.currentTriageQuestion; + if (question == null || question.items == null || question.items!.isEmpty) { + return SizedBox.shrink(); + } + return AnimatedSwitcher( duration: const Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { final offsetAnimation = Tween( begin: const Offset(1.0, 0.0), end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeInOut, - )); + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + ), + ); return SlideTransition( position: offsetAnimation, @@ -140,7 +356,7 @@ class _TriageScreenState extends State { ); }, child: Container( - key: ValueKey(currentQuestionIndex), + key: ValueKey(question.items!.first.id ?? viewModel.getTriageEvidence().length.toString()), width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 24.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -148,11 +364,36 @@ class _TriageScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - currentQuestion.question.toText16(weight: FontWeight.w500), + // Main question text + (question.text ?? "").toText16(weight: FontWeight.w600, color: AppColors.textColor), SizedBox(height: 24.h), - ...List.generate(currentQuestion.options.length, (index) { - bool selected = currentQuestion.selectedOptionIndex == index; - return _buildOptionItem(index, selected, currentQuestion.options[index].text); + + // Show all items with dividers + ...List.generate(question.items!.length, (itemIndex) { + final item = question.items![itemIndex]; + final itemId = item.id ?? ""; + final choices = item.choices ?? []; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Item name (sub-question) + (item.name ?? "").toText14(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 8.h), + // Choices for this item + ...List.generate(choices.length, (choiceIndex) { + bool selected = viewModel.getTriageChoiceForItem(itemId) == choiceIndex; + return _buildOptionItem(itemId, choiceIndex, selected, choices[choiceIndex].label ?? ""); + }), + + // Add divider between items (but not after the last one) + if (itemIndex < question.items!.length - 1) ...[ + SizedBox(height: 8.h), + Divider(color: AppColors.bottomNAVBorder, thickness: 1), + SizedBox(height: 10.h), + ], + ], + ); }), ], ), @@ -160,9 +401,9 @@ class _TriageScreenState extends State { ); } - Widget _buildOptionItem(int index, bool selected, String optionText) { + Widget _buildOptionItem(String itemId, int choiceIndex, bool selected, String optionText) { return GestureDetector( - onTap: () => _onOptionSelected(index), + onTap: () => _onOptionSelectedForItem(itemId, choiceIndex), child: Container( margin: EdgeInsets.only(bottom: 12.h), child: Row( @@ -181,16 +422,29 @@ class _TriageScreenState extends State { child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), - Expanded(child: optionText.toText14(weight: FontWeight.w500)), + Expanded(child: optionText.toText13(weight: FontWeight.w500)), ], ), ), ); } - Widget _buildStickyBottomCard(BuildContext context) { - final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions); - final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore); + Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { + // Get the top condition with highest probability + final conditions = viewModel.currentConditions ?? []; + String suggestedCondition = "Analyzing..."; + double probability = 0.0; + + if (conditions.isNotEmpty) { + // Sort by probability descending + final sortedConditions = List.from(conditions); + sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); + + final topCondition = sortedConditions.first; + suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown"; + probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage + } + // final bool isHighConfidence = probability >= 70.0; return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -223,14 +477,14 @@ class _TriageScreenState extends State { ), SizedBox(height: 16.h), CustomRoundedProgressBar( - percentage: currentScore, + percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( - text: "$currentScore% ", + text: "${probability.toStringAsFixed(1)}% ", style: TextStyle( color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, @@ -249,6 +503,8 @@ class _TriageScreenState extends State { ), ), ), + // Show high confidence message + SizedBox(height: 12.h), Row( children: [ @@ -256,7 +512,7 @@ class _TriageScreenState extends State { child: CustomButton( text: "Previous".needTranslation, onPressed: isFirstQuestion ? () {} : _onPreviousPressed, - isDisabled: isFirstQuestion, + isDisabled: isFirstQuestion || viewModel.isTriageDiagnosisLoading, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, @@ -266,7 +522,8 @@ class _TriageScreenState extends State { SizedBox(width: 12.w), Expanded( child: CustomButton( - text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation, + text: "Next".needTranslation, + isDisabled: viewModel.isTriageDiagnosisLoading, onPressed: _onNextPressed, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index fd003080..b4384203 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -90,8 +91,8 @@ class _UserInfoSelectionScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - title.toText16(weight: FontWeight.w500), - subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + title.toText14(weight: FontWeight.w500), + subTitle.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), ], ), ], @@ -106,7 +107,7 @@ class _UserInfoSelectionScreenState extends State { Widget _getDivider() { return Divider( color: AppColors.dividerColor, - ).paddingSymmetrical(0, 16.h); + ).paddingSymmetrical(0, 8.h); } @override @@ -114,8 +115,25 @@ class _UserInfoSelectionScreenState extends State { AppState appState = getIt.get(); String name = ""; + int? userAgeFromDOB; if (appState.isAuthenticated) { - name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} "; + final user = appState.getAuthenticatedUser(); + name = "${user!.firstName!} ${user.lastName!} "; + + // Calculate age from authenticated user's DOB if available + if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { + try { + DateTime dob = DateUtil.convertStringToDate(user.dateofBirth!); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || (now.month == dob.month && now.day < dob.day)) { + age--; + } + userAgeFromDOB = age; + } catch (e) { + // If date parsing fails, ignore + } + } } else { name = "Guest"; } @@ -132,8 +150,9 @@ class _UserInfoSelectionScreenState extends State { // Get display values String genderText = viewModel.selectedGender ?? "Not set"; - // Show age calculated from DOB, not the DOB itself - String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set"; + // Show age calculated from DOB (prefer viewModel's age, fallback to calculated from user's DOB) + int? displayAge = viewModel.selectedAge ?? userAgeFromDOB; + String ageText = displayAge != null ? "$displayAge Years" : "Not set"; String heightText = viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set"; String weightText = @@ -144,7 +163,7 @@ class _UserInfoSelectionScreenState extends State { Expanded( child: CollapsingListView( title: "Symptoms Checker".needTranslation, - isLeading: Navigator.canPop(context), + isLeading: true, child: SingleChildScrollView( child: Column( children: [ @@ -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, @@ -228,48 +247,46 @@ class _UserInfoSelectionScreenState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), child: SafeArea( top: false, - child: Padding( - padding: EdgeInsets.all(8.h), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: CustomButton( - text: "No, Edit all".needTranslation, - icon: AppAssets.edit_icon, - iconColor: AppColors.primaryRedColor, - onPressed: () { - context.read().setUserInfoPage(0, isSinglePageEdit: false); - context.navigateWithName(AppRoutes.userInfoFlowManager); - }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), - borderColor: Colors.transparent, - textColor: AppColors.primaryRedColor, - fontSize: 16.f, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "No, Edit all".needTranslation, + icon: AppAssets.edit_icon, + iconColor: AppColors.primaryRedColor, + onPressed: () { + context.read().setUserInfoPage(0, isSinglePageEdit: false); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, ), - SizedBox(width: 12.w), - Expanded( - child: CustomButton( - text: "Yes, It is".needTranslation, - icon: AppAssets.tickIcon, - 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, - fontSize: 16.f, - ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Yes, It is".needTranslation, + icon: AppAssets.tickIcon, + 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, + fontSize: 16.f, ), - ], - ), - ], - ).paddingSymmetrical(24.w, 0), - ), + ), + ], + ), + ], + ).paddingSymmetrical(24.w, 0), ), ); } diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart index d73f3877..8366545e 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -35,7 +33,6 @@ class AgeSelectionPage extends StatelessWidget { initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1), onDateChanged: (date) { symptomsViewModel.setDateOfBirth(date); - log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}'); }, ) ], diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart new file mode 100644 index 00000000..f75a71bc --- /dev/null +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -0,0 +1,652 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:provider/provider.dart'; + +/// Which vital sign is being shown in the details screen. +enum VitalSignMetric { + bmi, + height, + weight, + bloodPressure, + temperature, + heartRate, + respiratoryRate, +} + +class VitalSignDetailsArgs { + final VitalSignMetric metric; + final String title; + final String icon; + final String unit; + + /// Optional bounds used for graph shading and labels. + final double? low; + final double? high; + + const VitalSignDetailsArgs({ + required this.metric, + required this.title, + required this.icon, + required this.unit, + this.low, + this.high, + }); +} + +class VitalSignDetailsPage extends StatefulWidget { + final VitalSignDetailsArgs args; + + const VitalSignDetailsPage({super.key, required this.args}); + + @override + State createState() => _VitalSignDetailsPageState(); +} + +class _VitalSignDetailsPageState extends State { + bool _isGraphVisible = true; + + VitalSignDetailsArgs get args => widget.args; + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: 'Vital Sign Details'.needTranslation, + child: Consumer( + builder: (context, viewModel, child) { + final latest = viewModel.vitalSignList.isNotEmpty ? viewModel.vitalSignList.first : null; + + final history = _buildSeries(viewModel.vitalSignList, args); + final latestValueText = _latestValueText(latest); + final status = _statusForLatest(latest); + final scheme = VitalSignUiModel.scheme(status: status, label: args.title); + + return SingleChildScrollView( + child: Column( + spacing: 16.h, + children: [ + _headerCard( + context, + title: args.title, + icon: args.icon, + valueText: latestValueText, + status: status, + scheme: scheme, + latestDate: latest?.vitalSignDate, + ), + _whatIsThisResultCard(context), + _historyCard(context, history: history), + ], + ).paddingAll(24.h), + ); + }, + ), + ); + } + + Widget _headerCard( + BuildContext context, { + required String title, + required String icon, + required String valueText, + required String? status, + required VitalSignUiModel scheme, + required DateTime? latestDate, + }) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -1), + + + ], + ), + SizedBox(height: 8.h), + (latestDate != null + ? ('Result of ${latestDate.toString().split(' ').first}'.needTranslation) + : ('Result of --'.needTranslation)) + .toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: valueText.toText28( + isBold: true, + color: scheme.iconFg, + letterSpacing: -2, + ), + ), + SizedBox(width: 4.h), + if (status != null) + Column( + spacing: 6.h, + children: [ + status.toText10(weight: FontWeight.w500, color: AppColors.greyTextColor), + Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: scheme.iconFg, + ), + ], + ), + ], + ), + ), + ], + ), + if (args.low != null || args.high != null) + Text( + _referenceText(context), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.greyTextColor, + ), + softWrap: true, + ), + ], + ), + ); + } + + String _referenceText(BuildContext context) { + if (args.low != null && args.high != null) { + return 'Reference range: ${args.low} – ${args.high} ${args.unit}'.needTranslation; + } + if (args.low != null) { + return 'Reference range: ≥ ${args.low} ${args.unit}'.needTranslation; + } + if (args.high != null) { + return 'Reference range: ≤ ${args.high} ${args.unit}'.needTranslation; + } + return ''; + } + + Widget _whatIsThisResultCard(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + 'What is this result?'.needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor), + _descriptionText(context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + ], + ), + ); + } + + Widget _historyCard(BuildContext context, {required List history}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + height: _isGraphVisible + ? 260.h + : (history.length < 3) + ? (history.length * 64) + 80.h + : 260.h, + padding: EdgeInsets.all(15.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _isGraphVisible ? 'History flowchart'.needTranslation : 'History'.needTranslation, + style: TextStyle( + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 24.h, + height: 24.h, + alignment: Alignment.center, + child: InkWell( + onTap: () { + setState(() { + _isGraphVisible = !_isGraphVisible; + }); + }, + child: Utils.buildSvgWithAssets( + icon: _isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph, + width: 24.h, + height: 24.h, + ), + ), + ), + // SizedBox(width: 16.h), + // Container( + // width: 24.h, + // height: 24.h, + // alignment: Alignment.center, + // child: Utils.buildSvgWithAssets( + // icon: AppAssets.ic_date_filter, + // width: 24.h, + // height: 24.h, + // ), + // ), + ], + ), + ], + ).paddingOnly(bottom: _isGraphVisible ? 16.h : 24.h), + + if (history.isEmpty) + Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) + else if (_isGraphVisible) + _buildHistoryGraph(history) + else + _buildHistoryList(context, history), + ], + ), + ); + } + + Widget _buildHistoryGraph(List history) { + final minY = _minY(history); + final maxY = _maxY(history); + final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); + + return CustomGraph( + dataPoints: history, + makeGraphBasedOnActualValue: true, + leftLabelReservedSize: 40, + showGridLines: true, + showShadow: true, + leftLabelInterval: _leftInterval(history), + maxY: maxY, + minY: minY, + maxX: history.length.toDouble() - .75, + horizontalInterval: _leftInterval(history), + leftLabelFormatter: (value) { + // Show labels at interval points + if (args.high != null && (value - args.high!).abs() < 0.1) { + return _axisLabel('High'); + } + if (args.low != null && (value - args.low!).abs() < 0.1) { + return _axisLabel('Low'); + } + // Show numeric labels at regular intervals + return _axisLabel(value.toStringAsFixed(0)); + }, + getDrawingHorizontalLine: (value) { + // Draw reference lines for high/low bounds + if (args.high != null && (value - args.high!).abs() < 0.1) { + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.2), + strokeWidth: 1, + dashArray: [5, 5], + ); + } + if (args.low != null && (value - args.low!).abs() < 0.1) { + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.2), + strokeWidth: 1, + dashArray: [5, 5], + ); + } + // Draw grid lines at intervals + return FlLine( + color: AppColors.bgGreenColor.withOpacity(0.2), + strokeWidth: 1, + dashArray: [5, 5], + ); + }, + graphColor: AppColors.bgGreenColor, + graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), + graphGridColor: scheme.iconFg, + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return const SizedBox.shrink(); + if (value == 0) return _bottomLabel(data[value.toInt()].label); + if (value == data.length - 1) return _bottomLabel(data[value.toInt()].label, isLast: true); + if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); + return const SizedBox.shrink(); + }, + rangeAnnotations: _rangeAnnotations(history), + minX: (history.length == 1) ? null : -.2, + scrollDirection: Axis.horizontal, + height: 180.h, + ); + } + + + Widget _buildHistoryList(BuildContext context, List history) { + final items = history.reversed.toList(); + final height = items.length < 3 ? items.length * 64.0 : 180.h; + return SizedBox( + height: height, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: items.length, + separatorBuilder: (_, __) => Divider( + color: AppColors.borderOnlyColor.withValues(alpha: 0.1), + height: 1, + ), + itemBuilder: (context, index) { + final dp = items[index]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + ('${dp.actualValue} ${dp.unitOfMeasurement ?? ''}').toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + ), + ], + ), + ); + }, + ), + ); + } + + double _minY(List points) { + // IMPORTANT: y-axis uses actual numeric values (from actualValue). + final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + final min = values.reduce((a, b) => a < b ? a : b); + final double boundLow = args.low ?? min; + return (min < boundLow ? min : boundLow) - 1; + } + + double _maxY(List points) { + // IMPORTANT: y-axis uses actual numeric values (from actualValue). + final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + final max = values.reduce((a, b) => a > b ? a : b); + final double boundHigh = args.high ?? max; + return (max > boundHigh ? max : boundHigh) + 1; + } + + double _leftInterval(List points) { + // Keep it stable; graph will mostly show just two labels. + final range = (_maxY(points) - _minY(points)).abs(); + if (range <= 0) return 1; + return (range / 4).clamp(1, 20); + } + + RangeAnnotations? _rangeAnnotations(List points) { + if (args.low == null && args.high == null) return null; + + final minY = _minY(points); + final maxY = _maxY(points); + + final List ranges = []; + + if (args.low != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: minY, + y2: args.low!, + color: AppColors.highAndLow.withOpacity(0.05), + ), + ); + } + + if (args.low != null && args.high != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: args.low!, + y2: args.high!, + color: AppColors.bgGreenColor.withOpacity(0.05), + ), + ); + } + + if (args.high != null) { + ranges.add( + HorizontalRangeAnnotation( + y1: args.high!, + y2: maxY, + color: AppColors.criticalLowAndHigh.withOpacity(0.05), + ), + ); + } + + return RangeAnnotations(horizontalRangeAnnotations: ranges); + } + + List _buildSeries(List vitals, VitalSignDetailsArgs args) { + final List points = []; + + // Build a chronological series (oldest -> newest), skipping null/zero values. + final sorted = List.from(vitals); + sorted.sort((a, b) { + final ad = a.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + final bd = b.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + return ad.compareTo(bd); + }); + + double? metricValue(VitalSignResModel v) { + switch (args.metric) { + case VitalSignMetric.bmi: + return _toDouble(v.bodyMassIndex); + case VitalSignMetric.height: + return _toDouble(v.heightCm); + case VitalSignMetric.weight: + return _toDouble(v.weightKg); + case VitalSignMetric.temperature: + return _toDouble(v.temperatureCelcius); + case VitalSignMetric.heartRate: + return _toDouble(v.heartRate ?? v.pulseBeatPerMinute); + case VitalSignMetric.respiratoryRate: + return _toDouble(v.respirationBeatPerMinute); + case VitalSignMetric.bloodPressure: + // Graph only systolic for now (simple single-series). + return _toDouble(v.bloodPressureHigher); + } + } + + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + double index = 0; + for (final v in sorted) { + final mv = metricValue(v); + if (mv == null) continue; + if (mv == 0) continue; + + final dt = v.vitalSignDate ?? DateTime.now(); + final label = '${monthNames[dt.month - 1]}, ${dt.year}'; + + points.add( + DataPoint( + value: index, + label: label, + actualValue: mv.toStringAsFixed(0), + time: dt, + displayTime: '${dt.day}/${dt.month}/${dt.year}', + unitOfMeasurement: args.unit, + ), + ); + index += 1; + } + + return points; + } + + double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is num) return v.toDouble(); + return double.tryParse(v.toString()); + } + + String _latestValueText(VitalSignResModel? latest) { + if (latest == null) return '--'; + + switch (args.metric) { + case VitalSignMetric.bmi: + final v = _toDouble(latest.bodyMassIndex); + return v == null ? '--' : v.toStringAsFixed(0); + case VitalSignMetric.height: + final v = _toDouble(latest.heightCm); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.weight: + final v = _toDouble(latest.weightKg); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.temperature: + final v = _toDouble(latest.temperatureCelcius); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.heartRate: + final v = _toDouble(latest.heartRate ?? latest.pulseBeatPerMinute); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.respiratoryRate: + final v = _toDouble(latest.respirationBeatPerMinute); + return v == null ? '--' : '${v.toStringAsFixed(0)} ${args.unit}'; + case VitalSignMetric.bloodPressure: + final s = _toDouble(latest.bloodPressureHigher); + final d = _toDouble(latest.bloodPressureLower); + if (s == null || d == null) return '--'; + return '${s.toStringAsFixed(0)}/${d.toStringAsFixed(0)}'; + } + } + + String? _statusForLatest(VitalSignResModel? latest) { + if (latest == null) return null; + + switch (args.metric) { + case VitalSignMetric.bmi: + return VitalSignUiModel.bmiStatus(latest.bodyMassIndex); + case VitalSignMetric.bloodPressure: + return VitalSignUiModel.bloodPressureStatus(systolic: latest.bloodPressureHigher, diastolic: latest.bloodPressureLower); + case VitalSignMetric.height: + return null; + case VitalSignMetric.weight: + return latest.weightKg != null ? 'Normal' : null; + case VitalSignMetric.temperature: + return null; + case VitalSignMetric.heartRate: + return (latest.heartRate ?? latest.pulseBeatPerMinute) != null ? 'Normal' : null; + case VitalSignMetric.respiratoryRate: + return latest.respirationBeatPerMinute != null ? 'Normal' : null; + } + } + + String _descriptionText(BuildContext context) { + switch (args.metric) { + case VitalSignMetric.bmi: + return 'BMI is a measurement based on height and weight that estimates body fat.'.needTranslation; + case VitalSignMetric.height: + return 'Height is measured in centimeters and is used to calculate BMI and dosage recommendations.'.needTranslation; + case VitalSignMetric.weight: + return 'Weight helps track overall health, nutrition, and changes over time.'.needTranslation; + case VitalSignMetric.bloodPressure: + return 'Blood pressure reflects the force of blood against artery walls. It is shown as systolic/diastolic.'.needTranslation; + case VitalSignMetric.temperature: + return 'Body temperature reflects how hot your body is and may change with infection or inflammation.'.needTranslation; + case VitalSignMetric.heartRate: + return 'Heart rate refers to the number of heart beats per minute.'.needTranslation; + case VitalSignMetric.respiratoryRate: + return 'Respiratory rate is the number of breaths taken per minute.'.needTranslation; + } + } + + String _nextStepsText(BuildContext context) { + switch (args.metric) { + case VitalSignMetric.bmi: + return 'Maintain a balanced diet and regular activity. If your BMI is high or low, consider consulting your doctor.'.needTranslation; + case VitalSignMetric.height: + return 'No action is needed unless your measurement looks incorrect. Update it during your next visit.'.needTranslation; + case VitalSignMetric.weight: + return 'Monitor weight changes. Sudden gain or loss may require medical advice.'.needTranslation; + case VitalSignMetric.bloodPressure: + return 'Keep tracking your blood pressure. High or low readings should be discussed with your doctor.'.needTranslation; + case VitalSignMetric.temperature: + return 'If you have a persistent fever or symptoms, contact your healthcare provider.'.needTranslation; + case VitalSignMetric.heartRate: + return 'Track your heart rate trends. If you feel dizziness or chest pain, seek medical care.'.needTranslation; + case VitalSignMetric.respiratoryRate: + return 'If you notice shortness of breath or abnormal breathing, seek medical advice.'.needTranslation; + } + } + + Widget _nextStepsCard(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 'What should I do next?'.needTranslation.toText16(weight: FontWeight.w600), + SizedBox(height: 8.h), + _nextStepsText(context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, maxLine: 10), + ], + ), + ); + } + + Widget _axisLabel(String value) { + return Text( + value, + style: TextStyle( + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + fontSize: 8.f, + color: AppColors.textColor, + ), + ); + } + + Widget _bottomLabel(String label, {bool isLast = false}) { + return Padding( + padding: EdgeInsets.only( + top: 8.0, + right: isLast ? 16.h : 0, + ), + child: label.toText8(fontWeight: FontWeight.w500), + ); + } + +} diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index bd25641a..fbf9fc6d 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_details_page.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; class VitalSignPage extends StatefulWidget { @@ -22,6 +24,13 @@ class VitalSignPage extends StatefulWidget { } class _VitalSignPageState extends State { + void _openDetails(VitalSignDetailsArgs args) { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignDetailsPage(args: args), + ), + ); + } @override void initState() { @@ -65,7 +74,14 @@ class _VitalSignPageState extends State { value: latestVitalSign?.bodyMassIndex?.toString() ?? '--', unit: '', status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex), - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.bmi, + title: 'BMI', + icon: AppAssets.bmiVital, + unit: '', + ), + ), ), SizedBox(height: 16.h), @@ -76,7 +92,14 @@ class _VitalSignPageState extends State { value: latestVitalSign?.heightCm?.toString() ?? '--', unit: 'cm', status: null, - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.height, + title: 'Height', + icon: AppAssets.heightVital, + unit: 'cm', + ), + ), ), SizedBox(height: 16.h), @@ -87,7 +110,14 @@ class _VitalSignPageState extends State { value: latestVitalSign?.weightKg?.toString() ?? '--', unit: 'kg', status: (latestVitalSign?.weightKg != null) ? 'Normal' : null, - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.weight, + title: 'Weight', + icon: AppAssets.weightVital, + unit: 'kg', + ), + ), ), SizedBox(height: 16.h), @@ -105,7 +135,16 @@ class _VitalSignPageState extends State { systolic: latestVitalSign?.bloodPressureHigher, diastolic: latestVitalSign?.bloodPressureLower, ), - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.bloodPressure, + title: 'Blood Pressure', + icon: AppAssets.bloodPressure, + unit: 'mmHg', + low: 90, + high: 140, + ), + ), ), SizedBox(height: 16.h), @@ -116,7 +155,16 @@ class _VitalSignPageState extends State { value: latestVitalSign?.temperatureCelcius?.toString() ?? '--', unit: '°C', status: null, - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.temperature, + title: 'Temperature', + icon: AppAssets.temperature, + unit: '°C', + low: 36.1, + high: 37.2, + ), + ), ), ], ), @@ -130,7 +178,7 @@ class _VitalSignPageState extends State { children: [ // Body anatomy image with Heart Rate card overlaid at bottom SizedBox( - height: 480.h, + height: 420.h, width: double.infinity, child: Stack( clipBehavior: Clip.none, @@ -148,7 +196,7 @@ class _VitalSignPageState extends State { Align( alignment: Alignment.bottomCenter, child: SizedBox( - height: 420.h, + height: 460.h, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), child: Container( @@ -182,13 +230,22 @@ class _VitalSignPageState extends State { value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--', unit: 'bpm', status: 'Normal', - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.heartRate, + title: 'Heart Rate', + icon: AppAssets.heart, + unit: 'bpm', + low: 60, + high: 100, + ), + ), ), ), ], ), ), - SizedBox(height: 12.h), + SizedBox(height: 12.h), // Respiratory rate Card _buildVitalSignCard( @@ -197,7 +254,16 @@ class _VitalSignPageState extends State { value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', unit: 'bpm', status: 'Normal', - onTap: () {}, + onTap: () => _openDetails( + const VitalSignDetailsArgs( + metric: VitalSignMetric.respiratoryRate, + title: 'Respiratory rate', + icon: AppAssets.respRate, + unit: 'bpm', + low: 12, + high: 20, + ), + ), ), ], ), @@ -242,15 +308,15 @@ class _VitalSignPageState extends State { Row( children: [ Container( - padding: EdgeInsets.all(10.h), + padding: EdgeInsets.all(8.h), decoration: BoxDecoration( color: scheme.iconBg, borderRadius: BorderRadius.circular(12.r), ), child: Utils.buildSvgWithAssets( icon: icon, - width: 20.w, - height: 20.h, + width: 22.w, + height: 22.h, iconColor: scheme.iconFg, fit: BoxFit.contain, ), @@ -266,10 +332,15 @@ class _VitalSignPageState extends State { ), SizedBox(height: 14.h), Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), - decoration: BoxDecoration( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h), + // decoration: BoxDecoration( + // color: AppColors.bgScaffoldColor, + // borderRadius: BorderRadius.circular(10.r), + // ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bgScaffoldColor, - borderRadius: BorderRadius.circular(10.r), + borderRadius: 10.r, + hasShadow: false, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -295,23 +366,24 @@ class _VitalSignPageState extends State { labelText: status, backgroundColor: scheme.chipBg, textColor: scheme.chipFg, + ) else const SizedBox.shrink(), ], ), ), - SizedBox(height: 8.h), - Align( - alignment: AlignmentDirectional.centerEnd, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - width: 18.w, - height: 18.h, - iconColor: AppColors.textColorLight, - fit: BoxFit.contain, - ), - ), + // SizedBox(height: 8.h), + // Align( + // alignment: AlignmentDirectional.centerEnd, + // child: Utils.buildSvgWithAssets( + // icon: AppAssets.arrow_forward, + // width: 18.w, + // height: 18.h, + // iconColor: AppColors.textColorLight, + // fit: BoxFit.contain, + // ), + // ), ], ), ), diff --git a/lib/presentation/water_monitor/water_consumption_screen.dart b/lib/presentation/water_monitor/water_consumption_page.dart similarity index 95% rename from lib/presentation/water_monitor/water_consumption_screen.dart rename to lib/presentation/water_monitor/water_consumption_page.dart index 5d08bcd6..2bd429c0 100644 --- a/lib/presentation/water_monitor/water_consumption_screen.dart +++ b/lib/presentation/water_monitor/water_consumption_page.dart @@ -21,14 +21,14 @@ import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; -class WaterConsumptionScreen extends StatefulWidget { - const WaterConsumptionScreen({super.key}); +class WaterConsumptionPage extends StatefulWidget { + const WaterConsumptionPage({super.key}); @override - State createState() => _WaterConsumptionScreenState(); + State createState() => _WaterConsumptionPageState(); } -class _WaterConsumptionScreenState extends State { +class _WaterConsumptionPageState extends State { @override void initState() { super.initState(); @@ -98,37 +98,14 @@ class _WaterConsumptionScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "History".needTranslation.toText16(isBold: true), Row( children: [ - InkWell( - onTap: () => viewModel.setGraphView(!viewModel.isGraphView), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: ScaleTransition( - scale: animation, - child: child, - ), - ); - }, - child: Container( - key: ValueKey(viewModel.isGraphView), - child: Utils.buildSvgWithAssets( - icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, - height: 24.h, - width: 24.h, - ), - ), - ), - ), + "History".needTranslation.toText16(isBold: true), SizedBox(width: 8.w), InkWell( onTap: () => _showHistoryDurationBottomsheet(context, viewModel), child: Container( - padding: EdgeInsets.symmetric(vertical: 6.h, horizontal: 6.h), + padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 6.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( backgroundColor: AppColors.greyColor, borderRadius: 8.r, @@ -137,13 +114,36 @@ class _WaterConsumptionScreenState extends State { child: Row( children: [ viewModel.selectedDurationFilter.toText12(fontWeight: FontWeight.w500), - Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 16.h), ], ), ), ) ], ), + InkWell( + onTap: () => viewModel.setGraphView(!viewModel.isGraphView), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(viewModel.isGraphView), + child: Utils.buildSvgWithAssets( + icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, + height: 24.h, + width: 24.h, + ), + ), + ), + ), ], ), if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else ...[SizedBox(height: 16.h), _buildHistoryGraph()] diff --git a/lib/presentation/water_monitor/water_monitor_settings_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_page.dart similarity index 92% rename from lib/presentation/water_monitor/water_monitor_settings_screen.dart rename to lib/presentation/water_monitor/water_monitor_settings_page.dart index 1b783ca7..302940c9 100644 --- a/lib/presentation/water_monitor/water_monitor_settings_screen.dart +++ b/lib/presentation/water_monitor/water_monitor_settings_page.dart @@ -10,16 +10,25 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; -class WaterMonitorSettingsScreen extends StatefulWidget { - 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 + void initState() { + super.initState(); + dialogService = getIt.get(); + } + // No need to call initialize() here since it's already called in water_consumption_screen // The ViewModel is shared via Provider, so data is already loaded @@ -59,8 +68,6 @@ class _WaterMonitorSettingsScreenState extends State required Function(String) onSelected, bool useUpperCase = false, }) { - final dialogService = getIt.get(); - dialogService.showFamilyBottomSheetWithoutHWithChild( label: title.needTranslation, message: "", @@ -263,9 +270,20 @@ class _WaterMonitorSettingsScreenState extends State onPressed: () async { final success = await viewModel.saveSettings(); if (!success && viewModel.validationError != null) { - _showSnackBar(context, viewModel.validationError!); + dialogService.showErrorBottomSheet( + message: viewModel.validationError!, + ); } else if (success) { - _showSnackBar(context, "Settings saved successfully"); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: "Settings saved successfully".needTranslation, + ), + callBackFunc: () {}, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); } }, borderRadius: 12.r, @@ -340,16 +358,4 @@ class _WaterMonitorSettingsScreenState extends State ), ); } - - // Show snackbar for validation errors and success messages - void _showSnackBar(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - duration: const Duration(seconds: 3), - behavior: SnackBarBehavior.floating, - backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor, - ), - ); - } } diff --git a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart index 6546f776..2359904b 100644 --- a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart +++ b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart @@ -18,23 +18,29 @@ class WaterActionButtonsWidget extends StatelessWidget { return Consumer(builder: (context, vm, _) { final cupAmount = vm.selectedCupCapacityMl; final isGoalAchieved = vm.progressPercent >= 100 || vm.nextDrinkTime.toLowerCase().contains('goal achieved'); + final isDisabled = vm.isLoading || isGoalAchieved; return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - InkWell( - onTap: () async { - if (cupAmount > 0) { - await vm.undoUserActivity(); - } - }, - child: Utils.buildSvgWithAssets( - icon: AppAssets.minimizeIcon, - height: 20.h, - width: 20.h, - iconColor: AppColors.textColor, + Opacity( + opacity: vm.isLoading ? 0.4 : 1.0, + child: InkWell( + onTap: vm.isLoading + ? null + : () async { + if (cupAmount > 0) { + await vm.undoUserActivity(); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.minimizeIcon, + height: 20.h, + width: 20.h, + iconColor: AppColors.textColor, + ), ), ), Container( @@ -51,9 +57,9 @@ class WaterActionButtonsWidget extends StatelessWidget { ), ), Opacity( - opacity: isGoalAchieved ? 0.4 : 1.0, + opacity: isDisabled ? 0.4 : 1.0, child: InkWell( - onTap: isGoalAchieved + onTap: isDisabled ? null : () async { if (cupAmount > 0) { @@ -88,7 +94,7 @@ class WaterActionButtonsWidget extends StatelessWidget { ), _buildActionButton( context: context, - onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen), + onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsPage), title: "Settings".needTranslation, icon: Icon( Icons.settings, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 78045b53..059969cd 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,21 @@ 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 '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/covid19test/covid19_landing_page.dart'; +import '../core/dependencies.dart'; +import '../features/monthly_reports/monthly_reports_repo.dart'; +import '../features/monthly_reports/monthly_reports_view_model.dart'; +import '../presentation/monthly_reports/monthly_reports_page.dart'; +import '../presentation/parking/paking_page.dart'; +import '../services/error_handler_service.dart'; +import 'package:provider/provider.dart'; + class AppRoutes { static const String initialRoute = '/initialRoute'; static const String loginScreen = '/loginScreen'; @@ -47,24 +60,30 @@ 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'; + static const String monthlyReports = '/monthlyReportsPage'; + static const String qrParking = '/qrParkingPage'; + + // Health Trackers + static const String healthTrackersPage = '/healthTrackersListScreen'; + static const String addHealthTrackerEntryPage = '/addHealthTrackerEntryPage'; + static const String healthTrackerDetailPage = '/healthTrackerDetailPage'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -78,11 +97,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 +109,34 @@ 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, + ); + + monthlyReports: (context) => ChangeNotifierProvider( + create: (_) => MonthlyReportsViewModel( + monthlyReportsRepo: getIt(), + errorHandlerService: getIt(), + ), + child: const MonthlyReportsPage(), + ), + qrParking: (context) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), + }, }; } diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 497a0092..30926749 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -14,6 +14,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/family_files/family_file_add_widget.dart'; +import '../widgets/medication_reminder/reminder_timer_dialog.dart'; + abstract class DialogService { Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed, Function()? onCancelPressed}); @@ -33,6 +35,8 @@ abstract class DialogService { Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}); Future showAddFamilyFileSheet({String? label, String? message, required Function() onVerificationPress}); + + Future showReminderBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}); // TODO : Need to be Fixed showPhoneNumberPickerSheet ( From Login ADn Signup Bottom Sheet Move Here } @@ -62,18 +66,16 @@ class DialogServiceImp implements DialogService { message: message, showCancel: onCancelPressed != null ? true : false, onOkPressed: () { - print('ok button is pressed'); if (onOkPressed != null) { - print('onOkPressed is not null'); onOkPressed(); - }else { + } else { Navigator.pop(context); } }, onCancelPressed: () { if (onCancelPressed != null) { onCancelPressed(); - }else { + } else { Navigator.pop(context); } }, @@ -108,7 +110,8 @@ class DialogServiceImp implements DialogService { } @override - Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + Future showCommonBottomSheetWithoutH( + {String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight( @@ -162,7 +165,20 @@ class DialogServiceImp implements DialogService { } @override - Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { + Future showReminderBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight( + context, + title: label ?? "", + child: child ?? SizedBox(), + callBackFunc: () {}, + ); + } + + @override + Future showPhoneNumberPickerSheet( + {String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight(context, @@ -184,7 +200,8 @@ class DialogServiceImp implements DialogService { } } -Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { +Widget exceptionBottomSheetWidget( + {required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { return Column( children: [ (message).toText16(isBold: false, color: AppColors.textColor), @@ -239,7 +256,8 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin ); } -Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { +Widget showPhoneNumberPickerWidget( + {required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { return Column( children: [ diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 043eefdc..b5366618 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -26,6 +26,7 @@ import 'package:lottie/lottie.dart'; import 'core/cache_consts.dart'; import 'core/utils/push_notification_handler.dart'; + class SplashPage extends StatefulWidget { const SplashPage({super.key}); diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index c631f5b8..9fbdc17b 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -2,111 +2,114 @@ 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 + static const Color lightGreyTextColor = Color(0xFF959595); + static const Color labelColorYellow = Color(0xFFFBCB6E); } diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index 1f5d8060..ad47cd2b 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// /// **Parameters:** /// - [dataPoints]: List of `DataPoint` objects to plot. +/// - [secondaryDataPoints]: Optional list for a second line (e.g., diastolic in blood pressure). /// - [leftLabelFormatter]: Function to build left axis labels. /// - [bottomLabelFormatter]: Function to build bottom axis labels. /// - [width]: Optional width of the chart. @@ -18,6 +19,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// - [maxY], [maxX], [minX]: Axis bounds. /// - [spotColor]: Color of the touched spot marker. /// - [graphColor]: Color of the line. +/// - [secondaryGraphColor]: Color of the secondary line. /// - [graphShadowColor]: Color of the area below the line. /// - [graphGridColor]: Color of the grid lines. /// - [bottomLabelColor]: Color of bottom axis labels. @@ -43,6 +45,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// ) class CustomGraph extends StatelessWidget { final List dataPoints; + final List? secondaryDataPoints; // For dual-line graphs (e.g., blood pressure) final double? width; final double height; final double? maxY; @@ -50,6 +53,7 @@ class CustomGraph extends StatelessWidget { final double? minX; final Color spotColor; final Color graphColor; + final Color? secondaryGraphColor; // Color for secondary line final Color graphShadowColor; final Color graphGridColor; final Color bottomLabelColor; @@ -79,6 +83,7 @@ class CustomGraph extends StatelessWidget { const CustomGraph( {super.key, required this.dataPoints, + this.secondaryDataPoints, required this.leftLabelFormatter, this.width, required this.scrollDirection, @@ -89,6 +94,7 @@ class CustomGraph extends StatelessWidget { this.isFullScreeGraph = false, this.spotColor = AppColors.bgGreenColor, this.graphColor = AppColors.bgGreenColor, + this.secondaryGraphColor, this.graphShadowColor = AppColors.graphGridColor, this.graphGridColor = AppColors.graphGridColor, this.bottomLabelColor = AppColors.textColor, @@ -225,7 +231,7 @@ class CustomGraph extends StatelessWidget { return FlSpot(entry.key.toDouble(), value); }).toList(); - var data = [ + var data = [ LineChartBarData( spots: allSpots, isCurved: true, @@ -254,6 +260,31 @@ class CustomGraph extends StatelessWidget { ) ]; + // Add secondary line if provided (for dual-line graphs like blood pressure) + if (secondaryDataPoints != null && secondaryDataPoints!.isNotEmpty) { + final List secondarySpots = secondaryDataPoints!.asMap().entries.map((entry) { + double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; + return FlSpot(entry.key.toDouble(), value); + }).toList(); + + data.add( + LineChartBarData( + spots: secondarySpots, + isCurved: true, + isStrokeCapRound: true, + isStrokeJoinRound: true, + barWidth: 2, + gradient: LinearGradient( + colors: [secondaryGraphColor ?? AppColors.blueColor, secondaryGraphColor ?? AppColors.blueColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + dotData: FlDotData(show: showLinePoints), + belowBarData: BarAreaData(show: false), + ), + ); + } + return data; } } diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 94ed9f8b..9f101d47 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -3,17 +3,14 @@ import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; - -import '../core/dependencies.dart'; - -// TODO: Import AppColors if bgRedColor is defined there -// import 'package:hmg_patient_app_new/core/ui_utils/app_colors.dart'; +import 'package:hmg_patient_app_new/widgets/time_picker_widget.dart'; class TextInputWidget extends StatelessWidget { final String labelText; @@ -49,6 +46,7 @@ class TextInputWidget extends StatelessWidget { final int maxLines; final Color? hintColor; final bool? isHideSwitcher; + final bool? isArrowTrailing; // final List countryList; // final Function(Country)? onCountryChange; @@ -87,6 +85,7 @@ class TextInputWidget extends StatelessWidget { this.minLines = 3, this.maxLines = 6, this.isHideSwitcher, + this.isArrowTrailing, // this.countryList = const [], // this.onCountryChange, }); @@ -167,7 +166,8 @@ class TextInputWidget extends StatelessWidget { ], ), ), - if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context), + if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context, isArrowTrailing: isArrowTrailing ?? false), + if (selectionType == SelectionTypeEnum.time) _buildTimePickerIcon(context, isArrowTrailing: isArrowTrailing ?? false), if (selectionType == SelectionTypeEnum.search) _buildTrailingIconForSearch(context), ], ), @@ -200,7 +200,7 @@ class TextInputWidget extends StatelessWidget { child: Utils.buildSvgWithAssets(icon: leadingIcon!)); } - Widget _buildTrailingIcon(BuildContext context) { + Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) { final AppState appState = getIt.get(); return Container( height: 40.h, @@ -233,7 +233,47 @@ class TextInputWidget extends StatelessWidget { onChange!(picked.toString()); } }, - child: Utils.buildSvgWithAssets(icon: AppAssets.calendar), + child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.calendar), + ), + ); + } + + Widget _buildTimePickerIcon(BuildContext context, {bool isArrowTrailing = false}) { + return Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.zero, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.whiteColor, + ), + child: GestureDetector( + onTap: () async { + // Parse existing time if available + TimeOfDay? initialTime; + if (controller?.text.isNotEmpty ?? false) { + initialTime = TimePickerWidget.parseTime(controller!.text); + } + + final picked = await TimePickerWidget.show( + context, + initialTime: initialTime, + use24HourFormat: false, // You can make this configurable if needed + onTimeSelected: (time) { + if (onChange != null) { + final formattedTime = TimePickerWidget.formatTime(time); + onChange!(formattedTime); + } + }, + ); + + // Update controller if time was picked + if (picked != null && controller != null) { + controller!.text = TimePickerWidget.formatTime(picked); + } + }, + child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.alarm_clock_icon), ), ); } diff --git a/lib/widgets/medication_reminder/reminder_timer_dialog.dart b/lib/widgets/medication_reminder/reminder_timer_dialog.dart new file mode 100644 index 00000000..62dbd963 --- /dev/null +++ b/lib/widgets/medication_reminder/reminder_timer_dialog.dart @@ -0,0 +1,155 @@ +// import 'package:easy_localization/easy_localization.dart'; +// import 'package:flutter/material.dart'; +// import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +// +// import '../../generated/locale_keys.g.dart'; +// import '../../theme/colors.dart'; +// +// class ReminderTimerDialog extends StatefulWidget { +// final Function()? onSetReminderPress; +// final String message; +// +// const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); +// +// +// @override +// State createState() => _ReminderTimerDialogState(); +// } +// +// class _ReminderTimerDialogState extends State { +// final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; +// final List selectedTimes = ["Morning"]; // Default selection +// +// +// @override +// Widget build(BuildContext context) { +// return // +// Column( +// children: [ +// Container( +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, +// hasShadow: true,), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// // Checkboxes list +// children: options.map((time) => buildCircleCheckbox(time)).toList(), +// ).paddingAll(16), +// ), +// const SizedBox(height: 25), +// // Buttons Row +// Row( +// children: [ +// Expanded( +// child: ElevatedButton.icon( +// onPressed: () => Navigator.pop(context), +// icon: const Icon(Icons.close, color: AppColors.errorColor), +// label: Text( +// LocaleKeys.cancel.tr(), +// style: TextStyle( +// color: AppColors.errorColor, +// fontWeight: FontWeight.w500, +// fontSize: 14 +// ), +// ), +// style: ElevatedButton.styleFrom( +// backgroundColor: AppColors.secondaryLightRedColor, +// elevation: 0, +// padding: const EdgeInsets.symmetric(vertical: 14), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12), +// ), +// ), +// ), +// ), +// const SizedBox(width: 12), +// Expanded( +// child: ElevatedButton.icon( +// onPressed: () { +// Navigator.pop(context, selectedTimes); +// }, +// icon: const Icon(Icons.notifications_rounded), +// label: Text( +// LocaleKeys.setReminder.tr(), +// style: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 14 +// ), +// ), +// style: ElevatedButton.styleFrom( +// backgroundColor: AppColors.successColor, +// foregroundColor: AppColors.whiteColor, +// elevation: 0, +// padding: const EdgeInsets.symmetric(vertical: 14), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12), +// ), +// ), +// ), +// ), +// ], +// ), +// const SizedBox(height: 30), +// ], +// ); +// } +// +// Widget buildCircleCheckbox(String label) { +// final bool isSelected = selectedTimes.contains(label); +// return InkWell( +// onTap: () { +// setState(() { +// if (isSelected) { +// selectedTimes.remove(label); +// } else { +// selectedTimes.add(label); +// } +// }); +// }, +// borderRadius: BorderRadius.circular(25), +// child: Padding( +// padding: const EdgeInsets.symmetric(vertical: 8.0), +// child: Row( +// children: [ +// // Custom circle checkbox +// Container( +// width: 15, +// height: 15, +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// border: Border.all( +// color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, +// width: 1, +// ), +// color: isSelected ? AppColors.errorColor: AppColors.transparent, +// ), +// ), +// const SizedBox(width: 12), +// // Label text +// Text( +// label, +// style: const TextStyle(fontSize: 16, color: Colors.black87), +// ), +// ], +// ), +// ), +// ); +// } +// +// +// void showCircleCheckboxDialog(BuildContext context) async { +// final selected = await showDialog>( +// context: context, +// builder: (context) => const ReminderTimerDialog(), +// ); +// +// if (selected != null && selected.isNotEmpty) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar(content: Text('Reminders set for: ${selected.join(', ')}')), +// ); +// } +// } +// } +// +// +// diff --git a/lib/widgets/time_picker_widget.dart b/lib/widgets/time_picker_widget.dart new file mode 100644 index 00000000..71d9ab2e --- /dev/null +++ b/lib/widgets/time_picker_widget.dart @@ -0,0 +1,348 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +/// A reusable time picker widget that can be used anywhere in the app +/// Shows a bottom sheet with iOS-style time picker +class TimePickerWidget { + /// Shows a time picker bottom sheet + /// + /// [context] - BuildContext for showing the bottom sheet + /// [initialTime] - Initial time to display (defaults to current time) + /// [use24HourFormat] - Whether to use 24-hour format (defaults to false) + /// [onTimeSelected] - Callback when time is selected + /// + /// Returns the selected TimeOfDay or null if cancelled + static Future show( + BuildContext context, { + TimeOfDay? initialTime, + bool use24HourFormat = false, + bool displaySelectedTime = false, + Function(TimeOfDay)? onTimeSelected, + }) async { + final selectedTime = initialTime ?? TimeOfDay.now(); + + final result = await showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (BuildContext context) { + return _TimePickerBottomSheet( + initialTime: selectedTime, + use24HourFormat: use24HourFormat, + displaySelectedTime: displaySelectedTime, + onTimeChanged: (time) { + // Time is being changed in real-time + }, + ); + }, + ); + + if (result != null && onTimeSelected != null) { + onTimeSelected(result); + } + + return result; + } + + /// Formats TimeOfDay to string (HH:mm format) + static String formatTime(TimeOfDay time, {bool use24HourFormat = false}) { + if (use24HourFormat) { + return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } else { + final hour = time.hourOfPeriod == 0 ? 12 : time.hourOfPeriod; + final period = time.period == DayPeriod.am ? 'AM' : 'PM'; + return '${hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')} $period'; + } + } + + /// Parses time string to TimeOfDay + static TimeOfDay? parseTime(String timeString) { + try { + final parts = timeString.split(':'); + if (parts.length == 2) { + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1].split(' ')[0]); + return TimeOfDay(hour: hour, minute: minute); + } + } catch (e) { + return null; + } + return null; + } +} + +class _TimePickerBottomSheet extends StatefulWidget { + final TimeOfDay initialTime; + final bool use24HourFormat; + final bool displaySelectedTime; + final Function(TimeOfDay) onTimeChanged; + + const _TimePickerBottomSheet({ + required this.initialTime, + required this.use24HourFormat, + required this.displaySelectedTime, + required this.onTimeChanged, + }); + + @override + State<_TimePickerBottomSheet> createState() => _TimePickerBottomSheetState(); +} + +class _TimePickerBottomSheetState extends State<_TimePickerBottomSheet> { + late int selectedHour; + late int selectedMinute; + late DayPeriod selectedPeriod; + + @override + void initState() { + super.initState(); + selectedHour = widget.use24HourFormat ? widget.initialTime.hour : widget.initialTime.hourOfPeriod; + if (selectedHour == 0 && !widget.use24HourFormat) selectedHour = 12; + selectedMinute = widget.initialTime.minute; + selectedPeriod = widget.initialTime.period; + } + + TimeOfDay _getCurrentTime() { + if (widget.use24HourFormat) { + return TimeOfDay(hour: selectedHour, minute: selectedMinute); + } else { + int hour = selectedHour; + if (selectedPeriod == DayPeriod.pm && hour != 12) { + hour += 12; + } else if (selectedPeriod == DayPeriod.am && hour == 12) { + hour = 0; + } + return TimeOfDay(hour: hour, minute: selectedMinute); + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.dividerColor, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Select Time".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Utils.buildSvgWithAssets( + icon: AppAssets.cancel, + width: 24.h, + height: 24.h, + iconColor: AppColors.textColor, + ), + ), + ], + ), + ), + + // Time Picker + SizedBox( + height: 250.h, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Hour Picker + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: widget.use24HourFormat ? selectedHour : (selectedHour - 1), + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + if (widget.use24HourFormat) { + selectedHour = index; + } else { + selectedHour = index + 1; + } + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: List.generate( + widget.use24HourFormat ? 24 : 12, + (index) { + final hour = widget.use24HourFormat ? index : index + 1; + return Center( + child: Text( + hour.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ); + }, + ), + ), + ), + + // Separator + Text( + ':', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + + // Minute Picker + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: selectedMinute, + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + selectedMinute = index; + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: List.generate( + 60, + (index) => Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + ), + ), + ), + + // AM/PM Picker (only for 12-hour format) + if (!widget.use24HourFormat) + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: selectedPeriod == DayPeriod.am ? 0 : 1, + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + selectedPeriod = index == 0 ? DayPeriod.am : DayPeriod.pm; + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: [ + Center( + child: Text( + 'AM', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + Center( + child: Text( + 'PM', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + if (widget.displaySelectedTime) + // Current Time Display + Container( + margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + padding: EdgeInsets.symmetric(vertical: 12.h), + decoration: BoxDecoration( + color: AppColors.lightGrayBGColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Center( + child: TimePickerWidget.formatTime( + _getCurrentTime(), + use24HourFormat: widget.use24HourFormat, + ).toText20( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ), + + // Action Buttons + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + child: Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: "Cancel".needTranslation, + onPressed: () => Navigator.pop(context), + textColor: AppColors.textColor, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + height: 56.h, + text: "Confirm".needTranslation, + onPressed: () { + Navigator.pop(context, _getCurrentTime()); + }, + textColor: AppColors.whiteColor, + backgroundColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/time_picker_widget_usage_example.dart b/lib/widgets/time_picker_widget_usage_example.dart new file mode 100644 index 00000000..9ff6c356 --- /dev/null +++ b/lib/widgets/time_picker_widget_usage_example.dart @@ -0,0 +1,165 @@ +// Example usage of TimePickerWidget +// +// This file demonstrates how to use the TimePickerWidget in your Flutter app. +// The TimePickerWidget is a reusable component that shows a bottom sheet with +// an iOS-style time picker. + +// ============================================================================ +// EXAMPLE 1: Using with TextInputWidget +// ============================================================================ +/* +TextInputWidget( + labelText: "Appointment Time", + hintText: "Select time", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + print("Selected time: $value"); + }, +) +*/ + +// ============================================================================ +// EXAMPLE 2: Direct usage with custom button +// ============================================================================ +/* +ElevatedButton( + onPressed: () async { + final selectedTime = await TimePickerWidget.show( + context, + initialTime: TimeOfDay.now(), + use24HourFormat: false, + onTimeSelected: (time) { + print("Time selected: ${TimePickerWidget.formatTime(time)}"); + }, + ); + + if (selectedTime != null) { + print("Final time: ${TimePickerWidget.formatTime(selectedTime)}"); + } + }, + child: Text("Pick Time"), +) +*/ + +// ============================================================================ +// EXAMPLE 3: Using with 24-hour format +// ============================================================================ +/* +TextInputWidget( + labelText: "Meeting Time", + hintText: "Select time (24h)", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + // The value will be formatted as "14:30" for 2:30 PM in 24h format + print("Selected time (24h): $value"); + }, +) + +// Or programmatically: +final time = await TimePickerWidget.show( + context, + use24HourFormat: true, // Enable 24-hour format +); +*/ + +// ============================================================================ +// EXAMPLE 4: Parsing and formatting times +// ============================================================================ +/* +// Parse time string to TimeOfDay +String timeString = "02:30 PM"; +TimeOfDay? parsedTime = TimePickerWidget.parseTime(timeString); + +// Format TimeOfDay to string +TimeOfDay time = TimeOfDay(hour: 14, minute: 30); +String formatted12h = TimePickerWidget.formatTime(time); // "02:30 PM" +String formatted24h = TimePickerWidget.formatTime(time, use24HourFormat: true); // "14:30" +*/ + +// ============================================================================ +// EXAMPLE 5: Complete form example with date and time +// ============================================================================ +/* +class AppointmentForm extends StatefulWidget { + @override + _AppointmentFormState createState() => _AppointmentFormState(); +} + +class _AppointmentFormState extends State { + final TextEditingController dateController = TextEditingController(); + final TextEditingController timeController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Date picker + TextInputWidget( + labelText: "Appointment Date", + hintText: "Select date", + controller: dateController, + selectionType: SelectionTypeEnum.calendar, + isReadOnly: true, + onChange: (value) { + print("Date selected: $value"); + }, + ), + + SizedBox(height: 16), + + // Time picker + TextInputWidget( + labelText: "Appointment Time", + hintText: "Select time", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + print("Time selected: $value"); + }, + ), + ], + ); + } +} +*/ + +// ============================================================================ +// Features: +// ============================================================================ +// ✅ iOS-style cupertino picker (works on both iOS and Android) +// ✅ Support for 12-hour format (with AM/PM) +// ✅ Support for 24-hour format +// ✅ Beautiful bottom sheet UI matching app design +// ✅ Real-time preview of selected time +// ✅ Confirm/Cancel buttons +// ✅ Easy integration with TextInputWidget +// ✅ Parse and format time utilities +// ✅ Fully customizable and reusable + +// ============================================================================ +// API Reference: +// ============================================================================ +// TimePickerWidget.show() - Shows the time picker bottom sheet +// Parameters: +// - context: BuildContext (required) +// - initialTime: TimeOfDay? (optional, defaults to current time) +// - use24HourFormat: bool (optional, defaults to false) +// - onTimeSelected: Function(TimeOfDay)? (optional callback) +// Returns: Future +// +// TimePickerWidget.formatTime() - Formats TimeOfDay to string +// Parameters: +// - time: TimeOfDay (required) +// - use24HourFormat: bool (optional, defaults to false) +// Returns: String (e.g., "02:30 PM" or "14:30") +// +// TimePickerWidget.parseTime() - Parses time string to TimeOfDay +// Parameters: +// - timeString: String (required, e.g., "02:30 PM") +// Returns: TimeOfDay? +