diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index 827a0904..96626d5d 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -1,8 +1,7 @@ -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; - sealed class BaseNotificationModel { abstract final NotificationType type; abstract final bool isRead; + abstract final DateTime createdAt; } class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel { @@ -13,6 +12,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod final String planName; final String customerName; final String chatId; + @override + final DateTime createdAt; PlanSubscriptionStartedToAuthorNotification({ required this.type, @@ -20,15 +21,17 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod required this.customerName, required this.chatId, required this.isRead, + required this.createdAt, }); - factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionStartedToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], chatId: json['chat_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -40,20 +43,24 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; PlanSubscriptionCanceledToAuthorNotification({ required this.type, required this.planName, required this.customerName, required this.isRead, + required this.createdAt, }); - factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -65,20 +72,18 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel { final bool isRead; final String planName; final int planId; + @override + final DateTime createdAt; - ChosenPlanStartedToClientNotification({ - required this.type, - required this.planName, - required this.planId, - required this.isRead, - }); + ChosenPlanStartedToClientNotification({required this.type, required this.planName, required this.planId, required this.isRead, required this.createdAt}); - factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToClientNotification( type: type, planName: json['plan_name'], planId: json['plan_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -89,15 +94,18 @@ class ChosenPlanTomorrowReminderToClientNotification implements BaseNotification @override final bool isRead; final String planName; + @override + final DateTime createdAt; - ChosenPlanTomorrowReminderToClientNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + ChosenPlanTomorrowReminderToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { - return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false); + factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanTomorrowReminderToClientNotification( + type: type, + planName: json['plan_name'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); } } @@ -106,11 +114,17 @@ class DiaryReminderNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - DiaryReminderNotification({required this.type, required this.isRead}); + DiaryReminderNotification({required this.type, required this.isRead, required this.createdAt}); - factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { - return DiaryReminderNotification(type: type, isRead: isRead ?? false); + factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DiaryReminderNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); } } @@ -120,18 +134,17 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel { @override final bool isRead; final DateTime date; + @override + final DateTime createdAt; - ConsultantLeftCommentNotification({ - required this.type, - required this.date, - required this.isRead, - }); + ConsultantLeftCommentNotification({required this.type, required this.date, required this.isRead, required this.createdAt}); - factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ConsultantLeftCommentNotification( type: type, - date: parseDate(json['diary_date']), + date: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()), isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -141,16 +154,16 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - PersonalizedDietIsDefinedNotification({ - required this.type, - required this.isRead, - }); + PersonalizedDietIsDefinedNotification({required this.type, required this.isRead, required this.createdAt}); - factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PersonalizedDietIsDefinedNotification( type: type, isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -161,17 +174,16 @@ class ChosenPlanFinishedToClientNotification implements BaseNotificationModel { @override final bool isRead; final String planName; - ChosenPlanFinishedToClientNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + @override + final DateTime createdAt; + ChosenPlanFinishedToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToClientNotification( type: type, planName: json['plan_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -182,17 +194,16 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification @override final bool isRead; final String planName; - PlanSubscriptionCanceledToCustomerNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + @override + final DateTime createdAt; + PlanSubscriptionCanceledToCustomerNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToCustomerNotification( type: type, planName: json['plan_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -204,19 +215,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; ChosenPlanStartedToConsultantNotification({ required this.type, required this.isRead, required this.planName, required this.customerName, + required this.createdAt, }); - factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToConsultantNotification( type: type, isRead: isRead ?? false, planName: json['plan_name'], customerName: json['client_name'], + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -228,19 +243,23 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; ChosenPlanTomorrowReminderToConsultantNotification({ required this.type, required this.planName, required this.customerName, required this.isRead, + required this.createdAt, }); - factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanTomorrowReminderToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -252,19 +271,23 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; PlanSubscriptionPaidToAuthorNotification({ required this.type, required this.isRead, required this.planName, required this.customerName, + required this.createdAt, }); - factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionPaidToAuthorNotification( type: type, isRead: isRead ?? false, planName: json['plan_name'], customerName: json['user_name'], + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -277,21 +300,25 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode final String planName; final String customerName; final int planId; + @override + final DateTime createdAt; ChosenPlanFinishedToConsultantNotification({ required this.type, required this.planName, required this.customerName, required this.planId, required this.isRead, + required this.createdAt, }); - factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], planId: json['plan_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -301,16 +328,212 @@ class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - ChosenPlanLastDayFinishedForUser({ - required this.type, - required this.isRead, - }); + ChosenPlanLastDayFinishedForUser({required this.type, required this.isRead, required this.createdAt}); - factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanLastDayFinishedForUser( type: type, isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ChosenPlanConsultantLastDayStarted implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final int planId; + final String chatId; + final String chatUrl; + @override + final DateTime createdAt; + + ChosenPlanConsultantLastDayStarted({ + required this.type, + required this.planName, + required this.planId, + required this.chatId, + required this.chatUrl, + required this.isRead, + required this.createdAt, + }); + + factory ChosenPlanConsultantLastDayStarted.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanConsultantLastDayStarted( + type: type, + planName: json['plan_name'], + planId: json['plan_id'], + chatId: json['chat_id'], + chatUrl: json['chat_url'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ComplaintToModerationForUser implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String message; + final int entityId; + final String entityType; + @override + final DateTime createdAt; + + ComplaintToModerationForUser({ + required this.type, + required this.message, + required this.entityId, + required this.entityType, + required this.isRead, + required this.createdAt, + }); + + factory ComplaintToModerationForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ComplaintToModerationForUser( + type: type, + message: json['message'], + entityId: json['entity_id'], + entityType: json['entity_type'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class CommentReplied implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String entityName; + final int commentedEntityId; + final String commentedEntityType; + final String author; + final int authorId; + final int parentCommentId; + @override + final DateTime createdAt; + + CommentReplied({ + required this.type, + required this.entityName, + required this.commentedEntityId, + required this.commentedEntityType, + required this.author, + required this.authorId, + required this.parentCommentId, + required this.isRead, + required this.createdAt, + }); + + factory CommentReplied.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return CommentReplied( + type: type, + entityName: json['entity_name'], + commentedEntityId: json['commented_entity_id'], + commentedEntityType: json['commented_entity_type'], + author: json['author'], + authorId: json['author_id'], + parentCommentId: json['parrent_comment_id'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ScoreAdded implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String scoreKey; + final int scorableId; + final String scorableType; + final String scorableName; + final String scorableSlug; + final int scoreValue; + @override + final DateTime createdAt; + + ScoreAdded({ + required this.type, + required this.scoreKey, + required this.scorableId, + required this.scorableType, + required this.scorableName, + required this.scorableSlug, + required this.scoreValue, + required this.isRead, + required this.createdAt, + }); + + factory ScoreAdded.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ScoreAdded( + type: type, + scoreKey: json['score_key'], + scorableId: json['scorable_id'], + scorableType: json['scorable_type'], + scorableName: json['scorable_name'] ?? '', + scorableSlug: json['scorable_slug'] ?? '', + scoreValue: json['score_value'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class EntityCommented implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String entityName; + final int entityId; + final String entityType; + final String entityTranslatedType; + final String entityUrl; + final String authorName; + final int authorId; + final String authorSlug; + @override + final DateTime createdAt; + + EntityCommented({ + required this.type, + required this.entityName, + required this.entityId, + required this.entityType, + required this.entityTranslatedType, + required this.entityUrl, + required this.authorName, + required this.authorId, + required this.authorSlug, + required this.isRead, + required this.createdAt, + }); + + factory EntityCommented.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return EntityCommented( + type: type, + entityName: json['entity_name'], + entityId: json['entity_id'], + entityType: json['entity_type'], + entityTranslatedType: json['entity_traslated_type'], + entityUrl: json['entity_url'], + authorName: json['author_name'], + authorId: json['author_id'], + authorSlug: json['author_slug'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -320,11 +543,10 @@ class UnknownNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - UnknownNotification({ - required this.type, - this.isRead = true, - }); + UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); } enum NotificationType { @@ -342,23 +564,37 @@ enum NotificationType { planSubscriptionPaidToAuthor, chosenPlanFinishedToConsultant, chosenPlanLastDayFinishedForUser, + // Новые типы + chosenPlanConsultantLastDayStarted, + complaintToModerationForUser, + commentReplied, + scoreAdded, + entityCommented, unknown; static NotificationType getTypeFromString(String type) => switch (type) { - 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, - 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, - 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, - 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, - 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, - 'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient, - 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer, - 'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, - 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, - 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, - 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, - _ => NotificationType.unknown, - }; + 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, + 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, + 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, + 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, + 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, + 'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient, + 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer, + 'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, + 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, + 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, + 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, + // Новые маппинги + 'App\\Notifications\\ChosenPlan\\LastDayStartedForConsultant' => NotificationType.chosenPlanConsultantLastDayStarted, + 'App\\Notifications\\ComplaintToModerationNotify' => NotificationType.complaintToModerationForUser, + 'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied, + 'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded, + 'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented, + _ => NotificationType.unknown, + }; } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 61eba9a7..d4e6e17d 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -13,10 +13,7 @@ class NotificationsCubit extends Cubit { final WsManager wsManager; final NotificationsRepository notificationsRepository; StreamSubscription? _newNotificationsSubscription; - NotificationsCubit({ - required this.wsManager, - required this.notificationsRepository, - }) : super(const NotificationsState.empty()); + NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty()); Future connectToWebSocket() async { await _newNotificationsSubscription?.cancel(); @@ -27,22 +24,95 @@ class NotificationsCubit extends Cubit { BaseNotificationModel _parseNotification(Map notificationJson, NotificationType type) { try { + final createdAt = notificationJson['created_at']; + final isRead = notificationJson['read_at'] != null; + return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type), - NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(notificationJson, type), + NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + // New notification types + NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { @@ -56,32 +126,80 @@ class NotificationsCubit extends Cubit { final type = NotificationType.getTypeFromString(stringifiedType); final notification = _parseNotification(json, type); _notifications.insert(0, notification); - emit( - NotificationsState.loaded( - notifications: _notifications, - notificationsCount: _notifications.length, - hasUnreadNotifications: true, - ), - ); + + final currentState = state; + if (currentState is NotificationsStateLoaded) { + emit(currentState.copyWith(notifications: _notifications, notificationsCount: _notifications.length, hasUnreadNotifications: true)); + } } Future getNotifications() async { try { - final notifications = await notificationsRepository.getNotifications(page: 1); - _notifications.clear(); - _notifications.addAll(notifications ?? []); - emit( - NotificationsState.loaded( - notifications: _notifications, - notificationsCount: _notifications.length, - hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty, - ), - ); + final result = await notificationsRepository.getNotifications(page: 1); + if (result != null) { + final notifications = result['notifications'] as List; + final pagination = result['pagination'] as Map; + + _notifications.clear(); + _notifications.addAll(notifications); + + final currentPage = pagination['current_page'] as int; + final lastPage = pagination['last_page'] as int; + final hasNextPage = currentPage < lastPage; + + emit( + NotificationsState.loaded( + notifications: _notifications, + notificationsCount: _notifications.length, + hasUnreadNotifications: notifications.where((elem) => elem.isRead == false).isNotEmpty, + currentPage: currentPage, + hasNextPage: hasNextPage, + ), + ); + } } catch (e, st) { log(e.toString(), stackTrace: st); } } + Future loadMoreNotifications() async { + final currentState = state; + if (currentState is NotificationsStateLoaded && currentState.hasNextPage && !currentState.isLoadingMore) { + try { + emit(currentState.copyWith(isLoadingMore: true)); + + final nextPage = currentState.currentPage + 1; + final result = await notificationsRepository.getNotifications(page: nextPage); + + if (result != null) { + final newNotifications = result['notifications'] as List; + final pagination = result['pagination'] as Map; + + _notifications.addAll(newNotifications); + + final currentPage = pagination['current_page'] as int; + final lastPage = pagination['last_page'] as int; + final hasNextPage = currentPage < lastPage; + + emit( + currentState.copyWith( + notifications: _notifications, + notificationsCount: _notifications.length, + currentPage: currentPage, + hasNextPage: hasNextPage, + isLoadingMore: false, + ), + ); + } else { + emit(currentState.copyWith(isLoadingMore: false)); + } + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(currentState.copyWith(isLoadingMore: false)); + } + } + } + Future markNotificationsAsRead() async { final currentState = state; if (currentState is NotificationsStateLoaded) { diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart index 9c221b31..391e199e 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart @@ -14,6 +14,9 @@ abstract class NotificationsState with _$NotificationsState { required int notificationsCount, @Default(false) bool isLoading, @Default(false) bool hasUnreadNotifications, + @Default(1) int currentPage, + @Default(true) bool hasNextPage, + @Default(false) bool isLoadingMore, }) = NotificationsStateLoaded; const factory NotificationsState.error({ diff --git a/lib/features/notifications/presentation/notifications_screen.dart b/lib/features/notifications/presentation/notifications_screen.dart index ff48617f..58e3c65f 100644 --- a/lib/features/notifications/presentation/notifications_screen.dart +++ b/lib/features/notifications/presentation/notifications_screen.dart @@ -10,9 +10,38 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class NotificationsScreen extends StatelessWidget { +class NotificationsScreen extends StatefulWidget { const NotificationsScreen({super.key}); + @override + State createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { + final cubit = context.read(); + final state = cubit.state; + if (state is NotificationsStateLoaded && state.hasNextPage && !state.isLoadingMore) { + cubit.loadMoreNotifications(); + } + } + } + @override Widget build(BuildContext context) { final s = S.of(context); @@ -20,62 +49,65 @@ class NotificationsScreen extends StatelessWidget { builder: (BuildContext context, NotificationsState state) { return Scaffold( backgroundColor: appTheme.gray100, - appBar: AppBar( - title: const Text('Уведомления'), - ), + appBar: AppBar(title: const Text('Уведомления')), body: state is NotificationsStateLoaded - ? Stack(children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container( - padding: const EdgeInsets.only(top: 16), - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + ? Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Container( + padding: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), + ), + child: state.isLoading + ? const Center(child: Loader()) + : CustomScrollView( + controller: _scrollController, + slivers: [ + SliverList.separated( + separatorBuilder: (context, index) => + const Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: CustomDivider()), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: NotificationWidget(notification: state.notifications[index]), + ); + }, + itemCount: state.notifications.length, + ), + if (state.isLoadingMore) + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: Loader()), + ), + ), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: CustomOutlinedButton( + text: s.markAsRead, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () async { + context.read().markNotificationsAsRead(); + }, ), ), - child: state.isLoading - ? const Center(child: Loader()) - : CustomScrollView( - slivers: [ - SliverList.separated( - separatorBuilder: (context, index) => const Padding( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: CustomDivider(), - ), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: NotificationWidget( - notification: state.notifications[index], - ), - ); - }, - itemCount: state.notifications.length, - ) - ], - ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: CustomOutlinedButton( - text: s.markAsRead, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: - ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), - onPressed: () async { - context.read().markNotificationsAsRead(); - }, - ), - ), - ) - ]) + ], + ) : const Placeholder(), ); }, diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 66b6cf18..18c02a13 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -15,46 +15,39 @@ class NotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return switch (notification) { PlanSubscriptionStartedToAuthorNotification() => _PlanSubscriptionStartedToAuthorWidget( - notification: notification as PlanSubscriptionStartedToAuthorNotification, - ), + notification: notification as PlanSubscriptionStartedToAuthorNotification, + ), PlanSubscriptionCanceledToAuthorNotification() => _PlanSubscriptionCanceledToAuthorWidget( - notification: notification as PlanSubscriptionCanceledToAuthorNotification, - ), - ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget( - notification: notification as ChosenPlanStartedToClientNotification, - ), + notification: notification as PlanSubscriptionCanceledToAuthorNotification, + ), + ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(notification: notification as ChosenPlanStartedToClientNotification), ChosenPlanTomorrowReminderToClientNotification() => _ChosenPlanTomorrowReminderToClientWidget( - notification: notification as ChosenPlanTomorrowReminderToClientNotification, - ), - DiaryReminderNotification() => _DiaryReminderWidget( - notification: notification as DiaryReminderNotification, - ), - ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget( - notification: notification as ConsultantLeftCommentNotification, - ), - PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget( - notification: notification as PersonalizedDietIsDefinedNotification, - ), - ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget( - notification: notification as ChosenPlanFinishedToClientNotification, - ), + notification: notification as ChosenPlanTomorrowReminderToClientNotification, + ), + DiaryReminderNotification() => _DiaryReminderWidget(notification: notification as DiaryReminderNotification), + ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(notification: notification as ConsultantLeftCommentNotification), + PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(notification: notification as PersonalizedDietIsDefinedNotification), + ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(notification: notification as ChosenPlanFinishedToClientNotification), PlanSubscriptionCanceledToCustomerNotification() => _PlanSubscriptionCanceledToCustomerWidget( - notification: notification as PlanSubscriptionCanceledToCustomerNotification, - ), + notification: notification as PlanSubscriptionCanceledToCustomerNotification, + ), ChosenPlanStartedToConsultantNotification() => _ChosenPlanStartedToConsultantWidget( - notification: notification as ChosenPlanStartedToConsultantNotification, - ), + notification: notification as ChosenPlanStartedToConsultantNotification, + ), ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget( - notification: notification as ChosenPlanTomorrowReminderToConsultantNotification, - ), - PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget( - notification: notification as PlanSubscriptionPaidToAuthorNotification, - ), + notification: notification as ChosenPlanTomorrowReminderToConsultantNotification, + ), + PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(notification: notification as PlanSubscriptionPaidToAuthorNotification), ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget( - notification: notification as ChosenPlanFinishedToConsultantNotification, - ), - ChosenPlanLastDayFinishedForUser() => Text((notification as ChosenPlanLastDayFinishedForUser).type.toString()), - UnknownNotification() => const SizedBox.shrink(), + notification: notification as ChosenPlanFinishedToConsultantNotification, + ), + ChosenPlanLastDayFinishedForUser() => _ChosenPlanLastDayFinishedForUserWidget(notification: notification as ChosenPlanLastDayFinishedForUser), + ChosenPlanConsultantLastDayStarted() => _ChosenPlanConsultantLastDayStartedWidget(notification: notification as ChosenPlanConsultantLastDayStarted), + ComplaintToModerationForUser() => _ComplaintToModerationForUserWidget(notification: notification as ComplaintToModerationForUser), + CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied), + ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded), + EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented), + UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), }; } } @@ -69,6 +62,7 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -83,6 +77,7 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -94,10 +89,7 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { Widget build(BuildContext context) { final s = S.of(context); - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: s.planStarted(notification.planName), - ); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.planStarted(notification.planName), createdAt: notification.createdAt); } } @@ -111,6 +103,7 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.tomorrowYourPlanWillStart(notification.planName), + createdAt: notification.createdAt, ); } } @@ -121,10 +114,7 @@ class _DiaryReminderWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: s.fillYasterday, - ); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.fillYasterday, createdAt: notification.createdAt); } } @@ -138,6 +128,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')), + createdAt: notification.createdAt, ); } } @@ -147,7 +138,7 @@ class _PersonalizedDietIsDefinedWidget extends StatelessWidget { final PersonalizedDietIsDefinedNotification notification; @override Widget build(BuildContext context) { - return Container(); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Персональная диета определена', createdAt: notification.createdAt); } } @@ -161,6 +152,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', + createdAt: notification.createdAt, ); } } @@ -175,6 +167,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.youCanceledSubscriptionToPlan(notification.planName), + createdAt: notification.createdAt, ); } } @@ -189,6 +182,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.planWithUserStarted(notification.planName, notification.customerName), + createdAt: notification.createdAt, ); } } @@ -203,6 +197,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName), + createdAt: notification.createdAt, ); } } @@ -217,6 +212,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userPaidThePlan(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -227,58 +223,223 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Column(children: [ - Text( - '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), - const SizedBox(height: 12), - Text( - s.userConfirmedPlanCompletionWithoutSum, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), - const SizedBox(height: 12), - CustomOutlinedButton( - text: s.collectCharge, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), - onPressed: () { - GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); - }, - ), - const SizedBox(height: 12), - ]); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${s.congratsYouHaveFinishedPlan} «${notification.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + CustomOutlinedButton( + text: s.collectCharge, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () { + GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); + }, + ), + ], + ), + ), + if (!notification.isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), + ], + ); + } + + String _getMonthName(int month) { + switch (month) { + case 1: + return 'янв'; + case 2: + return 'фев'; + case 3: + return 'мар'; + case 4: + return 'апр'; + case 5: + return 'май'; + case 6: + return 'июн'; + case 7: + return 'июл'; + case 8: + return 'авг'; + case 9: + return 'сен'; + case 10: + return 'окт'; + case 11: + return 'ноя'; + case 12: + return 'дек'; + default: + return ''; + } } } // CardSettingsScreen class _BaseNotificationWidget extends StatelessWidget { - const _BaseNotificationWidget({required this.notificationsText, required this.isRead}); + const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt}); final String notificationsText; final bool isRead; + final DateTime createdAt; @override Widget build(BuildContext context) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.6, - child: Text( - notificationsText, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - if (!isRead) ...[ - const Spacer(), - Container( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, + Row( + children: [ + Expanded( + child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), - width: 8, - height: 8, - ) - ] + if (!isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), ], ); } + + String _getMonthName(int month) { + switch (month) { + case 1: + return 'янв'; + case 2: + return 'фев'; + case 3: + return 'мар'; + case 4: + return 'апр'; + case 5: + return 'май'; + case 6: + return 'июн'; + case 7: + return 'июл'; + case 8: + return 'авг'; + case 9: + return 'сен'; + case 10: + return 'окт'; + case 11: + return 'ноя'; + case 12: + return 'дек'; + default: + return ''; + } + } +} + +class _ChosenPlanLastDayFinishedForUserWidget extends StatelessWidget { + const _ChosenPlanLastDayFinishedForUserWidget({required this.notification}); + final ChosenPlanLastDayFinishedForUser notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Последний день плана завершен', createdAt: notification.createdAt); + } +} + +class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget { + const _ChosenPlanConsultantLastDayStartedWidget({required this.notification}); + final ChosenPlanConsultantLastDayStarted notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Последний день плана начался: ${notification.planName}', + createdAt: notification.createdAt, + ); + } +} + +class _ComplaintToModerationForUserWidget extends StatelessWidget { + const _ComplaintToModerationForUserWidget({required this.notification}); + final ComplaintToModerationForUser notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Жалоба отправлена на модерацию', createdAt: notification.createdAt); + } +} + +class _CommentRepliedWidget extends StatelessWidget { + const _CommentRepliedWidget({required this.notification}); + final CommentReplied notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ответ на комментарий: ${notification.entityName}', + createdAt: notification.createdAt, + ); + } +} + +class _ScoreAddedWidget extends StatelessWidget { + const _ScoreAddedWidget({required this.notification}); + final ScoreAdded notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Добавлены баллы: ${notification.scoreValue}', + createdAt: notification.createdAt, + ); + } +} + +class _EntityCommentedWidget extends StatelessWidget { + const _EntityCommentedWidget({required this.notification}); + final EntityCommented notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Комментарий к ${notification.entityTranslatedType}: ${notification.entityName}', + createdAt: notification.createdAt, + ); + } +} + +class _UnknownNotificationWidget extends StatelessWidget { + const _UnknownNotificationWidget({required this.notification}); + final UnknownNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}', + createdAt: notification.createdAt, + ); + } } diff --git a/lib/features/notifications/repository/notifications_repository.dart b/lib/features/notifications/repository/notifications_repository.dart index beef645f..267faa89 100644 --- a/lib/features/notifications/repository/notifications_repository.dart +++ b/lib/features/notifications/repository/notifications_repository.dart @@ -1,14 +1,15 @@ -import 'package:eatvio_app/features/notifications/models/notification_model.dart'; +import 'dart:developer'; import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; class NotificationsRepository { final NotificationsService notificationsService; NotificationsRepository({required this.notificationsService}); - Future?> getNotifications({required int page}) { + Future?> getNotifications({required int page}) { try { return notificationsService.getNotifications(page: page); } catch (e) { + log(e.toString()); rethrow; } } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index ec2aad74..61f545ab 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -5,53 +5,137 @@ class NotificationsService { final Dio dio; NotificationsService(this.dio); - Future?> getNotifications({required int page}) async { + Future?> getNotifications({required int page}) async { try { final response = await dio.get('/api/profile/notifications?page=$page'); if (response.statusCode == 200) { final data = response.data['data'] as List; + final notifications = data.map((notificationJson) { try { final stringifiedType = notificationJson['data']['notification_class_name']; final readDate = notificationJson['read_at']; + final createdAt = notificationJson['created_at']; final type = NotificationType.getTypeFromString(stringifiedType); + return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => - PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionCanceledToAuthor => - PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanStartedToClientNotification => - ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanTomorrowReminderToClientNotification => - ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.personalizedDietIsDefined => - PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanFinishedToClient => - ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionCanceledToCustomer => - PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanStartedToConsultantNotification => - ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionPaidToAuthor => - PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanFinishedToConsultant => - ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( - notificationJson['data'], - type, - isRead: readDate != null, - ), - NotificationType.unknown => UnknownNotification(type: type), + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + // Новые типы уведомлений + NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), }; } catch (e) { - return UnknownNotification(type: NotificationType.unknown); + return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now()); } }).toList(); - return notifications.where((notification) => notification.type != NotificationType.unknown).toList(); + + // Извлекаем информацию о пагинации из корня ответа + final currentPage = response.data['current_page'] as int; + final lastPage = response.data['last_page'] as int; + final perPage = response.data['per_page'] as int; + final total = response.data['total'] as int; + + return { + 'notifications': notifications, + 'pagination': {'current_page': currentPage, 'last_page': lastPage, 'per_page': perPage, 'total': total}, + }; } return null; } catch (e) { @@ -67,3 +151,5 @@ class NotificationsService { } } } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); diff --git a/lib/main.dart b/lib/main.dart index 8147c18a..6b44348c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await dotenv.load(fileName: "env/.env"); + await dotenv.load(fileName: "env/.env"); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission();