diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index c691b0b2..cc028255 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -24,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; @@ -111,23 +112,13 @@ class AppRouter { name: 'authScreen', builder: (context, state) => const AuthScreen(), routes: [ - GoRoute( - path: 'registration-screen', - name: 'registrationScreen', - builder: (context, state) => const RegistrationScreen(), - ), + GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute( path: 'forget-password-screen', name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen( - isAuth: state.extra as bool, - ), - ), - GoRoute( - path: 'scan-qr-code-screen', - name: 'scanQRCodeScreen', - builder: (context, state) => const ScanQRCodeScreen(), + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), ), + GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), ], ), ShellRoute( @@ -165,19 +156,23 @@ class AppRouter { GoRoute( path: '/diary-screen', name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage( - child: DiaryScreen(), - ), + pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), ), GoRoute( - path: ConsultationsScreen.path, - name: ConsultationsScreen.name, - pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())), - GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())), + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())), + path: '/chat-select-screen', + name: 'chatSelectScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + ), + GoRoute( + path: '/notifications-screen', + name: 'notificationsScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), + ), GoRoute( path: '/menu-screen', name: 'menuScreen', @@ -194,11 +189,7 @@ class AppRouter { ), ], ), - GoRoute( - name: 'blockedUsersScreen', - path: '/blocked-users-screen', - builder: (context, state) => const BlockedUsersScreen(), - ), + GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), GoRoute( path: '/welcome-screen', name: 'welcomeScreen', @@ -241,26 +232,10 @@ class AppRouter { } }, ), - GoRoute( - path: ConsultationsArchiveScreen.path, - name: ConsultationsArchiveScreen.name, - builder: (context, state) => const ConsultationsArchiveScreen(), - ), - GoRoute( - path: ConsultantFinancesScreen.path, - name: ConsultantFinancesScreen.name, - builder: (context, state) => const ConsultantFinancesScreen(), - ), - GoRoute( - path: UserPurchasesScreen.path, - name: UserPurchasesScreen.name, - builder: (context, state) => const UserPurchasesScreen(), - ), - GoRoute( - path: '/user-finance-screen', - name: 'userFinanceScreen', - builder: (context, state) => const UserFinanceScreen(), - ), + GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), + GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), + GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), + GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), GoRoute( path: CardSettingsScreen.path, name: CardSettingsScreen.name, @@ -275,11 +250,7 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutPlanScreen( - slug: slug, - navigateToComments: false, - index: null, - ); + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); } final data = state.extra as Map; @@ -293,29 +264,23 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutRecipeScreen( - recipeSlug: slug, - navigateToComments: false, - index: null, - ); + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); } final data = state.extra as Map; - return AboutRecipeScreen( - recipeSlug: data['recipe'] as String, - index: data['index'] as int?, - navigateToComments: data['navigateToComments'] as bool, - ); + return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); }, ), GoRoute( path: RecipeCreateScreen.path, name: RecipeCreateScreen.name, - builder: (context, state) => RecipeCreateScreen( - recipe: state.extra != null ? state.extra as Recipe : null, - ), + builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), + ), + GoRoute( + path: '/profile-screen', + name: 'profileScreen', + builder: (context, state) => ProfileScreen(author: state.extra as Author), ), - GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)), GoRoute( path: '/subs-screen/:slug', name: 'subsScreen', @@ -327,11 +292,7 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutDietScreen( - slug: slug, - index: null, - navigateToComments: false, - ); + return AboutDietScreen(slug: slug, index: null, navigateToComments: false); } final data = state.extra as Map; return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); @@ -343,19 +304,18 @@ class AppRouter { builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), ), GoRoute( - path: '/blog-post-screen', - name: 'blogPostScreen', - builder: (context, state) { - final id = state.uri.queryParameters['id']; - final parsedId = int.tryParse(id ?? ''); - if (parsedId != null) { - return BlogPostScreen( - id: parsedId, - ); - } + path: '/blog-post-screen', + name: 'blogPostScreen', + builder: (context, state) { + final id = state.uri.queryParameters['id']; + final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { + return BlogPostScreen(id: parsedId); + } - return BlogPostScreen(id: state.extra as int); - }), + return BlogPostScreen(id: state.extra as int); + }, + ), GoRoute( path: '/chat-screen', name: 'chatScreen', @@ -363,10 +323,7 @@ class AppRouter { final data = state.extra as Map; final uuid = data['uuid'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen( - uuid: uuid, - sendInitialMessage: sendInitialMessage, - ); + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); }, ), GoRoute( @@ -399,7 +356,11 @@ class AppRouter { name: 'physicalFormScreen', builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), ), - GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)), + GoRoute( + path: 'pressure-screen', + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), GoRoute( path: 'change-password-screen', name: 'changePasswordScreen', @@ -416,9 +377,7 @@ class AppRouter { GoRoute( path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen( - slug: state.extra == null ? null : state.extra as String, - ), + builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), ), GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), @@ -446,7 +405,11 @@ class AppRouter { GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), - GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen(slug: state.extra as String), + ), GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), GoRoute( @@ -456,10 +419,7 @@ class AppRouter { final data = state.extra as Map; final planId = data['planId'] as int; final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen( - planId: planId, - successfulPayment: successfulPayment, - ); + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); }, ), GoRoute( @@ -469,10 +429,7 @@ class AppRouter { final data = state.extra as Map; final planId = data['planId'] as int; final url = data['url'] as String; - return PaymentScreen( - url: url, - planId: planId, - ); + return PaymentScreen(url: url, planId: planId); }, ), GoRoute( @@ -482,10 +439,7 @@ class AppRouter { final data = state.extra as Map; final diaryId = data['diaryId'] as int; final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen( - diaryId: diaryId, - currentPlanDay: currentPlanDay, - ); + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); }, ), GoRoute( @@ -493,16 +447,9 @@ class AppRouter { name: 'verifyScreen', builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), - GoRoute( - path: '/verify-email-screen', - name: 'verifyEmailScreen', - builder: (context, state) => const VerifyEmailScreen(), - ), - GoRoute( - path: MyRecipesScreen.path, - name: MyRecipesScreen.name, - builder: (context, state) => const MyRecipesScreen(), - ), + GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), + GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), ], ); } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9d0b5297..971ebb2c 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -43,8 +43,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_ import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; @@ -136,14 +139,16 @@ Future init() async { sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory(() => DiaryCubit( - chatRepository: sl(), - diaryRepository: sl(), - glossaryRepository: sl(), - profileRepository: sl(), - dietRepository: sl(), - settingsRepository: sl(), - )); + sl.registerFactory( + () => DiaryCubit( + chatRepository: sl(), + diaryRepository: sl(), + glossaryRepository: sl(), + profileRepository: sl(), + dietRepository: sl(), + settingsRepository: sl(), + ), + ); sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); @@ -172,6 +177,7 @@ Future init() async { sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => BlogPostCubit(sl())); sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); + sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); // Repository @@ -204,6 +210,7 @@ Future init() async { sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); sl.registerLazySingleton(() => CommentsRepository(service: sl())); + sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); // Services @@ -239,22 +246,12 @@ Future init() async { sl.registerLazySingleton(() => CommentsService(sl())); sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => ConsultantRatingService(sl())); + sl.registerLazySingleton(() => NotificationSettingsService(sl())); // External sl.registerLazySingleton( - () => Dio( - BaseOptions( - baseUrl: baseUrl, - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - )..interceptors.addAll( - [ - TokenInterceptor(), - PhoneVerifyErrorInterceptor(), - ], - ), + () => + Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) + ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), ); } diff --git a/lib/features/menu/models/notification_settings_model.dart b/lib/features/menu/models/notification_settings_model.dart new file mode 100644 index 00000000..2879a0ab --- /dev/null +++ b/lib/features/menu/models/notification_settings_model.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'notification_settings_model.g.dart'; +part 'notification_settings_model.freezed.dart'; + +@freezed +abstract class NotificationSettingsModel with _$NotificationSettingsModel { + const factory NotificationSettingsModel({ + @JsonKey(name: 'new_updates_notification') required bool newUpdatesNotification, + @JsonKey(name: 'weight_measurements_notification') required bool weightMeasurementsNotification, + @JsonKey(name: 'is_email_notifications_enabled') required bool isEmailNotificationsEnabled, + @JsonKey(name: 'is_push_notifications_enabled') required bool isPushNotificationsEnabled, + @JsonKey(name: 'diary_reminder_notifications') required DiaryReminderNotifications diaryReminderNotifications, + }) = _NotificationSettingsModel; + + factory NotificationSettingsModel.fromJson(Map json) => _$NotificationSettingsModelFromJson(json); +} + +@freezed +abstract class DiaryReminderNotifications with _$DiaryReminderNotifications { + const factory DiaryReminderNotifications({ + @JsonKey(name: 'first_reminder') required ReminderSettings firstReminder, + @JsonKey(name: 'second_reminder') required ReminderSettings secondReminder, + @JsonKey(name: 'third_reminder') required ReminderSettings thirdReminder, + }) = _DiaryReminderNotifications; + + factory DiaryReminderNotifications.fromJson(Map json) => _$DiaryReminderNotificationsFromJson(json); +} + +@freezed +abstract class ReminderSettings with _$ReminderSettings { + const factory ReminderSettings({ + required bool enabled, + @JsonKey(name: 'local_time_hours') required int localTimeHours, + @JsonKey(name: 'local_time_mins') required int localTimeMins, + }) = _ReminderSettings; + + factory ReminderSettings.fromJson(Map json) => _$ReminderSettingsFromJson(json); +} diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart new file mode 100644 index 00000000..6c5c567c --- /dev/null +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart @@ -0,0 +1,33 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; + +class NotificationSettingsCubit extends Cubit { + final NotificationSettingsRepository _notificationSettingsRepository; + + NotificationSettingsCubit({required NotificationSettingsRepository notificationSettingsRepository}) + : _notificationSettingsRepository = notificationSettingsRepository, + super(const NotificationSettingsStateLoading()); + + Future getNotificationSettings() async { + emit(const NotificationSettingsStateLoading()); + try { + final notificationSettings = await _notificationSettingsRepository.getNotificationSettings(); + emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); + } catch (e, stackTrace) { + log('Error getting notification settings: $e', stackTrace: stackTrace); + emit(NotificationSettingsStateError(errorMessage: e.toString())); + } + } + + Future updateNotificationSettings(Map data) async { + try { + final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data); + emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); + } catch (e, stackTrace) { + log('Error updating notification settings: $e', stackTrace: stackTrace); + emit(NotificationSettingsStateError(errorMessage: e.toString())); + } + } +} diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart new file mode 100644 index 00000000..ef461f7c --- /dev/null +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; + +abstract class NotificationSettingsState extends Equatable { + const NotificationSettingsState(); + + @override + List get props => []; +} + +class NotificationSettingsStateLoading extends NotificationSettingsState { + const NotificationSettingsStateLoading(); +} + +class NotificationSettingsStateLoaded extends NotificationSettingsState { + final NotificationSettingsModel notificationSettings; + + const NotificationSettingsStateLoaded({required this.notificationSettings}); + + @override + List get props => [notificationSettings]; +} + +class NotificationSettingsStateError extends NotificationSettingsState { + final String errorMessage; + + const NotificationSettingsStateError({required this.errorMessage}); + + @override + List get props => [errorMessage]; +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 2ebce8f4..5c350d3c 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -67,6 +67,8 @@ class MenuScreen extends StatelessWidget { _divider(context), _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), + _tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')), + _divider(context), _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), _divider(context), _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart new file mode 100644 index 00000000..2c7f4e04 --- /dev/null +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -0,0 +1,262 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class NotificationsSettingsScreen extends StatelessWidget { + const NotificationsSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..getNotificationSettings(), child: const NotificationsSettingsContent()); + } +} + +class NotificationsSettingsContent extends StatelessWidget { + const NotificationsSettingsContent({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Уведомления')), + body: BlocBuilder( + builder: (context, state) { + if (state is NotificationSettingsStateLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is NotificationSettingsStateError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Ошибка: ${state.errorMessage}'), + ElevatedButton( + onPressed: () { + context.read().getNotificationSettings(); + }, + child: const Text('Повторить'), + ), + ], + ), + ); + } else if (state is NotificationSettingsStateLoaded) { + return _buildSettingsContent(context, state.notificationSettings); + } + return const Center(child: Text('Нет данных')); + }, + ), + ); + } + + Widget _buildSettingsContent(BuildContext context, NotificationSettingsModel notificationSettings) { + final cubit = context.read(); + return Column( + children: [ + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Уведомление о новых обновлениях', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.newUpdatesNotification, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'new_updates_notification': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Уведомления о взвешивании', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.weightMeasurementsNotification, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'weight_measurements_notification': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Email уведомления', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.isEmailNotificationsEnabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'is_email_notifications_enabled': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Push уведомления', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.isPushNotificationsEnabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'is_push_notifications_enabled': newValue}); + }, + ), + ), + _divider(context), + const SizedBox(height: 16), + _buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit), + ], + ); + } + + Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Напоминания о дневнике', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: appTheme.blueA400), + ), + ), + _buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), + _buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), + _buildReminderTile(context, 'Третье напоминание', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), + ], + ); + } + + void _updateNotificationSetting(NotificationSettingsCubit cubit, Map data) { + cubit.updateNotificationSettings(data); + } + + void _updateReminderSetting(NotificationSettingsCubit cubit, String reminderType, Map data) { + final updateData = { + 'diary_reminder_notifications': {reminderType: data}, + }; + cubit.updateNotificationSettings(updateData); + } + + Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { + return _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: reminder.enabled + ? () { + _showTimePicker(context, reminder, reminderType, cubit); + } + : null, + title: title, + subtitle: reminder.enabled + ? Row( + children: [ + Icon(Icons.access_time, size: 16, color: appTheme.blueA400), + const SizedBox(width: 8), + Text( + '${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}', + style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500), + ), + const SizedBox(width: 4), + Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CupertinoSwitch( + value: reminder.enabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateReminderSetting(cubit, reminderType, { + 'enabled': newValue, + 'local_time_hours': reminder.localTimeHours, + 'local_time_mins': reminder.localTimeMins, + }); + }, + ), + ], + ), + ); + } + + void _showTimePicker(BuildContext context, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { + int selectedHour = reminder.localTimeHours; + int selectedMinute = reminder.localTimeMins; + + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return Container( + height: 300, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton(child: const Text('Отмена'), onPressed: () => Navigator.of(context).pop()), + CupertinoButton( + child: const Text('Готово'), + onPressed: () { + _updateReminderSetting(cubit, reminderType, { + 'enabled': reminder.enabled, + 'local_time_hours': selectedHour, + 'local_time_mins': selectedMinute, + }); + Navigator.of(context).pop(); + }, + ), + ], + ), + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + initialDateTime: DateTime(2024, 1, 1, selectedHour, selectedMinute), + onDateTimeChanged: (DateTime newDateTime) { + selectedHour = newDateTime.hour; + selectedMinute = newDateTime.minute; + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + + ListTile _tile({required String title, required Function()? onTap, Widget? trailing, EdgeInsets? padding, Widget? subtitle}) { + return ListTile( + contentPadding: padding, + onTap: onTap, + title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)), + subtitle: subtitle, + trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38), + ); + } + + Center _divider(BuildContext context) { + return Center( + child: Container( + height: 0.4, + width: MediaQuery.of(context).size.width - 32, + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), + ), + ); + } +} diff --git a/lib/features/menu/repositories/notification_settings_repository.dart b/lib/features/menu/repositories/notification_settings_repository.dart new file mode 100644 index 00000000..e0aeaa31 --- /dev/null +++ b/lib/features/menu/repositories/notification_settings_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; + +class NotificationSettingsRepository { + final NotificationSettingsService _notificationSettingsService; + + NotificationSettingsRepository(this._notificationSettingsService); + + Future getNotificationSettings() async { + return await _notificationSettingsService.getNotificationSettings(); + } + + Future updateNotificationSettings(Map data) async { + return await _notificationSettingsService.updateNotificationSettings(data: data); + } +} diff --git a/lib/features/menu/services/notification_settings_service.dart b/lib/features/menu/services/notification_settings_service.dart new file mode 100644 index 00000000..f489267f --- /dev/null +++ b/lib/features/menu/services/notification_settings_service.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'notification_settings_service.g.dart'; + +@RestApi() +abstract class NotificationSettingsService { + factory NotificationSettingsService(Dio dio, {String baseUrl}) = _NotificationSettingsService; + + @GET('/api/profile/notification_settings') + Future getNotificationSettings(); + + @POST('/api/profile/update_notification_settings') + Future updateNotificationSettings({@Body() required Map data}); +} diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index b7aa0d59..1c304f29 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -58,8 +58,8 @@ class _SettingsScreenState extends State { children: [ _additionalInfo(state), const SizedBox(height: 12), - _notifications(state), - const SizedBox(height: 12), + // _notifications(state), + // const SizedBox(height: 12), _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -147,6 +147,7 @@ class _SettingsScreenState extends State { ); } + // ignore: unused_element Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start,