diff --git a/.gitignore b/.gitignore index 530ea713..1503b5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ env/.env .env android/key.properties app/upload-keystore.jks -coverage/ \ No newline at end of file +coverage/ +backend/ \ No newline at end of file diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart index 0556fab6..5a93013c 100644 --- a/lib/core/routing/content_routes.dart +++ b/lib/core/routing/content_routes.dart @@ -17,12 +17,25 @@ 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 +43,25 @@ 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,15 +69,25 @@ class ContentRoutes { name: 'aboutDietScreen', builder: (context, state) { final slug = state.uri.queryParameters['slug']; + 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, + navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null), + highlightedCommentId: data['commentId'] as int?, ); }, ), @@ -65,12 +101,26 @@ 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/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/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index 0254d3c7..6da09b9e 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -568,6 +568,7 @@ class _BlogPostCreateScreenState extends State { CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController), const SizedBox(height: 80), _createBlogPostButton(), + const SizedBox(height: 80), ], ), ), 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..3375a312 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -215,6 +215,8 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + final int planId; + final int customerId; @override final DateTime createdAt; ChosenPlanStartedToConsultantNotification({ @@ -222,6 +224,8 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel required this.isRead, required this.planName, required this.customerName, + required this.planId, + required this.customerId, required this.createdAt, }); @@ -231,6 +235,8 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel isRead: isRead ?? false, planName: json['plan_name'], customerName: json['client_name'], + planId: json['plan_id'] ?? 0, + customerId: json['client_id'] ?? 0, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } @@ -420,6 +426,7 @@ class CommentReplied implements BaseNotificationModel { final String author; final int authorId; final int parentCommentId; + final int? commentId; @override final DateTime createdAt; @@ -431,6 +438,7 @@ class CommentReplied implements BaseNotificationModel { required this.author, required this.authorId, required this.parentCommentId, + this.commentId, required this.isRead, required this.createdAt, }); @@ -444,6 +452,7 @@ class CommentReplied implements BaseNotificationModel { 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()), ); @@ -504,6 +513,7 @@ class EntityCommented implements BaseNotificationModel { final String authorName; final int authorId; final String authorSlug; + final int? commentId; @override final DateTime createdAt; @@ -517,6 +527,7 @@ class EntityCommented implements BaseNotificationModel { required this.authorName, required this.authorId, required this.authorSlug, + this.commentId, required this.isRead, required this.createdAt, }); @@ -532,6 +543,7 @@ class EntityCommented implements BaseNotificationModel { 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 +843,15 @@ 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 { @@ -1015,16 +1035,28 @@ class StayOnTrackToAuthorNotification implements BaseNotificationModel { final bool isRead; final String planName; final String clientName; + final int planId; + final int clientId; @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.clientName, + required this.planId, + required this.clientId, + 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'] ?? '', + planId: json['plan_id'] ?? 0, + clientId: json['client_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1326,16 +1358,28 @@ class StayOnTrackToCustomerNotification implements BaseNotificationModel { final bool isRead; final String planName; final String consultantName; + final int planId; + final int consultantId; @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.consultantName, + required this.planId, + required this.consultantId, + 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'] ?? '', + planId: json['plan_id'] ?? 0, + consultantId: json['consultant_id'] ?? 0, isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1652,14 +1696,16 @@ class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel final String productName; final String oldStatus; final String newStatus; + final String status; // Added to capture single status if old/new aren't provided @override final DateTime createdAt; ProductStatusUpdatedForAuthorNotification({ required this.type, required this.productName, - required this.oldStatus, - required this.newStatus, + this.oldStatus = '', + this.newStatus = '', + this.status = '', required this.isRead, required this.createdAt, }); @@ -1670,6 +1716,7 @@ class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel productName: json['product_name'] ?? '', oldStatus: json['old_status'] ?? '', newStatus: json['new_status'] ?? '', + status: json['status'] ?? '', isRead: isRead ?? false, createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); @@ -1724,6 +1771,309 @@ class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationMod } } +// 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 complaintType; + final String entityName; + + ComplaintRegisteredNotification({ + required this.type, + required this.isRead, + required this.createdAt, + required this.complaintType, + 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()), + complaintType: json['complaint_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; + + ChosenDietExpiredNotification({ + required this.type, + required this.isRead, + required this.createdAt, + required this.dietName, + }); + + 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'] ?? '', + ); + } +} + +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 subscriberName; + final int subscriberId; + + UserSubscribedNotification({ + required this.type, + required this.isRead, + required this.createdAt, + required this.subscriberName, + required this.subscriberId, + }); + + 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()), + subscriberName: json['subscriber_name'] ?? '', + subscriberId: json['subscriber_id'] ?? 0, + ); + } +} + enum NotificationType { // Diet related dietCancelled, @@ -1731,6 +2081,7 @@ enum NotificationType { dietExpired, dietWillStartedIn, dietCanNotBeStarted, + chosenDietExpired, // New // Plan related personalDietCreationNeeded, @@ -1760,12 +2111,17 @@ enum NotificationType { // Comments entityCommented, commentReplied, + commentComplaint, // New + commentDeleted, // New // Profile phoneChanged, + consultantEducationUpdated, // New + completeRegistration, // New // Subscription subscription, + userSubscribed, // New // Badges badgeAwarded, @@ -1773,16 +2129,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 +2155,9 @@ enum NotificationType { // Diary diaryReminderForUsers, + // Chat + newChatMessage, // New + // Legacy types (keeping for backward compatibility) planSubscriptionStartedToAuthor, planSubscriptionCanceledToAuthor, @@ -1865,7 +2229,23 @@ enum NotificationType { 'stay-on-track-to-customer' => NotificationType.stayOnTrackToCustomer, '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..54c84507 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -267,10 +267,82 @@ 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()); } } diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 0169f96d..1c2d1c58 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -9,9 +9,194 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + class NotificationWidget extends StatelessWidget { const NotificationWidget({super.key, required this.notification}); final BaseNotificationModel notification; + + void _onTap(BuildContext context) { + // Mark as read first + context.read().markNotificationsAsRead(); + + switch (notification) { + case CommentReplied n: + _navigateToEntity(context, n.commentedEntityType, n.commentedEntityId, n.commentId); + case EntityCommented n: + _navigateToEntity(context, n.entityType, n.entityId, n.commentId); + case PersonalDietIsDefinedToCustomerNotification n: + context.push(RouteConstants.aboutDietScreen, extra: {'id': n.dietId}); + case PlanSubscriptionStartedToAuthorNotification n: + if (n.chatId.isNotEmpty && n.chatId != '0') { + context.push(RouteConstants.chatScreen, extra: int.tryParse(n.chatId) ?? 0); + } else { + // Fallback if no chat ID + context.push(RouteConstants.profileScreen); + } + case ConsultantAssignedNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case StayOnTrackToAuthorNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case StayOnTrackToCustomerNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case ChosenPlanStartedToClientNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case ChosenPlanStartedToConsultantNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case ProductStatusUpdatedForAuthorNotification(): + context.push(RouteConstants.profileScreen); + // New Cases with Safety Checks + case NewChatMessageNotification n: + if (n.chatId != 0) context.push(RouteConstants.chatScreen, extra: n.chatId); + case FinanceQuestionCreatedNotification(): + context.push(RouteConstants.userFinanceScreen); + case ChosenDietExpiredNotification(): + context.push(RouteConstants.myDietsScreen); + case ContentUnpublishedNotification(): + context.push(RouteConstants.profileScreen); + case CompleteRegistrationNotification(): + context.push(RouteConstants.profileScreen); // Or edit profile + case ConsultantEducationUpdatedNotification(): + context.push(RouteConstants.profileScreen); + case ComplaintRegisteredNotification(): + context.push(RouteConstants.profileScreen); + case ConsultantRequestStartedNotification(): + // Navigate to where requests are shown + context.push(RouteConstants.profileScreen); + case ClientCloseComplaintSuccessNotification(): + context.push(RouteConstants.profileScreen); + case ClientRefundSuccessNotification(): + context.push(RouteConstants.userFinanceScreen); + case ConsultantRefundSuccessNotification(): + context.push(RouteConstants.userFinanceScreen); + case ClientRejectSuccessNotification(): + context.push(RouteConstants.aboutPlanScreen); // Best guess + case ConsultantRejectSuccessNotification(): + context.push(RouteConstants.aboutPlanScreen); + case ScoreSubstractedNotification(): + context.push(RouteConstants.profileScreen); + case ScoreAdded(): + context.push(RouteConstants.profileScreen); + case DiaryReminderForUsersNotification(): + context.push(RouteConstants.myDietsScreen); // Likely where diary is + case MotivateUserNotification(): + context.push(RouteConstants.myDietsScreen); + case CommentComplaintNotification(): + context.push(RouteConstants.profileScreen); + case CommentDeletedNotification(): + context.push(RouteConstants.profileScreen); + case UserSubscribedNotification(): + context.push(RouteConstants.profileScreen); + case BadgeAwardedNotification(): + context.push(RouteConstants.profileScreen); + case BadgeRevokedNotification(): + context.push(RouteConstants.profileScreen); + case SubscriptionNotification(): + context.push(RouteConstants.userFinanceScreen); + case PhoneChangedNotification(): + context.push(RouteConstants.profileScreen); + case UserDailyReportCompletedNotification(): + // Attempt to go to plan if we had ID, but we don't in this model. Go to plans list. + context.push(RouteConstants.myPlansScreen); + case ClientCancelPlanSubscriptionToSubscriberNotification(): + context.push(RouteConstants.myPlansScreen); + case ClientCancelPlanSubscriptionToAuthorNotification(): + context.push(RouteConstants.myPlansScreen); + case ClientStartPlanSubscriptionToAuthorNotification(): + context.push(RouteConstants.myPlansScreen); + case ClientPaidPlanSubscriptionToSubscriberNotification(): + context.push(RouteConstants.userFinanceScreen); + case ConsultantConfirmPlanSubscriptionToAuthorNotification(): + context.push(RouteConstants.myPlansScreen); + case ConsultantLeftDiaryCommentNotification(): + // Needs planID to go to specific plan, model has planName. + // Optimally we'd go to the diary. For now plans screen. + context.push(RouteConstants.myPlansScreen); + case ClientPaidPlanSubscriptionToAuthorNotification(): + context.push(RouteConstants.userFinanceScreen); + case ConsultantConfirmPlanSubscriptionToSubscriberNotification(): + context.push(RouteConstants.myPlansScreen); + case ClientStartPlanSubscriptionToSubscriberNotification(): + context.push(RouteConstants.myPlansScreen); + case ChosenPlanClientLastDayStartedNotification(): + context.push(RouteConstants.myPlansScreen); + case ChosenPlanClientLastDayFinishedNotification(): + context.push(RouteConstants.myPlansScreen); + case PersonalDietCreationNeededNotification(): + context.push(RouteConstants.myPlansScreen); + case DietCanNotBeStartedNotification(): + context.push(RouteConstants.myDietsScreen); + case DietWillStartedInNotification(): + context.push(RouteConstants.myDietsScreen); + case DietExpiredNotification(): + context.push(RouteConstants.myDietsScreen); + case DietStartedNotification(): + context.push(RouteConstants.myDietsScreen); + case DietCancelledNotification(): + context.push(RouteConstants.myDietsScreen); + case MealReminderNotification(): + context.push(RouteConstants.myDietsScreen); + case WorkoutReminderNotification(): + context.push(RouteConstants.myDietsScreen); + case GoalAchievedNotification(): + context.push(RouteConstants.profileScreen); + case PaymentSuccessfulNotification(): + context.push(RouteConstants.userFinanceScreen); + case PaymentFailedNotification(): + context.push(RouteConstants.userFinanceScreen); + case ConsultantUnassignedNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case PlanSubscriptionRenewedNotification(): + context.push(RouteConstants.userFinanceScreen); + case PlanSubscriptionExpiredNotification(): + context.push(RouteConstants.userFinanceScreen); + case ComplaintToModerationForUser(): + context.push(RouteConstants.profileScreen); + case ChosenPlanConsultantLastDayStarted n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case ChosenPlanLastDayFinishedForUser(): + context.push(RouteConstants.myPlansScreen); + case ChosenPlanFinishedToConsultantNotification n: + context.push(RouteConstants.aboutPlanScreen, extra: {'id': n.planId}); + case PlanSubscriptionPaidToAuthorNotification(): + context.push(RouteConstants.userFinanceScreen); + case ChosenPlanTomorrowReminderToConsultantNotification(): + context.push(RouteConstants.myPlansScreen); + case ChosenPlanTomorrowReminderToClientNotification(): + context.push(RouteConstants.myPlansScreen); + case PlanSubscriptionCanceledToCustomerNotification(): + context.push(RouteConstants.myPlansScreen); + case ChosenPlanFinishedToClientNotification(): + context.push(RouteConstants.myPlansScreen); + case PersonalizedDietIsDefinedNotification(): + context.push(RouteConstants.myDietsScreen); + case ConsultantLeftCommentNotification(): + context.push(RouteConstants.myPlansScreen); + case DiaryReminderNotification(): + context.push(RouteConstants.myDietsScreen); + case PlanSubscriptionCanceledToAuthorNotification(): + context.push(RouteConstants.myPlansScreen); + default: + // Do nothing if no logical destination + break; + } + } + + void _navigateToEntity(BuildContext context, String type, int id, int? commentId) { + if (type.contains('BlogPost')) { + context.push(RouteConstants.blogPostScreen, extra: {'id': id, 'highlightedCommentId': commentId}); + } else if (type.contains('Recipe')) { + context.push(RouteConstants.aboutRecipesScreen, extra: {'id': id, 'highlightedCommentId': commentId}); + } else if (type.contains('Plan')) { + context.push(RouteConstants.aboutPlanScreen, extra: {'id': id, 'highlightedCommentId': commentId}); + } else if (type.contains('Diet')) { + context.push(RouteConstants.aboutDietScreen, extra: {'id': id, 'highlightedCommentId': commentId}); + } + } + +// }; +// } @override Widget build(BuildContext context) { return switch (notification) { @@ -117,6 +302,20 @@ 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), }; } @@ -131,8 +330,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -146,8 +346,9 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), + notificationsText: 'Отмена подписки. Клиент ${notification.customerName} отказался от плана "${notification.planName}"', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -159,7 +360,12 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); } } @@ -172,8 +378,9 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.tomorrowYourPlanWillStart(notification.planName), + notificationsText: 'Напоминание: Завтра старт плана "${notification.planName}"', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -184,7 +391,12 @@ class _DiaryReminderWidget extends StatelessWidget { @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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); } } @@ -197,8 +409,9 @@ class _ConsultantLeftCommentWidget extends StatelessWidget { 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -208,7 +421,12 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); } } @@ -221,8 +439,9 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', + notificationsText: 'Поздравляем! План «${notification.planName}» успешно завершен! 🏆', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -236,8 +455,9 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.youCanceledSubscriptionToPlan(notification.planName), + notificationsText: 'Подписка на план "${notification.planName}" отменена.', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -251,8 +471,9 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.planWithUserStarted(notification.planName, notification.customerName), + notificationsText: 'Начало работы: План "${notification.planName}" с клиентом ${notification.customerName}', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -266,8 +487,9 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName), + notificationsText: 'Напоминание: Завтра старт работы с ${notification.customerName} по плану "${notification.planName}"', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -281,8 +503,9 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.userPaidThePlan(notification.customerName, notification.planName), + notificationsText: 'Оплата получена! Клиент ${notification.customerName} оплатил план "${notification.planName}" 💰', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -293,16 +516,19 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + return GestureDetector( + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + behavior: HitTestBehavior.opaque, + child: 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)), + 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), @@ -331,6 +557,7 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), ), ], + ), ); } @@ -377,6 +604,7 @@ class _BaseNotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: onTap, + behavior: HitTestBehavior.opaque, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -385,8 +613,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 +674,7 @@ 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 +685,9 @@ class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Последний день плана начался: ${notification.planName}', + notificationsText: 'Внимание: Начался последний день плана "${notification.planName}"', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -462,7 +697,12 @@ 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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); } } @@ -473,8 +713,9 @@ class _CommentRepliedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Ответ на комментарий: ${notification.entityName}', + notificationsText: 'Вам ответили в комментариях к: ${notification.entityName}', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -486,8 +727,9 @@ class _ScoreAddedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Добавлены баллы: ${notification.scoreValue}', + notificationsText: 'Поздравляем! Вам начислены баллы: ${notification.scoreValue} ⭐', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -499,8 +741,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -512,8 +755,9 @@ class _UnknownNotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}', + notificationsText: 'У вас новое уведомление! Пожалуйста, обновите приложение, чтобы увидеть детали.', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -526,8 +770,9 @@ class _PlanSubscriptionExpiredWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" истекла', + notificationsText: 'Срок действия подписки на план "${notification.planName}" истек. Продлите, чтобы продолжить! ⏳', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -539,8 +784,9 @@ 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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -552,8 +798,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -565,8 +812,9 @@ 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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -578,8 +826,9 @@ 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.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -591,8 +840,9 @@ 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.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -604,8 +854,9 @@ 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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -617,8 +868,9 @@ 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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -630,8 +882,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -644,8 +897,9 @@ class _DietCancelledWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" отменена', + notificationsText: 'Диета "${notification.dietName}" была отменена.', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -657,8 +911,9 @@ class _DietStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" началась', + notificationsText: 'Старт диеты "${notification.dietName}"! Успехов! 🥗', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -670,8 +925,9 @@ class _DietExpiredWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Диета "${notification.dietName}" истекла', + notificationsText: 'Срок действия диеты "${notification.dietName}" подошел к концу.', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -683,8 +939,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -696,8 +953,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -709,8 +967,9 @@ class _PersonalDietCreationNeededWidget 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.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -722,8 +981,9 @@ class _ChosenPlanClientLastDayFinishedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Последний день плана "${notification.planName}" завершен', + notificationsText: 'Поздравляем с завершением плана "${notification.planName}"! 🎉', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -735,8 +995,9 @@ class _ChosenPlanClientLastDayStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Последний день плана "${notification.planName}" начался', + notificationsText: 'Сегодня последний день плана "${notification.planName}"! Финиш близко! 🏁', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -748,8 +1009,9 @@ class _StayOnTrackToAuthorWidget 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.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -761,8 +1023,9 @@ class _ClientStartPlanSubscriptionToSubscriberWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" началась', + notificationsText: 'У вас новая подписка на план "${notification.planName}"! 🌟', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -774,8 +1037,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -787,8 +1051,9 @@ 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}" (${notification.amount}) 💸', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -800,8 +1065,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -813,8 +1079,9 @@ class _ConsultantConfirmPlanSubscriptionToAuthorWidget 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -826,8 +1093,9 @@ class _ClientPaidPlanSubscriptionToSubscriberWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Оплачена подписка на план "${notification.planName}" на сумму ${notification.amount}', + notificationsText: 'Оплата подписки на план "${notification.planName}" прошла успешно (${notification.amount}) ✅', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -839,8 +1107,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -852,8 +1121,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -865,8 +1135,9 @@ class _ClientCancelPlanSubscriptionToSubscriberWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка на план "${notification.planName}" отменена', + notificationsText: 'Ваша подписка на план "${notification.planName}" была отменена.', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -878,8 +1149,9 @@ 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.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -891,8 +1163,9 @@ class _StayOnTrackToCustomerWidget 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -904,8 +1177,9 @@ class _PhoneChangedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Номер телефона изменен с ${notification.oldPhone} на ${notification.newPhone}', + notificationsText: 'Ваш номер телефона был успешно изменен на ${notification.newPhone} 📱', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -917,8 +1191,9 @@ class _SubscriptionWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Подписка ${notification.subscriptionType} на план "${notification.planName}"', + notificationsText: 'Оформлена подписка ${notification.subscriptionType} на план "${notification.planName}" 🎉', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -930,8 +1205,9 @@ class _BadgeAwardedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Получен значок "${notification.badgeName}": ${notification.badgeDescription}', + notificationsText: 'Новая награда! Вы получили значок "${notification.badgeName}": ${notification.badgeDescription} 🏅', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -943,8 +1219,9 @@ class _BadgeRevokedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Отозван значок "${notification.badgeName}": ${notification.reason}', + notificationsText: 'Значок "${notification.badgeName}" был отозван. Причина: ${notification.reason}', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -956,8 +1233,9 @@ class _ConsultantRequestStartedWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Запрос к консультанту ${notification.consultantName} начат', + notificationsText: 'Запрос отправлен консультанту ${notification.consultantName} 📩', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -969,8 +1247,9 @@ class _ClientCloseComplaintSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Жалоба на ${notification.complaintType} "${notification.entityName}" успешно закрыта', + notificationsText: 'Ваша жалоба на ${notification.complaintType} "${notification.entityName}" была рассмотрена и закрыта.', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -982,8 +1261,9 @@ 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}). Причина: ${notification.reason}', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -995,8 +1275,9 @@ 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.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -1008,8 +1289,9 @@ class _ClientRejectSuccessWidget extends StatelessWidget { Widget build(BuildContext context) { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Отклонение по плану "${notification.planName}": ${notification.reason}', + notificationsText: 'Отказ по плану "${notification.planName}" принят: ${notification.reason}', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -1021,8 +1303,9 @@ 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}": ${notification.reason}', createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -1034,8 +1317,9 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -1045,10 +1329,22 @@ class _ProductStatusUpdatedForAuthorWidget extends StatelessWidget { final ProductStatusUpdatedForAuthorNotification notification; @override Widget build(BuildContext context) { + String text; + if (notification.oldStatus.isNotEmpty && notification.newStatus.isNotEmpty) { + text = 'Статус продукта "${notification.productName}" изменен с ${notification.oldStatus} на ${notification.newStatus}'; + } else if (notification.newStatus.isNotEmpty) { + text = 'Статус продукта "${notification.productName}" изменен на ${notification.newStatus}'; + } else if (notification.status.isNotEmpty) { + text = 'Статус продукта "${notification.productName}": ${notification.status}'; + } else { + text = 'Статус продукта "${notification.productName}" обновлен'; + } + return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: 'Статус продукта "${notification.productName}" изменен с ${notification.oldStatus} на ${notification.newStatus}', + notificationsText: text, createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } @@ -1058,7 +1354,12 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); } } @@ -1069,9 +1370,163 @@ 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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +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, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), + ); + } +} + +class _UserSubscribedWidget extends StatelessWidget { + const _UserSubscribedWidget({required this.notification}); + final UserSubscribedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'У вас новый подписчик: ${notification.subscriberName}! 👋', + createdAt: notification.createdAt, + onTap: () => (context.findAncestorWidgetOfExactType() as NotificationWidget)._onTap(context), ); } } 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/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/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..cb7504ec 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); diff --git a/test/features/notifications/presentation/widgets/notification_widget_test.dart b/test/features/notifications/presentation/widgets/notification_widget_test.dart index f624993a..d6a8025d 100644 --- a/test/features/notifications/presentation/widgets/notification_widget_test.dart +++ b/test/features/notifications/presentation/widgets/notification_widget_test.dart @@ -160,6 +160,8 @@ void main() { type: NotificationType.chosenPlanStartedToConsultant, planName: 'Test Plan', customerName: 'Test Customer', + planId: 1, + customerId: 1, isRead: false, createdAt: testDate, ); @@ -586,6 +588,8 @@ void main() { type: NotificationType.stayOnTrackToAuthor, planName: 'Test Plan', clientName: 'Test Client', + planId: 1, + clientId: 1, isRead: false, createdAt: testDate, ); @@ -752,6 +756,8 @@ void main() { type: NotificationType.stayOnTrackToCustomer, planName: 'Test Plan', consultantName: 'Test Consultant', + planId: 1, + consultantId: 1, isRead: false, createdAt: testDate, ); 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);