From a1aaa041d14c09b72e5fa66e2c854d52d9f1e30f Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 28 Dec 2025 22:53:46 +0300 Subject: [PATCH] Water Intake Final Touches --- assets/images/svg/graph_icon.svg | 5 + assets/images/svg/green_tick_icon.svg | 3 + assets/images/svg/list_icon.svg | 8 + assets/images/svg/yellow_arrow_down_icon.svg | 3 + lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 59 +- lib/core/app_assets.dart | 4 + lib/core/dependencies.dart | 9 +- lib/core/utils/size_utils.dart | 11 + .../insert_user_activity_request_model.dart | 26 + .../undo_user_activity_request_model.dart | 26 + .../update_user_detail_request_model.dart | 88 +++ .../models/user_progress_models.dart | 111 +++ .../water_monitor/water_monitor_repo.dart | 320 ++++++++ .../water_monitor_view_model.dart | 714 +++++++++++++++++- .../water_consumption_screen.dart | 656 +++++++++++++--- .../water_monitor_settings_screen.dart | 24 +- .../shimmer/common_shimmer_widget.dart | 11 +- 18 files changed, 1912 insertions(+), 168 deletions(-) create mode 100644 assets/images/svg/graph_icon.svg create mode 100644 assets/images/svg/green_tick_icon.svg create mode 100644 assets/images/svg/list_icon.svg create mode 100644 assets/images/svg/yellow_arrow_down_icon.svg create mode 100644 lib/features/water_monitor/models/insert_user_activity_request_model.dart create mode 100644 lib/features/water_monitor/models/undo_user_activity_request_model.dart create mode 100644 lib/features/water_monitor/models/update_user_detail_request_model.dart create mode 100644 lib/features/water_monitor/models/user_progress_models.dart create mode 100644 lib/features/water_monitor/water_monitor_repo.dart diff --git a/assets/images/svg/graph_icon.svg b/assets/images/svg/graph_icon.svg new file mode 100644 index 0000000..7bb6fbb --- /dev/null +++ b/assets/images/svg/graph_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/green_tick_icon.svg b/assets/images/svg/green_tick_icon.svg new file mode 100644 index 0000000..e041191 --- /dev/null +++ b/assets/images/svg/green_tick_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/list_icon.svg b/assets/images/svg/list_icon.svg new file mode 100644 index 0000000..e68f20b --- /dev/null +++ b/assets/images/svg/list_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/yellow_arrow_down_icon.svg b/assets/images/svg/yellow_arrow_down_icon.svg new file mode 100644 index 0000000..f2ca09f --- /dev/null +++ b/assets/images/svg/yellow_arrow_down_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f366329..039787b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,7 +210,7 @@ class ApiClientImp implements ApiClient { final int statusCode = response.statusCode; log("uri: ${Uri.parse(url.trim())}"); log("body: ${json.encode(body)}"); - // log("response.body: ${response.body}"); + log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index a5283c0..8a91a6f 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -437,14 +437,6 @@ var RATE_DOCTOR_RESPONSE = 'Services/OUTPs.svc/REST/insertAppointmentQuestionRat var GET_PATIENT_ALLERGIES = 'Services/Patients.svc/REST/GetPatientAllergies'; -// H2O -var H2O_GET_USER_PROGRESS = "Services/H2ORemainder.svc/REST/H2O_GetUserProgress"; -var H2O_INSERT_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2O_InsertUserActivity"; -var H2O_GET_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_GetUserDetails_New"; -var H2O_UPDATE_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New"; -var H2O_UNDO_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity"; -//E_Referral Services - // Encillary Orders var GET_ANCILLARY_ORDERS = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; @@ -670,25 +662,6 @@ var addPayFortApplePayResponse = "Services/PayFort_Serv.svc/REST/AddResponse"; // Auth Provider Consts -const String INSERT_DEVICE_IMEI = 'Services/Patients.svc/REST/Patient_INSERTDeviceIMEI'; -const String SELECT_DEVICE_IMEI = 'Services/Patients.svc/REST/Patient_SELECTDeviceIMEIbyIMEI'; -const String CHECK_PATIENT_AUTH = 'Services/Authentication.svc/REST/CheckPatientAuthentication'; -const GET_MOBILE_INFO = 'Services/Authentication.svc/REST/GetMobileLoginInfo'; - -const FORGOT_PASSWORD = 'Services/Authentication.svc/REST/CheckActivationCodeForSendFileNo'; -const CHECK_PATIENT_FOR_REGISTRATION = "Services/Authentication.svc/REST/CheckPatientForRegisteration"; - -const CHECK_USER_STATUS = "Services/NHIC.svc/REST/GetPatientInfo"; -const REGISTER_USER = 'Services/Authentication.svc/REST/PatientRegistration'; -const LOGGED_IN_USER_URL = 'Services/MobileNotifications.svc/REST/Insert_PatientMobileDeviceInfo'; - -const FORGOT_PATIENT_ID = 'Services/Authentication.svc/REST/SendPatientIDSMSByMobileNumber'; -const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard'; -const PROFILE_SETTING = 'Services/Patients.svc/REST/GetPateintInfoForUpdate'; -const SAVE_SETTING = 'Services/Patients.svc/REST/UpdatePateintInfo'; - -const DEACTIVATE_ACCOUNT = 'Services/Patients.svc/REST/PatientAppleActivation_InsertUpdate'; - var ER_CREATE_ADVANCE_PAYMENT = "services/Doctors.svc/REST/ER_CreateAdvancePaymentForClinic"; //family Files @@ -868,6 +841,7 @@ class ApiConsts { //WATER CONSUMPTION static String h2oGetUserProgress = "Services/H2ORemainder.svc/REST/H2O_GetUserProgress"; static String h2oInsertUserActivity = "Services/H2ORemainder.svc/REST/H2O_InsertUserActivity"; + static String h2oInsertUserDetailsNew = "Services/H2ORemainder.svc/REST/H2O_InsertUserDetails_New"; static String h2oGetUserDetail = "Services/H2ORemainder.svc/REST/H2O_GetUserDetails_New"; static String h2oUpdateUserDetail = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New"; static String h2oUndoUserActivity = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity"; @@ -883,3 +857,34 @@ class ApiConsts { class ApiKeyConstants { static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng'; } + +//flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_InsertUserActivity +// flutter: {"IdentificationNo":"2530976584","MobileNumber":"504278212","QuantityIntake":200,"VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":200.00,"PercentageConsumed":9.41,"PercentageLeft":90.59,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[{"Quantity":200.000,"CreatedDate":"\/Date(1766911222217+0300)\/"}],"VerificationCode":null,"isSMSSent":false} + +// URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2o_UndoUserActivity +// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} + +// Progress":2 means weekly data + +// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress +// flutter: {"Progress":2,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":null,"UserProgressForWeekData":[{"DayNumber":1,"DayDate":null,"DayName":"Sunday","PercentageConsumed":0},{"DayNumber":7,"DayDate":null,"DayName":"Saturday","PercentageConsumed":0},{"DayNumber":6,"DayDate":null,"DayName":"Friday","PercentageConsumed":0},{"DayNumber":5,"DayDate":null,"DayName":"Thursday","PercentageConsumed":0},{"DayNumber":4,"DayDate":null,"DayName":"Wednesday","PercentageConsumed":0},{"DayNumber":3,"DayDate":null,"DayName":"Tuesday","PercentageConsumed":0},{"DayNumber":2,"DayDate":null,"DayName":"Monday","PercentageConsumed":0}],"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} + +// Progress":1 means daily data + +//URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress +// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} + +// Progress":1 means monthly data + +// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress +// flutter: {"Progress":3,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":[{"MonthNumber":1,"MonthName":"January","PercentageConsumed":0},{"MonthNumber":2,"MonthName":"February","PercentageConsumed":0},{"MonthNumber":3,"MonthName":"March","PercentageConsumed":0},{"MonthNumber":4,"MonthName":"April","PercentageConsumed":0},{"MonthNumber":5,"MonthName":"May","PercentageConsumed":0},{"MonthNumber":6,"MonthName":"June","PercentageConsumed":0},{"MonthNumber":7,"MonthName":"July","PercentageConsumed":0},{"MonthNumber":8,"MonthName":"August","PercentageConsumed":0},{"MonthNumber":9,"MonthName":"September","PercentageConsumed":0},{"MonthNumber":10,"MonthName":"October","PercentageConsumed":0},{"MonthNumber":11,"MonthName":"November","PercentageConsumed":0},{"MonthNumber":12,"MonthName":"December","PercentageConsumed":0}],"UserProgressForTodayData":null,"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 34224f8..a4fbc62 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -261,6 +261,10 @@ class AppAssets { static const String minimizeIcon = '$svgBasePath/minimize_icon.svg'; static const String addIconDark = '$svgBasePath/add_icon_dark.svg'; static const String glassIcon = '$svgBasePath/glass_icon.svg'; + static const String graphIcon = '$svgBasePath/graph_icon.svg'; + static const String listIcon = '$svgBasePath/list_icon.svg'; + static const String yellowArrowDownIcon = '$svgBasePath/yellow_arrow_down_icon.svg'; + static const String greenTickIcon = '$svgBasePath/green_tick_icon.svg'; // PNGS static const String hmgLogo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 96cbfa9..ee3b7eb 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -45,6 +45,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_r import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/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_calculators/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; @@ -125,10 +126,8 @@ class AppDependencies { getIt.registerLazySingleton(() => ContactUsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => HmgServicesRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => SymptomsCheckerRepoImp(loggerService: getIt(), apiClient: getIt())); - getIt.registerLazySingleton(() => BloodDonationRepoImp( - loggerService: getIt(), - apiClient: getIt(), - )); + getIt.registerLazySingleton(() => BloodDonationRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => WaterMonitorRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton @@ -268,7 +267,7 @@ class AppDependencies { () => HealthProvider(), ); - getIt.registerLazySingleton(() => WaterMonitorViewModel()); + getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt())); // Screen-specific VMs → Factory // getIt.registerFactory( diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index 8a0703e..4fdc09c 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:math' as math; import 'package:flutter/material.dart'; // These are the Viewport values of your Figma Design. @@ -6,6 +7,16 @@ 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; + + /// Height with max cap for tablets + double get hCapped => isTablet ? math.min(h, this * 1.3) : h; +} + + extension ResponsiveExtension on num { double get _screenWidth => SizeUtils.width; diff --git a/lib/features/water_monitor/models/insert_user_activity_request_model.dart b/lib/features/water_monitor/models/insert_user_activity_request_model.dart new file mode 100644 index 0000000..a0af649 --- /dev/null +++ b/lib/features/water_monitor/models/insert_user_activity_request_model.dart @@ -0,0 +1,26 @@ +class InsertUserActivityRequestModel { + String? identificationNo; + String? mobileNumber; + num? quantityIntake; + + InsertUserActivityRequestModel({ + this.identificationNo, + this.mobileNumber, + this.quantityIntake, + }); + + Map toJson() { + final map = {}; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (quantityIntake != null) map['QuantityIntake'] = quantityIntake; + return map; + } + + factory InsertUserActivityRequestModel.fromJson(Map json) => InsertUserActivityRequestModel( + identificationNo: json['IdentificationNo']?.toString(), + mobileNumber: json['MobileNumber']?.toString(), + quantityIntake: json['QuantityIntake'], + ); +} + diff --git a/lib/features/water_monitor/models/undo_user_activity_request_model.dart b/lib/features/water_monitor/models/undo_user_activity_request_model.dart new file mode 100644 index 0000000..01dbd92 --- /dev/null +++ b/lib/features/water_monitor/models/undo_user_activity_request_model.dart @@ -0,0 +1,26 @@ +class UndoUserActivityRequestModel { + num? progress; + String? mobileNumber; + String? identificationNo; + + UndoUserActivityRequestModel({ + this.progress, + this.mobileNumber, + this.identificationNo, + }); + + Map toJson() { + final map = {}; + if (progress != null) map['Progress'] = progress; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + return map; + } + + factory UndoUserActivityRequestModel.fromJson(Map json) => UndoUserActivityRequestModel( + progress: json['Progress'], + mobileNumber: json['MobileNumber']?.toString(), + identificationNo: json['IdentificationNo']?.toString(), + ); +} + diff --git a/lib/features/water_monitor/models/update_user_detail_request_model.dart b/lib/features/water_monitor/models/update_user_detail_request_model.dart new file mode 100644 index 0000000..33dbbb2 --- /dev/null +++ b/lib/features/water_monitor/models/update_user_detail_request_model.dart @@ -0,0 +1,88 @@ +class UpdateUserDetailRequestModel { + // User detail fields + String? activityID; + String? dOB; + String? email; + String? firstName; + String? lastName; + String? firstNameN; + String? middleName; + String? middleNameN; + String? lastNameN; + String? gender; + num? height; + bool? isHeightInCM; + bool? isWeightInKG; + String? zipCode; + num? weight; + bool? isNotificationOn; + String? mobileNumber; + String? identificationNo; + + UpdateUserDetailRequestModel({ + this.activityID, + this.dOB, + this.email, + this.firstName, + this.lastName, + this.firstNameN, + this.middleName, + this.middleNameN, + this.lastNameN, + this.gender, + this.height, + this.isHeightInCM, + this.isWeightInKG, + this.zipCode, + this.weight, + this.isNotificationOn, + this.mobileNumber, + this.identificationNo, + }); + + Map toJson() { + final map = {}; + + // User detail fields + if (activityID != null) map['ActivityID'] = activityID; + if (dOB != null) map['DOB'] = dOB; + if (email != null) map['Email'] = email; + if (firstName != null) map['FirstName'] = firstName; + if (lastName != null) map['LastName'] = lastName; + if (firstNameN != null) map['FirstNameN'] = firstNameN; + if (middleName != null) map['MiddleName'] = middleName; + if (middleNameN != null) map['MiddleNameN'] = middleNameN; + if (lastNameN != null) map['LastNameN'] = lastNameN; + if (gender != null) map['Gender'] = gender; + if (height != null) map['Height'] = height; + if (isHeightInCM != null) map['IsHeightInCM'] = isHeightInCM; + if (isWeightInKG != null) map['IsWeightInKG'] = isWeightInKG; + if (zipCode != null) map['ZipCode'] = zipCode; + if (weight != null) map['Weight'] = weight; + if (isNotificationOn != null) map['IsNotificationOn'] = isNotificationOn; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + return map; + } + + factory UpdateUserDetailRequestModel.fromJson(Map json) => UpdateUserDetailRequestModel( + activityID: json['ActivityID']?.toString(), + dOB: json['DOB']?.toString(), + email: json['Email']?.toString(), + firstName: json['FirstName']?.toString(), + lastName: json['LastName']?.toString(), + firstNameN: json['FirstNameN']?.toString(), + middleName: json['MiddleName']?.toString(), + middleNameN: json['MiddleNameN']?.toString(), + lastNameN: json['LastNameN']?.toString(), + gender: json['Gender']?.toString(), + height: json['Height'], + isHeightInCM: json['IsHeightInCM'] as bool?, + isWeightInKG: json['IsWeightInKG'] as bool?, + zipCode: json['ZipCode']?.toString(), + weight: json['Weight'], + isNotificationOn: json['IsNotificationOn'] as bool?, + mobileNumber: json['MobileNumber']?.toString(), + identificationNo: json['IdentificationNo']?.toString(), + ); +} diff --git a/lib/features/water_monitor/models/user_progress_models.dart b/lib/features/water_monitor/models/user_progress_models.dart new file mode 100644 index 0000000..667653e --- /dev/null +++ b/lib/features/water_monitor/models/user_progress_models.dart @@ -0,0 +1,111 @@ +/// Model for today's water progress data +class UserProgressForTodayModel { + num? quantityConsumed; + num? percentageConsumed; + num? percentageLeft; + num? quantityLimit; + + UserProgressForTodayModel({ + this.quantityConsumed, + this.percentageConsumed, + this.percentageLeft, + this.quantityLimit, + }); + + factory UserProgressForTodayModel.fromJson(Map json) => UserProgressForTodayModel( + quantityConsumed: json['QuantityConsumed'], + percentageConsumed: json['PercentageConsumed'], + percentageLeft: json['PercentageLeft'], + quantityLimit: json['QuantityLimit'], + ); + + Map toJson() { + return { + 'QuantityConsumed': quantityConsumed, + 'PercentageConsumed': percentageConsumed, + 'PercentageLeft': percentageLeft, + 'QuantityLimit': quantityLimit, + }; + } +} + +/// Model for weekly water progress data +class UserProgressForWeekModel { + int? dayNumber; + String? dayDate; + String? dayName; + num? percentageConsumed; + + UserProgressForWeekModel({ + this.dayNumber, + this.dayDate, + this.dayName, + this.percentageConsumed, + }); + + factory UserProgressForWeekModel.fromJson(Map json) => UserProgressForWeekModel( + dayNumber: json['DayNumber'] as int?, + dayDate: json['DayDate']?.toString(), + dayName: json['DayName']?.toString(), + percentageConsumed: json['PercentageConsumed'], + ); + + Map toJson() { + return { + 'DayNumber': dayNumber, + 'DayDate': dayDate, + 'DayName': dayName, + 'PercentageConsumed': percentageConsumed, + }; + } +} + +/// Model for monthly water progress data +class UserProgressForMonthModel { + int? monthNumber; + String? monthName; + num? percentageConsumed; + + UserProgressForMonthModel({ + this.monthNumber, + this.monthName, + this.percentageConsumed, + }); + + factory UserProgressForMonthModel.fromJson(Map json) => UserProgressForMonthModel( + monthNumber: json['MonthNumber'] as int?, + monthName: json['MonthName']?.toString(), + percentageConsumed: json['PercentageConsumed'], + ); + + Map toJson() { + return { + 'MonthNumber': monthNumber, + 'MonthName': monthName, + 'PercentageConsumed': percentageConsumed, + }; + } +} + +/// Model for user progress history data +class UserProgressHistoryModel { + num? quantity; + String? createdDate; + + UserProgressHistoryModel({ + this.quantity, + this.createdDate, + }); + + factory UserProgressHistoryModel.fromJson(Map json) => UserProgressHistoryModel( + quantity: json['Quantity'], + createdDate: json['CreatedDate']?.toString(), + ); + + Map toJson() { + return { + 'Quantity': quantity, + 'CreatedDate': createdDate, + }; + } +} diff --git a/lib/features/water_monitor/water_monitor_repo.dart b/lib/features/water_monitor/water_monitor_repo.dart new file mode 100644 index 0000000..f52c971 --- /dev/null +++ b/lib/features/water_monitor/water_monitor_repo.dart @@ -0,0 +1,320 @@ +import 'dart:developer'; + +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/features/water_monitor/models/insert_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.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 WaterMonitorRepo { + /// Fetch user details for water monitoring. + /// The request will include the standard payload injected by ApiClient and + /// additionally these three parameters: Progress, MobileNumber, IdentificationNo. + Future>> getUserDetailsForWaterMonitoring({ + required num progress, + required String mobileNumber, + required String identificationNo, + }); + + /// Fetch user progress for water monitoring (H2O_GetUserProgress). + Future>> getUserProgressForWaterMonitoring({ + required ProgressType progressType, + required String mobileNumber, + required String identificationNo, + }); + + /// Update user details for water monitoring. + Future>> updateOrInsertUserDetailForWaterMonitoring( + {required UpdateUserDetailRequestModel requestModel, bool isUpdate = false}); + + /// Insert user activity (water intake). + Future>> insertUserActivity({ + required InsertUserActivityRequestModel requestModel, + }); + + /// Undo last user activity. + Future>> undoUserActivity({ + required UndoUserActivityRequestModel requestModel, + }); +} + +class WaterMonitorRepoImp implements WaterMonitorRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + WaterMonitorRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>> getUserDetailsForWaterMonitoring({ + required num progress, + required String mobileNumber, + required String identificationNo, + }) async { + Map request = { + "Progress": progress, + "MobileNumber": mobileNumber, + "IdentificationNo": identificationNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.h2oGetUserDetail, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("response h2oGetUserDetail: ${response.toString()}"); + // Extract only the specific key from the API response as requested + dynamic extracted; + if (response is Map && response.containsKey('UserDetailData_New')) { + extracted = response['UserDetailData_New']; + } else { + extracted = null; + } + + 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>> getUserProgressForWaterMonitoring({ + required ProgressType progressType, + required String mobileNumber, + required String identificationNo, + }) async { + final progressValue = progressType == ProgressType.today ? 1 : (progressType == ProgressType.week ? 2 : 3); + + Map request = { + "Progress": progressValue, + "MobileNumber": mobileNumber, + "IdentificationNo": identificationNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.h2oGetUserProgress, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Try a few likely nested keys and fall back to the full response + dynamic extracted; + if (response is Map) { + switch (progressType) { + case ProgressType.today: + if (response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } + break; + case ProgressType.week: + if (response.containsKey('UserProgressForWeekData')) { + extracted = response['UserProgressForWeekData']; + } + break; + case ProgressType.month: + if (response.containsKey('UserProgressForMonthData')) { + extracted = response['UserProgressForMonthData']; + } + break; + } + + // fallbacks + if (extracted == null) { + if (response.containsKey('UserProgress')) { + extracted = response['UserProgress']; + } else if (response.containsKey('UserProgressData_New')) { + extracted = response['UserProgressData_New']; + } else { + extracted = response; + } + } + } 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>> updateOrInsertUserDetailForWaterMonitoring({ + required UpdateUserDetailRequestModel requestModel, + bool isUpdate = false, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + // Use different endpoint based on isUpdate flag + final endpoint = isUpdate ? ApiConsts.h2oUpdateUserDetail : ApiConsts.h2oInsertUserDetailsNew; + await apiClient.post( + endpoint, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map && response.containsKey('UserDetailData_New')) { + extracted = response['UserDetailData_New']; + } else { + extracted = null; + } + + 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>> insertUserActivity({ + required InsertUserActivityRequestModel requestModel, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.h2oInsertUserActivity, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract UserProgressForTodayData from the response + dynamic extracted; + if (response is Map && response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } 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>> undoUserActivity({ + required UndoUserActivityRequestModel requestModel, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.h2oUndoUserActivity, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("response h2oUndoUserActivity: ${response.toString()}"); + // Extract UserProgressForTodayData from the response + dynamic extracted; + if (response is Map && response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } 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())); + } + } +} diff --git a/lib/features/water_monitor/water_monitor_view_model.dart b/lib/features/water_monitor/water_monitor_view_model.dart index 4e8d5f5..4ad8f1e 100644 --- a/lib/features/water_monitor/water_monitor_view_model.dart +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -1,9 +1,25 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/user_progress_models.dart'; import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart'; +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/navigation_service.dart'; class WaterMonitorViewModel extends ChangeNotifier { + WaterMonitorRepo waterMonitorRepo; + + WaterMonitorViewModel({required this.waterMonitorRepo}); + // Controllers final TextEditingController nameController = TextEditingController(); final TextEditingController heightController = TextEditingController(); @@ -21,6 +37,11 @@ class WaterMonitorViewModel extends ChangeNotifier { String _selectedNumberOfReminders = '3 Time'; String _selectedGender = "Male"; + // ConsumptionScreen + + String _selectedDuration = "Daily"; + bool _isGraphView = true; + // Validation error message String? _validationError; @@ -37,6 +58,10 @@ class WaterMonitorViewModel extends ChangeNotifier { String get selectedGender => _selectedGender; + String get selectedDurationFilter => _selectedDuration; + + bool get isGraphView => _isGraphView; + // Activity level options List get activityLevels => ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; @@ -47,10 +72,299 @@ class WaterMonitorViewModel extends ChangeNotifier { // Gender options List get genderOptions => ["Male", "Female"]; +//Duration Options + List get durationFilters => ["Daily", "Weekly", "Monthly"]; + + // Network/data + final AppState _appState = GetIt.instance(); + final NavigationService _navigationService = GetIt.instance(); + + bool _isLoading = false; + dynamic _userDetailData; + + // Progress data lists + List _todayProgressList = []; + List _weekProgressList = []; + List _monthProgressList = []; + List _historyList = []; + + bool get isLoading => _isLoading; + + dynamic get userDetailData => _userDetailData; + + // Getters for progress data + List get todayProgressList => _todayProgressList; + + List get weekProgressList => _weekProgressList; + + List get monthProgressList => _monthProgressList; + + List get historyList => _historyList; + + // 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; + } + } + // Initialize method to be called when needed - void initialize() { + Future initialize() async { _loadSettings(); _initializeDefaultCups(); + await fetchUserDetailsForMonitoring(); + await fetchUserProgressForMonitoring(); + } + + /// Map selected duration to ProgressType enum + ProgressType _getProgressTypeFromDuration() { + switch (_selectedDuration) { + case 'Daily': + return ProgressType.today; + case 'Weekly': + return ProgressType.week; + case 'Monthly': + return ProgressType.month; + default: + return ProgressType.today; + } + } + + /// Fetch user progress data based on selected duration + Future fetchUserProgressForMonitoring() async { + try { + _isLoading = true; + notifyListeners(); + + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', ''); + final identification = authenticated.patientIdentificationNo ?? ''; + final progressType = _getProgressTypeFromDuration(); + + final result = await waterMonitorRepo.getUserProgressForWaterMonitoring( + progressType: progressType, + mobileNumber: mobile, + identificationNo: identification, + ); + + result.fold((failure) { + log('Error fetching user progress: ${failure.message}'); + }, (apiModel) { + log("User Progress Data ($_selectedDuration): ${apiModel.data.toString()}"); + + // Parse the response based on progress type + try { + if (apiModel.data != null && apiModel.data is List) { + switch (progressType) { + case ProgressType.today: + _todayProgressList.clear(); + for (var item in apiModel.data) { + if (item is Map) { + _todayProgressList.add(UserProgressForTodayModel.fromJson(item as Map)); + } + } + + break; + + case ProgressType.week: + _weekProgressList.clear(); + for (var item in apiModel.data) { + if (item is Map) { + _weekProgressList.add(UserProgressForWeekModel.fromJson(item as Map)); + } + } + log('Week Progress: ${_weekProgressList.length} items'); + break; + + case ProgressType.month: + _monthProgressList.clear(); + for (var item in apiModel.data) { + if (item is Map) { + _monthProgressList.add(UserProgressForMonthModel.fromJson(item as Map)); + } + } + log('Month Progress: ${_monthProgressList.length} items'); + break; + } + } + } catch (e) { + log('Error parsing progress data: $e'); + } + }); + } catch (e) { + log('Exception in fetchUserProgressForMonitoring: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future fetchUserDetailsForMonitoring() async { + try { + _isLoading = true; + + notifyListeners(); + + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return; + } + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', ''); + final identification = authenticated.patientIdentificationNo ?? ''; + + final result = await waterMonitorRepo.getUserDetailsForWaterMonitoring(progress: 1, mobileNumber: mobile, identificationNo: identification); + result.fold((failure) { + _userDetailData = null; + }, (apiModel) { + log("_userDetailData: ${apiModel.data.toString()}"); + _userDetailData = apiModel.data; + + // Populate form fields from the fetched data + if (_userDetailData != null) { + _populateFormFields(_userDetailData); + + // Calculate and set daily water goal based on the fetched user details + final calculatedGoal = calculateDailyWaterIntake(); + log("calculatedGoal: $calculatedGoal"); + if (calculatedGoal > 0) { + setDailyGoal(calculatedGoal.toInt()); + } + } + }); + } catch (e) { + _userDetailData = null; + } finally { + _isLoading = false; + notifyListeners(); + + if (_userDetailData == null) { + try { + _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsScreen); + } catch (navErr) { + log('Navigation to water monitor settings failed: $navErr'); + } + } + } + } + + /// Populates form fields from the API response data + void _populateFormFields(dynamic data) { + if (data == null) return; + + try { + // Parse the response and populate fields + if (data is Map) { + // Name + if (data['FirstName'] != null) { + nameController.text = data['FirstName'].toString(); + } + + // Gender + if (data['Gender'] != null) { + final gender = data['Gender'].toString(); + if (gender == 'M' || gender == 'Male') { + _selectedGender = 'Male'; + } else if (gender == 'F' || gender == 'Female') { + _selectedGender = 'Female'; + } + } + + // Age - calculate from DOB if available + if (data['DOB'] != null) { + final dob = data['DOB'].toString(); + final age = _calculateAgeFromDOB(dob); + if (age > 0) { + ageController.text = age.toString(); + } + } + + // Height + if (data['Height'] != null) { + heightController.text = data['Height'].toString(); + // Set height unit + if (data['IsHeightInCM'] != null) { + _selectedHeightUnit = data['IsHeightInCM'] == true ? 'cm' : 'in'; + } + } + + // Weight + if (data['Weight'] != null) { + weightController.text = data['Weight'].toString(); + // Set weight unit + if (data['IsWeightInKG'] != null) { + _selectedWeightUnit = data['IsWeightInKG'] == true ? 'kg' : 'lb'; + } + } + + // Activity Level - map ActivityID to activity level + if (data['ActivityID'] != null) { + final activityId = data['ActivityID'].toString(); + _selectedActivityLevel = _getActivityLevelFromID(activityId); + } + + // Number of reminders (if available in response) + // Note: This may not be in the response, keeping default if not present + + notifyListeners(); + } + } catch (e) { + log('Error populating form fields: $e'); + } + } + + /// Calculate age from DOB string in format /Date(milliseconds+0300)/ + int _calculateAgeFromDOB(String dobString) { + try { + // Parse the /Date(milliseconds+0300)/ format + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dobString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + final dob = DateTime.fromMillisecondsSinceEpoch(milliseconds); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || (now.month == dob.month && now.day < dob.day)) { + age--; + } + return age; + } + } + } catch (e) { + log('Error calculating age from DOB: $e'); + } + return 0; + } + + /// Map activity ID to activity level string + String _getActivityLevelFromID(String activityId) { + switch (activityId) { + case '1': + return "Almost Inactive (no exercise)"; + case '2': + return "Lightly active"; + case '3': + return "Lightly active (1-3) days per week"; + case '4': + return "Super active (very hard exercise)"; + default: + return "Lightly active"; + } } // Reset all fields to default @@ -67,6 +381,20 @@ class WaterMonitorViewModel extends ChangeNotifier { notifyListeners(); } + // Setters with notification + void setFilterDuration(String duration) async { + _selectedDuration = duration; + notifyListeners(); + // Fetch new progress data when duration filter changes + await fetchUserProgressForMonitoring(); + } + + // Setters with notification + void setGraphView(bool value) { + _isGraphView = value; + notifyListeners(); + } + // Setters with notification void setGender(String gender) { _selectedGender = gender; @@ -229,15 +557,125 @@ class WaterMonitorViewModel extends ChangeNotifier { } } + String _getApiCompatibleGender() { + if (_selectedGender == "Female") { + return "F"; + } + return "M"; + } + // Save settings Future saveSettings() async { if (!isValid) { notifyListeners(); // Notify so error can be read return false; } - _validationError = null; // Clear error on success - notifyListeners(); - return true; + + try { + _isLoading = true; + _validationError = null; + notifyListeners(); + + // Determine if this is an update or insert + final isUpdate = _userDetailData != null; + + // Get authenticated user for mobile number and identification number + final authenticated = _appState.getAuthenticatedUser(); + final mobile = (authenticated?.mobileNumber?.replaceAll('+', '') ?? '').replaceFirst("0", ""); + final identification = authenticated?.patientIdentificationNo ?? ''; + final zipCode = authenticated?.zipCode ?? '966'; + final email = authenticated?.emailAddress ?? ''; + + // Get activity ID based on selected activity level + String activityID = _getActivityID(); + + // Create request model with user detail fields only + final requestModel = UpdateUserDetailRequestModel( + firstName: nameController.text.trim(), + lastName: nameController.text.trim(), + firstNameN: nameController.text.trim(), + middleName: '', + middleNameN: '', + lastNameN: nameController.text.trim(), + gender: _getApiCompatibleGender(), + dOB: _calculateDOBFromAge(), + height: double.tryParse(heightController.text.trim()), + isHeightInCM: _selectedHeightUnit == 'cm', + weight: double.tryParse(weightController.text.trim()), + isWeightInKG: _selectedWeightUnit == 'kg', + activityID: activityID, + isNotificationOn: true, + mobileNumber: mobile, + identificationNo: identification, + email: email, + zipCode: zipCode, + ); + + // Call the API + final result = await waterMonitorRepo.updateOrInsertUserDetailForWaterMonitoring( + requestModel: requestModel, + isUpdate: isUpdate, + ); + + return result.fold( + (failure) { + _validationError = failure.message; + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + // Update local data with response + _userDetailData = apiModel.data; + // Calculate and set daily goal based on the inputs + final calculatedGoal = calculateDailyWaterIntake(); + + log("calculatedGoal: $calculatedGoal"); + if (calculatedGoal > 0) { + setDailyGoal(calculatedGoal.toInt()); + } + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + _validationError = e.toString(); + _isLoading = false; + notifyListeners(); + return false; + } + } + + // Helper method to get activity ID based on activity level + String _getActivityID() { + switch (_selectedActivityLevel) { + case "Almost Inactive (no exercise)": + return "1"; + case "Lightly active": + return "2"; + case "Lightly active (1-3) days per week": + return "3"; + case "Super active (very hard exercise)": + return "4"; + default: + return "2"; + } + } + + // Helper method to calculate DOB from age + String _calculateDOBFromAge() { + final age = int.tryParse(ageController.text.trim()) ?? 0; + if (age > 0) { + final currentYear = DateTime.now().year; + final birthYear = currentYear - age; + // Create a DateTime for January 1st of the birth year + final birthDate = DateTime(birthYear, 1, 1); + // Convert to API format: /Date(milliseconds+0300)/ using DateUtil + return DateUtil.convertDateToString(birthDate); + } + return ""; } // Load settings @@ -258,8 +696,9 @@ class WaterMonitorViewModel extends ChangeNotifier { String? _selectedCupId; int _totalConsumedMl = 1000; int _dailyGoalMl = 2000; + // Calibration: portion of the bottle SVG height that is fillable (0.0 - 1.0) - double _fillableHeightPercent = 0.823; + double _fillableHeightPercent = 0.7; List get cups => _cups; @@ -290,6 +729,129 @@ class WaterMonitorViewModel extends ChangeNotifier { // Convenience percent (0 - 100) double get progressPercent => (progress * 100.0).clamp(0.0, 100.0).toDouble(); + // Calculate hydration status based on progress + String get hydrationStatus { + final percent = progressPercent; + + if (percent >= 90) { + return "Well Hydrated"; + } else if (percent >= 70) { + return "Hydrated"; + } else if (percent >= 50) { + return "Moderately Hydrated"; + } else if (percent >= 30) { + return "Slightly Dehydrated"; + } else { + return "Dehydrated"; + } + } + + // Get hydration status color + Color get hydrationStatusColor { + final percent = progressPercent; + + if (percent >= 90) { + return const Color(0xFF00C853); // Dark Green + } else if (percent >= 70) { + return const Color(0xFF4CAF50); // Green + } else if (percent >= 50) { + return const Color(0xFFFFC107); // Amber + } else if (percent >= 30) { + return const Color(0xFFFF9800); // Orange + } else { + return const Color(0xFFF44336); // Red + } + } + + String get nextDrinkTime { + if (progressPercent >= 100) { + return "Goal Achieved!"; + } + + // Get number of reminders from selected string (e.g., "3 Time" -> 3) + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + + // Define waking hours (e.g., 6 AM to 10 PM = 16 hours) + const wakingHoursStart = 6; // 6 AM + const wakingHoursEnd = 22; // 10 PM + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + + // Calculate interval between drinks in hours + final intervalHours = totalWakingHours / remindersPerDay; + + // Get current time + final now = DateTime.now(); + final currentHour = now.hour + (now.minute / 60.0); + + // If before waking hours, next drink is at start time + if (currentHour < wakingHoursStart) { + return "${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // If after waking hours, next drink is tomorrow morning + if (currentHour >= wakingHoursEnd) { + return "Tomorrow ${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // Calculate which interval we're in + final hoursSinceWakeup = currentHour - wakingHoursStart; + final currentInterval = (hoursSinceWakeup / intervalHours).floor(); + + // Calculate next drink time + final nextDrinkHour = wakingHoursStart + ((currentInterval + 1) * intervalHours); + + // If next drink time is past bedtime, show tomorrow + if (nextDrinkHour >= wakingHoursEnd) { + return "Tomorrow ${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // Format the time + final hour = nextDrinkHour.floor(); + final minute = ((nextDrinkHour - hour) * 60).round(); + + // Convert to 12-hour format + 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"; + } + + /// Get time until next drink in minutes + int get minutesUntilNextDrink { + if (progressPercent >= 100) return 0; + + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + const wakingHoursStart = 6; + const wakingHoursEnd = 22; + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + final intervalHours = totalWakingHours / remindersPerDay; + + final now = DateTime.now(); + final currentHour = now.hour + (now.minute / 60.0); + + if (currentHour < wakingHoursStart) { + final minutesUntil = ((wakingHoursStart - currentHour) * 60).round(); + return minutesUntil; + } + + if (currentHour >= wakingHoursEnd) { + final hoursUntilTomorrow = 24 - currentHour + wakingHoursStart; + return (hoursUntilTomorrow * 60).round(); + } + + final hoursSinceWakeup = currentHour - wakingHoursStart; + final currentInterval = (hoursSinceWakeup / intervalHours).floor(); + final nextDrinkHour = wakingHoursStart + ((currentInterval + 1) * intervalHours); + + if (nextDrinkHour >= wakingHoursEnd) { + final hoursUntilTomorrow = 24 - currentHour + wakingHoursStart; + return (hoursUntilTomorrow * 60).round(); + } + + final minutesUntil = ((nextDrinkHour - currentHour) * 60).round(); + return minutesUntil.clamp(0, 1440); + } + // Allow updating consumed and goal through the vm so UI doesn't manipulate internal fields directly void setTotalConsumedMl(int ml) { _totalConsumedMl = ml; @@ -401,8 +963,150 @@ class WaterMonitorViewModel extends ChangeNotifier { } } + // Returns the currently selected cup capacity in ml (0 if none) + int get selectedCupCapacityMl => selectedCup?.capacityMl ?? 0; + + /// Increment the consumed amount by the currently selected cup capacity. + /// This centralizes business logic here so UI just calls this method. + void incrementBySelectedCup() { + if (selectedCup != null) { + addConsumedMl(selectedCup!.capacityMl); + } + } + + /// Decrement the consumed amount by the currently selected cup capacity. + /// Ensures value is clamped inside the VM (subtractConsumedMl already clamps). + void decrementBySelectedCup() { + if (selectedCup != null) { + subtractConsumedMl(selectedCup!.capacityMl); + } + } + void resetDaily() { _totalConsumedMl = 0; notifyListeners(); } + + /// Insert user activity (record water intake) + Future insertUserActivity({required int quantityIntake}) async { + try { + _isLoading = true; + notifyListeners(); + + // Get authenticated user info + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return false; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst("0", ""); + final identification = authenticated.patientIdentificationNo ?? ''; + + // Create request model + final requestModel = InsertUserActivityRequestModel( + identificationNo: identification, + mobileNumber: mobile, + quantityIntake: quantityIntake, + ); + + // Call the API + final result = await waterMonitorRepo.insertUserActivity(requestModel: requestModel); + + return result.fold( + (failure) { + log('Error inserting user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + log("Insert user activity success: ${apiModel.data.toString()}"); + // Update consumed amount 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 && progressData.containsKey('QuantityConsumed')) { + final consumed = progressData['QuantityConsumed']; + if (consumed != null) { + _totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl; + } + } + + fetchUserProgressForMonitoring(); + } + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + log('Exception in insertUserActivity: $e'); + _isLoading = false; + notifyListeners(); + return false; + } + } + + /// Undo last user activity + Future undoUserActivity() async { + try { + _isLoading = true; + notifyListeners(); + + // Get authenticated user info + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return false; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst("0", ""); + final identification = authenticated.patientIdentificationNo ?? ''; + + // Create request model + final requestModel = UndoUserActivityRequestModel( + progress: 1, + mobileNumber: mobile, + identificationNo: identification, + ); + + // Call the API + final result = await waterMonitorRepo.undoUserActivity(requestModel: requestModel); + + return result.fold( + (failure) { + log('Error undoing user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + log("Undo user activity success: ${apiModel.data.toString()}"); + + // Update consumed amount 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 && progressData.containsKey('QuantityConsumed')) { + final consumed = progressData['QuantityConsumed']; + if (consumed != null) { + _totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl; + } + } + } + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + log('Exception in undoUserActivity: $e'); + _isLoading = false; + notifyListeners(); + return false; + } + } } diff --git a/lib/presentation/water_monitor/water_consumption_screen.dart b/lib/presentation/water_monitor/water_consumption_screen.dart index 198f65b..625839e 100644 --- a/lib/presentation/water_monitor/water_consumption_screen.dart +++ b/lib/presentation/water_monitor/water_consumption_screen.dart @@ -4,16 +4,21 @@ import 'dart:math' as math; 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/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/bottle_shape_clipper.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart'; import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_consumption_progress_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/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; class WaterConsumptionScreen extends StatefulWidget { const WaterConsumptionScreen({super.key}); @@ -26,19 +31,16 @@ class _WaterConsumptionScreenState extends State { @override void initState() { super.initState(); - // Initialize VM once the widget is mounted - WidgetsBinding.instance.addPostFrameCallback((_) { - final vm = Provider.of(context, listen: false); - vm.initialize(); - // optional: log current progress for debugging - // ignore: avoid_print - log('WaterMonitor initialized: consumed=${vm.totalConsumedMl}, goal=${vm.dailyGoalMl}, progress=${vm.progressPercent}'); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final vm = context.read(); + await vm.initialize(); + log('WaterMonitor initialized: consumed=${vm.totalConsumedMl}, goal=${vm.dailyGoalMl}, progress=${vm.progressPercent}'); }); } - _buildHydrationTipsWidget() { + Widget _buildHydrationTipsWidget() { return Container( - margin: EdgeInsets.symmetric(horizontal: 24.h), + margin: EdgeInsets.symmetric(horizontal: 24.w), padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -56,39 +58,32 @@ class _WaterConsumptionScreenState extends State { ], ), SizedBox(height: 8.h), - " • ${"Drink before you feel thirsty"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + " • ${"Drink before you feel thirsty"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), SizedBox(height: 4.h), - " • ${"Keep a refillable bottle next to you"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + " • ${"Keep a refillable bottle next to you"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), SizedBox(height: 4.h), - " • ${"Track your daily intake to stay motivated"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + " • ${"Track your daily intake to stay motivated"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), SizedBox(height: 4.h), - " • ${"Choose sparkling water instead of soda"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + " • ${"Choose sparkling water instead of soda"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), SizedBox(height: 8.h), ], ), ); } - _buildStatsColumn() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Next Drink Time".toText18(weight: FontWeight.w600, color: AppColors.textColor), - "03:00 AM".toText32(weight: FontWeight.w600, color: AppColors.blueColor), - SizedBox(height: 12.h), - _buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "2000ml"), - SizedBox(height: 8.h), - _buildStatusColumn(title: "Remaining".needTranslation, subTitle: "300ml"), - SizedBox(height: 8.h), - _buildStatusColumn(title: "Completed".needTranslation, subTitle: "90%", subTitleColor: AppColors.successColor), - SizedBox(height: 8.h), - _buildStatusColumn(title: "Hydration Status".needTranslation, subTitle: "Hydrated", subTitleColor: AppColors.blueColor), - SizedBox(height: 8.h), - ], - ); - } - - _buildWaterIntakeSummaryWidget() { + Widget _buildWaterIntakeSummaryWidget() { return Container( width: double.infinity, padding: EdgeInsets.all(24.w), @@ -102,44 +97,68 @@ class _WaterConsumptionScreenState extends State { ], ), ), - child: LayoutBuilder( - builder: (context, constraints) { - // Responsive: stack vertically on small screens - final isSmallScreen = constraints.maxWidth < 600; + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: isTablet ? 2 : 3, + child: Consumer(builder: (context, vm, _) { + if (vm.isLoading) { + // show shimmer placeholder while fetching server-side user details + return _buildLoadingShimmer(isForHistory: false); + } - if (isSmallScreen) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(flex: 3, child: _buildStatsColumn()), - SizedBox(width: 16.w), - Expanded(flex: 2, child: _buildWaterBottleWidget()), - ], - ); - } else { - // Larger screens: more space for bottle - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible(flex: 2, child: _buildStatsColumn()), - SizedBox(width: 32.w), - Flexible(flex: 1, child: _buildWaterBottleWidget()), - ], - ); - } - }, + final goalMl = vm.dailyGoalMl; + final consumed = vm.totalConsumedMl; + final remaining = (goalMl - consumed) > 0 ? (goalMl - consumed) : 0; + final completedPercent = "${(vm.progress * 100).clamp(0.0, 100.0).toStringAsFixed(0)}%"; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Next Drink Time".toText18(weight: FontWeight.w600, color: AppColors.textColor), + vm.nextDrinkTime.toText32(weight: FontWeight.w600, color: AppColors.blueColor), + SizedBox(height: 12.h), + _buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "${goalMl}ml"), + SizedBox(height: 8.h), + _buildStatusColumn(title: "Remaining".needTranslation, subTitle: "${remaining}ml"), + SizedBox(height: 8.h), + _buildStatusColumn(title: "Completed".needTranslation, subTitle: completedPercent, subTitleColor: AppColors.successColor), + SizedBox(height: 8.h), + _buildStatusColumn( + title: "Hydration Status".needTranslation, + subTitle: vm.hydrationStatus, + subTitleColor: vm.hydrationStatusColor, + ), + ], + ); + }), + ), + SizedBox(width: isTablet ? 32 : 16.w), + Expanded( + flex: isTablet ? 1 : 2, + child: _buildWaterBottleWidget(), + ), + ], + ), + _buildBottomActionWidgets(), + ], ), ); } - _buildStatusColumn({required String title, required String subTitle, Color? subTitleColor}) { + Widget _buildStatusColumn({required String title, required String subTitle, Color? subTitleColor}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "$title:".toText16(weight: FontWeight.w500, color: AppColors.textColor), - subTitle.toText12(fontWeight: FontWeight.w600, color: subTitleColor ?? AppColors.greyTextColor), + "$title: ".toText16(weight: FontWeight.w500, color: AppColors.textColor), + subTitle.toText12( + fontWeight: FontWeight.w600, + color: subTitleColor ?? AppColors.greyTextColor, + ), ], ); } @@ -147,86 +166,507 @@ class _WaterConsumptionScreenState extends State { Widget _buildWaterBottleWidget() { return Consumer( builder: (context, vm, _) { - // final progressPercent = 100.0; final progressPercent = (vm.progress * 100).clamp(0.0, 100.0); - log("progressPercent: ${progressPercent}"); - // SVG original dimensions - final svgOriginalWidth = 143.w; - final svgOriginalHeight = 315.h; + log("progressPercent: $progressPercent"); + + // SVG aspect ratio + const svgAspectRatio = 315.0 / 143.0; // ~2.2 - // Responsive sizing - scale based on screen width but maintain aspect ratio - final bottleWidth = 120.w; // Adjust this for your layout - final aspectRatio = svgOriginalHeight / svgOriginalWidth; // 2. 2027 - final bottleHeight = bottleWidth * aspectRatio; + // Responsive bottle sizing with device-specific constraints + double bottleWidth; + if (isTablet) { + // Tablet: use logical pixels (not scaled) with a reasonable max + bottleWidth = math.min(SizeUtils.width * 0.15, 180.0); // 15% of width, max 180 + } else if (isFoldable) { + // Foldable: moderate scaling + bottleWidth = math.min(100.w, 160.0); + } else { + // Phone: use your responsive . w with a cap + bottleWidth = math.min(120.w, 140.0); + } - // Exact fillable area based on SVG paths (provided by VM calibration) - final fillableHeightPercent = vm.fillableHeightPercent; - final fillableWidthPercent = 0.8; // 80.7% is body width - final bottomPaddingPercent = 0; // 0% padding from bottom (8px of 315px) + final bottleHeight = bottleWidth * svgAspectRatio; + + // Fillable area percentages (calibrated to your SVG) + final fillableHeightPercent = 0.7; + const fillableWidthPercent = 0.8; final fillableHeight = bottleHeight * fillableHeightPercent; final fillableWidth = bottleWidth * fillableWidthPercent; - final bottomPadding = bottleHeight * bottomPaddingPercent; + + // Device-specific positioning offsets + final double leftOffset = isTablet ? 4.w : 8.w; + final double bottomOffset = isTablet ? -65.h : -78.h; + return SizedBox( height: bottleHeight, width: bottleWidth, child: Stack( + fit: StackFit.expand, alignment: Alignment.center, children: [ - // Bottle SVG + // Bottle SVG outline Center( child: Utils.buildSvgWithAssets( icon: AppAssets.waterBottle, height: bottleHeight, width: bottleWidth, - fit: BoxFit.contain, // Important for scaling + fit: BoxFit.contain, ), ), - // Water wave - positioned from bottom - Positioned( - bottom: bottomPadding, - right: 10.w, - child: ClipPath( - clipper: BottleShapeClipper(), // Use custom clipper for exact bottle shape + // Wave and bubbles clipped to bottle shape + Positioned.fill( + left: leftOffset, + bottom: bottomOffset, + child: Center( child: SizedBox( - height: fillableHeight, width: fillableWidth, - child: Align( - alignment: Alignment.bottomCenter, - child: WaterConsumptionProgressWidget( - progress: progressPercent, - // Pass a square size so the painter's vertical range matches the clip area. - // Use the larger of width/height so the waves render correctly and won't be squashed. - size: math.max(fillableWidth, fillableHeight), - // Provide exact container dimensions so the painter draws waves using the bottle's rect - containerWidth: fillableWidth, - containerHeight: fillableHeight, - waveDuration: const Duration(milliseconds: 3000), - waveColor: AppColors.blueColor, + height: fillableHeight, + child: ClipPath( + clipper: BottleShapeClipper(), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // Animated wave + Positioned( + child: WaterConsumptionProgressWidget( + progress: progressPercent, + size: math.min(fillableWidth, fillableHeight), + containerWidth: fillableWidth, + containerHeight: fillableHeight, + waveDuration: const Duration(milliseconds: 3000), + waveColor: AppColors.blueColor, + ), + ), + + // Bubbles (only show if progress > 10%) + if (progressPercent > 10) + Positioned( + bottom: fillableHeight * 0.12, + child: Utils.buildSvgWithAssets( + icon: AppAssets.waterBottleOuterBubbles, + // Cap bubble size based on device type + height: isTablet ? math.min(45.0, fillableHeight * 0.2) : math.min(55.0, fillableHeight * 0.22), + width: fillableWidth * 0.65, + ), + ), + ], ), ), ), ), ), + ], + ), + ); + }, + ); + } + + _buildBottomActionWidgets() { + return Consumer(builder: (context, vm, _) { + final cupAmount = vm.selectedCupCapacityMl; + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () async { + // Undo last activity via API + if (cupAmount > 0) { + await vm.undoUserActivity(); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.minimizeIcon, + height: 20.w, + width: 20.w, + iconColor: AppColors.textColor, + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor, + borderRadius: 99.r, + hasShadow: true, + ), + child: (cupAmount > 0 ? "+ $cupAmount ml" : "+ 0ml").toText12( + fontWeight: FontWeight.w600, + color: AppColors.whiteColor, + ), + ), + InkWell( + onTap: () async { + // Insert user activity via API + if (cupAmount > 0) { + await vm.insertUserActivity(quantityIntake: cupAmount); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.addIconDark, + ), + ), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildActionButton( + onTap: () => showSwitchCupBottomSheet(context), + overlayWidget: AppAssets.refreshIcon, + title: "Switch Cup".needTranslation, + icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w), + ), + _buildActionButton( + onTap: () {}, + title: "Add Water".needTranslation, + icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w), + ), + _buildActionButton( + onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen), + title: "Settings".needTranslation, + icon: Icon( + Icons.settings, + color: AppColors.blueColor, + size: 24.w, + ), + ), + ], + ), + ], + ); + }); + } - // Bubbles positioned dynamically based on water level - if (progressPercent > 10) + _buildActionButton({String? overlayWidget, required String title, required Widget icon, required VoidCallback onTap}) { + return InkWell( + onTap: onTap, + child: Column( + children: [ + Stack( + children: [ + Container( + height: 46.w, + width: 46.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor.withValues(alpha: 0.14), + borderRadius: 12.r, + hasShadow: true, + ), + child: Center(child: icon), + ), + if (overlayWidget != null) ...[ Positioned( - bottom: 40.h, - child: Utils.buildSvgWithAssets( - icon: AppAssets.waterBottleOuterBubbles, - height: 60.h, - width: fillableWidth * 0.7, + top: 0, + right: 0, + child: Container( + padding: EdgeInsets.all(2.w), + height: 16.w, + width: 16.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor, + borderRadius: 100.r, + hasShadow: true, + ), + child: Center(child: Utils.buildSvgWithAssets(icon: AppAssets.refreshIcon, iconColor: AppColors.whiteColor)), ), ), + ] ], ), + SizedBox(height: 4.h), + title.toText10(), + ], + ), + ); + } + + Widget buildHistoryListTile({required String title, required String subTitle}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14(weight: FontWeight.w500, color: AppColors.labelTextColor), + subTitle.toText18(weight: FontWeight.w600), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon) + ], + ).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, WaterMonitorViewModel viewModel, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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, + ), + ), + ), + ), + SizedBox(width: 8.w), + InkWell( + onTap: () => _showHistoryDurationBottomsheet(context, viewModel), + child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_calendar_icon, height: 24.h, width: 24.h)) + ], + ), + ], + ), + if (!viewModel.isGraphView) + _buildHistoryListView(viewModel) + else + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 32.h), + child: "Graph view coming soon".toText14(color: AppColors.greyTextColor), + ), + ), + ], + ); + }), + ); + } + + 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: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: isForHistory ? 60.h : 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.r), + ), + ), ); }, ); } + Widget _buildHistoryListView(WaterMonitorViewModel viewModel) { + final selectedDuration = viewModel.selectedDurationFilter; + + // Build list items based on duration + List listItems = []; + + if (selectedDuration == 'Daily') { + if (viewModel.todayProgressList.isNotEmpty) { + final todayData = viewModel.todayProgressList.first; + listItems.add( + buildHistoryListTile( + title: "Today's Progress", + subTitle: "${todayData.quantityConsumed?.toStringAsFixed(0) ?? '0'} ml / ${todayData.quantityLimit?.toStringAsFixed(0) ?? '0'} ml", + ), + ); + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + listItems.add( + buildHistoryListTile( + title: "Percentage Completed", + subTitle: "${todayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for today".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } else if (selectedDuration == 'Weekly') { + if (viewModel.weekProgressList.isNotEmpty) { + for (var dayData in viewModel.weekProgressList) { + listItems.add( + buildHistoryListTile( + title: dayData.dayName ?? 'Unknown', + subTitle: "${dayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + if (dayData != viewModel.weekProgressList.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for this week".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } else if (selectedDuration == 'Monthly') { + if (viewModel.monthProgressList.isNotEmpty) { + for (var monthData in viewModel.monthProgressList) { + listItems.add( + buildHistoryListTile( + title: monthData.monthName ?? 'Unknown', + subTitle: "${monthData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + if (monthData != viewModel.monthProgressList.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for this year".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } + + // Return scrollable list with min and max height constraints + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: 80.h, + maxHeight: 270.h, + ), + child: viewModel.isLoading + ? _buildLoadingShimmer().paddingOnly(top: 16.h) + : listItems.isEmpty + ? Center( + child: "No history data available".toText14(color: AppColors.greyTextColor), + ) + : ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + itemCount: listItems.length, + separatorBuilder: (context, index) => SizedBox.shrink(), + itemBuilder: (context, index) => listItems[index], + ), + ); + } + + // 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: 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: () {}, + ); + } + + void _showHistoryDurationBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Duration".needTranslation, + items: viewModel.durationFilters, + selectedValue: viewModel.selectedDurationFilter, + onSelected: viewModel.setFilterDuration, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -245,7 +685,7 @@ class _WaterConsumptionScreenState extends State { text: "Set Reminder".needTranslation, textColor: AppColors.successColor, backgroundColor: AppColors.successLightBgColor, - onPressed: () => {}, + onPressed: () {}, icon: AppAssets.bell, iconColor: AppColors.successColor, borderRadius: 12.r, @@ -259,11 +699,13 @@ class _WaterConsumptionScreenState extends State { SizedBox(height: 16.h), _buildWaterIntakeSummaryWidget(), SizedBox(height: 16.h), + _buildHistoryGraphOrList(), + SizedBox(height: 16.h), _buildHydrationTipsWidget(), + SizedBox(height: 16.h), ], ), ), ); - // ); } } diff --git a/lib/presentation/water_monitor/water_monitor_settings_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_screen.dart index e49c8a3..5f1bce5 100644 --- a/lib/presentation/water_monitor/water_monitor_settings_screen.dart +++ b/lib/presentation/water_monitor/water_monitor_settings_screen.dart @@ -6,30 +6,16 @@ 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/water_monitor/water_monitor_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/cup_bottomsheet_widgets.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:provider/provider.dart'; -class WaterMonitorSettingsScreen extends StatefulWidget { +class WaterMonitorSettingsScreen extends StatelessWidget { const WaterMonitorSettingsScreen({super.key}); - @override - State createState() => _WaterMonitorSettingsScreenState(); -} - -class _WaterMonitorSettingsScreenState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().initialize(); - }); - } - - void _showSnackbar(String text) { + void _showSnackbar(String text, BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(text), @@ -276,13 +262,13 @@ class _WaterMonitorSettingsScreenState extends State child: CustomButton( text: "Save".needTranslation, onPressed: () async { - showSwitchCupBottomSheet(context); - return; final success = await viewModel.saveSettings(); if (!success && viewModel.validationError != null) { _showSnackBar(context, viewModel.validationError!); } else if (success) { - _showSnackBar(context, "Saved Successfully".needTranslation); + _showSnackBar(context, "Settings saved successfully".needTranslation); + // Navigate back on success + Navigator.pop(context); } }, borderRadius: 12.r, diff --git a/lib/widgets/shimmer/common_shimmer_widget.dart b/lib/widgets/shimmer/common_shimmer_widget.dart index d6a2906..3d935cb 100644 --- a/lib/widgets/shimmer/common_shimmer_widget.dart +++ b/lib/widgets/shimmer/common_shimmer_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -11,9 +12,7 @@ class CommonShimmerWidget extends StatelessWidget { return SizedBox( child: Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), + borderRadius: BorderRadius.all(Radius.circular(10.r)), border: Border.all(color: AppColors.lightGreyEFColor, width: 1), boxShadow: [ BoxShadow( @@ -27,7 +26,11 @@ class CommonShimmerWidget extends StatelessWidget { padding: const EdgeInsets.all(12.0), child: Column( children: [ - Container(height: 100).toShimmer(), + Container( + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(24.r)), + )).toShimmer(), 16.height, Container(height: 24).toShimmer(), 16.height,