fix: show personal diet name and consultant notifications

This commit is contained in:
2026-11-06 14:22:55 +03:00
parent ad70d0988e
commit 6c4572af55
26 changed files with 323 additions and 34 deletions

View File

@@ -194,6 +194,7 @@ class _ChatScreenState extends State<ChatScreen> {
: mapConsultationProgress(
state: chosenPlan.chosenPlanState,
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
personalDietName: chosenPlan.personalDietName,
);
final bannerType = resolveChatBanner(
progress: progress,

View File

@@ -24,6 +24,7 @@ abstract class ConsultationModel with _$ConsultationModel {
@JsonKey(name: 'consultant_id') required int consultantId,
required List<DiaryModel> diaries,
@JsonKey(name: 'personal_diet_id') required int? personalDietId,
@JsonKey(name: 'personal_diet_name') String? personalDietName,
}) = _ConsultationModel;
factory ConsultationModel.fromJson(Map<String, dynamic> json) => _$ConsultationModelFromJson(json);

View File

@@ -10,12 +10,14 @@ class ConsultationProgress {
required this.showProgressPlank,
required this.dietStatus,
required this.phase,
this.personalDietName,
});
final Set<ConsultationStep> completedSteps;
final bool showProgressPlank;
final PersonalDietStatus dietStatus;
final ConsultationPhase phase;
final String? personalDietName;
bool isStepCompleted(ConsultationStep step) => completedSteps.contains(step);

View File

@@ -97,7 +97,12 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
borderRadius: BorderRadius.circular(100),
),
child: Text(
s.personal_diet_proposed,
widget.consultationsModel.personalDietName == null ||
widget.consultationsModel.personalDietName!.isEmpty
? s.personal_diet_proposed
: s.personal_diet_proposed_named(
widget.consultationsModel.personalDietName!,
),
style: TextStyles.textXSSemibold.copyWith(
color: appTheme.greenA700,
),

View File

@@ -42,8 +42,12 @@ class PersonalDietStatusChip extends StatelessWidget {
return (s.payment_failed, appTheme.lightRed, appTheme.red);
}
if (progress.dietStatus == PersonalDietStatus.proposed) {
final dietName = progress.personalDietName;
final label = dietName == null || dietName.isEmpty
? s.personal_diet_proposed
: s.personal_diet_proposed_named(dietName);
return (
s.personal_diet_proposed,
label,
appTheme.transparentGreen,
appTheme.greenA700,
);

View File

@@ -4,12 +4,14 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
ConsultationProgress mapConsultationProgress({
required ChosenPlanStateEnum state,
required bool hasPersonalDiet,
String? personalDietName,
}) {
return ConsultationProgress(
completedSteps: _completedSteps(state),
showProgressPlank: _showProgressPlank(state),
dietStatus: _dietStatus(state, hasPersonalDiet),
phase: _phase(state),
personalDietName: personalDietName,
);
}

View File

@@ -2174,6 +2174,56 @@ class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationMod
}
}
class PersonalDietIsDefinedToAuthorNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final int chosenPlanId;
final int dietId;
final String planName;
final String userName;
final String consultantName;
final String dietName;
final String role;
final String peerSlug;
final String peerImagePath;
PersonalDietIsDefinedToAuthorNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.chosenPlanId,
required this.dietId,
required this.planName,
required this.userName,
required this.consultantName,
required this.dietName,
required this.role,
required this.peerSlug,
required this.peerImagePath,
});
factory PersonalDietIsDefinedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PersonalDietIsDefinedToAuthorNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
chosenPlanId: json['chosen_plan_id'] ?? 0,
dietId: json['diet_id'] ?? 0,
planName: json['plan_name'] ?? '',
userName: json['user_name'] ?? '',
consultantName: json['consultant_name'] ?? '',
dietName: json['diet_name'] ?? '',
role: json['role']?.toString() ?? '',
peerSlug: json['peer_slug'] ?? '',
peerImagePath: json['peer_image_path'] ?? '',
);
}
}
// New Model Classes for Comprehensive Support
class ConsultantEducationUpdatedNotification implements BaseNotificationModel {
@@ -2480,6 +2530,7 @@ enum NotificationType {
chosenPlanFinishedToConsultant,
stayOnTrackToCustomer,
personalDietIsDefinedToCustomer,
personalDietIsDefinedToAuthor,
// Comments
entityCommented,
@@ -2602,6 +2653,7 @@ enum NotificationType {
'stay-on-track-to-customer' => NotificationType.stayOnTrackToCustomer,
'diary-reminder-for-users' => NotificationType.diaryReminderForUsers,
'personal-diet-is-defined-to-customer' => NotificationType.personalDietIsDefinedToCustomer,
'personal-diet-is-defined-to-author' => NotificationType.personalDietIsDefinedToAuthor,
// New additions
'consultant-education-updated' => NotificationType.consultantEducationUpdated,

View File

@@ -277,6 +277,12 @@ class NotificationsCubit extends Cubit<NotificationsState> {
isRead: isRead,
createdAt: createdAt,
),
NotificationType.personalDietIsDefinedToAuthor => PersonalDietIsDefinedToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
notificationJson,
type,

View File

@@ -122,6 +122,9 @@ class NotificationWidget extends StatelessWidget {
PersonalDietIsDefinedToCustomerNotification() => _PersonalDietIsDefinedToCustomerWidget(
notification: notification as PersonalDietIsDefinedToCustomerNotification,
),
PersonalDietIsDefinedToAuthorNotification() => _PersonalDietIsDefinedToAuthorWidget(
notification: notification as PersonalDietIsDefinedToAuthorNotification,
),
// New Widgets
ConsultantEducationUpdatedNotification() => _ConsultantEducationUpdatedWidget(notification: notification as ConsultantEducationUpdatedNotification),
ComplaintRegisteredNotification() => _ComplaintRegisteredWidget(notification: notification as ComplaintRegisteredNotification),
@@ -1273,6 +1276,27 @@ class _PersonalDietIsDefinedToCustomerWidget extends StatelessWidget {
}
}
class _PersonalDietIsDefinedToAuthorWidget extends StatelessWidget {
const _PersonalDietIsDefinedToAuthorWidget({required this.notification});
final PersonalDietIsDefinedToAuthorNotification notification;
@override
Widget build(BuildContext context) {
final userName = notification.userName.isNotEmpty ? notification.userName : 'Пользователь';
final dietName = notification.dietName.isNotEmpty ? ', выбранная диета: «${notification.dietName}»' : '';
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Вы предложили персональную диету пользователю $userName$dietName',
createdAt: notification.createdAt,
onTap: notification.peerSlug.isEmpty
? null
: () {
context.push(RouteConstants.profileScreen, extra: notification.peerSlug);
},
);
}
}
class _ConsultantEducationUpdatedWidget extends StatelessWidget {
const _ConsultantEducationUpdatedWidget({required this.notification});
final ConsultantEducationUpdatedNotification notification;

View File

@@ -413,6 +413,12 @@ class NotificationsService {
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.personalDietIsDefinedToAuthor => PersonalDietIsDefinedToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
notificationJson['data'],
type,

View File

@@ -15,6 +15,7 @@ abstract class ChosenPlanModel with _$ChosenPlanModel {
@JsonKey(name: 'last_state') required ChosenPlanStateEnum chosenPlanState,
@JsonKey(name: 'chat_id') required String chatId,
@JsonKey(name: 'consultant_id') required int consultantId,
@JsonKey(name: 'personal_diet_name') String? personalDietName,
@JsonKey(name: 'plan') required PlanModel plan,
}) = _ChosenPlanModel;

View File

@@ -28,36 +28,38 @@ class MessageLookup extends MessageLookupByLibrary {
static String m2(dietDuration, dietDayWord, planDuration, planDayWord) =>
"Not suitable: diet lasts ${dietDuration} ${dietDayWord}, plan lasts ${planDuration} ${planDayWord}.";
static String m3(planName) => "План «${planName}» начался";
static String m3(name) => "Diet «${name}» proposed";
static String m4(planName, name) =>
static String m4(planName) => "План «${planName}» начался";
static String m5(planName, name) =>
"План «${planName}» с пользователем ${name} начался";
static String m5(duration, dayWord) => "Plan for ${duration} ${dayWord}";
static String m6(duration, dayWord) => "Plan for ${duration} ${dayWord}";
static String m6(percent, amount) =>
static String m7(percent, amount) =>
"Discount ${percent}% — you save ${amount} ₽";
static String m7(date) => "Start: ${date}";
static String m8(date) => "Start: ${date}";
static String m8(step) => "шаг ${step} из 2";
static String m9(step) => "шаг ${step} из 2";
static String m9(planName, name) =>
static String m10(planName, name) =>
"Завтра начинается план «${planName}» с пользователем ${name}";
static String m10(planName) =>
static String m11(planName) =>
"Завтра начнется прохождение плана «${planName}»";
static String m11(name, planName) =>
static String m12(name, planName) =>
"Пользователь ${name} отменил подписку на план «${planName}»";
static String m12(name, planName) =>
static String m13(name, planName) =>
"Пользователь ${name} оплатил план «${planName}»";
static String m13(name, planName) =>
static String m14(name, planName) =>
"Пользователь ${name} подписался на план «${planName}»";
static String m14(planName) => "Вы отменили подписку на план «${planName}»";
static String m15(planName) => "Вы отменили подписку на план «${planName}»";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
@@ -696,6 +698,7 @@ class MessageLookup extends MessageLookupByLibrary {
"personal_diet_proposed": MessageLookupByLibrary.simpleMessage(
"Personal diet proposed",
),
"personal_diet_proposed_named": m3,
"personal_diet_replaces_tomorrow": MessageLookupByLibrary.simpleMessage(
"The change takes effect tomorrow",
),
@@ -715,18 +718,18 @@ class MessageLookup extends MessageLookupByLibrary {
"Физическая форма",
),
"place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"),
"planStarted": m3,
"planStarted": m4,
"planWillBeConfirmedAfterThreeDays": MessageLookupByLibrary.simpleMessage(
"Если вы не подтвердите, то план через три дня подтвердится автоматически",
),
"planWithUserStarted": m4,
"planWithUserStarted": m5,
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
"plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"),
"plan_desc_example": MessageLookupByLibrary.simpleMessage(
"Большое подробное, привлекательное описание плана",
),
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_duration_label": m5,
"plan_duration_label": m6,
"plan_label_desc": MessageLookupByLibrary.simpleMessage(
"План для поддержания формы",
),
@@ -763,7 +766,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Promo code applied!",
),
"promo_code": MessageLookupByLibrary.simpleMessage("Promo code"),
"promo_discount_hint": m6,
"promo_discount_hint": m7,
"promo_invalid": MessageLookupByLibrary.simpleMessage(
"Promo code not found or expired",
),
@@ -884,13 +887,13 @@ class MessageLookup extends MessageLookupByLibrary {
"Выбрана дата старта",
),
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
"start_date_label": m7,
"start_date_label": m8,
"statistic": MessageLookupByLibrary.simpleMessage("Статистика"),
"step": MessageLookupByLibrary.simpleMessage("Шаг"),
"step_by_step_photo_recipe": MessageLookupByLibrary.simpleMessage(
"Пошаговый фоторецепт",
),
"step_widget": m8,
"step_widget": m9,
"successfully_published": MessageLookupByLibrary.simpleMessage(
"успешно опубликован",
),
@@ -915,8 +918,8 @@ class MessageLookup extends MessageLookupByLibrary {
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
"to_order": MessageLookupByLibrary.simpleMessage("Заказать"),
"tommorowPlanWithUserWillStart": m9,
"tomorrowYourPlanWillStart": m10,
"tommorowPlanWithUserWillStart": m10,
"tomorrowYourPlanWillStart": m11,
"try_again": MessageLookupByLibrary.simpleMessage("Попробовать снова"),
"try_again_2": MessageLookupByLibrary.simpleMessage("Попробуйте ещё раз"),
"typeAmoutOfReturn": MessageLookupByLibrary.simpleMessage(
@@ -950,7 +953,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Верхние показатели давления:",
),
"upper": MessageLookupByLibrary.simpleMessage("Верхнее"),
"userCanceldThePlanSubscription": m11,
"userCanceldThePlanSubscription": m12,
"userConfirmedPlanCompletion": MessageLookupByLibrary.simpleMessage(
"Пользователь подтвердил прохождения плана. В ближайшее время Вы получите",
),
@@ -958,8 +961,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Пользователь подтвердил прохождения плана.",
),
"userPaidThePlan": m12,
"userSubscribedToPlan": m13,
"userPaidThePlan": m13,
"userSubscribedToPlan": m14,
"user_authorization": MessageLookupByLibrary.simpleMessage(
"Аутентификация",
),
@@ -1089,7 +1092,7 @@ class MessageLookup extends MessageLookupByLibrary {
"youCanComplain": MessageLookupByLibrary.simpleMessage(
"Если вас что-то не устроило, вы можете подать жалобу",
),
"youCanceledSubscriptionToPlan": m14,
"youCanceledSubscriptionToPlan": m15,
"youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите отменить заказ?",
),

View File

@@ -26,12 +26,14 @@ class MessageLookup extends MessageLookupByLibrary {
static String m2(dietDuration, dietDayWord, planDuration, planDayWord) =>
"Не подходит: диета на ${dietDuration} ${dietDayWord}, план на ${planDuration} ${planDayWord}.";
static String m5(duration, dayWord) => "План на ${duration} ${dayWord}";
static String m3(name) => "Диета «${name}» предложена";
static String m6(percent, amount) =>
static String m6(duration, dayWord) => "План на ${duration} ${dayWord}";
static String m7(percent, amount) =>
"Скидка ${percent}% — вы экономите ${amount} ₽";
static String m7(date) => "Старт: ${date}";
static String m8(date) => "Старт: ${date}";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
@@ -340,6 +342,7 @@ class MessageLookup extends MessageLookupByLibrary {
"personal_diet_proposed": MessageLookupByLibrary.simpleMessage(
"Диета предложена",
),
"personal_diet_proposed_named": m3,
"personal_diet_replaces_tomorrow": MessageLookupByLibrary.simpleMessage(
"Замена действует со следующего дня",
),
@@ -355,7 +358,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Большое подробное, привлекательное описание плана",
),
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_duration_label": m5,
"plan_duration_label": m6,
"plan_label_desc": MessageLookupByLibrary.simpleMessage(
"План для поддержания формы",
),
@@ -383,7 +386,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Промокод применён!",
),
"promo_code": MessageLookupByLibrary.simpleMessage("Промокод"),
"promo_discount_hint": m6,
"promo_discount_hint": m7,
"promo_invalid": MessageLookupByLibrary.simpleMessage(
"Промокод не найден или истёк",
),
@@ -444,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Выбрана дата старта",
),
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
"start_date_label": m7,
"start_date_label": m8,
"suggestDiet": MessageLookupByLibrary.simpleMessage("Предложить диету"),
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",

View File

@@ -4574,6 +4574,16 @@ class S {
);
}
/// `Diet «{name}» proposed`
String personal_diet_proposed_named(String name) {
return Intl.message(
'Diet «$name» proposed',
name: 'personal_diet_proposed_named',
desc: '',
args: [name],
);
}
/// `Personal diet not proposed`
String get personal_diet_not_proposed {
return Intl.message(

View File

@@ -540,6 +540,12 @@
"video_call_notification_end": "End call",
"order_in_progress": "Order in progress",
"personal_diet_proposed": "Personal diet proposed",
"personal_diet_proposed_named": "Diet «{name}» proposed",
"@personal_diet_proposed_named": {
"placeholders": {
"name": {"type": "String"}
}
},
"personal_diet_not_proposed": "Personal diet not proposed",
"change_personal_diet": "Change personal diet",
"personal_diet_replaces_tomorrow": "The change takes effect tomorrow",

View File

@@ -295,6 +295,12 @@
"choose_plan_start_date_with_personal_diet": "Выберите дату старта плана питания с учетом составления персональной диеты",
"order_in_progress": "Заказ в работе",
"personal_diet_proposed": "Диета предложена",
"personal_diet_proposed_named": "Диета «{name}» предложена",
"@personal_diet_proposed_named": {
"placeholders": {
"name": {"type": "String"}
}
},
"personal_diet_not_proposed": "Диета не предложена",
"change_personal_diet": "Заменить диету",
"personal_diet_replaces_tomorrow": "Замена действует со следующего дня",

View File

@@ -55,6 +55,7 @@ void main() {
final progress = mapConsultationProgress(
state: ChosenPlanStateEnum.personalDietSet,
hasPersonalDiet: true,
personalDietName: 'Индивидуальный план питания',
);
expect(progress.showProgressPlank, isTrue);
expect(progress.dietStatus, PersonalDietStatus.proposed);
@@ -73,7 +74,10 @@ void main() {
expect(find.text(s.order_in_progress), findsNWidgets(2));
expect(find.byType(PersonalDietStatusChip), findsOneWidget);
expect(find.text(s.personal_diet_proposed), findsOneWidget);
expect(
find.text(s.personal_diet_proposed_named('Индивидуальный план питания')),
findsOneWidget,
);
expect(find.byType(ConsultationProgressPlank), findsOneWidget);
expect(find.byIcon(Icons.check), findsNWidgets(2));
},

View File

@@ -174,11 +174,13 @@ void main() {
'consultant_id': 1,
'diaries': [],
'personal_diet_id': null,
'personal_diet_name': 'Индивидуальный план питания',
};
final model = ConsultationModel.fromJson(json);
expect(model.chatId, 'direct-chat-uuid');
expect(model.personalDietName, 'Индивидуальный план питания');
});
test('fromJson allows missing chat_id', () {

View File

@@ -153,6 +153,29 @@ void main() {
});
});
testWidgets('shows personal diet name in status chip', (tester) async {
final consultation = mockConsultation.copyWith(
personalDietId: 1,
personalDietName: 'Индивидуальный план питания',
);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(
wrap(ConsultationCardWidget(consultationsModel: consultation)),
);
await tester.pumpAndSettle();
final s = S.of(tester.element(find.byType(ConsultationCardWidget)));
expect(
find.text(
s.personal_diet_proposed_named('Индивидуальный план питания'),
),
findsOneWidget,
);
});
});
testWidgets('renders diary days', (tester) async {
await mockNetworkImagesFor(() async {
await tester.pumpWidget(

View File

@@ -21,11 +21,16 @@ Widget _wrap(Widget child) {
);
}
Widget _chip(ChosenPlanStateEnum state, {bool hasPersonalDiet = true}) {
Widget _chip(
ChosenPlanStateEnum state, {
bool hasPersonalDiet = true,
String? personalDietName,
}) {
return PersonalDietStatusChip(
progress: mapConsultationProgress(
state: state,
hasPersonalDiet: hasPersonalDiet,
personalDietName: personalDietName,
),
);
}
@@ -55,6 +60,24 @@ void main() {
expect(find.text(s.personal_diet_proposed), findsOneWidget);
});
testWidgets('proposed с названием показывает имя диеты', (tester) async {
await tester.pumpWidget(
_wrap(
_chip(
ChosenPlanStateEnum.stateStarted,
personalDietName: 'Индивидуальный план питания',
),
),
);
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
expect(
find.text(s.personal_diet_proposed_named('Индивидуальный план питания')),
findsOneWidget,
);
});
testWidgets('notProposed показывает personal_diet_not_proposed', (
tester,
) async {

View File

@@ -275,5 +275,15 @@ void main() {
);
}
});
test('personalDietName передаётся в ConsultationProgress', () {
final progress = mapConsultationProgress(
state: ChosenPlanStateEnum.personalDietSet,
hasPersonalDiet: true,
personalDietName: 'Индивидуальный план питания',
);
expect(progress.personalDietName, 'Индивидуальный план питания');
});
});
}

View File

@@ -92,6 +92,7 @@ void main() {
DiaryReminderForUsersNotification.fromJson(base, NotificationType.diaryReminderForUsers);
DiaryReminderForUsersNotification.fromJson({'created_at': c, 'data': <String, dynamic>{}}, NotificationType.diaryReminderForUsers);
PersonalDietIsDefinedToCustomerNotification.fromJson(base, NotificationType.personalDietIsDefinedToCustomer);
PersonalDietIsDefinedToAuthorNotification.fromJson(base, NotificationType.personalDietIsDefinedToAuthor);
ConsultantEducationUpdatedNotification.fromJson(base, NotificationType.consultantEducationUpdated);
ComplaintRegisteredNotification.fromJson(base, NotificationType.complaintRegistered);
ContentUnpublishedNotification.fromJson(base, NotificationType.contentUnpublished);

View File

@@ -50,6 +50,7 @@ void main() {
'stay-on-track-to-customer': NotificationType.stayOnTrackToCustomer,
'diary-reminder-for-users': NotificationType.diaryReminderForUsers,
'personal-diet-is-defined-to-customer': NotificationType.personalDietIsDefinedToCustomer,
'personal-diet-is-defined-to-author': NotificationType.personalDietIsDefinedToAuthor,
};
cases.forEach((key, expected) {
@@ -671,6 +672,34 @@ void main() {
expect(notification.type, NotificationType.personalizedDietIsDefined);
});
test('PersonalDietIsDefinedToAuthorNotification.fromJson parses all fields', () {
final json = {
'chosen_plan_id': 12,
'diet_id': 34,
'plan_name': 'Plan',
'user_name': 'Client',
'consultant_name': 'Consultant',
'diet_name': 'Personal diet',
'role': 'consultant',
'peer_slug': 'client-slug',
'peer_image_path': 'avatars/client.png',
'created_at': '2024-01-30T12:00:00Z',
};
final notification = PersonalDietIsDefinedToAuthorNotification.fromJson(
json,
NotificationType.personalDietIsDefinedToAuthor,
isRead: false,
);
expect(notification.chosenPlanId, 12);
expect(notification.dietId, 34);
expect(notification.userName, 'Client');
expect(notification.dietName, 'Personal diet');
expect(notification.peerSlug, 'client-slug');
expect(notification.type, NotificationType.personalDietIsDefinedToAuthor);
});
test('ChosenPlanFinishedToClientNotification.fromJson parses all fields', () {
final json = {
'plan_name': 'Finished Plan',

View File

@@ -1212,6 +1212,31 @@ void main() {
expect(find.byType(NotificationWidget), findsOneWidget);
});
testWidgets('displays PersonalDietIsDefinedToAuthorNotification', (
tester,
) async {
final notification = PersonalDietIsDefinedToAuthorNotification(
type: NotificationType.personalDietIsDefinedToAuthor,
isRead: false,
createdAt: testDate,
chosenPlanId: 1,
dietId: 1,
planName: 'Plan',
userName: 'Client',
consultantName: 'Consultant',
dietName: 'Test Diet',
role: 'consultant',
peerSlug: 'client-slug',
peerImagePath: '',
);
await tester.pumpWidget(createTestWidget(notification));
await tester.pumpAndSettle();
expect(find.textContaining('Вы предложили персональную диету пользователю Client'), findsOneWidget);
expect(find.textContaining('Test Diet'), findsOneWidget);
});
testWidgets('displays UnknownNotification', (tester) async {
final notification = UnknownNotification(
type: NotificationType.unknown,

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
@@ -1454,6 +1455,42 @@ void main() {
expect(result, isNotNull);
expect(result!['notifications'], isA<List>());
expect(result['notifications'].length, 1);
expect(result['notifications'].first, isA<PersonalDietIsDefinedToCustomerNotification>());
});
test('getNotifications - обрабатывает PersonalDietIsDefinedToAuthor', () async {
const page = 1;
final response = Response(
data: {
'data': [
{
'type': 'personal-diet-is-defined-to-author',
'data': {
'diet_name': 'Test Diet',
'diet_id': 1,
'user_name': 'Client',
},
'read_at': null,
'created_at': '2024-01-01T12:00:00Z',
},
],
'current_page': 1,
'last_page': 1,
'per_page': 10,
'total': 1,
},
statusCode: 200,
requestOptions: RequestOptions(path: '/api/profile/notifications'),
);
when(mockDio.get(any)).thenAnswer((_) async => response);
final result = await notificationsService.getNotifications(page: page);
expect(result, isNotNull);
expect(result!['notifications'], isA<List>());
expect(result['notifications'].length, 1);
expect(result['notifications'].first, isA<PersonalDietIsDefinedToAuthorNotification>());
});
test('getNotifications - обрабатывает UserDailyReportCompleted', () async {

View File

@@ -60,6 +60,7 @@ void main() {
'last_state': 'initied',
'chat_id': 'chat-1',
'consultant_id': 1,
'personal_diet_name': 'Индивидуальный план питания',
'plan': {
'id': 1,
'slug': 'test-plan',
@@ -94,6 +95,7 @@ void main() {
expect(chosenPlan.optionType, 'options1');
expect(chosenPlan.chosenPlanState, ChosenPlanStateEnum.stateInited);
expect(chosenPlan.chatId, 'chat-1');
expect(chosenPlan.personalDietName, 'Индивидуальный план питания');
});
test('toJson serializes ChosenPlanModel correctly', () {
@@ -207,6 +209,7 @@ void main() {
'last_state': 'initied',
'chat_id': 'chat-1',
'consultant_id': 1,
'personal_diet_name': 'Индивидуальный план питания',
'plan': {
'id': 1,
'slug': 'test-plan',