Added Graph

pull/135/head
faizatflutter 2 weeks ago
parent 333046951c
commit d4dd475e0b

@ -63,6 +63,7 @@ class CacheConst {
static const String pharmacyAutorzieToken = 'PHARMACY_AUTORZIE_TOKEN';
static const String h2oUnit = 'H2O_UNIT';
static const String h2oReminder = 'H2O_REMINDER';
static const String waterReminderEnabled = 'WATER_REMINDER_ENABLED';
static const String livecareClinicData = 'LIVECARE_CLINIC_DATA';
static const String doctorScheduleDateSel = 'DOCTOR_SCHEDULE_DATE_SEL';
static const String appointmentHistoryMedical = 'APPOINTMENT_HISTORY_MEDICAL';

@ -1,25 +1,25 @@
///class used to provide value for the [DynamicResultChart] to plot the values
class DataPoint {
///values that is displayed on the graph and dot is plotted on this
final double value;
///label shown on the bottom of the graph
String label;
String referenceValue;
String actualValue;
String? unitOfMeasurement ;
String? unitOfMeasurement;
DateTime time;
String displayTime;
DataPoint(
{required this.value,
DataPoint({
required this.value,
required this.label,
required this.referenceValue,
required this.actualValue,
required this.time,
required this.displayTime,
this.unitOfMeasurement
this.unitOfMeasurement,
this.referenceValue = '',
});
@override

@ -1,4 +1,5 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
@ -47,7 +48,6 @@ 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';
import 'package:hmg_patient_app_new/services/cache_service.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
@ -56,6 +56,7 @@ import 'package:hmg_patient_app_new/services/firebase_service.dart';
import 'package:hmg_patient_app_new/services/localauth_service.dart';
import 'package:hmg_patient_app_new/services/logger_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/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:local_auth/local_auth.dart';
import 'package:logger/web.dart';
@ -104,6 +105,13 @@ class AppDependencies {
final sharedPreferences = await SharedPreferences.getInstance();
getIt.registerLazySingleton<CacheService>(() => CacheServiceImp(sharedPreferences: sharedPreferences, loggerService: getIt()));
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
getIt.registerLazySingleton<NotificationService>(() => NotificationServiceImp(
flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin,
loggerService: getIt(),
));
getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt()));
getIt.registerLazySingleton<LocalAuthService>(
() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()),
@ -140,7 +148,8 @@ class AppDependencies {
() => RadiologyViewModel(radiologyRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()),
);
getIt.registerLazySingleton<PrescriptionsViewModel>(() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<PrescriptionsViewModel>(
() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<InsuranceViewModel>(() => InsuranceViewModel(insuranceRepo: getIt(), errorHandlerService: getIt()));
@ -149,19 +158,15 @@ class AppDependencies {
getIt.registerLazySingleton<AppointmentRatingViewModel>(
() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
getIt.registerLazySingleton<AppointmentRatingViewModel>(() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()));
getIt.registerLazySingleton<PayfortViewModel>(
() => PayfortViewModel(
payfortRepo: getIt(),
errorHandlerService: getIt(),
),
() => PayfortViewModel(payfortRepo: getIt(), errorHandlerService: getIt()),
);
getIt.registerLazySingleton<HabibWalletViewModel>(
() => HabibWalletViewModel(
habibWalletRepo: getIt(),
errorHandlerService: getIt(),
errorHandlerService: getIt()
),
);
@ -201,6 +206,7 @@ class AppDependencies {
errorHandlerService: getIt(),
localAuthService: getIt()),
);
getIt.registerLazySingleton<ProfileSettingsViewModel>(() => ProfileSettingsViewModel());
getIt.registerLazySingleton<DateRangeSelectorRangeViewModel>(() => DateRangeSelectorRangeViewModel());
@ -208,10 +214,7 @@ class AppDependencies {
getIt.registerLazySingleton<DoctorFilterViewModel>(() => DoctorFilterViewModel());
getIt.registerLazySingleton<AppointmentViaRegionViewmodel>(
() => AppointmentViaRegionViewmodel(
navigationService: getIt(),
appState: getIt(),
),
() => AppointmentViaRegionViewmodel(navigationService: getIt(), appState: getIt()),
);
getIt.registerLazySingleton<EmergencyServicesViewModel>(
@ -222,7 +225,8 @@ class AppDependencies {
appState: getIt(),
errorHandlerService: getIt(),
appointmentRepo: getIt(),
dialogService: getIt()),
dialogService: getIt(),
),
);
getIt.registerLazySingleton<LocationViewModel>(
@ -235,9 +239,7 @@ class AppDependencies {
getIt.registerLazySingleton<HealthCalcualtorViewModel>(() => HealthCalcualtorViewModel());
getIt.registerLazySingleton<TodoSectionViewModel>(
() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()),
);
getIt.registerLazySingleton<TodoSectionViewModel>(() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<SymptomsCheckerViewModel>(
() => SymptomsCheckerViewModel(
@ -246,6 +248,7 @@ class AppDependencies {
appState: getIt(),
),
);
getIt.registerLazySingleton<HmgServicesViewModel>(
() => HmgServicesViewModel(
bookAppointmentsRepo: getIt(),
@ -265,19 +268,8 @@ class AppDependencies {
),
);
getIt.registerLazySingleton<HealthProvider>(
() => HealthProvider(),
);
getIt.registerLazySingleton<HealthProvider>(() => HealthProvider());
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt()));
// Screen-specific VMs Factory
// getIt.registerFactory<BookAppointmentsViewModel>(
// () => BookAppointmentsViewModel(
// bookAppointmentsRepo: getIt(),
// dialogService: getIt(),
// errorHandlerService: getIt(),
// ),
// );
}
}

@ -12,8 +12,9 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:huawei_location/huawei_location.dart' as HmsLocation show FusedLocationProviderClient, Location, LocationSettingsRequest, LocationRequest;
import 'package:location/location.dart' show Location, PermissionStatus, LocationData;
import 'package:huawei_location/huawei_location.dart' as HmsLocation
show FusedLocationProviderClient, Location, LocationSettingsRequest, LocationRequest;
import 'package:location/location.dart' show Location;
import 'package:permission_handler/permission_handler.dart' show Permission, PermissionListActions, PermissionStatusGetters, openAppSettings;
class LocationUtils {
@ -59,37 +60,22 @@ class LocationUtils {
// }
void getLocation(
{Function(LatLng)? onSuccess,
VoidCallback? onFailure,
bool isShowConfirmDialog = false,
VoidCallback? onLocationDeniedForever}) async {
{Function(LatLng)? onSuccess, VoidCallback? onFailure, bool isShowConfirmDialog = false, VoidCallback? onLocationDeniedForever}) async {
this.isShowConfirmDialog = isShowConfirmDialog;
if (Platform.isIOS) {
getCurrentLocation(
onFailure: onFailure,
onSuccess: onSuccess,
onLocationDeniedForever: onLocationDeniedForever);
getCurrentLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever);
return;
}
if (await isGMSDevice ?? true) {
getCurrentLocation(
onFailure: onFailure,
onSuccess: onSuccess,
onLocationDeniedForever: onLocationDeniedForever);
getCurrentLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever);
return;
}
getHMSLocation(
onFailure: onFailure,
onSuccess: onSuccess,
onLocationDeniedForever: onLocationDeniedForever);
getHMSLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever);
}
void getCurrentLocation(
{Function(LatLng)? onSuccess,
VoidCallback? onFailure,
VoidCallback? onLocationDeniedForever}) async {
void getCurrentLocation({Function(LatLng)? onSuccess, VoidCallback? onFailure, VoidCallback? onLocationDeniedForever}) async {
var location = Location();
bool isLocationEnabled = await location.serviceEnabled();
@ -113,14 +99,12 @@ class LocationUtils {
}
} else if (permissionGranted == LocationPermission.deniedForever) {
appState.resetLocation();
if(onLocationDeniedForever == null && isShowConfirmDialog){
if (onLocationDeniedForever == null && isShowConfirmDialog) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!),
navigationService.navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText:
"Please grant location permission from app settings to see better results"
.needTranslation,
loadingText: "Please grant location permission from app settings to see better results".needTranslation,
isShowActionButtons: true,
onCancelTap: () {
navigationService.pop();
@ -253,10 +237,7 @@ class LocationUtils {
appState.userLong = locationData.longitude;
}
void getHMSLocation(
{VoidCallback? onFailure,
Function(LatLng p1)? onSuccess,
VoidCallback? onLocationDeniedForever}) async {
void getHMSLocation({VoidCallback? onFailure, Function(LatLng p1)? onSuccess, VoidCallback? onLocationDeniedForever}) async {
try {
var location = Location();
HmsLocation.FusedLocationProviderClient locationService = HmsLocation.FusedLocationProviderClient()..initFusedLocationService();
@ -279,14 +260,12 @@ class LocationUtils {
permissionGranted = await Geolocator.requestPermission();
if (permissionGranted == LocationPermission.deniedForever) {
appState.resetLocation();
if(onLocationDeniedForever == null && isShowConfirmDialog){
if (onLocationDeniedForever == null && isShowConfirmDialog) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!),
navigationService.navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText:
"Please grant location permission from app settings to see better results"
.needTranslation,
loadingText: "Please grant location permission from app settings to see better results".needTranslation,
isShowActionButtons: true,
onCancelTap: () {
navigationService.pop();

@ -14,8 +14,8 @@ class PostParamsModel {
String? sessionID;
String? setupID;
PostParamsModel(
{this.versionID,
PostParamsModel({
this.versionID,
this.channel,
this.languageID,
this.logInTokenID,
@ -26,7 +26,8 @@ class PostParamsModel {
this.latitude,
this.longitude,
this.deviceTypeID,
this.sessionID});
this.sessionID,
});
PostParamsModel.fromJson(Map<String, dynamic> json) {
versionID = json['VersionID'];

@ -6,8 +6,6 @@ class DateUtil {
/// convert String To Date function
/// [date] String we want to convert
static DateTime convertStringToDate(String? date) {
if (date == null) return DateTime.now();
if (date.isEmpty) return DateTime.now();
@ -522,6 +520,64 @@ class DateUtil {
}
return "";
}
/// Get short month name from full month name
/// [monthName] Full month name like "January"
/// Returns short form like "Jan"
static String getShortMonthName(String monthName) {
switch (monthName.toLowerCase()) {
case 'january':
return 'Jan';
case 'february':
return 'Feb';
case 'march':
return 'Mar';
case 'april':
return 'Apr';
case 'may':
return 'May';
case 'june':
return 'Jun';
case 'july':
return 'Jul';
case 'august':
return 'Aug';
case 'september':
return 'Sep';
case 'october':
return 'Oct';
case 'november':
return 'Nov';
case 'december':
return 'Dec';
default:
return monthName; // Return as-is if not recognized
}
}
/// Get short weekday name from full weekday name
/// [weekDayName] Full weekday name like "Monday"
/// Returns short form like "Mon"
static String getShortWeekDayName(String weekDayName) {
switch (weekDayName.toLowerCase().trim()) {
case 'monday':
return 'Mon';
case 'tuesday':
return 'Tue';
case 'wednesday':
return 'Wed';
case 'thursday':
return 'Thu';
case 'friday':
return 'Fri';
case 'saturday':
return 'Sat';
case 'sunday':
return 'Sun';
default:
return weekDayName; // Return as-is if not recognized
}
}
}
extension OnlyDate on DateTime {

@ -1,191 +0,0 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
class LocalNotification {
Function(String payload)? _onNotificationClick;
static LocalNotification? _instance;
static LocalNotification? getInstance() {
return _instance;
}
static init({required Function(String payload) onNotificationClick}) {
if (_instance == null) {
_instance = LocalNotification();
_instance?._onNotificationClick = onNotificationClick;
_instance?._initialize();
} else {
// assert(false,(){
// //TODO fix it
// "LocalNotification Already Initialized";
// });
}
}
_initialize() async {
try {
var initializationSettingsAndroid = new AndroidInitializationSettings('app_icon');
var initializationSettingsIOS = DarwinInitializationSettings();
var initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) {
switch (notificationResponse.notificationResponseType) {
case NotificationResponseType.selectedNotification:
// selectNotificationStream.add(notificationResponse.payload);
break;
case NotificationResponseType.selectedNotificationAction:
// if (notificationResponse.actionId == navigationActionId) {
// selectNotificationStream.add(notificationResponse.payload);
// }
break;
}
},
// onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
} catch (ex) {
print(ex.toString());
}
// flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse)
// {
// switch (notificationResponse.notificationResponseType) {
// case NotificationResponseType.selectedNotification:
// // selectNotificationStream.add(notificationResponse.payload);
// break;
// case NotificationResponseType.selectedNotificationAction:
// // if (notificationResponse.actionId == navigationActionId) {
// // selectNotificationStream.add(notificationResponse.payload);
// }
// // break;
// },}
//
// ,
//
// );
}
// void notificationTapBackground(NotificationResponse notificationResponse) {
// // ignore: avoid_print
// print('notification(${notificationResponse.id}) action tapped: '
// '${notificationResponse.actionId} with'
// ' payload: ${notificationResponse.payload}');
// if (notificationResponse.input?.isNotEmpty ?? false) {
// // ignore: avoid_print
// print('notification action tapped with input: ${notificationResponse.input}');
// }
// }
var _random = new Random();
_randomNumber({int from = 100000}) {
return _random.nextInt(from);
}
_vibrationPattern() {
var vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
vibrationPattern[1] = 1000;
vibrationPattern[2] = 5000;
vibrationPattern[3] = 2000;
return vibrationPattern;
}
Future? showNow({required String title, required String subtitle, required String payload}) {
Future.delayed(Duration(seconds: 1)).then((result) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'com.hmg.local_notification',
'HMG',
channelDescription: 'HMG',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
vibrationPattern: _vibrationPattern(),
ongoing: true,
autoCancel: false,
usesChronometer: true,
when: DateTime.now().millisecondsSinceEpoch - 120 * 1000,
);
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(25613, title, subtitle, platformChannelSpecifics, payload: payload).catchError((err) {
print(err);
});
});
}
Future scheduleNotification({required DateTime scheduledNotificationDateTime, required String title, required String description}) async {
///vibrationPattern
var vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
vibrationPattern[1] = 1000;
vibrationPattern[2] = 5000;
vibrationPattern[3] = 2000;
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('active-prescriptions', 'ActivePrescriptions',
// channelDescription: 'ActivePrescriptionsDescription',
// // icon: 'secondary_icon',
// sound: RawResourceAndroidNotificationSound('slow_spring_board'),
//
// ///change it to be as ionic
// // largeIcon: DrawableResourceAndroidBitmap('sample_large_icon'),///change it to be as ionic
// vibrationPattern: vibrationPattern,
// enableLights: true,
// color: const Color.fromARGB(255, 255, 0, 0),
// ledColor: const Color.fromARGB(255, 255, 0, 0),
// ledOnMs: 1000,
// ledOffMs: 500);
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails(sound: 'slow_spring_board.aiff');
// /change it to be as ionic
// var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.schedule(0, title, description, scheduledNotificationDateTime, platformChannelSpecifics);
}
///Repeat notification every day at approximately 10:00:00 am
Future showDailyAtTime() async {
// var time = Time(10, 0, 0);
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('repeatDailyAtTime channel id', 'repeatDailyAtTime channel name', channelDescription: 'repeatDailyAtTime description');
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
// var platformChannelSpecifics = NotificationDetails(
// androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.showDailyAtTime(
// 0,
// 'show daily title',
// 'Daily notification shown at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}',
// time,
// platformChannelSpecifics);
}
///Repeat notification weekly on Monday at approximately 10:00:00 am
Future showWeeklyAtDayAndTime() async {
// var time = Time(10, 0, 0);
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('show weekly channel id', 'show weekly channel name', channelDescription: 'show weekly description');
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
// var platformChannelSpecifics = NotificationDetails(
// androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
// await flutterLocalNotificationsPlugin.showWeeklyAtDayAndTime(
// 0,
// 'show weekly title',
// 'Weekly notification shown on Monday at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}',
// Day.Monday,
// time,
// platformChannelSpecifics);
}
String _toTwoDigitString(int value) {
return value.toString().padLeft(2, '0');
}
Future cancelNotification() async {
await flutterLocalNotificationsPlugin.cancel(0);
}
Future cancelAllNotifications() async {
await flutterLocalNotificationsPlugin.cancelAll();
}
}

@ -15,16 +15,11 @@ import 'package:flutter_callkit_incoming/entities/notification_params.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_ios_voip_kit_karmm/call_state_type.dart';
import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart';
// import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hmg_patient_app_new/core/utils/local_notifications.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
import '../cache_consts.dart';
// |--> Push Notification Background
@pragma('vm:entry-point')
Future<dynamic> backgroundMessageHandler(dynamic message) async {
@ -36,7 +31,7 @@ Future<dynamic> backgroundMessageHandler(dynamic message) async {
// showCallkitIncoming(message);
_incomingCall(message.data);
return;
} else {}
}
}
callPage(String sessionID, String token) async {}
@ -323,7 +318,7 @@ class PushNotificationHandler {
if (fcmToken != null) onToken(fcmToken);
// }
} catch (ex) {
print("Notification Exception: " + ex.toString());
print("Notification Exception: $ex");
}
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
}
@ -331,7 +326,7 @@ class PushNotificationHandler {
if (Platform.isIOS) {
final permission = await FirebaseMessaging.instance.requestPermission();
await FirebaseMessaging.instance.getAPNSToken().then((value) async {
log("APNS token: " + value.toString());
log("APNS token: $value");
await Utils.saveStringFromPrefs(CacheConst.apnsToken, value.toString());
});
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
@ -378,14 +373,14 @@ class PushNotificationHandler {
});
FirebaseMessaging.instance.getToken().then((String? token) {
print("Push Notification getToken: " + token!);
print("Push Notification getToken: ${token!}");
onToken(token!);
}).catchError((err) {
print(err);
});
FirebaseMessaging.instance.onTokenRefresh.listen((fcm_token) {
print("Push Notification onTokenRefresh: " + fcm_token);
print("Push Notification onTokenRefresh: $fcm_token");
onToken(fcm_token);
});
@ -401,7 +396,7 @@ class PushNotificationHandler {
}
newMessage(RemoteMessage remoteMessage) async {
print("Remote Message: " + remoteMessage.data.toString());
print("Remote Message: ${remoteMessage.data}");
if (remoteMessage.data.isEmpty) {
return;
}
@ -427,7 +422,7 @@ class PushNotificationHandler {
}
onToken(String token) async {
print("Push Notification Token: " + token);
print("Push Notification Token: $token");
await Utils.saveStringFromPrefs(CacheConst.pushToken, token);
}
@ -441,9 +436,7 @@ class PushNotificationHandler {
Future<void> requestPermissions() async {
try {
if (Platform.isIOS) {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(alert: true, badge: true, sound: true);
await FirebaseMessaging.instance.requestPermission(alert: true, badge: true, sound: true);
} else if (Platform.isAndroid) {
Map<Permission, PermissionStatus> statuses = await [
Permission.notification,

@ -351,10 +351,10 @@ class Utils {
).center;
}
static Widget getSuccessWidget({String? loadingText}) {
static Widget getSuccessWidget({String? loadingText, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: crossAxisAlignment,
children: [
Lottie.asset(AppAnimations.checkmark, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
@ -876,7 +876,6 @@ class Utils {
launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
static Color getCardBorderColor(int currentQueueStatus) {
switch (currentQueueStatus) {
case 0:

@ -23,14 +23,15 @@ extension CapExtension on String {
extension EmailValidator on String {
Widget get toWidget => Text(this);
Widget toText8({Color? color, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text(
Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) =>
Text(
this,
maxLines: maxlines,
overflow: textOverflow,
style: TextStyle(
fontSize: 8.f,
fontStyle: fontStyle ?? FontStyle.normal,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal),
color: color ?? AppColors.blackColor,
letterSpacing: 0,
),

@ -74,7 +74,6 @@ class WaterMonitorRepoImp implements WaterMonitorRepo {
},
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')) {

@ -4,6 +4,8 @@ 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/cache_consts.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/features/water_monitor/models/insert_user_activity_request_model.dart';
@ -13,7 +15,9 @@ import 'package:hmg_patient_app_new/features/water_monitor/models/user_progress_
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/cache_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/notification_service.dart';
class WaterMonitorViewModel extends ChangeNotifier {
WaterMonitorRepo waterMonitorRepo;
@ -78,9 +82,11 @@ class WaterMonitorViewModel extends ChangeNotifier {
// Network/data
final AppState _appState = GetIt.instance<AppState>();
final NavigationService _navigationService = GetIt.instance<NavigationService>();
final CacheService _cacheService = GetIt.instance<CacheService>();
bool _isLoading = false;
dynamic _userDetailData;
bool _isWaterReminderEnabled = false;
// Progress data lists
List<UserProgressForTodayModel> _todayProgressList = [];
@ -92,6 +98,8 @@ class WaterMonitorViewModel extends ChangeNotifier {
dynamic get userDetailData => _userDetailData;
bool get isWaterReminderEnabled => _isWaterReminderEnabled;
// Getters for progress data
List<UserProgressForTodayModel> get todayProgressList => _todayProgressList;
@ -117,12 +125,19 @@ class WaterMonitorViewModel extends ChangeNotifier {
// Initialize method to be called when needed
Future<void> initialize() async {
_loadSettings();
_initializeDefaultCups();
_loadReminderEnabledState();
await fetchUserDetailsForMonitoring();
// Fetch daily progress to get consumed amount and daily goal
await fetchUserProgressForMonitoring();
}
/// Load reminder enabled state from cache
void _loadReminderEnabledState() {
_isWaterReminderEnabled = _cacheService.getBool(key: CacheConst.waterReminderEnabled) ?? false;
log('Water reminder enabled state loaded: $_isWaterReminderEnabled');
}
/// Map selected duration to ProgressType enum
ProgressType _getProgressTypeFromDuration() {
switch (_selectedDuration) {
@ -150,7 +165,7 @@ class WaterMonitorViewModel extends ChangeNotifier {
return;
}
final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '');
final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst(RegExp(r'^0'), '');
final identification = authenticated.patientIdentificationNo ?? '';
final progressType = _getProgressTypeFromDuration();
@ -177,6 +192,19 @@ class WaterMonitorViewModel extends ChangeNotifier {
}
}
// Update consumed amount and daily goal from API response
if (_todayProgressList.isNotEmpty) {
final todayData = _todayProgressList.first;
if (todayData.quantityConsumed != null) {
_totalConsumedMl = todayData.quantityConsumed!.toInt();
log('Updated consumed from API: $_totalConsumedMl ml');
}
if (todayData.quantityLimit != null) {
_dailyGoalMl = todayData.quantityLimit!.toInt();
log('Updated daily goal from API: $_dailyGoalMl ml');
}
}
break;
case ProgressType.week:
@ -231,19 +259,11 @@ class WaterMonitorViewModel extends ChangeNotifier {
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) {
@ -514,49 +534,6 @@ class WaterMonitorViewModel extends ChangeNotifier {
return null;
}
// Calculate water intake based on user data
double calculateDailyWaterIntake() {
if (!isValid) return 0;
final weight = double.parse(weightController.text.trim());
final weightInKg = _convertWeightToKg(weight, _selectedWeightUnit);
// Base calculation: 30-35ml per kg of body weight
double baseIntake = weightInKg * 33;
// Adjust for activity level
double activityMultiplier = _getActivityMultiplier();
double totalIntake = baseIntake * activityMultiplier;
return totalIntake; // in ml
}
double _convertWeightToKg(double weight, String unit) {
switch (unit) {
case 'kg':
return weight;
case 'lb':
return weight * 0.453592;
default:
return weight;
}
}
double _getActivityMultiplier() {
switch (_selectedActivityLevel) {
case "Almost Inactive (no exercise)":
return 1.0;
case "Lightly active":
return 1.1;
case "Lightly active (1-3) days per week":
return 1.2;
case "Super active (very hard exercise)":
return 1.4;
default:
return 1.1;
}
}
String _getApiCompatibleGender() {
if (_selectedGender == "Female") {
return "F";
@ -624,16 +601,12 @@ class WaterMonitorViewModel extends ChangeNotifier {
notifyListeners();
return false;
},
(apiModel) {
(apiModel) async {
// 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());
}
// Fetch daily progress to get the updated goal and consumed data from API
await fetchUserProgressForMonitoring();
_isLoading = false;
notifyListeners();
@ -678,11 +651,6 @@ class WaterMonitorViewModel extends ChangeNotifier {
return "";
}
// Load settings
Future<void> _loadSettings() async {
notifyListeners();
}
@override
void dispose() {
nameController.dispose();
@ -694,8 +662,8 @@ class WaterMonitorViewModel extends ChangeNotifier {
List<WaterCupModel> _cups = [];
String? _selectedCupId;
int _totalConsumedMl = 1000;
int _dailyGoalMl = 2000;
int _totalConsumedMl = 0; // Loaded from API
int _dailyGoalMl = 0; // Loaded from API
// Calibration: portion of the bottle SVG height that is fillable (0.0 - 1.0)
double _fillableHeightPercent = 0.7;
@ -788,7 +756,6 @@ class WaterMonitorViewModel extends ChangeNotifier {
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";
}
@ -956,13 +923,6 @@ class WaterMonitorViewModel extends ChangeNotifier {
notifyListeners();
}
void addWaterIntake() {
if (selectedCup != null) {
_totalConsumedMl += selectedCup!.capacityMl;
notifyListeners();
}
}
// Returns the currently selected cup capacity in ml (0 if none)
int get selectedCupCapacityMl => selectedCup?.capacityMl ?? 0;
@ -982,11 +942,6 @@ class WaterMonitorViewModel extends ChangeNotifier {
}
}
void resetDaily() {
_totalConsumedMl = 0;
notifyListeners();
}
/// Insert user activity (record water intake)
Future<bool> insertUserActivity({required int quantityIntake}) async {
try {
@ -1023,16 +978,29 @@ class WaterMonitorViewModel extends ChangeNotifier {
},
(apiModel) {
log("Insert user activity success: ${apiModel.data.toString()}");
// Update consumed amount from the response
// 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 && progressData.containsKey('QuantityConsumed')) {
if (progressData is Map) {
// Update consumed amount
if (progressData.containsKey('QuantityConsumed')) {
final consumed = progressData['QuantityConsumed'];
if (consumed != null) {
_totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl;
log('Updated consumed after insert: $_totalConsumedMl ml');
}
}
// Update daily goal
if (progressData.containsKey('QuantityLimit')) {
final limit = progressData['QuantityLimit'];
if (limit != null) {
_dailyGoalMl = (limit is num) ? limit.toInt() : int.tryParse(limit.toString()) ?? _dailyGoalMl;
log('Updated daily goal after insert: $_dailyGoalMl ml');
}
}
}
// Refresh progress data to ensure consistency
fetchUserProgressForMonitoring();
}
@ -1086,13 +1054,25 @@ class WaterMonitorViewModel extends ChangeNotifier {
(apiModel) {
log("Undo user activity success: ${apiModel.data.toString()}");
// Update consumed amount from the response
// 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 && progressData.containsKey('QuantityConsumed')) {
if (progressData is Map) {
// Update consumed amount
if (progressData.containsKey('QuantityConsumed')) {
final consumed = progressData['QuantityConsumed'];
if (consumed != null) {
_totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl;
log('Updated consumed after undo: $_totalConsumedMl ml');
}
}
// Update daily goal
if (progressData.containsKey('QuantityLimit')) {
final limit = progressData['QuantityLimit'];
if (limit != null) {
_dailyGoalMl = (limit is num) ? limit.toInt() : int.tryParse(limit.toString()) ?? _dailyGoalMl;
log('Updated daily goal after undo: $_dailyGoalMl ml');
}
}
}
}
@ -1109,4 +1089,163 @@ class WaterMonitorViewModel extends ChangeNotifier {
return false;
}
}
/// Schedule water reminders based on user's reminder settings
Future<bool> scheduleWaterReminders() async {
try {
final notificationService = getIt.get<NotificationService>();
// Request permission first
final hasPermission = await notificationService.requestPermissions();
if (!hasPermission) {
log('Notification permission denied');
return false;
}
// Calculate reminder times based on _selectedNumberOfReminders
final reminderTimes = _calculateReminderTimes();
if (reminderTimes.isEmpty) {
log('No reminder times calculated');
return false;
}
// Schedule water reminders
await notificationService.scheduleWaterReminders(
reminderTimes: reminderTimes,
title: 'Time to Drink Water! 💧'.needTranslation,
body: 'Stay hydrated! Drink ${selectedCupCapacityMl}ml of water.'.needTranslation,
);
// Save reminder enabled state to cache
_isWaterReminderEnabled = true;
await _cacheService.saveBool(key: CacheConst.waterReminderEnabled, value: true);
log('Scheduled ${reminderTimes.length} water reminders successfully');
notifyListeners();
return true;
} catch (e) {
log('Exception in scheduleWaterReminders: $e');
return false;
}
}
/// Calculate reminder times based on selected number of reminders
List<DateTime> _calculateReminderTimes() {
try {
final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3;
const wakingHoursStart = 6; // 6 AM
const wakingHoursEnd = 22; // 10 PM
const totalWakingHours = wakingHoursEnd - wakingHoursStart;
final intervalHours = totalWakingHours / remindersPerDay;
List<DateTime> times = [];
final now = DateTime.now();
for (int i = 0; i < remindersPerDay; i++) {
final hourDecimal = wakingHoursStart + (i * intervalHours);
final hour = hourDecimal.floor();
final minute = ((hourDecimal - hour) * 60).round();
final reminderTime = DateTime(
now.year,
now.month,
now.day,
hour,
minute,
);
times.add(reminderTime);
}
return times;
} catch (e) {
log('Error calculating reminder times: $e');
return [];
}
}
/// Cancel all water reminders
Future<bool> cancelWaterReminders() async {
try {
final notificationService = GetIt.instance<NotificationService>();
// Get pending notifications and cancel water reminders (IDs 5000-5999)
final pendingNotifications = await notificationService.getPendingNotifications();
for (final notification in pendingNotifications) {
if (notification.id >= 5000 && notification.id < 6000) {
await notificationService.cancelNotification(notification.id);
}
}
// Save reminder disabled state to cache
_isWaterReminderEnabled = false;
await _cacheService.saveBool(key: CacheConst.waterReminderEnabled, value: false);
log('Cancelled all water reminders');
notifyListeners();
return true;
} catch (e) {
log('Exception in cancelWaterReminders: $e');
return false;
}
}
/// Get list of scheduled water reminder times
Future<List<DateTime>> getScheduledReminderTimes() async {
try {
final notificationService = GetIt.instance<NotificationService>();
final pendingNotifications = await notificationService.getPendingNotifications();
List<DateTime> times = [];
for (final notification in pendingNotifications) {
if (notification.id >= 5000 && notification.id < 6000) {
// Note: PendingNotificationRequest doesn't contain scheduled time
// We can only return the calculated times based on current settings
times = _calculateReminderTimes();
break;
}
}
return times;
} catch (e) {
log('Exception in getScheduledReminderTimes: $e');
return [];
}
}
/// Schedule a test notification after 5 seconds
/// Useful for testing notification functionality
Future<bool> scheduleTestNotification() async {
try {
final notificationService = GetIt.instance<NotificationService>();
// Request permission first
final hasPermission = await notificationService.requestPermissions();
if (!hasPermission) {
log('Notification permission denied for test notification');
return false;
}
// Schedule notification 5 seconds from now
final scheduledTime = DateTime.now().add(const Duration(seconds: 5));
await notificationService.scheduleNotification(
id: 9999,
// Use a unique ID for test notifications
title: 'Time to Drink Water! 💧'.needTranslation,
body: 'Stay hydrated! Drink ${selectedCupCapacityMl}ml of water.'.needTranslation,
scheduledDate: scheduledTime,
payload: 'test_notification',
);
log('Test notification scheduled for 5 seconds from now');
return true;
} catch (e) {
log('Exception in scheduleTestNotification: $e');
return false;
}
}
}

@ -33,7 +33,6 @@ import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provi
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_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/health_calculator_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/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -73,7 +72,7 @@ Future<void> callInitializations() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
AppDependencies.addDependencies();
await AppDependencies.addDependencies();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
HttpOverrides.global = MyHttpOverrides();
await callAppStateInitializations();
@ -192,11 +191,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Dr. AlHabib',
builder: (context, mchild) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(1.0),
),
child: mchild!);
return MediaQuery(data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)), child: mchild!);
},
showSemanticsDebugger: false,
debugShowCheckedModeBanner: false,

@ -1,22 +1,23 @@
import 'dart:developer';
import 'dart:math' as math;
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/common_models/data_points.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/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';
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/presentation/water_monitor/widgets/hydration_tips_widget.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_intake_summary_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: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:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
@ -32,357 +33,36 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final vm = context.read<WaterMonitorViewModel>();
await vm.initialize();
log('WaterMonitor initialized: consumed=${vm.totalConsumedMl}, goal=${vm.dailyGoalMl}, progress=${vm.progressPercent}');
await _refreshData();
});
}
Widget _buildHydrationTipsWidget() {
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: [
Utils.buildSvgWithAssets(icon: AppAssets.bulb_icon, width: 24.w, height: 24.h),
SizedBox(width: 8.w),
"Tips to stay hydrated".needTranslation.toText16(isBold: true),
],
),
SizedBox(height: 8.h),
"${"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,
),
SizedBox(height: 4.h),
"${"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,
),
SizedBox(height: 8.h),
],
),
);
/// Refresh data by calling initialize on the view model
Future<void> _refreshData() async {
final vm = context.read<WaterMonitorViewModel>();
await vm.initialize();
}
Widget _buildWaterIntakeSummaryWidget() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(24.w),
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(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.blueGradientColorOne,
AppColors.blueGradientColorTwo,
],
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: isTablet ? 2 : 3,
child: Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
if (vm.isLoading) {
// show shimmer placeholder while fetching server-side user details
return _buildLoadingShimmer(isForHistory: false);
}
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(),
],
),
);
}
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,
),
],
);
}
Widget _buildWaterBottleWidget() {
return Consumer<WaterMonitorViewModel>(
builder: (context, vm, _) {
final progressPercent = (vm.progress * 100).clamp(0.0, 100.0);
log("progressPercent: $progressPercent");
// SVG aspect ratio
const svgAspectRatio = 315.0 / 143.0; // ~2.2
// 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);
}
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;
// 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 outline
Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.waterBottle,
height: bottleHeight,
width: bottleWidth,
fit: BoxFit.contain,
),
),
// Wave and bubbles clipped to bottle shape
Positioned.fill(
left: leftOffset,
bottom: bottomOffset,
child: Center(
child: SizedBox(
width: fillableWidth,
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<WaterMonitorViewModel>(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,
borderRadius: BorderRadius.circular(10.r),
),
),
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,
),
),
],
),
],
);
});
}
_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(
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(),
],
),
},
);
}
@ -452,44 +132,14 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
),
],
),
if (!viewModel.isGraphView)
_buildHistoryListView(viewModel)
else
Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 32.h),
child: "Graph view coming soon".toText14(color: AppColors.greyTextColor),
),
),
SizedBox(height: 12.h),
if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else _buildHistoryFlowchart()
],
);
}),
);
}
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;
@ -524,14 +174,19 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
}
} else if (selectedDuration == 'Weekly') {
if (viewModel.weekProgressList.isNotEmpty) {
for (var dayData in viewModel.weekProgressList) {
// Show today + last 6 days (total 7 days)
final totalDays = viewModel.weekProgressList.length;
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList();
for (var dayData in weekDataToShow) {
listItems.add(
buildHistoryListTile(
title: dayData.dayName ?? 'Unknown',
subTitle: "${dayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
),
);
if (dayData != viewModel.weekProgressList.last) {
if (dayData != weekDataToShow.last) {
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
}
}
@ -547,14 +202,20 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
}
} else if (selectedDuration == 'Monthly') {
if (viewModel.monthProgressList.isNotEmpty) {
for (var monthData in viewModel.monthProgressList) {
// Show current month + last 6 months (total 7 months)
// Reverse order to show oldest to newest (top to bottom)
final totalMonths = viewModel.monthProgressList.length;
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList().reversed.toList();
for (var monthData in monthDataToShow) {
listItems.add(
buildHistoryListTile(
title: monthData.monthName ?? 'Unknown',
subTitle: "${monthData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
),
);
if (monthData != viewModel.monthProgressList.last) {
if (monthData != monthDataToShow.last) {
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
}
}
@ -572,10 +233,7 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
// Return scrollable list with min and max height constraints
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: 80.h,
maxHeight: 270.h,
),
constraints: BoxConstraints(minHeight: 80.h, maxHeight: 270.h),
child: viewModel.isLoading
? _buildLoadingShimmer().paddingOnly(top: 16.h)
: listItems.isEmpty
@ -592,6 +250,197 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
);
}
Widget _buildHistoryFlowchart() {
return Consumer<WaterMonitorViewModel>(
builder: (context, viewModel, _) {
final selectedDuration = viewModel.selectedDurationFilter;
// Build dynamic data points based on selected duration
List<DataPoint> dataPoints = [];
if (selectedDuration == 'Daily') {
// For daily, we show a single bar/point with today's percentage
if (viewModel.todayProgressList.isNotEmpty) {
final todayData = viewModel.todayProgressList.first;
final percentage = todayData.percentageConsumed?.toDouble() ?? 0.0;
dataPoints.add(
DataPoint(
value: percentage,
actualValue: percentage.toStringAsFixed(1),
label: 'Today',
displayTime: 'Today',
unitOfMeasurement: '%',
time: DateTime.now(),
),
);
}
} else if (selectedDuration == 'Weekly') {
// For weekly, show today + last 6 days (total 7 days)
if (viewModel.weekProgressList.isNotEmpty) {
final totalDays = viewModel.weekProgressList.length;
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList();
for (var dayData in weekDataToShow) {
final percentage = dayData.percentageConsumed?.toDouble() ?? 0.0;
final dayName = dayData.dayName ?? 'Day ${dayData.dayNumber}';
dataPoints.add(
DataPoint(
value: percentage,
actualValue: percentage.toStringAsFixed(1),
label: DateUtil.getShortWeekDayName(dayName),
displayTime: dayName,
unitOfMeasurement: '%',
time: DateTime.now(),
),
);
}
}
} else if (selectedDuration == 'Monthly') {
// For monthly, show current month + last 6 months (total 7 months)
// Reverse order to show oldest to newest (left to right)
if (viewModel.monthProgressList.isNotEmpty) {
final totalMonths = viewModel.monthProgressList.length;
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList().reversed.toList();
for (var monthData in monthDataToShow) {
final percentage = monthData.percentageConsumed?.toDouble() ?? 0.0;
final monthName = monthData.monthName ?? 'Month ${monthData.monthNumber}';
dataPoints.add(
DataPoint(
value: percentage,
actualValue: percentage.toStringAsFixed(1),
label: DateUtil.getShortMonthName(monthName),
displayTime: monthName,
unitOfMeasurement: '%',
time: DateTime.now(),
),
);
}
}
}
// If no data, show empty state
if (dataPoints.isEmpty) {
return Container(
padding: EdgeInsets.symmetric(vertical: 80.h),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bar_chart, size: 48.w, color: AppColors.greyTextColor.withValues(alpha: 0.5)),
SizedBox(height: 12.h),
"No chart data available".toText14(color: AppColors.greyTextColor),
],
),
),
);
}
// Show loading shimmer while fetching data
if (viewModel.isLoading) {
return Container(
padding: EdgeInsets.symmetric(vertical: 40.h),
child: _buildLoadingShimmer(),
);
}
// Define ranges for percentage (0-100%)
const double low = 25.0; // Below 25% is low
const double medium = 50.0; // 25-50% is medium
const double good = 75.0; // 50-75% is good
const double maxY = 100.0; // Max is 100%
return CustomGraph(
dataPoints: dataPoints,
makeGraphBasedOnActualValue: true,
leftLabelReservedSize: 50.w,
leftLabelInterval: 25,
showGridLines: true,
maxY: maxY,
minY: 0,
maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0,
minX: -0.2,
horizontalInterval: 25,
// Grid lines every 25%
showShadow: true,
getDrawingHorizontalLine: (value) {
// Draw dashed lines at 25%, 50%, 75%
if (value == low || value == medium || value == good) {
return FlLine(
color: AppColors.greyTextColor.withValues(alpha: 0.3),
strokeWidth: 1.5,
dashArray: [8, 4],
);
}
return FlLine(color: AppColors.transparent, strokeWidth: 0);
},
leftLabelFormatter: (value) {
// Show percentage labels at key points
if (value == 0) return '0%'.toText10(weight: FontWeight.w600);
if (value == 25) return '25%'.toText10(weight: FontWeight.w600);
if (value == 50) return '50%'.toText10(weight: FontWeight.w600);
if (value == 75) return '75%'.toText10(weight: FontWeight.w600);
if (value == 100) return '100%'.toText10(weight: FontWeight.w600);
return SizedBox.shrink();
},
graphColor: AppColors.successColor,
graphShadowColor: AppColors.successColor.withValues(alpha: 0.15),
bottomLabelFormatter: (value, data) {
if (data.isEmpty) return SizedBox.shrink();
// Only show labels for whole number positions (not fractional)
if ((value - value.round()).abs() > 0.01) {
return SizedBox.shrink();
}
int index = value.round();
if (index < 0 || index >= data.length) return SizedBox.shrink();
// For daily, show only index 0
if (selectedDuration == 'Daily' && index == 0) {
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: data[index].label.toText10(
weight: FontWeight.w600,
color: AppColors.labelTextColor,
),
);
}
// For weekly, show all 7 days (today + last 6 days)
if (selectedDuration == 'Weekly' && index < 7) {
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: data[index].label.toText10(
weight: FontWeight.w600,
color: AppColors.labelTextColor,
),
);
}
// For monthly, show all 7 months (current month + last 6 months)
if (selectedDuration == 'Monthly' && index < 7) {
return Padding(
padding: EdgeInsets.only(top: 8.h),
child: data[index].label.toText10(
weight: FontWeight.w600,
color: AppColors.labelTextColor,
),
);
}
return SizedBox.shrink();
},
scrollDirection: selectedDuration == 'Monthly' ? Axis.horizontal : Axis.vertical,
height: 250.h,
spotColor: AppColors.successColor,
);
},
);
}
// Reusable method to build selection row widget
Widget _buildSelectionRow({
required String value,
@ -634,7 +483,7 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
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)),
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)),
child: ListView.separated(
shrinkWrap: true,
itemCount: items.length,
@ -667,13 +516,125 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
);
}
/// Handle reminder button tap (Set or Cancel)
Future<void> _handleReminderButtonTap(WaterMonitorViewModel viewModel) async {
if (viewModel.isWaterReminderEnabled) {
// Cancel reminders
_showCancelReminderConfirmation(viewModel);
} else {
// Set reminders
await _setReminders(viewModel);
}
}
/// Show confirmation bottom sheet before cancelling reminders
void _showCancelReminderConfirmation(WaterMonitorViewModel viewModel) {
showCommonBottomSheetWithoutHeight(
title: 'Notice'.needTranslation,
context,
child: Utils.getWarningWidget(
loadingText: "Are you sure you want to cancel all water reminders?".needTranslation,
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
Navigator.pop(context);
await _cancelReminders(viewModel);
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
isDismissible: true,
);
}
/// Set water reminders
Future<void> _setReminders(WaterMonitorViewModel viewModel) async {
// Schedule reminders
final success = await viewModel.scheduleWaterReminders();
if (success) {
final times = await viewModel.getScheduledReminderTimes();
_showReminderScheduledDialog(times);
}
}
/// Cancel water reminders
Future<void> _cancelReminders(WaterMonitorViewModel viewModel) async {
final success = await viewModel.cancelWaterReminders();
}
/// Show bottom sheet with scheduled reminder times
void _showReminderScheduledDialog(List<DateTime> times) {
showCommonBottomSheetWithoutHeight(
title: 'Reminders Set!'.needTranslation,
context,
isCloseButtonVisible: false,
isDismissible: false,
child: Padding(
padding: EdgeInsets.only(top: 16.w, left: 16.w, right: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.getSuccessWidget(loadingText: 'Daily water reminders scheduled at:'.needTranslation),
SizedBox(height: 16.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: times
.map(
(time) => AppCustomChipWidget(
icon: AppAssets.bell,
iconColor: AppColors.quickLoginColor,
richText: _formatTime(time).toText14(),
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
),
)
.toList(),
),
SizedBox(height: 24.h),
// OK button
Row(
children: [
Expanded(
child: CustomButton(
height: 56.h,
text: 'OK'.needTranslation,
onPressed: () => Navigator.of(context).pop(),
textColor: AppColors.whiteColor,
),
),
],
),
],
),
),
callBackFunc: () {},
);
}
/// Format DateTime to readable time string
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';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "Water Consumption".needTranslation,
bottomChild: Container(
bottomChild: Consumer<WaterMonitorViewModel>(
builder: (context, viewModel, child) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
@ -682,30 +643,37 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
child: Padding(
padding: EdgeInsets.all(24.w),
child: CustomButton(
text: "Set Reminder".needTranslation,
textColor: AppColors.successColor,
backgroundColor: AppColors.successLightBgColor,
onPressed: () {},
icon: AppAssets.bell,
iconColor: AppColors.successColor,
text: viewModel.isWaterReminderEnabled ? "Cancel Reminders".needTranslation : "Set Reminder".needTranslation,
textColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor,
backgroundColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor.withValues(alpha: 0.1) : AppColors.successLightBgColor,
onPressed: () => _handleReminderButtonTap(viewModel),
icon: viewModel.isWaterReminderEnabled ? null : AppAssets.bell,
iconColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor,
borderRadius: 12.r,
borderColor: AppColors.transparent,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
),
);
},
),
child: RefreshIndicator(
onRefresh: _refreshData,
color: AppColors.blueColor,
backgroundColor: AppColors.whiteColor,
child: Column(
children: [
SizedBox(height: 16.h),
_buildWaterIntakeSummaryWidget(),
const WaterIntakeSummaryWidget(),
SizedBox(height: 16.h),
_buildHistoryGraphOrList(),
SizedBox(height: 16.h),
_buildHydrationTipsWidget(),
const HydrationTipsWidget(),
SizedBox(height: 16.h),
],
),
),
),
);
}
}

@ -12,10 +12,23 @@ 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 StatelessWidget {
class WaterMonitorSettingsScreen extends StatefulWidget {
const WaterMonitorSettingsScreen({super.key});
void _showSnackbar(String text, BuildContext context) {
@override
State<WaterMonitorSettingsScreen> createState() => _WaterMonitorSettingsScreenState();
}
class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<WaterMonitorViewModel>().initialize();
});
}
void _showSnackbar(String text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(text),
@ -266,9 +279,7 @@ class WaterMonitorSettingsScreen extends StatelessWidget {
if (!success && viewModel.validationError != null) {
_showSnackBar(context, viewModel.validationError!);
} else if (success) {
_showSnackBar(context, "Settings saved successfully".needTranslation);
// Navigate back on success
Navigator.pop(context);
_showSnackBar(context, "Settings saved successfully");
}
},
borderRadius: 12.r,
@ -339,7 +350,9 @@ class WaterMonitorSettingsScreen extends StatelessWidget {
content: Text(message),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor,
backgroundColor: message.contains('successfully')
? Colors.green
: AppColors.errorColor,
),
);
}

@ -0,0 +1,62 @@
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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class HydrationTipsWidget extends StatelessWidget {
const HydrationTipsWidget({super.key});
@override
Widget build(BuildContext context) {
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: [
Utils.buildSvgWithAssets(
icon: AppAssets.bulb_icon,
width: 24.w,
height: 24.h,
),
SizedBox(width: 8.w),
"Tips to stay hydrated".needTranslation.toText16(isBold: true),
],
),
SizedBox(height: 8.h),
"${"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,
),
SizedBox(height: 4.h),
"${"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,
),
SizedBox(height: 8.h),
],
),
);
}
}

@ -0,0 +1,176 @@
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/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/cup_bottomsheet_widgets.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
class WaterActionButtonsWidget extends StatelessWidget {
const WaterActionButtonsWidget({super.key});
@override
Widget build(BuildContext context) {
return Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
final cupAmount = vm.selectedCupCapacityMl;
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.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 {
if (cupAmount > 0) {
await vm.insertUserActivity(quantityIntake: cupAmount);
}
},
child: Utils.buildSvgWithAssets(
icon: AppAssets.addIconDark,
),
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildActionButton(
context: context,
onTap: () => showSwitchCupBottomSheet(context),
overlayWidget: AppAssets.refreshIcon,
title: "Switch Cup".needTranslation,
icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w),
),
_buildActionButton(
context: context,
onTap: () async {
final success = await vm.scheduleTestNotification();
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Test notification will appear in 5 seconds!'.needTranslation),
backgroundColor: AppColors.blueColor,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(16.w),
duration: const Duration(seconds: 2),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to schedule test notification'.needTranslation),
backgroundColor: AppColors.errorColor,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(16.w),
),
);
}
},
title: "Test Alert".needTranslation,
icon: Icon(
Icons.notifications_outlined,
color: AppColors.blueColor,
size: 24.w,
),
),
_buildActionButton(
context: context,
onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen),
title: "Settings".needTranslation,
icon: Icon(
Icons.settings,
color: AppColors.blueColor,
size: 24.w,
),
),
],
),
],
);
});
}
Widget _buildActionButton({
required BuildContext context,
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(
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(),
],
),
);
}
}

@ -0,0 +1,112 @@
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/utils/utils.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/water_consumption_progress_widget.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
class WaterBottleWidget extends StatelessWidget {
const WaterBottleWidget({super.key});
@override
Widget build(BuildContext context) {
return Consumer<WaterMonitorViewModel>(
builder: (context, vm, _) {
final progressPercent = (vm.progress * 100).clamp(0.0, 100.0);
// SVG aspect ratio
const svgAspectRatio = 315.0 / 143.0; // ~2.2
// Responsive bottle sizing with device-specific constraints
double bottleWidth;
if (isTablet) {
bottleWidth = math.min(SizeUtils.width * 0.15, 180.0);
} else if (isFoldable) {
bottleWidth = math.min(100.w, 160.0);
} else {
bottleWidth = math.min(120.w, 140.0);
}
final bottleHeight = bottleWidth * svgAspectRatio;
// Fillable area percentages
final fillableHeightPercent = 0.7;
const fillableWidthPercent = 0.8;
final fillableHeight = bottleHeight * fillableHeightPercent;
final fillableWidth = bottleWidth * fillableWidthPercent;
// 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 outline
Center(
child: Utils.buildSvgWithAssets(
icon: AppAssets.waterBottle,
height: bottleHeight,
width: bottleWidth,
fit: BoxFit.contain,
),
),
// Wave and bubbles clipped to bottle shape
Positioned.fill(
left: leftOffset,
bottom: bottomOffset,
child: Center(
child: SizedBox(
width: fillableWidth,
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,
height: isTablet ? math.min(45.0, fillableHeight * 0.2) : math.min(55.0, fillableHeight * 0.22),
width: fillableWidth * 0.65,
),
),
],
),
),
),
),
),
],
),
);
},
);
}
}

@ -11,15 +11,7 @@ class WaterConsumptionProgressWidget extends StatefulWidget {
final double? containerHeight;
final Duration? waveDuration;
const WaterConsumptionProgressWidget({
super.key,
required this.progress,
this.size = 100,
this.waveColor,
this.containerWidth,
this.containerHeight,
this.waveDuration,
});
const WaterConsumptionProgressWidget({super.key, required this.progress, this.size = 100, this.waveColor, this.containerWidth, this.containerHeight, this.waveDuration});
@override
State<WaterConsumptionProgressWidget> createState() => _WaterConsumptionProgressWidgetState();

@ -0,0 +1,116 @@
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/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_action_buttons_widget.dart';
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_bottle_widget.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class WaterIntakeSummaryWidget extends StatelessWidget {
const WaterIntakeSummaryWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.blueGradientColorOne,
AppColors.blueGradientColorTwo,
],
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: isTablet ? 2 : 3,
child: Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
if (vm.isLoading) {
return _buildLoadingShimmer();
}
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".needTranslation.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: const WaterBottleWidget(),
),
],
),
const WaterActionButtonsWidget(),
],
),
);
}
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,
),
],
);
}
Widget _buildLoadingShimmer() {
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: 40.h,
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(10.r),
),
),
);
},
);
}
}

@ -85,11 +85,8 @@ class AppRoutes {
userInfoFlowManager: (context) => UserInfoFlowManager(),
smartWatches: (context) => SmartwatchInstructionsPage(),
huaweiHealthExample: (context) => HuaweiHealthExample(),
healthCalculatorsPage: (context) => HealthCalculatorsPage(),
waterConsumptionScreen: (context) => WaterConsumptionScreen(),
waterMonitorSettingsScreen: (context) => WaterMonitorSettingsScreen(),
//
healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator),
healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter)
};

@ -0,0 +1,373 @@
import 'dart:typed_data';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz show TZDateTime, local, setLocalLocation, getLocation;
/// Abstract class defining the notification service interface
abstract class NotificationService {
/// Initialize the notification service
Future<void> initialize({Function(String payload)? onNotificationClick});
/// Request notification permissions (mainly for iOS)
Future<bool> requestPermissions();
/// Show an immediate notification
Future<void> showNotification({
required String title,
required String body,
String? payload,
});
/// Schedule a notification at a specific date and time
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
});
/// Schedule daily notifications at specific times
Future<void> scheduleDailyNotifications({
required List<DateTime> times,
required String title,
required String body,
String? payload,
});
/// Schedule water reminder notifications
Future<void> scheduleWaterReminders({
required List<DateTime> reminderTimes,
required String title,
required String body,
});
/// Cancel a specific notification by id
Future<void> cancelNotification(int id);
/// Cancel all scheduled notifications
Future<void> cancelAllNotifications();
/// Get list of pending notifications
Future<List<PendingNotificationRequest>> getPendingNotifications();
}
/// Implementation of NotificationService following the project architecture
class NotificationServiceImp implements NotificationService {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
final LoggerService loggerService;
NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService});
// Channel IDs for different notification types
static const String _waterReminderChannelId = 'water_reminder_channel';
static const String _waterReminderChannelName = 'Water Reminders';
static const String _waterReminderChannelDescription = 'Daily water intake reminders';
static const String _generalChannelId = 'hmg_general_channel';
static const String _generalChannelName = 'HMG Notifications';
static const String _generalChannelDescription = 'General notifications from HMG';
Function(String payload)? _onNotificationClick;
@override
Future<void> initialize({Function(String payload)? onNotificationClick}) async {
try {
// Initialize timezone database
tz.initializeTimeZones();
// Set local timezone (you can also use a specific timezone if needed)
// For example: tz.setLocalLocation(tz.getLocation('Asia/Riyadh'));
final locationName = DateTime.now().timeZoneName;
try {
tz.setLocalLocation(tz.getLocation(locationName));
} catch (e) {
// Fallback to UTC if specific timezone not found
loggerService.logInfo('Could not set timezone $locationName, using UTC');
tz.setLocalLocation(tz.getLocation('UTC'));
}
_onNotificationClick = onNotificationClick;
const androidSettings = AndroidInitializationSettings('app_icon');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initializationSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: _handleNotificationResponse,
);
loggerService.logInfo('NotificationService initialized successfully');
} catch (ex) {
loggerService.logError('Failed to initialize NotificationService: $ex');
}
}
/// Handle notification tap
void _handleNotificationResponse(NotificationResponse response) {
try {
if (response.payload != null && _onNotificationClick != null) {
_onNotificationClick!(response.payload!);
}
loggerService.logInfo('Notification tapped: ${response.payload}');
} catch (ex) {
loggerService.logError('Error handling notification response: $ex');
}
}
@override
Future<bool> requestPermissions() async {
try {
// Request permissions for iOS
final result =
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
// For Android 13+, permissions are requested at runtime
final androidResult = await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
loggerService.logInfo('Notification permissions: iOS=${result ?? true}, Android=${androidResult ?? true}');
return result ?? androidResult ?? true;
} catch (ex) {
loggerService.logError('Error requesting notification permissions: $ex');
return false;
}
}
@override
Future<void> showNotification({
required String title,
required String body,
String? payload,
}) async {
try {
final androidDetails = AndroidNotificationDetails(
_generalChannelId,
_generalChannelName,
channelDescription: _generalChannelDescription,
importance: Importance.high,
priority: Priority.high,
vibrationPattern: _getVibrationPattern(),
);
const iosDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await flutterLocalNotificationsPlugin.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
notificationDetails,
payload: payload,
);
loggerService.logInfo('Notification shown: $title');
} catch (ex) {
loggerService.logError('Error showing notification: $ex');
}
}
@override
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
}) async {
try {
final androidDetails = AndroidNotificationDetails(
_generalChannelId,
_generalChannelName,
channelDescription: _generalChannelDescription,
importance: Importance.high,
priority: Priority.high,
vibrationPattern: _getVibrationPattern(),
);
const iosDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await flutterLocalNotificationsPlugin.zonedSchedule(
id,
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
payload: payload,
);
loggerService.logInfo('Notification scheduled for: $scheduledDate');
} catch (ex) {
loggerService.logError('Error scheduling notification: $ex');
}
}
@override
Future<void> scheduleDailyNotifications({
required List<DateTime> times,
required String title,
required String body,
String? payload,
}) async {
try {
for (int i = 0; i < times.length; i++) {
final time = times[i];
await scheduleNotification(
id: i + 1000,
// Offset ID to avoid conflicts
title: title,
body: body,
scheduledDate: time,
payload: payload,
);
}
loggerService.logInfo('Scheduled ${times.length} daily notifications');
} catch (ex) {
loggerService.logError('Error scheduling daily notifications: $ex');
}
}
@override
Future<void> scheduleWaterReminders({
required List<DateTime> reminderTimes,
required String title,
required String body,
}) async {
try {
// Cancel existing water reminders first
await _cancelWaterReminders();
final androidDetails = AndroidNotificationDetails(
_waterReminderChannelId,
_waterReminderChannelName,
channelDescription: _waterReminderChannelDescription,
importance: Importance.high,
priority: Priority.high,
vibrationPattern: _getVibrationPattern(),
icon: 'app_icon',
styleInformation: const BigTextStyleInformation(''),
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
for (int i = 0; i < reminderTimes.length; i++) {
final reminderTime = reminderTimes[i];
final notificationId = 5000 + i; // Use 5000+ range for water reminders
// Schedule for today if time hasn't passed, otherwise schedule for tomorrow
DateTime scheduledDate = reminderTime;
if (scheduledDate.isBefore(DateTime.now())) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
await flutterLocalNotificationsPlugin.zonedSchedule(
notificationId,
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the same time
payload: 'water_reminder_$i',
);
}
loggerService.logInfo('Scheduled ${reminderTimes.length} water reminders');
} catch (ex) {
loggerService.logError('Error scheduling water reminders: $ex');
}
}
/// Cancel all water reminders (IDs 5000-5999)
Future<void> _cancelWaterReminders() async {
try {
final pendingNotifications = await getPendingNotifications();
for (final notification in pendingNotifications) {
if (notification.id >= 5000 && notification.id < 6000) {
await cancelNotification(notification.id);
}
}
loggerService.logInfo('Cancelled all water reminders');
} catch (ex) {
loggerService.logError('Error cancelling water reminders: $ex');
}
}
@override
Future<void> cancelNotification(int id) async {
try {
await flutterLocalNotificationsPlugin.cancel(id);
loggerService.logInfo('Cancelled notification with ID: $id');
} catch (ex) {
loggerService.logError('Error cancelling notification: $ex');
}
}
@override
Future<void> cancelAllNotifications() async {
try {
await flutterLocalNotificationsPlugin.cancelAll();
loggerService.logInfo('Cancelled all notifications');
} catch (ex) {
loggerService.logError('Error cancelling all notifications: $ex');
}
}
@override
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
try {
final pending = await flutterLocalNotificationsPlugin.pendingNotificationRequests();
loggerService.logInfo('Found ${pending.length} pending notifications');
return pending;
} catch (ex) {
loggerService.logError('Error getting pending notifications: $ex');
return [];
}
}
/// Get vibration pattern for notifications
Int64List _getVibrationPattern() {
final vibrationPattern = Int64List(4);
vibrationPattern[0] = 0;
vibrationPattern[1] = 500;
vibrationPattern[2] = 500;
vibrationPattern[3] = 500;
return vibrationPattern;
}
}

@ -5,30 +5,30 @@ import 'package:flutter_callkit_incoming/entities/call_event.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
// import 'package:hmg_patient_app_new/presentation/authantication/login.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart';
import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart';
import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart';
import 'package:hmg_patient_app_new/services/cache_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';
import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart';
import 'package:lottie/lottie.dart';
import 'package:provider/provider.dart';
import 'core/cache_consts.dart';
import 'core/utils/local_notifications.dart';
import 'core/utils/push_notification_handler.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
_SplashScreenState createState() => _SplashScreenState();
}
@ -47,9 +47,13 @@ class _SplashScreenState extends State<SplashPage> {
);
await authVm.getServicePrivilege();
Timer(Duration(seconds: 2, milliseconds: 500), () async {
bool isAppOpenedFromCall = await GetIt.instance<CacheService>().getBool(key: CacheConst.isAppOpenedFromCall) ?? false;
bool isAppOpenedFromCall = getIt.get<CacheService>().getBool(key: CacheConst.isAppOpenedFromCall) ?? false;
LocalNotification.init(onNotificationClick: (payload) {});
// Initialize NotificationService using dependency injection
final notificationService = getIt.get<NotificationService>();
await notificationService.initialize(onNotificationClick: (payload) {
// Handle notification click here
});
if (isAppOpenedFromCall) {
navigateToTeleConsult();
@ -78,7 +82,8 @@ class _SplashScreenState extends State<SplashPage> {
// GetIt.instance<CacheService>().remove(key: CacheConst.isAppOpenedFromCall);
Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall);
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation())));
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!)
.pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation())));
Navigator.pushReplacementNamed(
// context,
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
@ -216,7 +221,7 @@ class _SplashScreenState extends State<SplashPage> {
@override
void initState() {
authVm = context.read<AuthenticationViewModel>();
authVm = getIt<AuthenticationViewModel>();
super.initState();
initializeStuff();
}
@ -225,6 +230,8 @@ class _SplashScreenState extends State<SplashPage> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.whiteColor,
body: Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill).center);
body: Lottie.asset(AppAnimations.loadingAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill)
.center);
}
}

@ -104,4 +104,8 @@ class AppColors {
static const Color blueColor = Color(0xFF4EB5FF);
static const Color blueGradientColorOne = Color(0xFFF1F7FD);
static const Color blueGradientColorTwo = Color(0xFFD9EFFF);
// Shimmer
static const Color shimmerBaseColor = Color(0xFFE0E0E0);
static const Color shimmerHighlightColor = Color(0xFFF5F5F5);
}

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.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/theme/colors.dart';
/// A customizable line graph widget using `fl_chart`.
///
/// Displays a line chart with configurable axis labels, colors, and data points.
@ -56,25 +57,26 @@ class CustomGraph extends StatelessWidget {
final FontWeight? bottomLabelFontWeight;
final double? leftLabelInterval;
final double? leftLabelReservedSize;
final double? bottomLabelReservedSize;
final bool? showGridLines;
final GetDrawingGridLine? getDrawingHorizontalLine;
final double? horizontalInterval;
final double? minY;
final bool showShadow;
final double? cutOffY;
final RangeAnnotations? rangeAnnotations;
///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart
final Widget Function(double) leftLabelFormatter;
final Widget Function(double , List<DataPoint>) bottomLabelFormatter;
final Widget Function(double, List<DataPoint>) bottomLabelFormatter;
final Axis scrollDirection;
final bool showBottomTitleDates;
final bool isFullScreeGraph;
final bool makeGraphBasedOnActualValue;
const CustomGraph({
super.key,
const CustomGraph(
{super.key,
required this.dataPoints,
required this.leftLabelFormatter,
this.width,
@ -93,6 +95,7 @@ class CustomGraph extends StatelessWidget {
this.bottomLabelSize,
this.leftLabelInterval,
this.leftLabelReservedSize,
this.bottomLabelReservedSize,
this.makeGraphBasedOnActualValue = false,
required this.bottomLabelFormatter,
this.minX,
@ -101,8 +104,8 @@ class CustomGraph extends StatelessWidget {
this.horizontalInterval,
this.minY,
this.showShadow = false,
this.rangeAnnotations
});
this.cutOffY = 0,
this.rangeAnnotations});
@override
Widget build(BuildContext context) {
@ -113,10 +116,10 @@ class CustomGraph extends StatelessWidget {
height: height,
child: LineChart(
LineChartData(
minY: minY??0,
minY: minY ?? 0,
maxY: maxY,
maxX: maxX,
minX: minX ,
minX: minX,
lineTouchData: LineTouchData(
getTouchLineEnd: (_, __) => 0,
getTouchedSpotIndicator: (barData, indicators) {
@ -149,11 +152,8 @@ class CustomGraph extends StatelessWidget {
final dataPoint = dataPoints[spot.x.toInt()];
return LineTooltipItem(
'${dataPoint.actualValue} ${dataPoint.unitOfMeasurement??""} - ${dataPoint.displayTime}',
TextStyle(
color: Colors.black,
fontSize: 12.f,
fontWeight: FontWeight.w500),
'${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}',
TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500),
);
}
return null; // hides the rest
@ -165,7 +165,7 @@ class CustomGraph extends StatelessWidget {
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: leftLabelReservedSize??80,
reservedSize: leftLabelReservedSize ?? 80,
interval: leftLabelInterval ?? .1, // Let fl_chart handle it
getTitlesWidget: (value, _) {
return leftLabelFormatter(value);
@ -176,9 +176,9 @@ class CustomGraph extends StatelessWidget {
axisNameSize: 20,
sideTitles: SideTitles(
showTitles: showBottomTitleDates,
reservedSize: 20,
reservedSize: bottomLabelReservedSize ?? 20,
getTitlesWidget: (value, _) {
return bottomLabelFormatter(value, dataPoints, );
return bottomLabelFormatter(value, dataPoints);
},
interval: 1, // ensures 1:1 mapping with spots
),
@ -197,12 +197,13 @@ class CustomGraph extends StatelessWidget {
),
lineBarsData: _buildColoredLineSegments(dataPoints),
gridData: FlGridData(
show: showGridLines??true,
show: showGridLines ?? true,
drawVerticalLine: false,
horizontalInterval:horizontalInterval,
horizontalInterval: horizontalInterval,
// checkToShowHorizontalLine: (value) =>
// value >= 0 && value <= 100,
getDrawingHorizontalLine: getDrawingHorizontalLine??(value) {
getDrawingHorizontalLine: getDrawingHorizontalLine ??
(value) {
return FlLine(
color: graphGridColor,
strokeWidth: 1,
@ -210,17 +211,15 @@ class CustomGraph extends StatelessWidget {
);
},
),
rangeAnnotations: rangeAnnotations
),
rangeAnnotations: rangeAnnotations),
),
),
);
}
List<LineChartBarData> _buildColoredLineSegments(List<DataPoint> dataPoints) {
final List<FlSpot> allSpots = dataPoints.asMap().entries.map((entry) {
double value = (makeGraphBasedOnActualValue)?double.tryParse(entry.value.actualValue)??0.0:entry.value.value;
double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value;
return FlSpot(entry.key.toDouble(), value);
}).toList();
@ -241,6 +240,8 @@ class CustomGraph extends StatelessWidget {
),
belowBarData: BarAreaData(
show: showShadow,
applyCutOffY: cutOffY != null,
cutOffY: cutOffY ?? 0,
gradient: LinearGradient(
colors: [
graphShadowColor,

@ -30,6 +30,7 @@ dependencies:
# firebase_core: ^3.13.1
permission_handler: ^12.0.1
flutter_local_notifications: ^19.4.1
timezone: ^0.10.0
provider: ^6.1.5+1
get_it: ^8.2.0
just_audio: ^0.10.4

Loading…
Cancel
Save