diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 29232ced..db73bc05 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -9,7 +9,7 @@ env: jobs: build: - runs-on: [ dev-office ] + runs-on: [ ubuntu-latest ] steps: - uses: https://github.com/actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 522e6995..6b66540b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ env/.env android/key.properties app/upload-keystore.jks coverage/ +backend/ .fvm/ .fvmrc /eatvio \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/eatvioapp/app/HmsPushService.kt b/android/app/src/main/kotlin/com/eatvioapp/app/HmsPushService.kt index f2c7f3d9..f18b3992 100644 --- a/android/app/src/main/kotlin/com/eatvioapp/app/HmsPushService.kt +++ b/android/app/src/main/kotlin/com/eatvioapp/app/HmsPushService.kt @@ -1,6 +1,10 @@ package com.eatvioapp.app +import android.app.PendingIntent +import android.content.Intent import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage @@ -13,6 +17,45 @@ class HmsPushService : HmsMessageService() { override fun onMessageReceived(message: RemoteMessage?) { super.onMessageReceived(message) Log.i("HMS", "onMessageReceived: ${message?.data}") + + if (message == null) return + + // Берём action_url из data-поля, если оно есть. + val actionUrl = message.dataOfMap["action_url"] + + // Строим системное уведомление, по тапу по которому откроется MainActivity с action_url. + showNotification(message, actionUrl) + } + + private fun showNotification(message: RemoteMessage, actionUrl: String?) { + val context = applicationContext + + // Интент для запуска Flutter-активити. + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + if (!actionUrl.isNullOrBlank()) { + putExtra("action_url", actionUrl) + } + } + + val pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + val channelId = "hms_push_channel" + + val notification = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.mipmap.ic_launcher) // используем иконку приложения + .setContentTitle(message.notification?.title ?: context.getString(R.string.app_name)) + .setContentText(message.notification?.body ?: "") + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .build() + + NotificationManagerCompat.from(context).notify(message.messageId.hashCode(), notification) } } diff --git a/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt b/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt index 9b626e4c..97e76a0e 100644 --- a/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt @@ -1,8 +1,8 @@ package com.eatvioapp.app +import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.os.Bundle import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel @@ -14,12 +14,13 @@ import kotlinx.coroutines.launch class MainActivity: FlutterActivity() { private val channelName = "com.eatvioapp/hms_push" + private var methodChannel: MethodChannel? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) - .setMethodCallHandler { call, result -> + methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) + methodChannel?.setMethodCallHandler { call, result -> when (call.method) { "getHuaweiToken" -> { CoroutineScope(Dispatchers.IO).launch { @@ -71,5 +72,33 @@ class MainActivity: FlutterActivity() { else -> result.notImplemented() } } + + // Обрабатываем возможный action_url, с которым была открыта Activity (например, из HMS уведомления). + handleIntentForActionUrl(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + // Обработка нового интента, когда Activity уже запущена (тап по уведомлению). + handleIntentForActionUrl(intent) + } + + private fun handleIntentForActionUrl(intent: Intent?) { + if (intent == null) return + + // Вариант 1: action_url лежит в extras. + val fromExtras = intent.extras?.getString("action_url") + + // Вариант 2: action_url передан как data Uri. + val dataUri = intent.data + val fromData = dataUri?.toString() + + val actionUrl = fromExtras ?: fromData + if (actionUrl.isNullOrBlank()) return + + android.util.Log.i("HMS", "MainActivity.handleIntentForActionUrl: $actionUrl") + + // Проксируем URL во Flutter, где уже есть общая логика навигации по диплинкам. + methodChannel?.invokeMethod("openActionUrl", actionUrl) } } diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 776aea35..c98f7af2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -14,8 +14,8 @@ import 'package:go_router/go_router.dart'; class AppRouter { static GlobalKey navigatorKey = GlobalKey(); - // Домены для deep links - static const List _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru']; + // Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com) + static const List _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru', 'dev2.eatvio.com', 'www.dev2.eatvio.com']; static GoRouter appRouter = GoRouter( initialLocation: RouteConstants.updateScreen, @@ -30,64 +30,59 @@ class AppRouter { ], ); - /// Обработка редиректов для deep links - /// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам - static String? _handleRedirect(BuildContext context, GoRouterState state) { - final uri = state.uri; + static String? _resolveDeepLinkUri(Uri uri) { final host = uri.host.toLowerCase(); final segments = uri.pathSegments; - // Проверяем, является ли это deep link по домену final isDeepLink = _deepLinkHosts.contains(host); - - // Если это не deep link, не мешаем обычной навигации + if (!isDeepLink) { return null; } - // Логирование для отладки - developer.log( - 'Deep link detected: host=$host, path=${uri.path}, segments=$segments', - name: 'AppRouter', - ); + developer.log('Deep link detected: host=$host, path=${uri.path}, segments=$segments', name: 'AppRouter'); - // Проверка на пустой путь или некорректный формат if (segments.isEmpty) { developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } - // Проверка языка (первый сегмент должен быть ru или en) + if (segments.length >= 4 && + segments[0] == 'api' && + segments[1] == 'v2' && + segments[2] == 'chat' && + segments[3] == 'show') { + developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.chatSelectScreen; + } + if (segments[0] != 'ru' && segments[0] != 'en') { - developer.log( - 'Invalid language segment: ${segments[0]}, redirecting to feed', - name: 'AppRouter', - ); + developer.log('Invalid language segment: ${segments[0]}, redirecting to feed', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } - // Обработка deep links с учетом языка - // Формат: /{lang}/{type}/{identifier?} if (segments.length == 4) { - // Формат: /{lang}/diary/{...} final type = segments[1]; - + if (type == 'diary') { developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.diaryScreen; } - - developer.log( - 'Unknown deep link type for 4 segments: $type, redirecting to feed', - name: 'AppRouter', - ); + if (type == 'consultations' && segments[2] == 'status') { + final consultationId = segments[3]; + developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '${RouteConstants.consultationsScreen}?id=$consultationId'; + } + + developer.log('Unknown deep link type for 4 segments: $type, redirecting to feed', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } else if (segments.length == 3) { - // Формат: /{lang}/{type}/{identifier} final type = segments[1]; final contentIdentifier = segments[2]; @@ -104,55 +99,111 @@ class AppRouter { developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter'); FlutterNativeSplash.remove(); return '/about-plan-screen?slug=$contentIdentifier'; - case 'diet': - developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter'); + case 'diet': { + final dietId = int.tryParse(contentIdentifier); + if (dietId != null) { + developer.log('Deep link: diet/id=$dietId', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/about-diet-screen?id=$dietId'; + } + developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter'); FlutterNativeSplash.remove(); return '/about-diet-screen?slug=$contentIdentifier'; + } case 'chat': developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.chatSelectScreen; case 'profile': + // В диплинке передан slug — открываем профиль пользователя по slug. developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter'); FlutterNativeSplash.remove(); return '/profile-screen?slug=$contentIdentifier'; + case 'badge': + // В badge нет slug, открываем вкладку «Мой профиль». + developer.log('Deep link: badge → my-profile tab', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.myProfileScreen; case 'diary': developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.diaryScreen; + case 'consultant_diary_comment': + developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.diaryScreen; default: - developer.log( - 'Unknown deep link type: $type, redirecting to feed', - name: 'AppRouter', - ); + developer.log('Unknown deep link type: $type, redirecting to feed', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } } else if (segments.length == 2) { - // Формат: /{lang}/{type} - только для chat final type = segments[1]; - + if (type == 'chat') { developer.log('Deep link: chat', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.chatSelectScreen; - } else { - developer.log( - 'Unknown deep link type for 2 segments: $type, redirecting to feed', - name: 'AppRouter', - ); - FlutterNativeSplash.remove(); - return RouteConstants.feedScreen; } - } else { - // Неизвестный формат deep link - developer.log( - 'Unknown deep link format: ${segments.length} segments, redirecting to feed', - name: 'AppRouter', - ); + if (type == 'diary') { + developer.log('Deep link: diary', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.diaryScreen; + } + + developer.log('Unknown deep link type for 2 segments: $type, redirecting to feed', name: 'AppRouter'); FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } + + developer.log('Unknown deep link format: ${segments.length} segments, redirecting to feed', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.feedScreen; + } + + static String? _handleRedirect(BuildContext context, GoRouterState state) { + return _resolveDeepLinkUri(state.uri); + } + + static void navigateByDeepLinkUrl(String url) { + if (url.isEmpty) { + developer.log('navigateByDeepLinkUrl: empty url, redirecting to feed', name: 'AppRouter'); + appRouter.go(RouteConstants.feedScreen); + return; + } + + Uri? uri; + try { + uri = Uri.tryParse(url); + } catch (e) { + developer.log('navigateByDeepLinkUrl: failed to parse url=$url, error=$e, redirecting to feed', name: 'AppRouter'); + appRouter.go(RouteConstants.feedScreen); + return; + } + + if (uri == null) { + developer.log('navigateByDeepLinkUrl: uri is null for url=$url, redirecting to feed', name: 'AppRouter'); + appRouter.go(RouteConstants.feedScreen); + return; + } + + if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) { + uri = Uri( + scheme: 'https', + host: _deepLinkHosts.first, + pathSegments: uri.pathSegments, + queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters, + ); + } + + final target = _resolveDeepLinkUri(uri); + + if (target != null) { + appRouter.go(target); + } else { + developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter'); + appRouter.go(RouteConstants.feedScreen); + } } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index e85807ee..1d09d7a3 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -5,7 +5,6 @@ import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/services.dart'; - import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; diff --git a/lib/core/notifications/push_navigation_handler.dart b/lib/core/notifications/push_navigation_handler.dart new file mode 100644 index 00000000..b26db5e3 --- /dev/null +++ b/lib/core/notifications/push_navigation_handler.dart @@ -0,0 +1,91 @@ +import 'dart:developer' as developer; + +import 'package:eatvio_app/core/app_router.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях. +class PushNavigationHandler { + static bool _initialized = false; + static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push'); + + /// Инициализация обработчиков Firebase Messaging. + /// Должна вызываться один раз после старта приложения. + static Future initialize() async { + if (_initialized) return; + _initialized = true; + + // Сообщение, по которому запустили приложение из "убитого" состояния. + try { + final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + if (initialMessage != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _handleMessageInternal(initialMessage, source: 'initialMessage'); + }); + } + } catch (e) { + developer.log( + 'PushNavigationHandler: error while getting initialMessage: $e', + name: 'PushNavigationHandler', + ); + } + + // Тап по уведомлению, когда приложение было в фоне. + FirebaseMessaging.onMessageOpenedApp.listen( + (message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'), + ); + + // Подписка на события от native-кода (HMS через MethodChannel). + _hmsChannel.setMethodCallHandler((call) async { + if (call.method == 'openActionUrl') { + final url = call.arguments as String? ?? ''; + handleActionUrlFromNative(url); + } + }); + } + + /// Входная точка для навигации из native-кода (например, HMS через MethodChannel). + static void handleActionUrlFromNative(String url) { + if (url.isEmpty) { + developer.log( + 'PushNavigationHandler.handleActionUrlFromNative: empty url', + name: 'PushNavigationHandler', + ); + return; + } + + developer.log( + 'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', + name: 'PushNavigationHandler', + ); + + AppRouter.navigateByDeepLinkUrl(url); + } + + /// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё). + static void handleMessage(RemoteMessage message) { + _handleMessageInternal(message, source: 'manualHandle'); + } + + static void _handleMessageInternal(RemoteMessage message, {required String source}) { + final data = message.data; + final actionUrl = data['action_url'] as String?; + + if (actionUrl == null || actionUrl.isEmpty) { + developer.log( + 'PushNavigationHandler: no action_url in message data, source=$source, data=$data', + name: 'PushNavigationHandler', + ); + return; + } + + developer.log( + 'PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', + name: 'PushNavigationHandler', + ); + + AppRouter.navigateByDeepLinkUrl(actionUrl); + } +} + diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart index 0556fab6..dcb3d182 100644 --- a/lib/core/routing/content_routes.dart +++ b/lib/core/routing/content_routes.dart @@ -17,12 +17,20 @@ class ContentRoutes { name: 'aboutPlanScreen', builder: (context, state) { final slug = state.uri.queryParameters['slug']; + final commentIdStr = state.uri.queryParameters['commentId']; + final commentId = int.tryParse(commentIdStr ?? ''); + if (slug != null) { - return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); + return AboutPlanScreen(slug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId); } final data = state.extra as Map; - return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + return AboutPlanScreen( + slug: data['slug'] as String, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null), + highlightedCommentId: data['commentId'] as int?, + ); }, ), GoRoute( @@ -30,12 +38,20 @@ class ContentRoutes { name: 'aboutRecipesScreen', builder: (context, state) { final slug = state.uri.queryParameters['slug']; + final commentIdStr = state.uri.queryParameters['commentId']; + final commentId = int.tryParse(commentIdStr ?? ''); + if (slug != null) { - return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId); } 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? ?? (data['commentId'] != null), + highlightedCommentId: data['commentId'] as int?, + ); }, ), GoRoute( @@ -43,16 +59,28 @@ class ContentRoutes { name: 'aboutDietScreen', builder: (context, state) { final slug = state.uri.queryParameters['slug']; + final idStr = state.uri.queryParameters['id']; + final id = int.tryParse(idStr ?? ''); + final commentIdStr = state.uri.queryParameters['commentId']; + final commentId = int.tryParse(commentIdStr ?? ''); + if (slug != null) { - return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false); + return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId); } - final data = state.extra as Map; - return AboutDietScreen( - id: data['id'] as int?, - slug: data['slug'] as String?, - index: data['index'] as int?, - navigateToComments: data['navigateToComments'] as bool, - ); + if (id != null && id > 0) { + return AboutDietScreen(id: id, slug: null, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId); + } + final data = state.extra as Map?; + if (data != null) { + return AboutDietScreen( + id: data['id'] as int?, + slug: data['slug'] as String?, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null), + highlightedCommentId: data['commentId'] as int?, + ); + } + return const AboutDietScreen(id: null, slug: null, index: null, navigateToComments: false, highlightedCommentId: null); }, ), GoRoute( @@ -65,12 +93,20 @@ class ContentRoutes { name: 'blogPostScreen', builder: (context, state) { final id = state.uri.queryParameters['id']; + final commentIdStr = state.uri.queryParameters['commentId']; + final commentId = int.tryParse(commentIdStr ?? ''); final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { - return BlogPostScreen(id: parsedId); + return BlogPostScreen(id: parsedId, highlightedCommentId: commentId); } - return BlogPostScreen(id: state.extra as int); + if (state.extra is int) { + return BlogPostScreen(id: state.extra as int); + } + + final data = state.extra as Map; + return BlogPostScreen(id: data['id'] as int, highlightedCommentId: data['commentId'] as int?); }, ), ]; diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index 88a8db04..2557ddcf 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; @@ -31,8 +32,11 @@ class MainNavigationRoutes { profileAvatarCubit.getMyProfile(); }); } - - return BottomNavigationBarWidget(child: child); + + return _MainShellWithNavScope( + currentLocation: state.matchedLocation, + child: child, + ); }, routes: [ GoRoute( @@ -58,7 +62,7 @@ class MainNavigationRoutes { GoRoute( path: RouteConstants.diaryScreen, name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), + pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)), ), GoRoute( path: ConsultationsScreen.path, @@ -68,7 +72,10 @@ class MainNavigationRoutes { GoRoute( path: RouteConstants.chatSelectScreen, name: 'chatSelectScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + pageBuilder: (context, state) { + final initialSearchQuery = state.extra is String ? state.extra as String : null; + return NoTransitionPage(child: ChatsScreen(initialSearchQuery: initialSearchQuery)); + }, ), GoRoute( path: RouteConstants.notificationsScreen, @@ -94,3 +101,33 @@ class MainNavigationRoutes { ]; } } + +/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications) +/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing. +class _MainShellWithNavScope extends StatefulWidget { + const _MainShellWithNavScope({ + required this.child, + required this.currentLocation, + }); + + final Widget child; + final String currentLocation; + + @override + State<_MainShellWithNavScope> createState() => _MainShellWithNavScopeState(); +} + +class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> { + final BottomNavController _navController = BottomNavController(); + + @override + Widget build(BuildContext context) { + return BottomNavScope( + controller: _navController, + child: BottomNavigationBarWidget( + currentLocation: widget.currentLocation, + child: widget.child, + ), + ); + } +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 98e767da..05c8e244 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -204,7 +204,7 @@ class OtherRoutes { GoRoute( path: '${RouteConstants.subsScreen}/:slug', name: 'subsScreen', - builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), + builder: (context, state) => SubsScreen(slug: state.pathParameters['slug'], subType: state.extra as SubType), ), // Chat routes @@ -285,8 +285,7 @@ class OtherRoutes { builder: (context, state) { 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); }, ), GoRoute( diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 3a711177..cf84354c 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -11,6 +11,7 @@ import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; class WsManager { final WsService wsService; @@ -111,6 +112,7 @@ class WsManager { } }); _subscribe(); + await _connectCubitsToStreams(); } catch (e, st) { await Sentry.captureException(e, stackTrace: st); log(e.toString(), stackTrace: st); @@ -118,6 +120,20 @@ class WsManager { } } + /// Подключает ChatsCubit и NotificationsCubit к стримам сокета. + /// Вызывается после успешного initialize(), чтобы уведомления и чаты работали + /// и при входе уже залогиненного пользователя (редирект на feed), не только после экрана логина. + Future _connectCubitsToStreams() async { + try { + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + } catch (e, st) { + log('WsManager._connectCubitsToStreams: $e', stackTrace: st); + await Sentry.captureException(e, stackTrace: st); + } + } + Future _initPrivateNotificationsChannel() async { final token = await TokenStorage.getTokenFromStorage(); _privateNotificationChannel = client?.privateChannel( diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index bf9074d9..d1c1ccfd 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -21,7 +21,6 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; @@ -80,9 +79,6 @@ class _AuthScreenState extends State { try { await sl().getMyProfile(); await sl().getChatRoomsWithLoading(); - await sl().connectToWebSocket(); - await sl().getNotifications(); - await sl().connectToWebSocket(); await sl().registerPushToken(); showSuccessToast('Успешный вход'); } catch (e) { @@ -213,9 +209,6 @@ class _AuthScreenState extends State { await sl().initialize(); await sl().getMyProfile(); await sl().getChatRoomsWithLoading(); - await sl().connectToWebSocket(); - await sl().getNotifications(); - await sl().connectToWebSocket(); await sl().registerPushToken(); showSuccessToast('Успешный вход'); } catch (e) { diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 1b27237f..1ea307f0 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -13,7 +13,6 @@ import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/validation_utils.dart'; @@ -74,9 +73,6 @@ class _RegisterViaSocialScreenState extends State { try { await sl().getMyProfile(); await sl().getChatRoomsWithLoading(); - await sl().connectToWebSocket(); - await sl().getNotifications(); - await sl().connectToWebSocket(); await sl().registerPushToken(); showSuccessToast('Успешная регистрация'); } catch (e) { diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 723f95e9..4b90fe70 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -10,7 +10,6 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; @@ -76,9 +75,6 @@ class _ScanQRCodeScreenState extends State { try { await sl().getMyProfile(); await sl().getChatRoomsWithLoading(); - await sl().connectToWebSocket(); - await sl().getNotifications(); - await sl().connectToWebSocket(); await sl().registerPushToken(); } catch (e) { rethrow; diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 70593f8c..bf50518a 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -21,8 +21,9 @@ import 'package:share_plus/share_plus.dart'; class BlogPostScreen extends StatefulWidget { final int id; + final int? highlightedCommentId; - const BlogPostScreen({super.key, required this.id}); + const BlogPostScreen({super.key, required this.id, this.highlightedCommentId}); @override State createState() => _BlogPostScreenState(); @@ -303,7 +304,13 @@ class _BlogPostScreenState extends State { const SizedBox(height: 16), if (state is CommentsStateLoading) const Loader(), if (state is CommentsStateLoaded) - CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId), + CommentsWidget( + id: widget.id.toString(), + commentSourceType: CommentSourceType.blogPost, + comment: state.comment, + replyId: replyId, + highlightedCommentId: widget.highlightedCommentId, + ), ], ), ), diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart new file mode 100644 index 00000000..53ea8741 --- /dev/null +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class BottomNavController { + BottomNavController(); + + void Function(String path, Object? extra)? _onSwitchToRoute; + + void register(void Function(String path, Object? extra) callback) { + _onSwitchToRoute = callback; + } + + void switchToRoute(String path, [Object? extra]) { + _onSwitchToRoute?.call(path, extra); + } +} + +class BottomNavScope extends InheritedWidget { + const BottomNavScope({ + required this.controller, + required super.child, + super.key, + }); + + final BottomNavController controller; + + static BottomNavController? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()?.controller; + } + + static BottomNavController of(BuildContext context) { + final controller = maybeOf(context); + assert(controller != null, 'No BottomNavScope found in context'); + return controller!; + } + + @override + bool updateShouldNotify(BottomNavScope oldWidget) => controller != oldWidget.controller; +} diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index efac96bf..207b348a 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -18,9 +19,15 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; class BottomNavigationBarWidget extends StatefulWidget { - final Widget child; + const BottomNavigationBarWidget({ + super.key, + required this.child, + required this.currentLocation, + }); - const BottomNavigationBarWidget({super.key, required this.child}); + final Widget child; + /// Текущий путь из GoRouter (matchedLocation), чтобы подсветка таба совпадала с экраном при навигации через go() (диплинки, пуши). + final String currentLocation; @override State createState() => _BottomNavigationBarWidgetState(); @@ -66,6 +73,24 @@ class _BottomNavigationBarWidgetState extends State { } } + /// Switch to the tab that corresponds to [path], optionally with [extra] + /// (e.g. date for diary). If [path] is not in the current tab list, falls back to push. + void _switchToRoute(String path, [Object? extra]) { + if (pages == null) return; + final index = pages!.indexOf(path); + if (index >= 0 && index != currentBottomNavBarIndex) { + setState(() { + currentBottomNavBarIndex = index; + }); + GoRouter.of(context).replace(path, extra: extra); + } else if (index >= 0) { + // Same tab but with new extra (e.g. diary with date) + GoRouter.of(context).replace(path, extra: extra); + } else { + GoRouter.of(context).push(path, extra: extra); + } + } + static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500 Widget _buildLogoIcon() { @@ -121,6 +146,22 @@ class _BottomNavigationBarWidgetState extends State { // Инициализируем pages на основе profileModel pages ??= _getPages(isConsultant); + // Expose tab switching to descendants (e.g. notification widget) + final controller = BottomNavScope.maybeOf(context); + if (controller != null) { + controller.register(_switchToRoute); + } + + // Подсветка таба по текущему маршруту (совпадает при навигации через go(), в т.ч. диплинки/пуши). + final pathWithoutQuery = widget.currentLocation.split('?').first; + final indexFromRoute = pages!.indexOf(pathWithoutQuery); + final effectiveIndex = indexFromRoute >= 0 ? indexFromRoute : currentBottomNavBarIndex; + if (indexFromRoute >= 0 && indexFromRoute != currentBottomNavBarIndex) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() => currentBottomNavBarIndex = indexFromRoute); + }); + } + return Scaffold( body: widget.child, bottomNavigationBar: BottomNavigationBar( @@ -129,7 +170,7 @@ class _BottomNavigationBarWidgetState extends State { selectedFontSize: 10, unselectedFontSize: 10, selectedItemColor: appTheme.blueA400, - currentIndex: currentBottomNavBarIndex, + currentIndex: effectiveIndex, items: [ BottomNavigationBarItem( label: '', @@ -204,7 +245,7 @@ class _BottomNavigationBarWidgetState extends State { ImageConstant.icnChat, width: 20, height: 20, - colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen' + colorFilter: pages![effectiveIndex] == '/chat-select-screen' ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : const ColorFilter.mode(Colors.grey, BlendMode.srcIn), ), @@ -226,7 +267,7 @@ class _BottomNavigationBarWidgetState extends State { padding: const EdgeInsets.only(bottom: 2), child: SvgPicture.asset( ImageConstant.icnBell, - colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen' + colorFilter: pages![effectiveIndex] == '/notifications-screen' ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : const ColorFilter.mode(Colors.grey, BlendMode.srcIn), width: 20, diff --git a/lib/features/chat/presentation/pages/chats_screen.dart b/lib/features/chat/presentation/pages/chats_screen.dart index 539e971c..993bcfd3 100644 --- a/lib/features/chat/presentation/pages/chats_screen.dart +++ b/lib/features/chat/presentation/pages/chats_screen.dart @@ -11,7 +11,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ChatsScreen extends StatefulWidget { - const ChatsScreen({super.key}); + const ChatsScreen({super.key, this.initialSearchQuery}); + + /// When set (e.g. from notification tap), the search field is pre-filled + /// and chat list is filtered by this query. + final String? initialSearchQuery; @override State createState() => _ChatsScreenState(); @@ -23,12 +27,17 @@ class _ChatsScreenState extends State { @override void initState() { super.initState(); - // Инициализация ChatsCubit при первом открытии экрана + final query = widget.initialSearchQuery?.trim(); + if (query != null && query.isNotEmpty) { + searchController.text = query; + } WidgetsBinding.instance.addPostFrameCallback((_) { final cubit = context.read(); final state = cubit.state; if (state.state == BaseState.initial) { - cubit.getChatRoomsWithLoading(); + cubit.getChatRoomsWithLoading(query: query?.isNotEmpty == true ? query : null); + } else if (query != null && query.isNotEmpty) { + cubit.getChatRooms(query: query); } }); } diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index b1529280..7efa25f7 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -172,8 +172,8 @@ class _ConsultationCardWidgetState extends State { ); return; } - GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1}); - }, + GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id,}); + }, borderRadius: BorderRadius.circular(100), child: Ink( decoration: BoxDecoration( @@ -255,7 +255,7 @@ class _ConsultationCardWidgetState extends State { .where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished) .lastOrNull; if (lastActiveDay != null) { - GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1}); + GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id,}); return; } showErrorDialog( diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 0bbb4f82..93e24045 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -48,6 +48,11 @@ class DiaryCubit extends Cubit { String comment = ''; + void setInitialDate(DateTime? date) { + if (date == null) return; + selectedDay = date; + } + Future setSelectedDay(DateTime day) async { final prevDay = selectedDay; final prevMeal = selectedMeal; @@ -63,7 +68,7 @@ class DiaryCubit extends Cubit { return canSelect; } - Future fetchDiaryV2({bool needLoading = false, int? diaryId}) async { + Future fetchDiaryV2({bool needLoading = false, int? diaryId, DateTime? date}) async { final currentState = state; try { if (needLoading) { @@ -77,6 +82,7 @@ class DiaryCubit extends Cubit { return true; } } + final diaryDay = await getDiary(); if (diaryDay == null) { emit(const DiaryState.error(errorMessage: 'Diary not found')); diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index c7f832d4..07042f16 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -24,31 +24,29 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DiaryScreen extends StatelessWidget { - const DiaryScreen({super.key}); - + const DiaryScreen({super.key, required this.date}); + final DateTime? date; @override Widget build(BuildContext context) { - return const DiaryScreenContent(); + return DiaryScreenContent(date: date); } } class UserDiaryScreen extends StatelessWidget { static const path = '/user-diary-screen'; static const name = 'userDiaryScreen'; - const UserDiaryScreen({super.key, required this.diaryId, required this.currentPlanDay}); + const UserDiaryScreen({super.key, required this.diaryId}); final int diaryId; - final int currentPlanDay; @override Widget build(BuildContext context) { - return DiaryScreenContent(diaryId: diaryId, currentPlanDay: currentPlanDay); + return DiaryScreenContent(diaryId: diaryId,); } } class DiaryScreenContent extends StatefulWidget { - const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay}); + const DiaryScreenContent({super.key, this.diaryId,this.date}); final int? diaryId; - final int? currentPlanDay; - + final DateTime? date; @override State createState() => _DiaryScreenContentState(); } @@ -61,7 +59,11 @@ class _DiaryScreenContentState extends State { final s = S.of(context); return MultiBlocProvider( providers: [ - BlocProvider(create: (BuildContext context) => sl()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)), + BlocProvider( + create: (BuildContext context) => sl() + ..setInitialDate(widget.date) + ..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId), + ), BlocProvider(create: (context) => sl()), ], child: BlocBuilder( @@ -91,7 +93,7 @@ class _DiaryScreenContentState extends State { physics: const BouncingScrollPhysics(), child: Column( children: [ - if (widget.diaryId == null) const CalendarWidget(), + if (widget.diaryId == null) CalendarWidget(date: widget.date), if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()], if (state is DiaryStateLoaded) WakeupAndRetireWidget( diff --git a/lib/features/diary/presentation/widgets/calendar_widget.dart b/lib/features/diary/presentation/widgets/calendar_widget.dart index cce80a55..bcf1ab09 100644 --- a/lib/features/diary/presentation/widgets/calendar_widget.dart +++ b/lib/features/diary/presentation/widgets/calendar_widget.dart @@ -8,7 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:table_calendar/table_calendar.dart'; class CalendarWidget extends StatefulWidget { - const CalendarWidget({super.key}); + const CalendarWidget({super.key, this.date}); + final DateTime? date; @override State createState() => _CalendarWidgetState(); @@ -18,6 +19,23 @@ class _CalendarWidgetState extends State { CalendarFormat _calendarFormat = CalendarFormat.week; DateTime _focusedDay = DateTime.now(); DateTime _selectedDay = DateTime.now(); + @override + void initState() { + super.initState(); + if (widget.date != null) { + _selectedDay = widget.date!; + _focusedDay = widget.date!; + } + } + + @override + void didUpdateWidget(CalendarWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.date != null && widget.date != oldWidget.date) { + _selectedDay = widget.date!; + _focusedDay = widget.date!; + } + } @override Widget build(BuildContext context) { diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index 54d74215..3823b471 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -37,8 +37,9 @@ class AboutDietScreen extends StatefulWidget { final int? id; final int? index; final bool navigateToComments; + final int? highlightedCommentId; - const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id}); + const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id, this.highlightedCommentId}); @override State createState() => _AboutDietScreenState(); @@ -433,6 +434,7 @@ class _AboutDietScreenState extends State { commentSourceType: CommentSourceType.diet, comment: state.comment, replyId: replyId, + highlightedCommentId: widget.highlightedCommentId, ), ], ), diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index f8b3ecba..61ba39d2 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -23,14 +23,60 @@ class CommentsWidget extends StatefulWidget { final CommentSourceType commentSourceType; final CommentModel comment; final int? replyId; + final int? highlightedCommentId; - const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId}); + const CommentsWidget({ + super.key, + required this.id, + required this.commentSourceType, + required this.comment, + this.replyId, + this.highlightedCommentId, + }); @override State createState() => _CommentsWidgetState(); } class _CommentsWidgetState extends State { + final Map commentKeys = {}; + + @override + void initState() { + super.initState(); + if (widget.highlightedCommentId != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToHighlightedComment(); + }); + } + } + + @override + void didUpdateWidget(CommentsWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.highlightedCommentId != null && widget.highlightedCommentId != oldWidget.highlightedCommentId) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToHighlightedComment(); + }); + } else if (widget.comment != oldWidget.comment && widget.highlightedCommentId != null) { + // Comments might have just loaded + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToHighlightedComment(); + }); + } + } + + void _scrollToHighlightedComment() { + final key = commentKeys[widget.highlightedCommentId]; + if (key != null && key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } + } + @override Widget build(BuildContext context) { final s = S.of(context); @@ -49,8 +95,10 @@ class _CommentsWidgetState extends State { } Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) { + final key = commentKeys.putIfAbsent(comment.id, () => GlobalKey()); final isMe = (sl().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug; return GestureDetector( + key: key, onTap: () { showCommentsActionsBottomSheet( context, @@ -83,7 +131,7 @@ class _CommentsWidgetState extends State { child: Container( width: MediaQuery.of(context).size.width, decoration: BoxDecoration( - color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white, + color: (widget.replyId == comment.id || widget.highlightedCommentId == comment.id) ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white, borderRadius: BorderRadius.circular(16), ), child: Padding( diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index cc77616e..b178eb91 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -10,16 +10,24 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod @override final bool isRead; final String planName; + final int planId; final String customerName; + final int userId; + final String userPhotoPath; final String chatId; + final String chatUrl; @override final DateTime createdAt; PlanSubscriptionStartedToAuthorNotification({ required this.type, required this.planName, + required this.planId, required this.customerName, + required this.userId, + required this.userPhotoPath, required this.chatId, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -27,9 +35,13 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionStartedToAuthorNotification( type: type, - planName: json['plan_name'], - customerName: json['user_name'], - chatId: json['chat_id'], + planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + customerName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatId: json['chat_id']?.toString() ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -42,14 +54,22 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo @override final bool isRead; final String planName; + final int planId; final String customerName; + final int userId; + final String userPhotoPath; + final String chatUrl; @override final DateTime createdAt; PlanSubscriptionCanceledToAuthorNotification({ required this.type, required this.planName, + required this.planId, required this.customerName, + required this.userId, + required this.userPhotoPath, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -57,8 +77,12 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToAuthorNotification( type: type, - planName: json['plan_name'], - customerName: json['user_name'], + planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + customerName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -72,16 +96,28 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel { final bool isRead; final String planName; final int planId; + final String consultantName; + final String peerProfileUrl; @override final DateTime createdAt; - ChosenPlanStartedToClientNotification({required this.type, required this.planName, required this.planId, required this.isRead, required this.createdAt}); + ChosenPlanStartedToClientNotification({ + required this.type, + required this.planName, + required this.planId, + required this.consultantName, + required this.peerProfileUrl, + required this.isRead, + required this.createdAt, + }); factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToClientNotification( type: type, - planName: json['plan_name'], - planId: json['plan_id'], + planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + consultantName: json['consultant_name'] ?? '', + peerProfileUrl: json['peer_profile_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -94,15 +130,26 @@ class ChosenPlanTomorrowReminderToClientNotification implements BaseNotification @override final bool isRead; final String planName; + final String consultantName; + final int chatId; @override final DateTime createdAt; - ChosenPlanTomorrowReminderToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + ChosenPlanTomorrowReminderToClientNotification({ + required this.type, + required this.planName, + required this.consultantName, + required this.chatId, + required this.isRead, + required this.createdAt, + }); factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanTomorrowReminderToClientNotification( type: type, - planName: json['plan_name'], + planName: json['plan_name'] ?? '', + consultantName: json['consultant_name'] ?? '', + chatId: json['chat_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -114,15 +161,19 @@ class DiaryReminderNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + final int? diaryId; + final String? message; @override final DateTime createdAt; - DiaryReminderNotification({required this.type, required this.isRead, required this.createdAt}); + DiaryReminderNotification({required this.type, required this.isRead, this.diaryId, this.message, required this.createdAt}); factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return DiaryReminderNotification( type: type, isRead: isRead ?? false, + diaryId: json['diary_id'], + message: json['message'], createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } @@ -133,16 +184,36 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + final int chosenPlanId; + final int diaryId; final DateTime date; + final String planName; + final String userName; + final String consultantName; @override final DateTime createdAt; - ConsultantLeftCommentNotification({required this.type, required this.date, required this.isRead, required this.createdAt}); + ConsultantLeftCommentNotification({ + required this.type, + required this.chosenPlanId, + required this.diaryId, + required this.date, + required this.planName, + required this.userName, + required this.consultantName, + required this.isRead, + required this.createdAt, + }); factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ConsultantLeftCommentNotification( type: type, + chosenPlanId: json['chosen_plan_id'] ?? 0, + diaryId: json['diary_id'] ?? 0, date: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()), + planName: json['plan_name'] ?? '', + userName: json['user_name'] ?? '', + consultantName: json['consultant_name'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -174,14 +245,34 @@ class ChosenPlanFinishedToClientNotification implements BaseNotificationModel { @override final bool isRead; final String planName; + final int chosenPlanId; + final String consultantName; + final String consultantPhotoPath; + final String consultantProfileUrl; + final int consultantId; @override final DateTime createdAt; - ChosenPlanFinishedToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + ChosenPlanFinishedToClientNotification({ + required this.type, + required this.planName, + required this.chosenPlanId, + required this.consultantName, + required this.consultantPhotoPath, + required this.consultantProfileUrl, + required this.consultantId, + required this.isRead, + required this.createdAt, + }); factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToClientNotification( type: type, - planName: json['plan_name'], + planName: json['plan_name'] ?? '', + chosenPlanId: json['chosen_plan_id'] ?? 0, + consultantName: json['consultant_name'] ?? '', + consultantPhotoPath: json['consultant_photo_path'] ?? '', + consultantProfileUrl: json['consultant_profile_url'] ?? '', + consultantId: json['consultant_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -194,14 +285,34 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification @override final bool isRead; final String planName; + final int planId; + final String authorName; + final int authorId; + final String authorPhotoPath; + final String chatUrl; @override final DateTime createdAt; - PlanSubscriptionCanceledToCustomerNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + PlanSubscriptionCanceledToCustomerNotification({ + required this.type, + required this.planName, + required this.planId, + required this.authorName, + required this.authorId, + required this.authorPhotoPath, + required this.chatUrl, + required this.isRead, + required this.createdAt, + }); factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToCustomerNotification( type: type, - planName: json['plan_name'], + planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + authorName: json['author_name'] ?? '', + authorId: json['author_id'] ?? 0, + authorPhotoPath: json['author_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -215,6 +326,10 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + final int planId; + final int customerId; + final String peerProfileUrl; + final String consultationUrl; @override final DateTime createdAt; ChosenPlanStartedToConsultantNotification({ @@ -222,6 +337,10 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel required this.isRead, required this.planName, required this.customerName, + required this.planId, + required this.customerId, + required this.peerProfileUrl, + required this.consultationUrl, required this.createdAt, }); @@ -229,8 +348,12 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel return ChosenPlanStartedToConsultantNotification( type: type, isRead: isRead ?? false, - planName: json['plan_name'], - customerName: json['client_name'], + planName: json['plan_name'] ?? '', + customerName: json['client_name'] ?? '', + planId: json['plan_id'] ?? 0, + customerId: json['client_id'] ?? 0, + peerProfileUrl: json['peer_profile_url'] ?? '', + consultationUrl: json['consultation_url'] ?? '', createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } @@ -243,12 +366,14 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica final bool isRead; final String planName; final String customerName; + final int chatId; @override final DateTime createdAt; ChosenPlanTomorrowReminderToConsultantNotification({ required this.type, required this.planName, required this.customerName, + required this.chatId, required this.isRead, required this.createdAt, }); @@ -256,8 +381,9 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanTomorrowReminderToConsultantNotification( type: type, - planName: json['plan_name'], - customerName: json['client_name'], + planName: json['plan_name'] ?? '', + customerName: json['client_name'] ?? '', + chatId: json['chat_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -270,14 +396,22 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel @override final bool isRead; final String planName; + final int planId; final String customerName; + final int userId; + final String userPhotoPath; + final String chatUrl; @override final DateTime createdAt; PlanSubscriptionPaidToAuthorNotification({ required this.type, required this.isRead, required this.planName, + required this.planId, required this.customerName, + required this.userId, + required this.userPhotoPath, + required this.chatUrl, required this.createdAt, }); @@ -285,8 +419,12 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel return PlanSubscriptionPaidToAuthorNotification( type: type, isRead: isRead ?? false, - planName: json['plan_name'], - customerName: json['user_name'], + planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + customerName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } @@ -300,6 +438,9 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode final String planName; final String customerName; final int planId; + final int chosenPlanId; + final String clientPhotoPath; + final int clientId; @override final DateTime createdAt; ChosenPlanFinishedToConsultantNotification({ @@ -307,6 +448,9 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode required this.planName, required this.customerName, required this.planId, + required this.chosenPlanId, + required this.clientPhotoPath, + required this.clientId, required this.isRead, required this.createdAt, }); @@ -314,9 +458,12 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToConsultantNotification( type: type, - planName: json['plan_name'], - customerName: json['client_name'], - planId: json['chosen_plan_id'], + planName: json['plan_name'] ?? '', + customerName: json['client_name'] ?? '', + planId: json['plan_id'] ?? 0, + chosenPlanId: json['chosen_plan_id'] ?? 0, + clientPhotoPath: json['client_photo_path'] ?? '', + clientId: json['client_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -328,15 +475,31 @@ class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + final String planName; + final int chosenPlanId; + final String complaintUrl; + final String chatUrl; @override final DateTime createdAt; - ChosenPlanLastDayFinishedForUser({required this.type, required this.isRead, required this.createdAt}); + ChosenPlanLastDayFinishedForUser({ + required this.type, + required this.isRead, + required this.planName, + required this.chosenPlanId, + required this.complaintUrl, + required this.chatUrl, + required this.createdAt, + }); factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanLastDayFinishedForUser( type: type, isRead: isRead ?? false, + planName: json['plan_name'] ?? '', + chosenPlanId: json['chosen_plan_id'] ?? 0, + complaintUrl: json['complaintUrl'] ?? json['complaint_url'] ?? '', + chatUrl: json['chatUrl'] ?? json['chat_url'] ?? '', createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } @@ -367,10 +530,10 @@ class ChosenPlanConsultantLastDayStarted implements BaseNotificationModel { factory ChosenPlanConsultantLastDayStarted.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanConsultantLastDayStarted( type: type, - planName: json['plan_name'], - planId: json['plan_id'], - chatId: json['chat_id'], - chatUrl: json['chat_url'], + planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + chatId: json['chat_id']?.toString() ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -385,6 +548,7 @@ class ComplaintToModerationForUser implements BaseNotificationModel { final String message; final int entityId; final String entityType; + final String entityName; @override final DateTime createdAt; @@ -393,6 +557,7 @@ class ComplaintToModerationForUser implements BaseNotificationModel { required this.message, required this.entityId, required this.entityType, + required this.entityName, required this.isRead, required this.createdAt, }); @@ -400,15 +565,32 @@ class ComplaintToModerationForUser implements BaseNotificationModel { factory ComplaintToModerationForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ComplaintToModerationForUser( type: type, - message: json['message'], - entityId: json['entity_id'], - entityType: json['entity_type'], + message: json['message'] ?? '', + entityId: json['entity_id'] ?? 0, + entityType: json['entity_type'] ?? '', + entityName: json['entity_name'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } +enum CommentedEntityType { + diet, + blogPost, + unknown; + + factory CommentedEntityType.fromJson(String type) { + return switch (type) { + 'diet' => CommentedEntityType.diet, + 'Modules\\UserGeneratedPublicContent\\Models\\Diet' => CommentedEntityType.diet, + 'blog-post' => CommentedEntityType.blogPost, + 'Modules\\UserGeneratedPublicContent\\Models\\BlogPost' => CommentedEntityType.blogPost, + _ => CommentedEntityType.unknown, + }; + } +} + class CommentReplied implements BaseNotificationModel { @override final NotificationType type; @@ -416,10 +598,11 @@ class CommentReplied implements BaseNotificationModel { final bool isRead; final String entityName; final int commentedEntityId; - final String commentedEntityType; + final CommentedEntityType commentedEntityType; final String author; final int authorId; final int parentCommentId; + final int? commentId; @override final DateTime createdAt; @@ -431,6 +614,7 @@ class CommentReplied implements BaseNotificationModel { required this.author, required this.authorId, required this.parentCommentId, + this.commentId, required this.isRead, required this.createdAt, }); @@ -440,10 +624,11 @@ class CommentReplied implements BaseNotificationModel { type: type, entityName: json['entity_name'], commentedEntityId: json['commented_entity_id'], - commentedEntityType: json['commented_entity_type'], + commentedEntityType: CommentedEntityType.fromJson(json['commented_entity_type']), author: json['author'], authorId: json['author_id'], parentCommentId: json['parrent_comment_id'], + commentId: json['comment_id'], isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -498,12 +683,13 @@ class EntityCommented implements BaseNotificationModel { final bool isRead; final String entityName; final int entityId; - final String entityType; + final CommentedEntityType entityType; final String entityTranslatedType; final String entityUrl; final String authorName; final int authorId; final String authorSlug; + final int? commentId; @override final DateTime createdAt; @@ -517,6 +703,7 @@ class EntityCommented implements BaseNotificationModel { required this.authorName, required this.authorId, required this.authorSlug, + this.commentId, required this.isRead, required this.createdAt, }); @@ -526,12 +713,13 @@ class EntityCommented implements BaseNotificationModel { type: type, entityName: json['entity_name'], entityId: json['entity_id'], - entityType: json['entity_type'], + entityType: CommentedEntityType.fromJson(json['entity_type']), entityTranslatedType: json['entity_traslated_type'], entityUrl: json['entity_url'], authorName: json['author_name'], authorId: json['author_id'], authorSlug: json['author_slug'], + commentId: json['comment_id'], isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -831,7 +1019,11 @@ class UnknownNotification implements BaseNotificationModel { @override final DateTime createdAt; - UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); + UnknownNotification({required this.type, this.isRead = true, required this.createdAt}); + + factory UnknownNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return UnknownNotification(type: type, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String())); + } } class DietCancelledNotification implements BaseNotificationModel { @@ -840,15 +1032,17 @@ class DietCancelledNotification implements BaseNotificationModel { @override final bool isRead; final String dietName; + final int dietId; @override final DateTime createdAt; - DietCancelledNotification({required this.type, required this.dietName, required this.isRead, required this.createdAt}); + DietCancelledNotification({required this.type, required this.dietName, required this.dietId, required this.isRead, required this.createdAt}); factory DietCancelledNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return DietCancelledNotification( type: type, dietName: json['diet_name'] ?? '', + dietId: json['diet_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -861,15 +1055,17 @@ class DietStartedNotification implements BaseNotificationModel { @override final bool isRead; final String dietName; + final int dietId; @override final DateTime createdAt; - DietStartedNotification({required this.type, required this.dietName, required this.isRead, required this.createdAt}); + DietStartedNotification({required this.type, required this.dietName, required this.dietId, required this.isRead, required this.createdAt}); factory DietStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return DietStartedNotification( type: type, dietName: json['diet_name'] ?? '', + dietId: json['diet_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -882,15 +1078,17 @@ class DietExpiredNotification implements BaseNotificationModel { @override final bool isRead; final String dietName; + final int dietId; @override final DateTime createdAt; - DietExpiredNotification({required this.type, required this.dietName, required this.isRead, required this.createdAt}); + DietExpiredNotification({required this.type, required this.dietName, required this.dietId, required this.isRead, required this.createdAt}); factory DietExpiredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return DietExpiredNotification( type: type, dietName: json['diet_name'] ?? '', + dietId: json['diet_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -903,16 +1101,25 @@ class DietWillStartedInNotification implements BaseNotificationModel { @override final bool isRead; final String dietName; + final int dietId; final String startDate; @override final DateTime createdAt; - DietWillStartedInNotification({required this.type, required this.dietName, required this.startDate, required this.isRead, required this.createdAt}); + DietWillStartedInNotification({ + required this.type, + required this.dietName, + required this.dietId, + required this.startDate, + required this.isRead, + required this.createdAt, + }); factory DietWillStartedInNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return DietWillStartedInNotification( type: type, dietName: json['diet_name'] ?? '', + dietId: json['diet_id'] ?? 0, startDate: json['start_date'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), @@ -926,16 +1133,25 @@ class DietCanNotBeStartedNotification implements BaseNotificationModel { @override final bool isRead; final String dietName; + final int dietId; final String reason; @override final DateTime createdAt; - DietCanNotBeStartedNotification({required this.type, required this.dietName, required this.reason, required this.isRead, required this.createdAt}); + DietCanNotBeStartedNotification({ + required this.type, + required this.dietName, + required this.dietId, + required this.reason, + required this.isRead, + required this.createdAt, + }); factory DietCanNotBeStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return DietCanNotBeStartedNotification( type: type, dietName: json['diet_name'] ?? '', + dietId: json['diet_id'] ?? 0, reason: json['reason'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), @@ -948,18 +1164,27 @@ class PersonalDietCreationNeededNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - final String planName; - final String clientName; + final String userName; + final String consultantName; + final String myDietsUrl; @override final DateTime createdAt; - PersonalDietCreationNeededNotification({required this.type, required this.planName, required this.clientName, required this.isRead, required this.createdAt}); + PersonalDietCreationNeededNotification({ + required this.type, + required this.userName, + required this.consultantName, + required this.myDietsUrl, + required this.isRead, + required this.createdAt, + }); factory PersonalDietCreationNeededNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PersonalDietCreationNeededNotification( type: type, - planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', + userName: json['user_name'] ?? '', + consultantName: json['consultant_name'] ?? '', + myDietsUrl: json['myDietsUrl'] ?? json['my_diets_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -972,15 +1197,29 @@ class ChosenPlanClientLastDayFinishedNotification implements BaseNotificationMod @override final bool isRead; final String planName; + final int chosenPlanId; + final String complaintUrl; + final String chatUrl; @override final DateTime createdAt; - ChosenPlanClientLastDayFinishedNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + ChosenPlanClientLastDayFinishedNotification({ + required this.type, + required this.planName, + required this.chosenPlanId, + required this.complaintUrl, + required this.chatUrl, + required this.isRead, + required this.createdAt, + }); factory ChosenPlanClientLastDayFinishedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanClientLastDayFinishedNotification( type: type, planName: json['plan_name'] ?? '', + chosenPlanId: json['chosen_plan_id'] ?? 0, + complaintUrl: json['complaintUrl'] ?? json['complaint_url'] ?? '', + chatUrl: json['chatUrl'] ?? json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -993,15 +1232,26 @@ class ChosenPlanClientLastDayStartedNotification implements BaseNotificationMode @override final bool isRead; final String planName; + final int chosenPlanId; + final String lastDayDiaryUrl; @override final DateTime createdAt; - ChosenPlanClientLastDayStartedNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + ChosenPlanClientLastDayStartedNotification({ + required this.type, + required this.planName, + required this.chosenPlanId, + required this.lastDayDiaryUrl, + required this.isRead, + required this.createdAt, + }); factory ChosenPlanClientLastDayStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanClientLastDayStartedNotification( type: type, planName: json['plan_name'] ?? '', + chosenPlanId: json['chosen_plan_id'] ?? 0, + lastDayDiaryUrl: json['last_day_diary_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1014,17 +1264,29 @@ class StayOnTrackToAuthorNotification implements BaseNotificationModel { @override final bool isRead; final String planName; - final String clientName; + final String userName; + final int dayNum; + final int diaryDayId; @override final DateTime createdAt; - StayOnTrackToAuthorNotification({required this.type, required this.planName, required this.clientName, required this.isRead, required this.createdAt}); + StayOnTrackToAuthorNotification({ + required this.type, + required this.planName, + required this.userName, + required this.dayNum, + required this.diaryDayId, + required this.isRead, + required this.createdAt, + }); factory StayOnTrackToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return StayOnTrackToAuthorNotification( type: type, planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', + userName: json['user_name'] ?? '', + dayNum: json['day_num'] ?? 0, + diaryDayId: json['diary_day_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1037,15 +1299,41 @@ class ClientStartPlanSubscriptionToSubscriberNotification implements BaseNotific @override final bool isRead; final String planName; + final int planId; + final String authorName; + final int authorId; + final String authorPhotoPath; + final String chatUrl; + final String chatId; + final String startDate; @override final DateTime createdAt; - ClientStartPlanSubscriptionToSubscriberNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + ClientStartPlanSubscriptionToSubscriberNotification({ + required this.type, + required this.planName, + required this.planId, + required this.authorName, + required this.authorId, + required this.authorPhotoPath, + required this.chatUrl, + required this.chatId, + required this.startDate, + required this.isRead, + required this.createdAt, + }); factory ClientStartPlanSubscriptionToSubscriberNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ClientStartPlanSubscriptionToSubscriberNotification( type: type, planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + authorName: json['author_name'] ?? '', + authorId: json['author_id'] ?? 0, + authorPhotoPath: json['author_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', + chatId: json['chat_id']?.toString() ?? '', + startDate: json['start_date']?.toString() ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1058,14 +1346,22 @@ class ConsultantConfirmPlanSubscriptionToSubscriberNotification implements BaseN @override final bool isRead; final String planName; + final int planId; final String consultantName; + final int authorId; + final String authorPhotoPath; + final String chatUrl; @override final DateTime createdAt; ConsultantConfirmPlanSubscriptionToSubscriberNotification({ required this.type, required this.planName, + required this.planId, required this.consultantName, + required this.authorId, + required this.authorPhotoPath, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -1079,7 +1375,11 @@ class ConsultantConfirmPlanSubscriptionToSubscriberNotification implements BaseN return ConsultantConfirmPlanSubscriptionToSubscriberNotification( type: type, planName: json['plan_name'] ?? '', - consultantName: json['consultant_name'] ?? '', + planId: json['plan_id'] ?? 0, + consultantName: json['author_name'] ?? json['consultant_name'] ?? '', + authorId: json['author_id'] ?? 0, + authorPhotoPath: json['author_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1092,16 +1392,22 @@ class ClientPaidPlanSubscriptionToAuthorNotification implements BaseNotification @override final bool isRead; final String planName; + final int planId; final String clientName; - final double amount; + final int userId; + final String userPhotoPath; + final String chatUrl; @override final DateTime createdAt; ClientPaidPlanSubscriptionToAuthorNotification({ required this.type, required this.planName, + required this.planId, required this.clientName, - required this.amount, + required this.userId, + required this.userPhotoPath, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -1110,8 +1416,11 @@ class ClientPaidPlanSubscriptionToAuthorNotification implements BaseNotification return ClientPaidPlanSubscriptionToAuthorNotification( type: type, planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', - amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + planId: json['plan_id'] ?? 0, + clientName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1123,7 +1432,10 @@ class ConsultantLeftDiaryCommentNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + final int chosenPlanId; + final int diaryId; final String planName; + final String userName; final String consultantName; final DateTime diaryDate; @override @@ -1131,7 +1443,10 @@ class ConsultantLeftDiaryCommentNotification implements BaseNotificationModel { ConsultantLeftDiaryCommentNotification({ required this.type, + required this.chosenPlanId, + required this.diaryId, required this.planName, + required this.userName, required this.consultantName, required this.diaryDate, required this.isRead, @@ -1141,7 +1456,10 @@ class ConsultantLeftDiaryCommentNotification implements BaseNotificationModel { factory ConsultantLeftDiaryCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ConsultantLeftDiaryCommentNotification( type: type, + chosenPlanId: json['chosen_plan_id'] ?? 0, + diaryId: json['diary_id'] ?? 0, planName: json['plan_name'] ?? '', + userName: json['user_name'] ?? '', consultantName: json['consultant_name'] ?? '', diaryDate: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()), isRead: isRead ?? false, @@ -1156,14 +1474,22 @@ class ConsultantConfirmPlanSubscriptionToAuthorNotification implements BaseNotif @override final bool isRead; final String planName; - final String consultantName; + final int planId; + final String clientName; + final int userId; + final String userPhotoPath; + final String chatUrl; @override final DateTime createdAt; ConsultantConfirmPlanSubscriptionToAuthorNotification({ required this.type, required this.planName, - required this.consultantName, + required this.planId, + required this.clientName, + required this.userId, + required this.userPhotoPath, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -1172,7 +1498,11 @@ class ConsultantConfirmPlanSubscriptionToAuthorNotification implements BaseNotif return ConsultantConfirmPlanSubscriptionToAuthorNotification( type: type, planName: json['plan_name'] ?? '', - consultantName: json['consultant_name'] ?? '', + planId: json['plan_id'] ?? 0, + clientName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1185,14 +1515,22 @@ class ClientPaidPlanSubscriptionToSubscriberNotification implements BaseNotifica @override final bool isRead; final String planName; - final double amount; + final int planId; + final String authorName; + final int authorId; + final String authorPhotoPath; + final String chatUrl; @override final DateTime createdAt; ClientPaidPlanSubscriptionToSubscriberNotification({ required this.type, required this.planName, - required this.amount, + required this.planId, + required this.authorName, + required this.authorId, + required this.authorPhotoPath, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -1201,7 +1539,11 @@ class ClientPaidPlanSubscriptionToSubscriberNotification implements BaseNotifica return ClientPaidPlanSubscriptionToSubscriberNotification( type: type, planName: json['plan_name'] ?? '', - amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + planId: json['plan_id'] ?? 0, + authorName: json['author_name'] ?? '', + authorId: json['author_id'] ?? 0, + authorPhotoPath: json['author_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1214,14 +1556,24 @@ class ClientStartPlanSubscriptionToAuthorNotification implements BaseNotificatio @override final bool isRead; final String planName; + final int planId; final String clientName; + final int userId; + final String userPhotoPath; + final String chatId; + final String chatUrl; @override final DateTime createdAt; ClientStartPlanSubscriptionToAuthorNotification({ required this.type, required this.planName, + required this.planId, required this.clientName, + required this.userId, + required this.userPhotoPath, + required this.chatId, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -1230,7 +1582,12 @@ class ClientStartPlanSubscriptionToAuthorNotification implements BaseNotificatio return ClientStartPlanSubscriptionToAuthorNotification( type: type, planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', + planId: json['plan_id'] ?? 0, + clientName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatId: json['chat_id']?.toString() ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1243,14 +1600,22 @@ class ClientCancelPlanSubscriptionToAuthorNotification implements BaseNotificati @override final bool isRead; final String planName; + final int planId; final String clientName; + final int userId; + final String userPhotoPath; + final String chatUrl; @override final DateTime createdAt; ClientCancelPlanSubscriptionToAuthorNotification({ required this.type, required this.planName, + required this.planId, required this.clientName, + required this.userId, + required this.userPhotoPath, + required this.chatUrl, required this.isRead, required this.createdAt, }); @@ -1259,7 +1624,11 @@ class ClientCancelPlanSubscriptionToAuthorNotification implements BaseNotificati return ClientCancelPlanSubscriptionToAuthorNotification( type: type, planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', + planId: json['plan_id'] ?? 0, + clientName: json['user_name'] ?? '', + userId: json['user_id'] ?? 0, + userPhotoPath: json['user_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1272,15 +1641,35 @@ class ClientCancelPlanSubscriptionToSubscriberNotification implements BaseNotifi @override final bool isRead; final String planName; + final int planId; + final String authorName; + final int authorId; + final String authorPhotoPath; + final String chatUrl; @override final DateTime createdAt; - ClientCancelPlanSubscriptionToSubscriberNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + ClientCancelPlanSubscriptionToSubscriberNotification({ + required this.type, + required this.planName, + required this.planId, + required this.authorName, + required this.authorId, + required this.authorPhotoPath, + required this.chatUrl, + required this.isRead, + required this.createdAt, + }); factory ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ClientCancelPlanSubscriptionToSubscriberNotification( type: type, planName: json['plan_name'] ?? '', + planId: json['plan_id'] ?? 0, + authorName: json['author_name'] ?? '', + authorId: json['author_id'] ?? 0, + authorPhotoPath: json['author_photo_path'] ?? '', + chatUrl: json['chat_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1292,17 +1681,21 @@ class UserDailyReportCompletedNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + final int chosenPlanId; + final int diaryId; final String planName; final String clientName; - final DateTime reportDate; + final String consultantName; @override final DateTime createdAt; UserDailyReportCompletedNotification({ required this.type, + required this.chosenPlanId, + required this.diaryId, required this.planName, required this.clientName, - required this.reportDate, + required this.consultantName, required this.isRead, required this.createdAt, }); @@ -1310,9 +1703,11 @@ class UserDailyReportCompletedNotification implements BaseNotificationModel { factory UserDailyReportCompletedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return UserDailyReportCompletedNotification( type: type, + chosenPlanId: json['chosen_plan_id'] ?? 0, + diaryId: json['diary_id'] ?? 0, planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', - reportDate: parseDate(json['report_date'] ?? DateTime.now().toIso8601String()), + clientName: json['user_name'] ?? '', + consultantName: json['consultant_name'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1325,17 +1720,26 @@ class StayOnTrackToCustomerNotification implements BaseNotificationModel { @override final bool isRead; final String planName; - final String consultantName; + final int dayNum; + final int diaryDayId; @override final DateTime createdAt; - StayOnTrackToCustomerNotification({required this.type, required this.planName, required this.consultantName, required this.isRead, required this.createdAt}); + StayOnTrackToCustomerNotification({ + required this.type, + required this.planName, + required this.dayNum, + required this.diaryDayId, + required this.isRead, + required this.createdAt, + }); factory StayOnTrackToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return StayOnTrackToCustomerNotification( type: type, planName: json['plan_name'] ?? '', - consultantName: json['consultant_name'] ?? '', + dayNum: json['day_num'] ?? 0, + diaryDayId: json['diary_day_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1370,18 +1774,27 @@ class SubscriptionNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - final String subscriptionType; - final String planName; + final String userName; + final String userPhotoPath; + final String userProfileUrl; @override final DateTime createdAt; - SubscriptionNotification({required this.type, required this.subscriptionType, required this.planName, required this.isRead, required this.createdAt}); + SubscriptionNotification({ + required this.type, + required this.userName, + required this.userPhotoPath, + required this.userProfileUrl, + required this.isRead, + required this.createdAt, + }); factory SubscriptionNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return SubscriptionNotification( type: type, - subscriptionType: json['subscription_type'] ?? '', - planName: json['plan_name'] ?? '', + userName: json['user_name'] ?? '', + userPhotoPath: json['user_photo_path'] ?? '', + userProfileUrl: json['user_profile_url'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1393,18 +1806,27 @@ class BadgeAwardedNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - final String badgeName; - final String badgeDescription; + final String badgeTitle; + final String badgeType; + final String badgeImage; @override final DateTime createdAt; - BadgeAwardedNotification({required this.type, required this.badgeName, required this.badgeDescription, required this.isRead, required this.createdAt}); + BadgeAwardedNotification({ + required this.type, + required this.badgeTitle, + required this.badgeType, + required this.badgeImage, + required this.isRead, + required this.createdAt, + }); factory BadgeAwardedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return BadgeAwardedNotification( type: type, - badgeName: json['badge_name'] ?? '', - badgeDescription: json['badge_description'] ?? '', + badgeTitle: json['badge_title'] ?? '', + badgeType: json['badge_type'] ?? '', + badgeImage: json['badge_image'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1416,18 +1838,27 @@ class BadgeRevokedNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - final String badgeName; - final String reason; + final String badgeTitle; + final String badgeType; + final String badgeImage; @override final DateTime createdAt; - BadgeRevokedNotification({required this.type, required this.badgeName, required this.reason, required this.isRead, required this.createdAt}); + BadgeRevokedNotification({ + required this.type, + required this.badgeTitle, + required this.badgeType, + required this.badgeImage, + required this.isRead, + required this.createdAt, + }); factory BadgeRevokedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return BadgeRevokedNotification( type: type, - badgeName: json['badge_name'] ?? '', - reason: json['reason'] ?? '', + badgeTitle: json['badge_title'] ?? '', + badgeType: json['badge_type'] ?? '', + badgeImage: json['badge_image'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1460,24 +1891,16 @@ class ClientCloseComplaintSuccessNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - final String complaintType; - final String entityName; + final String planName; @override final DateTime createdAt; - ClientCloseComplaintSuccessNotification({ - required this.type, - required this.complaintType, - required this.entityName, - required this.isRead, - required this.createdAt, - }); + ClientCloseComplaintSuccessNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); factory ClientCloseComplaintSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ClientCloseComplaintSuccessNotification( type: type, - complaintType: json['complaint_type'] ?? '', - entityName: json['entity_name'] ?? '', + planName: json['plan_name'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1554,17 +1977,15 @@ class ClientRejectSuccessNotification implements BaseNotificationModel { @override final bool isRead; final String planName; - final String reason; @override final DateTime createdAt; - ClientRejectSuccessNotification({required this.type, required this.planName, required this.reason, required this.isRead, required this.createdAt}); + ClientRejectSuccessNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); factory ClientRejectSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ClientRejectSuccessNotification( type: type, planName: json['plan_name'] ?? '', - reason: json['reason'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1578,25 +1999,16 @@ class ConsultantRejectSuccessNotification implements BaseNotificationModel { final bool isRead; final String planName; final String clientName; - final String reason; @override final DateTime createdAt; - ConsultantRejectSuccessNotification({ - required this.type, - required this.planName, - required this.clientName, - required this.reason, - required this.isRead, - required this.createdAt, - }); + ConsultantRejectSuccessNotification({required this.type, required this.planName, required this.clientName, required this.isRead, required this.createdAt}); factory ConsultantRejectSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ConsultantRejectSuccessNotification( type: type, planName: json['plan_name'] ?? '', - clientName: json['client_name'] ?? '', - reason: json['reason'] ?? '', + clientName: json['user_name'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1650,16 +2062,24 @@ class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel @override final bool isRead; final String productName; - final String oldStatus; - final String newStatus; + final int productId; + final String author; + final int authorId; + final String status; + final String subject; + final String messageLine; @override final DateTime createdAt; ProductStatusUpdatedForAuthorNotification({ required this.type, required this.productName, - required this.oldStatus, - required this.newStatus, + required this.productId, + required this.author, + required this.authorId, + required this.status, + required this.subject, + required this.messageLine, required this.isRead, required this.createdAt, }); @@ -1668,8 +2088,12 @@ class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel return ProductStatusUpdatedForAuthorNotification( type: type, productName: json['product_name'] ?? '', - oldStatus: json['old_status'] ?? '', - newStatus: json['new_status'] ?? '', + productId: json['product_id'] ?? 0, + author: json['author'] ?? '', + authorId: json['author_id'] ?? 0, + status: json['status'] ?? '', + subject: json['subject'] ?? '', + messageLine: json['messageLine'] ?? json['message_line'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1681,15 +2105,20 @@ class DiaryReminderForUsersNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + final int? diaryId; + final String? message; @override final DateTime createdAt; - DiaryReminderForUsersNotification({required this.type, required this.isRead, required this.createdAt}); + DiaryReminderForUsersNotification({required this.type, required this.isRead, this.diaryId, this.message, required this.createdAt}); factory DiaryReminderForUsersNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + final data = json['data'] is Map ? json['data'] as Map : json; return DiaryReminderForUsersNotification( type: type, isRead: isRead ?? false, + diaryId: data['diary_id'], + message: data['message'], createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } @@ -1702,15 +2131,29 @@ class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationMod final bool isRead; @override final DateTime createdAt; - final String dietName; + final int chosenPlanId; final int dietId; + final String planName; + final String userName; + final String consultantName; + final String dietName; + final String role; + final String peerSlug; + final String peerImagePath; PersonalDietIsDefinedToCustomerNotification({ required this.type, required this.isRead, required this.createdAt, - required this.dietName, + required this.chosenPlanId, required this.dietId, + required this.planName, + required this.userName, + required this.consultantName, + required this.dietName, + required this.role, + required this.peerSlug, + required this.peerImagePath, }); factory PersonalDietIsDefinedToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { @@ -1718,8 +2161,288 @@ class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationMod type: type, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + chosenPlanId: json['chosen_plan_id'] ?? 0, + dietId: json['diet_id'] ?? 0, + planName: json['plan_name'] ?? '', + userName: json['user_name'] ?? '', + consultantName: json['consultant_name'] ?? '', dietName: json['diet_name'] ?? '', - dietId: json['diet_id'], + role: json['role']?.toString() ?? '', + peerSlug: json['peer_slug'] ?? '', + peerImagePath: json['peer_image_path'] ?? '', + ); + } +} + +// New Model Classes for Comprehensive Support + +class ConsultantEducationUpdatedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String consultantName; + + ConsultantEducationUpdatedNotification({required this.type, required this.isRead, required this.createdAt, required this.consultantName}); + + factory ConsultantEducationUpdatedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantEducationUpdatedNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + consultantName: json['consultant_name'] ?? '', + ); + } +} + +class ComplaintRegisteredNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String message; + final int entityId; + final String entityType; + final String entityName; + + ComplaintRegisteredNotification({ + required this.type, + required this.isRead, + required this.createdAt, + required this.message, + required this.entityId, + required this.entityType, + required this.entityName, + }); + + factory ComplaintRegisteredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ComplaintRegisteredNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + message: json['message'] ?? '', + entityId: json['entity_id'] ?? 0, + entityType: json['entity_type'] ?? '', + entityName: json['entity_name'] ?? '', + ); + } +} + +class ContentUnpublishedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String reason; + final String entityName; + + ContentUnpublishedNotification({required this.type, required this.isRead, required this.createdAt, required this.reason, required this.entityName}); + + factory ContentUnpublishedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ContentUnpublishedNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + reason: json['reason'] ?? '', + entityName: json['entity_name'] ?? '', + ); + } +} + +class FinanceQuestionCreatedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String questionText; + + FinanceQuestionCreatedNotification({required this.type, required this.isRead, required this.createdAt, required this.questionText}); + + factory FinanceQuestionCreatedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return FinanceQuestionCreatedNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + questionText: json['question_text'] ?? '', + ); + } +} + +class NewChatMessageNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String senderName; + final String messagePreview; + final int chatId; + + NewChatMessageNotification({ + required this.type, + required this.isRead, + required this.createdAt, + required this.senderName, + required this.messagePreview, + required this.chatId, + }); + + factory NewChatMessageNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return NewChatMessageNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + senderName: json['sender_name'] ?? '', + messagePreview: json['message_preview'] ?? '', + chatId: json['chat_id'] ?? 0, + ); + } +} + +class CompleteRegistrationNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + + CompleteRegistrationNotification({required this.type, required this.isRead, required this.createdAt}); + + factory CompleteRegistrationNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return CompleteRegistrationNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ChosenDietExpiredNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String dietName; + final int dietId; + + ChosenDietExpiredNotification({required this.type, required this.isRead, required this.createdAt, required this.dietName, required this.dietId}); + + factory ChosenDietExpiredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenDietExpiredNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + dietName: json['diet_name'] ?? '', + dietId: json['diet_id'] ?? 0, + ); + } +} + +class MotivateUserNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String message; + + MotivateUserNotification({required this.type, required this.isRead, required this.createdAt, required this.message}); + + factory MotivateUserNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return MotivateUserNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + message: json['message'] ?? '', + ); + } +} + +class CommentComplaintNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String commentPreview; + final String reason; + + CommentComplaintNotification({required this.type, required this.isRead, required this.createdAt, required this.commentPreview, required this.reason}); + + factory CommentComplaintNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return CommentComplaintNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + commentPreview: json['comment_preview'] ?? '', + reason: json['reason'] ?? '', + ); + } +} + +class CommentDeletedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String commentPreview; + + CommentDeletedNotification({required this.type, required this.isRead, required this.createdAt, required this.commentPreview}); + + factory CommentDeletedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return CommentDeletedNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + commentPreview: json['comment_preview'] ?? '', + ); + } +} + +class UserSubscribedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + final String userName; + final String userPhotoPath; + final String userProfileUrl; + + UserSubscribedNotification({ + required this.type, + required this.isRead, + required this.createdAt, + required this.userName, + required this.userPhotoPath, + required this.userProfileUrl, + }); + + factory UserSubscribedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return UserSubscribedNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + userName: json['user_name'] ?? '', + userPhotoPath: json['user_photo_path'] ?? '', + userProfileUrl: json['user_profile_url'] ?? '', ); } } @@ -1731,6 +2454,7 @@ enum NotificationType { dietExpired, dietWillStartedIn, dietCanNotBeStarted, + chosenDietExpired, // New // Plan related personalDietCreationNeeded, @@ -1760,12 +2484,17 @@ enum NotificationType { // Comments entityCommented, commentReplied, + commentComplaint, // New + commentDeleted, // New // Profile phoneChanged, + consultantEducationUpdated, // New + completeRegistration, // New // Subscription subscription, + userSubscribed, // New // Badges badgeAwarded, @@ -1773,16 +2502,21 @@ enum NotificationType { // Consultant consultantRequestStarted, + motivateUser, // New // Complaints clientCloseComplaintSuccess, complaintToModerationForUser, + complaintRegistered, // New + contentUnpublished, // New + contentUnpublishedComplaint, // New // Finance clientRefundSuccess, consultantRefundSuccess, clientRejectSuccess, consultantRejectSuccess, + financeQuestionCreated, // New // Score scoreAdded, @@ -1794,6 +2528,9 @@ enum NotificationType { // Diary diaryReminderForUsers, + // Chat + newChatMessage, // New + // Legacy types (keeping for backward compatibility) planSubscriptionStartedToAuthor, planSubscriptionCanceledToAuthor, @@ -1866,6 +2603,21 @@ enum NotificationType { 'diary-reminder-for-users' => NotificationType.diaryReminderForUsers, 'personal-diet-is-defined-to-customer' => NotificationType.personalDietIsDefinedToCustomer, + // New additions + 'consultant-education-updated' => NotificationType.consultantEducationUpdated, + 'complaint-registered' => NotificationType.complaintRegistered, + 'content-unpublished' => NotificationType.contentUnpublished, + 'content-unpublished-complaint' => NotificationType.contentUnpublishedComplaint, + 'finance-question-created' => NotificationType.financeQuestionCreated, + 'new-chat-message' => NotificationType.newChatMessage, + 'complete-registration' => NotificationType.completeRegistration, + 'chosen-diet-expired' => NotificationType.chosenDietExpired, + 'motivate-user' => NotificationType.motivateUser, + 'comment-complaint' => NotificationType.commentComplaint, + 'comment-deleted' => NotificationType.commentDeleted, + 'user-subscribed' => NotificationType.userSubscribed, + // 'diary-reminder' => NotificationType.diaryReminder, // Already legacy? Check if it maps to diaryReminderNotification + 'meal-reminder' => NotificationType.mealReminder, // Maps to legacy if string matches? No, legacy was App\\... // Legacy mapping 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 110607c0..51771c24 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -267,33 +267,113 @@ class NotificationsCubit extends Cubit { isRead: isRead, createdAt: createdAt, ), - NotificationType.unknown => UnknownNotification(type: type), + NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.newChatMessage => NewChatMessageNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.motivateUser => MotivateUserNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.commentComplaint => CommentComplaintNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.commentDeleted => CommentDeletedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.userSubscribed => UserSubscribedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt ?? jsonEncode(notificationJson))), }; } catch (e) { - return UnknownNotification(type: NotificationType.unknown); + return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now()); } } void _handleNotification(String notificationData) { final json = jsonDecode(notificationData) as Map; - // Улучшенная логика определения типа уведомления + // Улучшенная логика определения типа уведомления (порядок: database_type → type → notification_class_name) String? stringifiedType; - // Сначала проверяем поле 'type' в корне JSON - if (json.containsKey('type') && json['type'] != null) { + // Сначала проверяем поле 'database_type' в корне JSON + if (json.containsKey('database_type') && json['database_type'] != null) { + stringifiedType = json['database_type'].toString(); + log('Found database_type in root: $stringifiedType'); + } + // Затем поле 'type' в корне + else if (json.containsKey('type') && json['type'] != null) { stringifiedType = json['type'].toString(); log('Found type in root: $stringifiedType'); } - // Затем проверяем поле 'notification_class_name' в корне JSON + // Затем поле 'notification_class_name' в корне else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) { stringifiedType = json['notification_class_name'].toString(); log('Found notification_class_name in root: $stringifiedType'); } - // Если есть поле 'data', проверяем внутри него + // Если есть поле 'data', проверяем внутри него в том же порядке else if (json.containsKey('data') && json['data'] is Map) { final data = json['data'] as Map; - if (data.containsKey('type') && data['type'] != null) { + if (data.containsKey('database_type') && data['database_type'] != null) { + stringifiedType = data['database_type'].toString(); + log('Found database_type in data: $stringifiedType'); + } else if (data.containsKey('type') && data['type'] != null) { stringifiedType = data['type'].toString(); log('Found type in data: $stringifiedType'); } else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) { diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 0169f96d..cdde4bbf 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -1,10 +1,13 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; import 'package:eatvio_app/features/notifications/models/notification_model.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -12,6 +15,7 @@ import 'package:go_router/go_router.dart'; class NotificationWidget extends StatelessWidget { const NotificationWidget({super.key, required this.notification}); final BaseNotificationModel notification; + @override Widget build(BuildContext context) { return switch (notification) { @@ -117,11 +121,45 @@ class NotificationWidget extends StatelessWidget { PersonalDietIsDefinedToCustomerNotification() => _PersonalDietIsDefinedToCustomerWidget( notification: notification as PersonalDietIsDefinedToCustomerNotification, ), + // New Widgets + ConsultantEducationUpdatedNotification() => _ConsultantEducationUpdatedWidget(notification: notification as ConsultantEducationUpdatedNotification), + ComplaintRegisteredNotification() => _ComplaintRegisteredWidget(notification: notification as ComplaintRegisteredNotification), + ContentUnpublishedNotification() => _ContentUnpublishedWidget(notification: notification as ContentUnpublishedNotification), + FinanceQuestionCreatedNotification() => _FinanceQuestionCreatedWidget(notification: notification as FinanceQuestionCreatedNotification), + NewChatMessageNotification() => _NewChatMessageWidget(notification: notification as NewChatMessageNotification), + CompleteRegistrationNotification() => _CompleteRegistrationWidget(notification: notification as CompleteRegistrationNotification), + ChosenDietExpiredNotification() => _ChosenDietExpiredWidget(notification: notification as ChosenDietExpiredNotification), + MotivateUserNotification() => _MotivateUserWidget(notification: notification as MotivateUserNotification), + CommentComplaintNotification() => _CommentComplaintWidget(notification: notification as CommentComplaintNotification), + CommentDeletedNotification() => _CommentDeletedWidget(notification: notification as CommentDeletedNotification), + UserSubscribedNotification() => _UserSubscribedWidget(notification: notification as UserSubscribedNotification), UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), }; } } +void _navigateToBottomNavRoute(BuildContext context, String path, [Object? extra]) { + final controller = BottomNavScope.maybeOf(context); + if (controller != null) { + controller.switchToRoute(path, extra); + } else { + context.push(path, extra: extra); + } +} + +void _navigateToEntity(BuildContext context, CommentedEntityType type, int id, int? commentId) { + switch (type) { + case CommentedEntityType.blogPost: + context.push(RouteConstants.blogPostScreen, extra: {'id': id, 'highlightedCommentId': commentId}); + case CommentedEntityType.diet: + if (id > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': id, 'highlightedCommentId': commentId}); + } + case CommentedEntityType.unknown: + break; + } +} + class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { const _PlanSubscriptionStartedToAuthorWidget({required this.notification}); final PlanSubscriptionStartedToAuthorNotification notification; @@ -131,8 +169,11 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName), + notificationsText: '${s.userSubscribedToPlan(notification.customerName, notification.planName)}! 🎉', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.customerName.isNotEmpty ? notification.customerName : null); + }, ); } } @@ -142,12 +183,13 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { final PlanSubscriptionCanceledToAuthorNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), + notificationsText: 'Отмена подписки. Клиент ${notification.customerName} отказался от плана "${notification.planName}"', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.customerName.isNotEmpty ? notification.customerName : null); + }, ); } } @@ -157,9 +199,14 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { final ChosenPlanStartedToClientNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - - return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.planStarted(notification.planName), createdAt: notification.createdAt); + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ваш план "${notification.planName}" начался! Удачи! 🚀', + createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen); + }, + ); } } @@ -168,11 +215,9 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { final ChosenPlanTomorrowReminderToClientNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.tomorrowYourPlanWillStart(notification.planName), + notificationsText: 'Напоминание: Завтра старт плана "${notification.planName}"', createdAt: notification.createdAt, ); } @@ -183,8 +228,16 @@ class _DiaryReminderWidget extends StatelessWidget { final DiaryReminderNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.fillYasterday, createdAt: notification.createdAt); + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Не забудьте заполнить дневник за вчера! 📝', + createdAt: notification.createdAt, + onTap: () { + final yesterday = notification.createdAt.subtract(const Duration(days: 1)); + final dateOnly = DateTime(yesterday.year, yesterday.month, yesterday.day); + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen, dateOnly); + }, + ); } } @@ -193,12 +246,13 @@ class _ConsultantLeftCommentWidget extends StatelessWidget { final ConsultantLeftCommentNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')), + notificationsText: 'Новый комментарий от консультанта (за ${notification.date.format('dd.MM.yyyy')})', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen, notification.date); + }, ); } } @@ -208,7 +262,14 @@ class _PersonalizedDietIsDefinedWidget extends StatelessWidget { final PersonalizedDietIsDefinedNotification notification; @override Widget build(BuildContext context) { - return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Персональная диета определена', createdAt: notification.createdAt); + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Персональная диета определена', + createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.consultationsScreen); + }, + ); } } @@ -217,11 +278,9 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget { final ChosenPlanFinishedToClientNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', + notificationsText: 'Поздравляем! План «${notification.planName}» успешно завершен! 🏆', createdAt: notification.createdAt, ); } @@ -232,11 +291,9 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget { final PlanSubscriptionCanceledToCustomerNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.youCanceledSubscriptionToPlan(notification.planName), + notificationsText: 'Подписка на план "${notification.planName}" отменена.', createdAt: notification.createdAt, ); } @@ -247,11 +304,9 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget { final ChosenPlanStartedToConsultantNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.planWithUserStarted(notification.planName, notification.customerName), + notificationsText: 'Начало работы: План "${notification.planName}" с клиентом ${notification.customerName}', createdAt: notification.createdAt, ); } @@ -262,11 +317,9 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget { final ChosenPlanTomorrowReminderToConsultantNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName), + notificationsText: 'Напоминание: Завтра старт работы с ${notification.customerName} по плану "${notification.planName}"', createdAt: notification.createdAt, ); } @@ -277,12 +330,13 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget { final PlanSubscriptionPaidToAuthorNotification notification; @override Widget build(BuildContext context) { - final s = S.of(context); - return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.userPaidThePlan(notification.customerName, notification.planName), + notificationsText: 'Оплата получена! Клиент ${notification.customerName} оплатил план "${notification.planName}" 💰', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.consultantFinancesScreen); + }, ); } } @@ -293,44 +347,48 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${s.congratsYouHaveFinishedPlan} «${notification.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), - const SizedBox(height: 12), - Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), - const SizedBox(height: 12), - CustomOutlinedButton( - text: s.collectCharge, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), - onPressed: () { - GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId); - }, - ), - ], + return GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('План «${notification.planName}» завершен! Поздравляем! 🎉', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + CustomOutlinedButton( + text: s.collectCharge, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () { + GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId); + }, + ), + ], + ), ), - ), - if (!notification.isRead) - Container( - decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), - width: 8, - height: 8, - ), - ], - ), - const SizedBox(height: 4), - Text( - '${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}', - style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), - ), - ], + if (!notification.isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), + ], + ), ); } @@ -377,6 +435,7 @@ class _BaseNotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: onTap, + behavior: HitTestBehavior.opaque, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -385,8 +444,14 @@ class _BaseNotificationWidget extends StatelessWidget { Expanded( child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), + if (onTap != null) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Icon(Icons.chevron_right, color: appTheme.gray500, size: 20), + ), if (!isRead) Container( + margin: const EdgeInsets.only(left: 8), decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), width: 8, height: 8, @@ -440,7 +505,11 @@ class _ChosenPlanLastDayFinishedForUserWidget extends StatelessWidget { final ChosenPlanLastDayFinishedForUser notification; @override Widget build(BuildContext context) { - return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Последний день плана завершен', createdAt: notification.createdAt); + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Финишная прямая! Последний день плана завершен.', + createdAt: notification.createdAt, + ); } } @@ -451,8 +520,11 @@ class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Последний день плана начался: ${notification.planName}', + notificationsText: 'Внимание: Начался последний день плана "${notification.planName}"', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.consultationsScreen); + }, ); } } @@ -462,7 +534,11 @@ class _ComplaintToModerationForUserWidget extends StatelessWidget { final ComplaintToModerationForUser notification; @override Widget build(BuildContext context) { - return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Жалоба отправлена на модерацию', createdAt: notification.createdAt); + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ваша жалоба принята и находится на модерации.', + createdAt: notification.createdAt, + ); } } @@ -473,8 +549,13 @@ class _CommentRepliedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Ответ на комментарий: ${notification.entityName}', + notificationsText: 'Вам ответили в комментариях к: ${notification.entityName}', createdAt: notification.createdAt, + onTap: (notification.commentedEntityType == CommentedEntityType.unknown || notification.commentedEntityId <= 0) + ? null + : () { + _navigateToEntity(context, notification.commentedEntityType, notification.commentedEntityId, notification.commentId); + }, ); } } @@ -486,8 +567,11 @@ class _ScoreAddedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Добавлены баллы: ${notification.scoreValue}', + notificationsText: 'Поздравляем! Вам начислены баллы: ${notification.scoreValue} ⭐', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.consultantRatingScreen); + }, ); } } @@ -499,8 +583,13 @@ class _EntityCommentedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Комментарий к ${notification.entityTranslatedType}: ${notification.entityName}', + notificationsText: 'Новый комментарий к вашей публикации "${notification.entityTranslatedType}": ${notification.entityName}', createdAt: notification.createdAt, + onTap: (notification.entityType == CommentedEntityType.unknown || notification.entityId <= 0) + ? null + : () { + _navigateToEntity(context, notification.entityType, notification.entityId, notification.commentId); + }, ); } } @@ -510,11 +599,7 @@ class _UnknownNotificationWidget extends StatelessWidget { final UnknownNotification notification; @override Widget build(BuildContext context) { - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}', - createdAt: notification.createdAt, - ); + return _BaseNotificationWidget(isRead: false, notificationsText: 'Неизвестное уведомление', createdAt: DateTime.now(), onTap: null); } } @@ -526,7 +611,7 @@ class _PlanSubscriptionExpiredWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" истекла', + notificationsText: 'Срок действия подписки на план "${notification.planName}" истек. Продлите, чтобы продолжить! ⏳', createdAt: notification.createdAt, ); } @@ -539,7 +624,7 @@ class _PlanSubscriptionRenewedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}', + notificationsText: 'Успешное продление! Подписка на план "${notification.planName}" активна до ${notification.renewalDate} ✅', createdAt: notification.createdAt, ); } @@ -552,8 +637,11 @@ class _ConsultantAssignedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}', + notificationsText: 'Вам назначен консультант ${notification.consultantName} по плану "${notification.planName}". Приветствуйте! 👋', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.consultantName.isNotEmpty ? notification.consultantName : null); + }, ); } } @@ -565,7 +653,7 @@ class _ConsultantUnassignedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"', + notificationsText: 'Изменение в плане "${notification.planName}": консультант ${notification.consultantName} отстранен.', createdAt: notification.createdAt, ); } @@ -578,8 +666,11 @@ class _PaymentFailedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}', + notificationsText: 'Не удалось провести оплату за план "${notification.planName}". Причина: ${notification.errorMessage} ❌', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.userFinanceScreen); + }, ); } } @@ -591,8 +682,11 @@ class _PaymentSuccessfulWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}', + notificationsText: 'Оплата прошла успешно! План "${notification.planName}" оплачен на сумму ${notification.amount} ${notification.currency} 💳', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.userFinanceScreen); + }, ); } } @@ -604,7 +698,7 @@ class _GoalAchievedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}', + notificationsText: 'Цель достигнута! "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue} 🎯', createdAt: notification.createdAt, ); } @@ -617,7 +711,7 @@ class _WorkoutReminderWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)', + notificationsText: 'Время тренировки! "${notification.workoutName}" ждет вас (${notification.duration} мин) 💪', createdAt: notification.createdAt, ); } @@ -630,8 +724,11 @@ class _MealReminderWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)', + notificationsText: 'Пора подкрепиться! "${notification.mealName}" (${notification.calories} ккал) 🍎', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen); + }, ); } } @@ -644,8 +741,13 @@ class _DietCancelledWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" отменена', + notificationsText: 'Диета "${notification.dietName}" была отменена.', createdAt: notification.createdAt, + onTap: () { + if (notification.dietId > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + } + }, ); } } @@ -657,8 +759,13 @@ class _DietStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" началась', + notificationsText: 'Старт диеты "${notification.dietName}"! Успехов! 🥗', createdAt: notification.createdAt, + onTap: () { + if (notification.dietId > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + } + }, ); } } @@ -670,8 +777,13 @@ class _DietExpiredWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" истекла', + notificationsText: 'Срок действия диеты "${notification.dietName}" подошел к концу.', createdAt: notification.createdAt, + onTap: () { + if (notification.dietId > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + } + }, ); } } @@ -683,8 +795,13 @@ class _DietWillStartedInWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" начнется ${notification.startDate}', + notificationsText: 'Скоро старт! Диета "${notification.dietName}" начнется ${notification.startDate} ⏰', createdAt: notification.createdAt, + onTap: () { + if (notification.dietId > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + } + }, ); } } @@ -696,8 +813,13 @@ class _DietCanNotBeStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" не может быть начата: ${notification.reason}', + notificationsText: 'Внимание: Не удалось начать диету "${notification.dietName}". Причина: ${notification.reason}', createdAt: notification.createdAt, + onTap: () { + if (notification.dietId > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + } + }, ); } } @@ -709,8 +831,11 @@ class _PersonalDietCreationNeededWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Требуется создание персональной диеты для клиента ${notification.clientName} по плану "${notification.planName}"', + notificationsText: 'Задача: Создать персональную диету для клиента ${notification.userName}', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.myDietsScreen); + }, ); } } @@ -722,7 +847,7 @@ class _ChosenPlanClientLastDayFinishedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Последний день плана "${notification.planName}" завершен', + notificationsText: 'Поздравляем с завершением плана "${notification.planName}"! 🎉', createdAt: notification.createdAt, ); } @@ -735,8 +860,11 @@ class _ChosenPlanClientLastDayStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Последний день плана "${notification.planName}" начался', + notificationsText: 'Сегодня последний день плана "${notification.planName}"! Финиш близко! 🏁', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen); + }, ); } } @@ -748,8 +876,15 @@ class _StayOnTrackToAuthorWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Клиент ${notification.clientName} остается на плане "${notification.planName}"', + notificationsText: 'Клиент ${notification.userName} завершил день ${notification.dayNum} по плану "${notification.planName}" 💪', createdAt: notification.createdAt, + onTap: () { + if (notification.diaryDayId > 0) { + context.push(RouteConstants.userDiaryScreen, extra: {'diaryId': notification.diaryDayId}); + } else { + _navigateToBottomNavRoute(context, RouteConstants.consultationsScreen); + } + }, ); } } @@ -761,8 +896,11 @@ class _ClientStartPlanSubscriptionToSubscriberWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" началась', + notificationsText: 'У вас новая подписка на план "${notification.planName}"! 🌟', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen); + }, ); } } @@ -774,8 +912,11 @@ class _ConsultantConfirmPlanSubscriptionToSubscriberWidget extends StatelessWidg Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"', + notificationsText: 'Ваша подписка на план "${notification.planName}" подтверждена консультантом ${notification.consultantName} ✅', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.consultantName.isNotEmpty ? notification.consultantName : null); + }, ); } } @@ -787,8 +928,11 @@ class _ClientPaidPlanSubscriptionToAuthorWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Клиент ${notification.clientName} оплатил подписку на план "${notification.planName}" на сумму ${notification.amount}', + notificationsText: 'Поступление средств: Клиент ${notification.clientName} оплатил подписку на план "${notification.planName}" 💸', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.consultantFinancesScreen); + }, ); } } @@ -800,8 +944,11 @@ class _ConsultantLeftDiaryCommentWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Консультант ${notification.consultantName} оставил комментарий к дневнику по плану "${notification.planName}"', + notificationsText: 'Ваш консультант ${notification.consultantName} прокомментировал дневник по плану "${notification.planName}" 💬', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen, notification.diaryDate); + }, ); } } @@ -813,8 +960,11 @@ class _ConsultantConfirmPlanSubscriptionToAuthorWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"', + notificationsText: 'Клиент ${notification.clientName} подтвердил подписку на план "${notification.planName}" 🤝', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.clientName.isNotEmpty ? notification.clientName : null); + }, ); } } @@ -826,8 +976,11 @@ class _ClientPaidPlanSubscriptionToSubscriberWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Оплачена подписка на план "${notification.planName}" на сумму ${notification.amount}', + notificationsText: 'Оплата подписки на план "${notification.planName}" прошла успешно ✅', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.consultantFinancesScreen); + }, ); } } @@ -839,8 +992,11 @@ class _ClientStartPlanSubscriptionToAuthorWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Клиент ${notification.clientName} начал подписку на план "${notification.planName}"', + notificationsText: 'Новый подписчик! Клиент ${notification.clientName} начал подписку на план "${notification.planName}" 🎈', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.clientName.isNotEmpty ? notification.clientName : null); + }, ); } } @@ -852,8 +1008,11 @@ class _ClientCancelPlanSubscriptionToAuthorWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Клиент ${notification.clientName} отменил подписку на план "${notification.planName}"', + notificationsText: 'Отмена подписки: Клиент ${notification.clientName} отказался от плана "${notification.planName}"', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.clientName.isNotEmpty ? notification.clientName : null); + }, ); } } @@ -865,8 +1024,11 @@ class _ClientCancelPlanSubscriptionToSubscriberWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" отменена', + notificationsText: 'Ваша подписка на план "${notification.planName}" была отменена.', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen); + }, ); } } @@ -878,8 +1040,11 @@ class _UserDailyReportCompletedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Клиент ${notification.clientName} завершил ежедневный отчет по плану "${notification.planName}"', + notificationsText: 'Отчет получен: Клиент ${notification.clientName} заполнил дневник по плану "${notification.planName}" 📝', createdAt: notification.createdAt, + onTap: () { + context.push(UserDiaryScreen.path, extra: {'diaryId': notification.diaryId}); + }, ); } } @@ -891,7 +1056,7 @@ class _StayOnTrackToCustomerWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Консультант ${notification.consultantName} остается на плане "${notification.planName}"', + notificationsText: 'Вы продолжаете идти по плану "${notification.planName}" (день ${notification.dayNum}) 💪', createdAt: notification.createdAt, ); } @@ -904,7 +1069,7 @@ class _PhoneChangedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Номер телефона изменен с ${notification.oldPhone} на ${notification.newPhone}', + notificationsText: 'Ваш номер телефона был успешно изменен на ${notification.newPhone} 📱', createdAt: notification.createdAt, ); } @@ -917,8 +1082,11 @@ class _SubscriptionWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка ${notification.subscriptionType} на план "${notification.planName}"', + notificationsText: 'У вас новый подписчик: ${notification.userName} 🎉', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.subsScreen, extra: SubType.subscribers); + }, ); } } @@ -930,8 +1098,11 @@ class _BadgeAwardedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Получен значок "${notification.badgeName}": ${notification.badgeDescription}', + notificationsText: 'Новая награда! Вы получили значок "${notification.badgeTitle}" (${notification.badgeType}) 🏅', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.myProfileScreen); + }, ); } } @@ -943,8 +1114,11 @@ class _BadgeRevokedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Отозван значок "${notification.badgeName}": ${notification.reason}', + notificationsText: 'Значок "${notification.badgeTitle}" (${notification.badgeType}) был отозван.', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.myProfileScreen); + }, ); } } @@ -956,7 +1130,7 @@ class _ConsultantRequestStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Запрос к консультанту ${notification.consultantName} начат', + notificationsText: 'Запрос отправлен консультанту ${notification.consultantName} 📩', createdAt: notification.createdAt, ); } @@ -969,7 +1143,7 @@ class _ClientCloseComplaintSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Жалоба на ${notification.complaintType} "${notification.entityName}" успешно закрыта', + notificationsText: 'Ваша жалоба по плану "${notification.planName}" была рассмотрена и закрыта.', createdAt: notification.createdAt, ); } @@ -982,8 +1156,11 @@ class _ClientRefundSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Возврат средств по плану "${notification.planName}" на сумму ${notification.amount}: ${notification.reason}', + notificationsText: 'Вам одобрен возврат средств по плану "${notification.planName}" (${notification.amount}).', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.userFinanceScreen); + }, ); } } @@ -995,8 +1172,11 @@ class _ConsultantRefundSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Возврат средств клиенту ${notification.clientName} по плану "${notification.planName}" на сумму ${notification.amount}', + notificationsText: 'Выполнен возврат средств клиенту ${notification.clientName} по плану "${notification.planName}" (${notification.amount})', createdAt: notification.createdAt, + onTap: () { + context.push(RouteConstants.consultantFinancesScreen); + }, ); } } @@ -1008,8 +1188,11 @@ class _ClientRejectSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Отклонение по плану "${notification.planName}": ${notification.reason}', + notificationsText: 'Отказ по плану "${notification.planName}" принят.', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen); + }, ); } } @@ -1021,8 +1204,11 @@ class _ConsultantRejectSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Отклонение клиентом ${notification.clientName} по плану "${notification.planName}": ${notification.reason}', + notificationsText: 'Клиент ${notification.clientName} отказался от плана "${notification.planName}".', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.clientName.isNotEmpty ? notification.clientName : null); + }, ); } } @@ -1034,8 +1220,11 @@ class _ScoreSubstractedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Вычтено ${notification.scoreValue} баллов за ${notification.scorableName}', + notificationsText: 'Списание баллов: -${notification.scoreValue} за ${notification.scorableName}', createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.consultantRatingScreen); + }, ); } } @@ -1045,11 +1234,13 @@ class _ProductStatusUpdatedForAuthorWidget extends StatelessWidget { final ProductStatusUpdatedForAuthorNotification notification; @override Widget build(BuildContext context) { - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: 'Статус продукта "${notification.productName}" изменен с ${notification.oldStatus} на ${notification.newStatus}', - createdAt: notification.createdAt, - ); + final text = notification.subject.isNotEmpty + ? notification.subject + : notification.status.isNotEmpty + ? 'Статус продукта "${notification.productName}": ${notification.status}' + : 'Статус продукта "${notification.productName}" обновлен'; + + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: text, createdAt: notification.createdAt); } } @@ -1058,7 +1249,15 @@ class _DiaryReminderForUsersWidget extends StatelessWidget { final DiaryReminderForUsersNotification notification; @override Widget build(BuildContext context) { - return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Напоминание о заполнении дневника', createdAt: notification.createdAt); + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Не забудьте заполнить дневник сегодня! 📓', + createdAt: notification.createdAt, + onTap: () { + final date = notification.createdAt; + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen, date); + }, + ); } } @@ -1069,8 +1268,160 @@ class _PersonalDietIsDefinedToCustomerWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - onTap: () => GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId, 'navigateToComments': false}), - notificationsText: 'Персональная диета "${notification.dietName}" установлена', + notificationsText: 'Для вас составлена персональная диета "${notification.dietName}"! 🥗', + createdAt: notification.createdAt, + onTap: () { + if (notification.dietId > 0) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + } + }, + ); + } +} + +class _ConsultantEducationUpdatedWidget extends StatelessWidget { + const _ConsultantEducationUpdatedWidget({required this.notification}); + final ConsultantEducationUpdatedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Консультант ${notification.consultantName} обновил данные об образовании.', + createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.myProfileScreen); + }, + ); + } +} + +class _ComplaintRegisteredWidget extends StatelessWidget { + const _ComplaintRegisteredWidget({required this.notification}); + final ComplaintRegisteredNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Мы получили вашу жалобу на ${notification.entityName}. Спасибо за сигнал!', + createdAt: notification.createdAt, + ); + } +} + +class _ContentUnpublishedWidget extends StatelessWidget { + const _ContentUnpublishedWidget({required this.notification}); + final ContentUnpublishedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ваша публикация ${notification.entityName} была скрыта. Причина: ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _FinanceQuestionCreatedWidget extends StatelessWidget { + const _FinanceQuestionCreatedWidget({required this.notification}); + final FinanceQuestionCreatedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Служба поддержки: Новый вопрос по финансам: ${notification.questionText}', + createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen); + }, + ); + } +} + +class _NewChatMessageWidget extends StatelessWidget { + const _NewChatMessageWidget({required this.notification}); + final NewChatMessageNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Входящее сообщение от ${notification.senderName}: "${notification.messagePreview}"', + createdAt: notification.createdAt, + onTap: () { + _navigateToBottomNavRoute(context, RouteConstants.chatSelectScreen, notification.senderName.isNotEmpty ? notification.senderName : null); + }, + ); + } +} + +class _CompleteRegistrationWidget extends StatelessWidget { + const _CompleteRegistrationWidget({required this.notification}); + final CompleteRegistrationNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Завершите регистрацию, чтобы получить доступ ко всем функциям! 🚀', + createdAt: notification.createdAt, + ); + } +} + +class _ChosenDietExpiredWidget extends StatelessWidget { + const _ChosenDietExpiredWidget({required this.notification}); + final ChosenDietExpiredNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Диета "${notification.dietName}" завершена. Время подвести итоги!', + createdAt: notification.createdAt, + ); + } +} + +class _MotivateUserWidget extends StatelessWidget { + const _MotivateUserWidget({required this.notification}); + final MotivateUserNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: notification.message, createdAt: notification.createdAt); + } +} + +class _CommentComplaintWidget extends StatelessWidget { + const _CommentComplaintWidget({required this.notification}); + final CommentComplaintNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Поступила жалоба на ваш комментарий: "${notification.commentPreview}". Причина: ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _CommentDeletedWidget extends StatelessWidget { + const _CommentDeletedWidget({required this.notification}); + final CommentDeletedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ваш комментарий был удален по правилам сообщества: "${notification.commentPreview}"', + createdAt: notification.createdAt, + ); + } +} + +class _UserSubscribedWidget extends StatelessWidget { + const _UserSubscribedWidget({required this.notification}); + final UserSubscribedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'У вас новый подписчик: ${notification.userName}! 👋', createdAt: notification.createdAt, ); } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index 1fd5e4d6..c934fb53 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -413,6 +413,78 @@ class NotificationsService { isRead: readDate != null, createdAt: createdAt, ), + NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson( // Reusing ContentUnpublishedNotification if structure is same, or create specific if different. Assuming similar for now or check backend. + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.newChatMessage => NewChatMessageNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.motivateUser => MotivateUserNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.commentComplaint => CommentComplaintNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.commentDeleted => CommentDeletedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.userSubscribed => UserSubscribedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), }; } catch (e) { diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index 03e94814..283ef0fc 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -32,10 +32,18 @@ import 'package:go_router/go_router.dart'; import '../../../../core/widgets/option_widget.dart'; class AboutPlanScreen extends StatefulWidget { - const AboutPlanScreen({super.key, required this.slug, required this.navigateToComments, required this.index}); final String slug; final bool navigateToComments; final int? index; + final int? highlightedCommentId; + + const AboutPlanScreen({ + super.key, + required this.slug, + required this.navigateToComments, + required this.index, + this.highlightedCommentId, + }); @override State createState() => _AboutPlanScreenState(); } @@ -468,13 +476,14 @@ class _AboutPlanScreenState extends State { ), const SizedBox(height: 16), if (commentsState is CommentsStateLoading) const Loader(), - if (commentsState is CommentsStateLoaded) - CommentsWidget( - id: state.planModel?.id.toString() ?? '', - commentSourceType: CommentSourceType.plan, - comment: commentsState.comment, - replyId: replyId, - ), + if (commentsState is CommentsStateLoaded) + CommentsWidget( + id: state.planModel?.id.toString() ?? '', + commentSourceType: CommentSourceType.plan, + comment: commentsState.comment, + replyId: replyId, + highlightedCommentId: widget.highlightedCommentId, + ), ], ), ), diff --git a/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart b/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart index b6d40645..693fe8d5 100644 --- a/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart +++ b/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart @@ -5,22 +5,30 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class SubCubit extends Cubit { final ProfileRepository profileRepository; - SubCubit({ - required this.profileRepository, - }) : super(const SubState()); + SubCubit({required this.profileRepository}) : super(const SubState()); - Future getSubscriptions(String slug, [String? query]) async { + Future getSubscriptions(String? slug, [String? query]) async { emit(const SubState.loading()); try { + if (slug == null) { + final myProfile = await profileRepository.getMyProfile(); + emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(myProfile.slug, query))); + return; + } emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(slug, query))); } catch (e) { emit(SubState.error(errorMessage: e.toString())); } } - Future getSubscribers(String slug, [String? query]) async { + Future getSubscribers(String? slug, [String? query]) async { emit(const SubState.loading()); try { + if (slug == null) { + final myProfile = await profileRepository.getMyProfile(); + emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(myProfile.slug, query))); + return; + } emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(slug, query))); } catch (e) { emit(SubState.error(errorMessage: e.toString())); diff --git a/lib/features/profile/presentation/pages/subs_screen.dart b/lib/features/profile/presentation/pages/subs_screen.dart index d5d782f2..6de5194e 100644 --- a/lib/features/profile/presentation/pages/subs_screen.dart +++ b/lib/features/profile/presentation/pages/subs_screen.dart @@ -12,7 +12,7 @@ enum SubType { subscribers, subscriptions } class SubsScreen extends StatefulWidget { final SubType subType; - final String slug; + final String? slug; const SubsScreen({ super.key, diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index a5448a86..ff9e4f0a 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -36,8 +36,9 @@ class AboutRecipeScreen extends StatefulWidget { final String recipeSlug; final int? index; final bool navigateToComments; + final int? highlightedCommentId; - const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments}); + const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments, this.highlightedCommentId}); @override State createState() => _AboutRecipeScreenState(); @@ -391,6 +392,7 @@ class _AboutRecipeScreenState extends State { commentSourceType: CommentSourceType.recipe, comment: state.comment, replyId: replyId, + highlightedCommentId: widget.highlightedCommentId, ), ], ), diff --git a/lib/main.dart b/lib/main.dart index af6db638..6513e685 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/notifications/push_navigation_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_portal/flutter_portal.dart'; @@ -13,9 +14,20 @@ Future main() async { await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + void initState() { + super.initState(); + PushNavigationHandler.initialize(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( diff --git a/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart b/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart index 721af4b3..85850c2e 100644 --- a/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart +++ b/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart @@ -109,7 +109,10 @@ void main() { ], child: wrap( const Scaffold( - body: BottomNavigationBarWidget(child: Text('Test Content')), + body: BottomNavigationBarWidget( + currentLocation: '/feed-screen', + child: Text('Test Content'), + ), ), ), ), @@ -145,7 +148,10 @@ void main() { ], child: wrap( const Scaffold( - body: BottomNavigationBarWidget(child: Text('Test Content')), + body: BottomNavigationBarWidget( + currentLocation: '/feed-screen', + child: Text('Test Content'), + ), ), ), ), diff --git a/test/features/diary/presentation/pages/diary_screen_smoke_test.dart b/test/features/diary/presentation/pages/diary_screen_smoke_test.dart index 9df724ce..16876e1d 100644 --- a/test/features/diary/presentation/pages/diary_screen_smoke_test.dart +++ b/test/features/diary/presentation/pages/diary_screen_smoke_test.dart @@ -88,7 +88,7 @@ void main() { when(diaryCubit.state).thenReturn(const DiaryState.loading()); when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); @@ -98,7 +98,7 @@ void main() { sl.registerFactory(() => diaryCubit); sl.registerFactory(() => commentsCubit); - await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1)))); await tester.pump(); expect(find.byType(DiaryScreen), findsOneWidget); @@ -109,7 +109,7 @@ void main() { when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true)); when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); @@ -119,7 +119,7 @@ void main() { sl.registerFactory(() => diaryCubit); sl.registerFactory(() => commentsCubit); - await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1)))); await tester.pumpAndSettle(const Duration(milliseconds: 300)); expect(find.byType(DiaryScreen), findsOneWidget); @@ -132,7 +132,7 @@ void main() { when(diaryCubit.state).thenReturn(const DiaryState.error(errorMessage: 'err')); when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); @@ -142,7 +142,7 @@ void main() { sl.registerFactory(() => diaryCubit); sl.registerFactory(() => commentsCubit); - await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1)))); await tester.pump(); expect(find.byType(DiaryScreen), findsOneWidget); @@ -161,7 +161,7 @@ void main() { when(diaryCubit.state).thenReturn(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1)); when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); @@ -171,7 +171,7 @@ void main() { sl.registerFactory(() => diaryCubit); sl.registerFactory(() => commentsCubit); - await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1)))); await tester.pump(const Duration(milliseconds: 200)); expect(find.byType(DiaryScreen), findsOneWidget); @@ -193,7 +193,7 @@ void main() { when(diaryCubit.state).thenReturn(state); when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); @@ -204,7 +204,7 @@ void main() { sl.registerFactory(() => diaryCubit); sl.registerFactory(() => commentsCubit); - await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1)))); await tester.pump(const Duration(milliseconds: 200)); expect(find.text('Nice'), findsOneWidget); @@ -217,7 +217,7 @@ void main() { when(diaryCubit.state).thenReturn(state); when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); when(diaryCubit.close()).thenAnswer((_) async => Future.value()); - when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true); final commentsCubit = MockConsultantCommentsCubit(); when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); @@ -227,7 +227,7 @@ void main() { sl.registerFactory(() => diaryCubit); sl.registerFactory(() => commentsCubit); - await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1, currentPlanDay: 1))); + await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1))); await tester.pump(const Duration(milliseconds: 200)); expect(find.byType(AppBar), findsWidgets); diff --git a/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart b/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart index a09bfe1d..18546734 100644 --- a/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart +++ b/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart @@ -101,7 +101,7 @@ void main() { registerMocks(cubit, commentsCubit); - await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null))); await tester.pump(); expect(find.byType(AboutDietScreen), findsOneWidget); @@ -127,7 +127,7 @@ void main() { registerMocks(cubit, commentsCubit); await mockNetworkImagesFor(() async { - await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null))); await tester.pump(); await tester.pump(const Duration(milliseconds: 600)); }); @@ -157,7 +157,7 @@ void main() { registerMocks(cubit, commentsCubit); await mockNetworkImagesFor(() async { - await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null))); await tester.pump(); await tester.pump(const Duration(milliseconds: 600)); }); @@ -183,7 +183,7 @@ void main() { registerMocks(cubit, commentsCubit); - await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null))); await tester.pump(); expect(find.byType(AboutDietScreen), findsOneWidget); diff --git a/test/features/notifications/models/notification_model_test.dart b/test/features/notifications/models/notification_model_test.dart index 28b21566..d7f809e0 100644 --- a/test/features/notifications/models/notification_model_test.dart +++ b/test/features/notifications/models/notification_model_test.dart @@ -124,7 +124,7 @@ void main() { }); test('UnknownNotification uses default createdAt when not provided', () { - final notification = UnknownNotification(type: NotificationType.unknown); + final notification = UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now()); expect(notification.type, NotificationType.unknown); expect(notification.isRead, true); @@ -485,8 +485,9 @@ void main() { test('PersonalDietCreationNeededNotification.fromJson parses all fields', () { final json = { - 'plan_name': 'Custom Plan', - 'client_name': 'John Doe', + 'user_name': 'John Doe', + 'consultant_name': 'Consultant', + 'my_diets_url': 'https://example.com/diets', 'created_at': '2024-01-20T12:00:00Z', }; @@ -496,8 +497,9 @@ void main() { isRead: true, ); - expect(notification.planName, 'Custom Plan'); - expect(notification.clientName, 'John Doe'); + expect(notification.userName, 'John Doe'); + expect(notification.consultantName, 'Consultant'); + expect(notification.myDietsUrl, 'https://example.com/diets'); expect(notification.isRead, true); expect(notification.type, NotificationType.personalDietCreationNeeded); }); @@ -510,8 +512,8 @@ void main() { NotificationType.personalDietCreationNeeded, ); - expect(notification.planName, ''); - expect(notification.clientName, ''); + expect(notification.userName, ''); + expect(notification.consultantName, ''); }); test('ChosenPlanClientLastDayFinishedNotification.fromJson parses all fields', () { @@ -551,7 +553,9 @@ void main() { test('StayOnTrackToAuthorNotification.fromJson parses all fields', () { final json = { 'plan_name': 'Track Plan', - 'client_name': 'Jane Smith', + 'user_name': 'Jane Smith', + 'day_num': 1, + 'diary_day_id': 10, 'created_at': '2024-01-24T12:00:00Z', }; @@ -562,7 +566,7 @@ void main() { ); expect(notification.planName, 'Track Plan'); - expect(notification.clientName, 'Jane Smith'); + expect(notification.userName, 'Jane Smith'); expect(notification.isRead, false); expect(notification.type, NotificationType.stayOnTrackToAuthor); }); diff --git a/test/features/notifications/presentation/notifications_cubit/notifications_cubit_test.dart b/test/features/notifications/presentation/notifications_cubit/notifications_cubit_test.dart index aa07a257..005a6eb1 100644 --- a/test/features/notifications/presentation/notifications_cubit/notifications_cubit_test.dart +++ b/test/features/notifications/presentation/notifications_cubit/notifications_cubit_test.dart @@ -50,6 +50,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Test Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -89,6 +91,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -99,6 +103,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 2', planId: 2, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -143,6 +149,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -168,6 +176,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -201,6 +211,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -231,6 +243,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -272,6 +286,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -311,6 +327,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -342,6 +360,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -349,6 +369,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 2', planId: 2, + consultantName: '', + peerProfileUrl: '', isRead: true, createdAt: DateTime.now(), ), @@ -376,6 +398,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: true, createdAt: DateTime.now(), ), @@ -383,6 +407,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 2', planId: 2, + consultantName: '', + peerProfileUrl: '', isRead: true, createdAt: DateTime.now(), ), @@ -426,6 +452,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Plan 1', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -475,6 +503,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -524,6 +554,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -568,6 +600,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -608,6 +642,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -645,6 +681,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -688,6 +726,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -757,6 +797,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -800,6 +842,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -843,6 +887,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -886,6 +932,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -929,6 +977,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -972,6 +1022,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -1015,6 +1067,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -1058,6 +1112,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -1100,6 +1156,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -1137,6 +1195,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -1174,6 +1234,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), @@ -1217,6 +1279,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Initial Plan', planId: 1, + consultantName: '', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), diff --git a/test/features/notifications/presentation/notifications_cubit/notifications_state_test.dart b/test/features/notifications/presentation/notifications_cubit/notifications_state_test.dart index 2a0b8c3b..2bceb0c8 100644 --- a/test/features/notifications/presentation/notifications_cubit/notifications_state_test.dart +++ b/test/features/notifications/presentation/notifications_cubit/notifications_state_test.dart @@ -36,8 +36,12 @@ void main() { final notification = PlanSubscriptionStartedToAuthorNotification( type: NotificationType.planSubscriptionStartedToAuthor, planName: 'Test Plan', + planId: 1, customerName: 'Test Customer', + userId: 0, + userPhotoPath: '', chatId: 'chat-123', + chatUrl: '', isRead: false, createdAt: DateTime.now(), ); diff --git a/test/features/notifications/presentation/widgets/notification_widget_test.dart b/test/features/notifications/presentation/widgets/notification_widget_test.dart index f624993a..d8f56164 100644 --- a/test/features/notifications/presentation/widgets/notification_widget_test.dart +++ b/test/features/notifications/presentation/widgets/notification_widget_test.dart @@ -31,8 +31,12 @@ void main() { final notification = PlanSubscriptionStartedToAuthorNotification( type: NotificationType.planSubscriptionStartedToAuthor, planName: 'Test Plan', + planId: 1, customerName: 'Test Customer', + userId: 0, + userPhotoPath: '', chatId: 'chat123', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -47,7 +51,11 @@ void main() { final notification = PlanSubscriptionCanceledToAuthorNotification( type: NotificationType.planSubscriptionCanceledToAuthor, planName: 'Test Plan', + planId: 1, customerName: 'Test Customer', + userId: 0, + userPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -63,6 +71,8 @@ void main() { type: NotificationType.chosenPlanStartedToClient, planName: 'Test Plan', planId: 1, + consultantName: 'Test Consultant', + peerProfileUrl: '', isRead: false, createdAt: testDate, ); @@ -77,6 +87,8 @@ void main() { final notification = ChosenPlanTomorrowReminderToClientNotification( type: NotificationType.chosenPlanTomorrowReminderToClient, planName: 'Test Plan', + consultantName: '', + chatId: 0, isRead: false, createdAt: testDate, ); @@ -103,7 +115,12 @@ void main() { testWidgets('displays ConsultantLeftCommentNotification', (tester) async { final notification = ConsultantLeftCommentNotification( type: NotificationType.consultantLeftComment, + chosenPlanId: 1, + diaryId: 0, date: testDate, + planName: '', + userName: '', + consultantName: '', isRead: false, createdAt: testDate, ); @@ -131,6 +148,11 @@ void main() { final notification = ChosenPlanFinishedToClientNotification( type: NotificationType.chosenPlanFinishedToClient, planName: 'Test Plan', + chosenPlanId: 1, + consultantName: '', + consultantPhotoPath: '', + consultantProfileUrl: '', + consultantId: 0, isRead: false, createdAt: testDate, ); @@ -145,6 +167,11 @@ void main() { final notification = PlanSubscriptionCanceledToCustomerNotification( type: NotificationType.planSubscriptionCanceledToCustomer, planName: 'Test Plan', + planId: 1, + authorName: '', + authorId: 0, + authorPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -160,6 +187,10 @@ void main() { type: NotificationType.chosenPlanStartedToConsultant, planName: 'Test Plan', customerName: 'Test Customer', + planId: 1, + customerId: 1, + peerProfileUrl: '', + consultationUrl: '', isRead: false, createdAt: testDate, ); @@ -175,6 +206,7 @@ void main() { type: NotificationType.chosenPlanTomorrowReminderToConsultant, planName: 'Test Plan', customerName: 'Test Customer', + chatId: 0, isRead: false, createdAt: testDate, ); @@ -189,7 +221,11 @@ void main() { final notification = PlanSubscriptionPaidToAuthorNotification( type: NotificationType.planSubscriptionPaidToAuthor, planName: 'Test Plan', + planId: 1, customerName: 'Test Customer', + userId: 0, + userPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -206,6 +242,9 @@ void main() { planName: 'Test Plan', customerName: 'Test Customer', planId: 1, + chosenPlanId: 1, + clientPhotoPath: '', + clientId: 0, isRead: false, createdAt: testDate, ); @@ -219,6 +258,10 @@ void main() { testWidgets('displays ChosenPlanLastDayFinishedForUser', (tester) async { final notification = ChosenPlanLastDayFinishedForUser( type: NotificationType.chosenPlanLastDayFinishedForUser, + planName: 'Test Plan', + chosenPlanId: 1, + complaintUrl: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -252,6 +295,7 @@ void main() { message: 'Test message', entityId: 1, entityType: 'test', + entityName: 'Test Entity', isRead: false, createdAt: testDate, ); @@ -267,7 +311,7 @@ void main() { type: NotificationType.commentReplied, entityName: 'Test Entity', commentedEntityId: 1, - commentedEntityType: 'test', + commentedEntityType: CommentedEntityType.diet, author: 'Test Author', authorId: 1, parentCommentId: 1, @@ -305,7 +349,7 @@ void main() { type: NotificationType.entityCommented, entityName: 'Test Entity', entityId: 1, - entityType: 'test', + entityType: CommentedEntityType.diet, entityTranslatedType: 'Test Type', entityUrl: 'test_url', authorName: 'Test Author', @@ -470,6 +514,7 @@ void main() { final notification = DietCancelledNotification( type: NotificationType.dietCancelled, dietName: 'Test Diet', + dietId: 1, isRead: false, createdAt: testDate, ); @@ -484,6 +529,7 @@ void main() { final notification = DietStartedNotification( type: NotificationType.dietStarted, dietName: 'Test Diet', + dietId: 1, isRead: false, createdAt: testDate, ); @@ -498,6 +544,7 @@ void main() { final notification = DietExpiredNotification( type: NotificationType.dietExpired, dietName: 'Test Diet', + dietId: 1, isRead: false, createdAt: testDate, ); @@ -512,6 +559,7 @@ void main() { final notification = DietWillStartedInNotification( type: NotificationType.dietWillStartedIn, dietName: 'Test Diet', + dietId: 1, startDate: '2024-02-01', isRead: false, createdAt: testDate, @@ -527,6 +575,7 @@ void main() { final notification = DietCanNotBeStartedNotification( type: NotificationType.dietCanNotBeStarted, dietName: 'Test Diet', + dietId: 1, reason: 'Test reason', isRead: false, createdAt: testDate, @@ -541,8 +590,9 @@ void main() { testWidgets('displays PersonalDietCreationNeededNotification', (tester) async { final notification = PersonalDietCreationNeededNotification( type: NotificationType.personalDietCreationNeeded, - planName: 'Test Plan', - clientName: 'Test Client', + userName: 'Test Client', + consultantName: '', + myDietsUrl: '', isRead: false, createdAt: testDate, ); @@ -557,6 +607,9 @@ void main() { final notification = ChosenPlanClientLastDayFinishedNotification( type: NotificationType.chosenPlanClientLastDayFinished, planName: 'Test Plan', + chosenPlanId: 1, + complaintUrl: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -571,6 +624,8 @@ void main() { final notification = ChosenPlanClientLastDayStartedNotification( type: NotificationType.chosenPlanClientLastDayStarted, planName: 'Test Plan', + chosenPlanId: 1, + lastDayDiaryUrl: '', isRead: false, createdAt: testDate, ); @@ -585,7 +640,9 @@ void main() { final notification = StayOnTrackToAuthorNotification( type: NotificationType.stayOnTrackToAuthor, planName: 'Test Plan', - clientName: 'Test Client', + userName: 'Test Client', + dayNum: 1, + diaryDayId: 0, isRead: false, createdAt: testDate, ); @@ -600,6 +657,13 @@ void main() { final notification = ClientStartPlanSubscriptionToSubscriberNotification( type: NotificationType.clientStartPlanSubscriptionToSubscriber, planName: 'Test Plan', + planId: 1, + authorName: '', + authorId: 0, + authorPhotoPath: '', + chatUrl: '', + chatId: '', + startDate: '', isRead: false, createdAt: testDate, ); @@ -614,7 +678,11 @@ void main() { final notification = ConsultantConfirmPlanSubscriptionToSubscriberNotification( type: NotificationType.consultantConfirmPlanSubscriptionToSubscriber, planName: 'Test Plan', + planId: 1, consultantName: 'Test Consultant', + authorId: 0, + authorPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -629,8 +697,11 @@ void main() { final notification = ClientPaidPlanSubscriptionToAuthorNotification( type: NotificationType.clientPaidPlanSubscriptionToAuthor, planName: 'Test Plan', + planId: 1, clientName: 'Test Client', - amount: 100.0, + userId: 0, + userPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -644,7 +715,10 @@ void main() { testWidgets('displays ConsultantLeftDiaryCommentNotification', (tester) async { final notification = ConsultantLeftDiaryCommentNotification( type: NotificationType.consultantLeftDiaryComment, + chosenPlanId: 1, + diaryId: 0, planName: 'Test Plan', + userName: '', consultantName: 'Test Consultant', diaryDate: testDate, isRead: false, @@ -661,7 +735,11 @@ void main() { final notification = ConsultantConfirmPlanSubscriptionToAuthorNotification( type: NotificationType.consultantConfirmPlanSubscriptionToAuthor, planName: 'Test Plan', - consultantName: 'Test Consultant', + planId: 1, + clientName: 'Test Consultant', + userId: 0, + userPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -676,7 +754,11 @@ void main() { final notification = ClientPaidPlanSubscriptionToSubscriberNotification( type: NotificationType.clientPaidPlanSubscriptionToSubscriber, planName: 'Test Plan', - amount: 100.0, + planId: 1, + authorName: '', + authorId: 0, + authorPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -691,7 +773,12 @@ void main() { final notification = ClientStartPlanSubscriptionToAuthorNotification( type: NotificationType.clientStartPlanSubscriptionToAuthor, planName: 'Test Plan', + planId: 1, clientName: 'Test Client', + userId: 0, + userPhotoPath: '', + chatId: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -706,7 +793,11 @@ void main() { final notification = ClientCancelPlanSubscriptionToAuthorNotification( type: NotificationType.clientCancelPlanSubscriptionToAuthor, planName: 'Test Plan', + planId: 1, clientName: 'Test Client', + userId: 0, + userPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -721,6 +812,11 @@ void main() { final notification = ClientCancelPlanSubscriptionToSubscriberNotification( type: NotificationType.clientCancelPlanSubscriptionToSubscriber, planName: 'Test Plan', + planId: 1, + authorName: '', + authorId: 0, + authorPhotoPath: '', + chatUrl: '', isRead: false, createdAt: testDate, ); @@ -734,9 +830,11 @@ void main() { testWidgets('displays UserDailyReportCompletedNotification', (tester) async { final notification = UserDailyReportCompletedNotification( type: NotificationType.userDailyReportCompleted, + chosenPlanId: 1, + diaryId: 0, planName: 'Test Plan', clientName: 'Test Client', - reportDate: testDate, + consultantName: '', isRead: false, createdAt: testDate, ); @@ -751,7 +849,8 @@ void main() { final notification = StayOnTrackToCustomerNotification( type: NotificationType.stayOnTrackToCustomer, planName: 'Test Plan', - consultantName: 'Test Consultant', + dayNum: 1, + diaryDayId: 0, isRead: false, createdAt: testDate, ); @@ -780,8 +879,9 @@ void main() { testWidgets('displays SubscriptionNotification', (tester) async { final notification = SubscriptionNotification( type: NotificationType.subscription, - subscriptionType: 'monthly', - planName: 'Test Plan', + userName: '', + userPhotoPath: '', + userProfileUrl: '', isRead: false, createdAt: testDate, ); @@ -795,8 +895,9 @@ void main() { testWidgets('displays BadgeAwardedNotification', (tester) async { final notification = BadgeAwardedNotification( type: NotificationType.badgeAwarded, - badgeName: 'Test Badge', - badgeDescription: 'Test Description', + badgeTitle: 'Test Badge', + badgeType: '', + badgeImage: '', isRead: false, createdAt: testDate, ); @@ -810,8 +911,9 @@ void main() { testWidgets('displays BadgeRevokedNotification', (tester) async { final notification = BadgeRevokedNotification( type: NotificationType.badgeRevoked, - badgeName: 'Test Badge', - reason: 'Test reason', + badgeTitle: 'Test Badge', + badgeType: '', + badgeImage: '', isRead: false, createdAt: testDate, ); @@ -839,8 +941,7 @@ void main() { testWidgets('displays ClientCloseComplaintSuccessNotification', (tester) async { final notification = ClientCloseComplaintSuccessNotification( type: NotificationType.clientCloseComplaintSuccess, - complaintType: 'test', - entityName: 'Test Entity', + planName: 'Test Plan', isRead: false, createdAt: testDate, ); @@ -887,7 +988,6 @@ void main() { final notification = ClientRejectSuccessNotification( type: NotificationType.clientRejectSuccess, planName: 'Test Plan', - reason: 'Test reason', isRead: false, createdAt: testDate, ); @@ -903,7 +1003,6 @@ void main() { type: NotificationType.consultantRejectSuccess, planName: 'Test Plan', clientName: 'Test Client', - reason: 'Test reason', isRead: false, createdAt: testDate, ); @@ -937,8 +1036,12 @@ void main() { final notification = ProductStatusUpdatedForAuthorNotification( type: NotificationType.productStatusUpdatedForAuthor, productName: 'Test Product', - oldStatus: 'pending', - newStatus: 'approved', + productId: 1, + author: '', + authorId: 0, + status: 'approved', + subject: '', + messageLine: '', isRead: false, createdAt: testDate, ); @@ -967,8 +1070,15 @@ void main() { type: NotificationType.personalDietIsDefinedToCustomer, isRead: false, createdAt: testDate, - dietName: 'Test Diet', + chosenPlanId: 1, dietId: 1, + planName: '', + userName: '', + consultantName: '', + dietName: 'Test Diet', + role: '', + peerSlug: '', + peerImagePath: '', ); await tester.pumpWidget(createTestWidget(notification)); diff --git a/test/features/notifications/repository/notifications_repository_test.dart b/test/features/notifications/repository/notifications_repository_test.dart index a80b3fb8..ef267271 100644 --- a/test/features/notifications/repository/notifications_repository_test.dart +++ b/test/features/notifications/repository/notifications_repository_test.dart @@ -24,6 +24,8 @@ void main() { type: NotificationType.chosenPlanStartedToClientNotification, planName: 'Test Plan', planId: 1, + consultantName: 'Test Consultant', + peerProfileUrl: '', isRead: false, createdAt: DateTime.now(), ), diff --git a/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart b/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart index 32f67e95..4fa46bb3 100644 --- a/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart +++ b/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart @@ -123,7 +123,7 @@ void main() { registerMocks(planCubit, commentsCubit); - await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null))); + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null))); await tester.pump(); expect(find.byType(AboutPlanScreen), findsOneWidget); @@ -149,7 +149,7 @@ void main() { registerMocks(planCubit, commentsCubit); await mockNetworkImagesFor(() async { - await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null))); + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null))); await tester.pump(); await tester.pump(); }); @@ -177,7 +177,7 @@ void main() { registerMocks(planCubit, commentsCubit); - await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null))); + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null))); await tester.pump(); expect(find.byType(AboutPlanScreen), findsOneWidget);