forked from eatvio/eatvio_app
Merge pull request 'Плашка статуса и диета в чате' (#1132) from alex_chevski/eatvio_app:feature/1115-consultation-status-plank into main
Reviewed-on: eatvio/eatvio_app#1132 Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru> Reviewed-by: misha <misha@noreply.localhost>
This commit is contained in:
@@ -192,7 +192,18 @@ class OtherRoutes {
|
||||
return CreatePlanScreen(plan: plan);
|
||||
},
|
||||
),
|
||||
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
|
||||
GoRoute(
|
||||
path: MyDietsScreen.path,
|
||||
name: MyDietsScreen.name,
|
||||
builder: (context, state) {
|
||||
final initialTabIndex = int.tryParse(
|
||||
state.uri.queryParameters['initialTabIndex'] ?? '',
|
||||
);
|
||||
return MyDietsScreen(
|
||||
initialTabIndex: initialTabIndex ?? 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
|
||||
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class WsManager {
|
||||
|
||||
Stream<ChannelReadEvent>? _directChatStream;
|
||||
Stream<ChannelReadEvent>? _clientTypingStream;
|
||||
Stream<ChannelReadEvent>? _chosenPlanStateChangedStream;
|
||||
|
||||
Stream<ChannelReadEvent>? get newMessagesStream =>
|
||||
_privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
||||
@@ -74,6 +75,9 @@ class WsManager {
|
||||
|
||||
Stream<ChannelReadEvent>? get clientTypingStream => _clientTypingStream;
|
||||
|
||||
Stream<ChannelReadEvent>? get chosenPlanStateChangedStream =>
|
||||
_chosenPlanStateChangedStream;
|
||||
|
||||
/// Сигнал о реконнекте сокета: эмитится после повторной подписки на каналы.
|
||||
///
|
||||
/// Подписчики используют его, чтобы догрузить пропущенные за время обрыва
|
||||
@@ -222,6 +226,9 @@ class WsManager {
|
||||
'App\\Events\\ChatMessageUpdated',
|
||||
);
|
||||
_clientTypingStream = _privateDirectChatChannel?.bind('client-typing');
|
||||
_chosenPlanStateChangedStream = _privateDirectChatChannel?.bind(
|
||||
'chosen-plan-state-changed',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initPresenceDirectChatChannel(String chatId) async {
|
||||
@@ -367,6 +374,7 @@ class WsManager {
|
||||
_privateDirectChatChannel?.unsubscribe();
|
||||
_directChatStream = null;
|
||||
_clientTypingStream = null;
|
||||
_chosenPlanStateChangedStream = null;
|
||||
}
|
||||
|
||||
void unsubscribePresenceDirectChatChannel() {
|
||||
@@ -401,6 +409,7 @@ class WsManager {
|
||||
_privateNotificationChannel = null;
|
||||
_directChatStream = null;
|
||||
_clientTypingStream = null;
|
||||
_chosenPlanStateChangedStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
|
||||
StreamSubscription<ChannelReadEvent>? typingSubscription;
|
||||
StreamSubscription<ChannelReadEvent>? messagesSubscription;
|
||||
StreamSubscription<ChannelReadEvent>? _chosenPlanStateSubscription;
|
||||
StreamSubscription<void>? _reconnectSubscription;
|
||||
bool _isUpdatingChosenPlan = false;
|
||||
int currentPage = 1;
|
||||
late int lastPage;
|
||||
bool isLoading = false;
|
||||
@@ -171,6 +173,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
log(e.toString());
|
||||
typingSubscription?.cancel();
|
||||
messagesSubscription?.cancel();
|
||||
_chosenPlanStateSubscription?.cancel();
|
||||
_reconnectSubscription?.cancel();
|
||||
_reconnectSubscription = null;
|
||||
wsManager.unsubscribeDirectChatChannel();
|
||||
@@ -260,19 +263,30 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
|
||||
Future<void> updateChosenPlanState() async {
|
||||
if (_isUpdatingChosenPlan) return;
|
||||
try {
|
||||
_isUpdatingChosenPlan = true;
|
||||
final currentState = state;
|
||||
if (currentState is! ChatRoomStateLoaded) {
|
||||
return;
|
||||
}
|
||||
final chosenPlanId = currentState.chatRoom.chosenPlanId;
|
||||
final chosenPlanId =
|
||||
currentState.chatRoom.chosenPlanId ??
|
||||
currentState.chatRoom.chosenPlan?.id;
|
||||
if (chosenPlanId == null) {
|
||||
return;
|
||||
}
|
||||
final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId);
|
||||
_emitLoaded((latest) => latest.copyWith.chatRoom(chosenPlan: chosenPlan));
|
||||
if (isClosed) return;
|
||||
_emitLoaded(
|
||||
(latest) => latest.copyWith(
|
||||
chatRoom: latest.chatRoom.copyWith(chosenPlan: chosenPlan),
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
} finally {
|
||||
_isUpdatingChosenPlan = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,8 +352,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
if (client == null) return;
|
||||
await messagesSubscription?.cancel();
|
||||
await typingSubscription?.cancel();
|
||||
await _chosenPlanStateSubscription?.cancel();
|
||||
messagesSubscription = null;
|
||||
typingSubscription = null;
|
||||
_chosenPlanStateSubscription = null;
|
||||
await wsManager.initPrivateDirectChatChannel(chatId);
|
||||
wsManager.subscribeDirectChatChannel();
|
||||
messagesSubscription = wsManager.directChatStream?.listen(
|
||||
@@ -361,6 +377,20 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
),
|
||||
);
|
||||
});
|
||||
_chosenPlanStateSubscription = wsManager
|
||||
.chosenPlanStateChangedStream
|
||||
?.listen(
|
||||
(_) {
|
||||
EasyDebounce.debounce(
|
||||
'chosen-plan-state-changed',
|
||||
const Duration(milliseconds: 250),
|
||||
() => unawaited(updateChosenPlanState()),
|
||||
);
|
||||
},
|
||||
onError: (Object e, StackTrace st) {
|
||||
unawaited(reportFrom(e, st, this));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onDirectChatEvent(ChannelReadEvent event) {
|
||||
@@ -731,6 +761,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
Future<void> close() async {
|
||||
EasyDebounce.cancel('client-typing');
|
||||
EasyDebounce.cancel('searchChatRoom');
|
||||
EasyDebounce.cancel('chosen-plan-state-changed');
|
||||
if (_presenceOnlineListener != null) {
|
||||
wsManager.removePresenceDirectChatListener(_presenceOnlineListener!);
|
||||
_presenceOnlineListener = null;
|
||||
@@ -739,6 +770,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
_timer?.cancel();
|
||||
await messagesSubscription?.cancel();
|
||||
await typingSubscription?.cancel();
|
||||
await _chosenPlanStateSubscription?.cancel();
|
||||
await _reconnectSubscription?.cancel();
|
||||
super.close();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_user_model.dart';
|
||||
@@ -24,9 +23,11 @@ import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_user_online_status_line.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/utils/chat_banner_resolver.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_consultation_header.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
import 'package:eatvio_app/features/chat/utils/chat_counterpart_utils.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
|
||||
@@ -63,8 +64,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
final ChatRoomCubit chatRoomCubit = sl<ChatRoomCubit>();
|
||||
final List<int> unreadMessageIds = [];
|
||||
final showableChatBadgeStates =
|
||||
ChosenPlanStateEnum.getShowableChatBadgeStates();
|
||||
late AppLifecycleListener _listener;
|
||||
List<File> pickedFiles = [];
|
||||
|
||||
@@ -188,49 +187,24 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
ChosenPlanStateEnum.statePendingPayment,
|
||||
ChosenPlanStateEnum.stateInCoordination,
|
||||
].contains(chosenPlan?.chosenPlanState);
|
||||
PacketOptions? chosenOption;
|
||||
if (chosenPlan?.optionType == 'options1') {
|
||||
chosenOption = chosenPlan?.plan.packet1Options;
|
||||
} else if (chosenPlan?.optionType == 'options2') {
|
||||
chosenOption = chosenPlan?.plan.packet2Options;
|
||||
} else if (chosenPlan?.optionType == 'options3') {
|
||||
chosenOption = chosenPlan?.plan.packet3Options;
|
||||
}
|
||||
|
||||
final showAboutOrderButton =
|
||||
state.chatRoom.chosenPlan != null &&
|
||||
state.chatRoom.chosenPlan!.chosenPlanState !=
|
||||
final chosenOption = chosenPlan?.chosenOption;
|
||||
final progress = chosenPlan == null
|
||||
? null
|
||||
: mapConsultationProgress(
|
||||
state: chosenPlan.chosenPlanState,
|
||||
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
|
||||
personalDietName: chosenPlan.personalDietName,
|
||||
);
|
||||
final bannerType = resolveChatBanner(
|
||||
progress: progress,
|
||||
amIConsultant: amIConsultant,
|
||||
hasCard: state.hasCard,
|
||||
state: chosenPlan?.chosenPlanState,
|
||||
);
|
||||
final showInfoButton =
|
||||
chosenPlan != null &&
|
||||
chosenPlan.chosenPlanState !=
|
||||
ChosenPlanStateEnum.stateCanceledByTheInitiator;
|
||||
BannerType? bannerType;
|
||||
if (chosenPlan == null ||
|
||||
chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
bannerType = null;
|
||||
} else if (amIConsultant &&
|
||||
!state.hasCard &&
|
||||
chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.card;
|
||||
} else if (amIConsultant &&
|
||||
chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.date;
|
||||
} else if (!amIConsultant &&
|
||||
(chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.stateStartDateHasBeenSet ||
|
||||
chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.statePendingPayment)) {
|
||||
bannerType = BannerType.payment;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone &&
|
||||
amIConsultant) {
|
||||
bannerType = BannerType.collectCharge;
|
||||
} else if (chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.stateLastDayFinished &&
|
||||
!amIConsultant) {
|
||||
bannerType = BannerType.finishPlan;
|
||||
} else if (chosenOption?.hasPersonalDiet == true &&
|
||||
amIConsultant &&
|
||||
chosenPlan.chosenPlanState != ChosenPlanStateEnum.stateDone) {
|
||||
bannerType = BannerType.setPersonalDiet;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
@@ -340,6 +314,28 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (progress != null)
|
||||
ChatConsultationHeader(
|
||||
progress: progress,
|
||||
showInfoButton: showInfoButton,
|
||||
onInfoTap: () {
|
||||
if (amIConsultant) {
|
||||
GoRouter.of(context).push(
|
||||
InfoByOrderScreen.path,
|
||||
extra: {
|
||||
'planId': chosenPlan?.id,
|
||||
'customerSlug':
|
||||
state.chatRoom.users.receiver.slug,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(
|
||||
InfoByOrderScreen.path,
|
||||
extra: {'planId': chosenPlan?.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: appTheme.whiteA700,
|
||||
@@ -384,22 +380,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final isPlanOrderMessage =
|
||||
message.type == ChatMessageType.planOrder;
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage &&
|
||||
chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options2') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options3') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
final messageOption = isPlanOrderMessage
|
||||
? chosenPlan?.chosenOption
|
||||
: null;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -450,21 +433,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final isPlanOrderMessage =
|
||||
message.type == ChatMessageType.planOrder;
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage && chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options2') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options3') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
final messageOption = isPlanOrderMessage
|
||||
? chosenPlan?.chosenOption
|
||||
: null;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -804,30 +775,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showAboutOrderButton)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 15,
|
||||
left: 16,
|
||||
child: AboutOrderButton(
|
||||
onTap: () async {
|
||||
if (amIConsultant) {
|
||||
GoRouter.of(context).push(
|
||||
InfoByOrderScreen.path,
|
||||
extra: {
|
||||
'planId': state.chatRoom.chosenPlan?.id,
|
||||
'customerSlug': state.chatRoom.users.receiver.slug,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(
|
||||
InfoByOrderScreen.path,
|
||||
extra: {'planId': state.chatRoom.chosenPlan?.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
|
||||
BannerType? resolveChatBanner({
|
||||
required ConsultationProgress? progress,
|
||||
required bool amIConsultant,
|
||||
required bool hasCard,
|
||||
required ChosenPlanStateEnum? state,
|
||||
}) {
|
||||
if (progress == null ||
|
||||
state == null ||
|
||||
progress.phase != ConsultationPhase.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (amIConsultant &&
|
||||
!hasCard &&
|
||||
state == ChosenPlanStateEnum.stateInCoordination) {
|
||||
return BannerType.card;
|
||||
}
|
||||
if (amIConsultant && state == ChosenPlanStateEnum.stateInCoordination) {
|
||||
return BannerType.date;
|
||||
}
|
||||
if (!amIConsultant &&
|
||||
(state == ChosenPlanStateEnum.stateStartDateHasBeenSet ||
|
||||
state == ChosenPlanStateEnum.statePendingPayment)) {
|
||||
return BannerType.payment;
|
||||
}
|
||||
if (state == ChosenPlanStateEnum.stateDone && amIConsultant) {
|
||||
return BannerType.collectCharge;
|
||||
}
|
||||
if (state == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
|
||||
return BannerType.finishPlan;
|
||||
}
|
||||
if (!amIConsultant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state == ChosenPlanStateEnum.statePaid &&
|
||||
progress.dietStatus == PersonalDietStatus.notProposed) {
|
||||
return BannerType.setPersonalDiet;
|
||||
}
|
||||
if ((state == ChosenPlanStateEnum.personalDietSet ||
|
||||
state == ChosenPlanStateEnum.stateStarted ||
|
||||
state == ChosenPlanStateEnum.stateInProgress) &&
|
||||
progress.dietStatus == PersonalDietStatus.proposed) {
|
||||
return BannerType.changePersonalDiet;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_progress_plank.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/personal_diet_status_chip.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatConsultationHeader extends StatelessWidget {
|
||||
const ChatConsultationHeader({
|
||||
super.key,
|
||||
required this.progress,
|
||||
required this.showInfoButton,
|
||||
required this.onInfoTap,
|
||||
});
|
||||
|
||||
final ConsultationProgress progress;
|
||||
final bool showInfoButton;
|
||||
final VoidCallback onInfoTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showPlank = progress.showProgressPlank;
|
||||
final showChip = progress.hasChip;
|
||||
if (!showPlank && !showChip && !showInfoButton) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final s = S.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.commentBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (progress.phase == ConsultationPhase.active || showPlank)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_title(s),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showInfoButton)
|
||||
_InfoButton(
|
||||
onTap: onInfoTap,
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (showInfoButton)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _InfoButton(
|
||||
onTap: onInfoTap,
|
||||
),
|
||||
),
|
||||
if (showPlank) ...[
|
||||
const SizedBox(height: 8),
|
||||
ConsultationProgressPlank(progress: progress),
|
||||
],
|
||||
if (showChip) ...[
|
||||
const SizedBox(height: 8),
|
||||
PersonalDietStatusChip(progress: progress),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _title(S s) {
|
||||
if (progress.isCanceled) return s.orderCanceled;
|
||||
if (progress.isPaymentFailed) return s.payment_failed;
|
||||
if (progress.isStepCompleted(ConsultationStep.finished)) return s.done;
|
||||
if (progress.showProgressPlank) return s.order_in_progress;
|
||||
return s.more_about_order;
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoButton extends StatelessWidget {
|
||||
const _InfoButton({required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 18,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -146,12 +146,37 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
||||
BannerType.setPersonalDiet => [
|
||||
SizedBox(
|
||||
width: size.width * 0.6,
|
||||
child: Text(s.createPersonalDiet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
child: Text(s.suggestDiet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(MyDietsScreen.name);
|
||||
GoRouter.of(context).pushNamed(
|
||||
MyDietsScreen.name,
|
||||
queryParameters: {'initialTabIndex': '2'},
|
||||
);
|
||||
},
|
||||
child: Text(s.goToMyDiets, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
BannerType.changePersonalDiet => [
|
||||
SizedBox(
|
||||
width: size.width * 0.6,
|
||||
child: Text(s.change_personal_diet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: size.width * 0.6,
|
||||
child: Text(s.personal_diet_replaces_tomorrow, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
MyDietsScreen.name,
|
||||
queryParameters: {'initialTabIndex': '2'},
|
||||
);
|
||||
},
|
||||
child: Text(s.goToMyDiets, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||
),
|
||||
@@ -244,4 +269,4 @@ class _StarIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet }
|
||||
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet, changePersonalDiet }
|
||||
|
||||
@@ -20,10 +20,11 @@ abstract class ConsultationModel with _$ConsultationModel {
|
||||
@JsonKey(name: 'user_profile_photo_url') required String userProfilePhotoUrl,
|
||||
@JsonKey(name: 'consultation_option_price') required int consultationOptionPrice,
|
||||
@JsonKey(name: 'start_date', fromJson: parseDate) required DateTime startDate,
|
||||
@JsonKey(name: 'end_date', fromJson: parseDate) required DateTime endDate,
|
||||
@JsonKey(name: 'end_date', fromJson: parseDateOrNull) DateTime? endDate,
|
||||
@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);
|
||||
@@ -40,6 +41,11 @@ abstract class DiaryModel with _$DiaryModel {
|
||||
factory DiaryModel.fromJson(Map<String, dynamic> json) => _$DiaryModelFromJson(json);
|
||||
}
|
||||
|
||||
DateTime? parseDateOrNull(dynamic date) {
|
||||
if (date == null) return null;
|
||||
return parseDate(date as String);
|
||||
}
|
||||
|
||||
DateTime parseUnformattedDate(String date) {
|
||||
final dateChunks = date.split('-').map(int.parse).toList();
|
||||
|
||||
|
||||
33
lib/features/consultations/models/consultation_progress.dart
Normal file
33
lib/features/consultations/models/consultation_progress.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
enum ConsultationStep { startDate, paid, inProgress, finished }
|
||||
|
||||
enum PersonalDietStatus { notRequired, notProposed, proposed }
|
||||
|
||||
enum ConsultationPhase { active, canceled, paymentFailed }
|
||||
|
||||
class ConsultationProgress {
|
||||
const ConsultationProgress({
|
||||
required this.completedSteps,
|
||||
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);
|
||||
|
||||
bool get hasDietChip => dietStatus != PersonalDietStatus.notRequired;
|
||||
|
||||
bool get hasStatusChip => phase != ConsultationPhase.active;
|
||||
|
||||
bool get hasChip => hasDietChip || hasStatusChip;
|
||||
|
||||
bool get isCanceled => phase == ConsultationPhase.canceled;
|
||||
|
||||
bool get isPaymentFailed => phase == ConsultationPhase.paymentFailed;
|
||||
}
|
||||
@@ -85,6 +85,30 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
widget.consultationsModel.planTitle,
|
||||
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (widget.consultationsModel.personalDietId != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.transparentGreen,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: Text(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConsultationProgressPlank extends StatelessWidget {
|
||||
const ConsultationProgressPlank({super.key, required this.progress});
|
||||
|
||||
final ConsultationProgress progress;
|
||||
|
||||
static const _steps = [
|
||||
ConsultationStep.startDate,
|
||||
ConsultationStep.paid,
|
||||
ConsultationStep.inProgress,
|
||||
ConsultationStep.finished,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!progress.showProgressPlank) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var index = 0; index < _steps.length; index++)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
color: index == 0
|
||||
? Colors.transparent
|
||||
: progress.isStepCompleted(_steps[index - 1])
|
||||
? appTheme.greenA700
|
||||
: appTheme.lightGray00,
|
||||
),
|
||||
),
|
||||
_ProgressStepCircle(
|
||||
key: ValueKey('consultation_step_circle_$index'),
|
||||
index: index,
|
||||
isCompleted: progress.isStepCompleted(_steps[index]),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
color: index == _steps.length - 1
|
||||
? Colors.transparent
|
||||
: progress.isStepCompleted(_steps[index])
|
||||
? appTheme.greenA700
|
||||
: appTheme.lightGray00,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
_stepLabel(S.of(context), _steps[index]),
|
||||
key: ValueKey('consultation_step_label_$index'),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyles.textFS10FW500.copyWith(
|
||||
color: progress.isStepCompleted(_steps[index])
|
||||
? appTheme.black
|
||||
: appTheme.gray,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _stepLabel(S s, ConsultationStep step) {
|
||||
switch (step) {
|
||||
case ConsultationStep.startDate:
|
||||
return s.startDateHasBeenSet;
|
||||
case ConsultationStep.paid:
|
||||
return s.order_paid;
|
||||
case ConsultationStep.inProgress:
|
||||
return s.order_in_progress;
|
||||
case ConsultationStep.finished:
|
||||
return s.done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressStepCircle extends StatelessWidget {
|
||||
const _ProgressStepCircle({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.isCompleted,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final bool isCompleted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isCompleted ? appTheme.greenA700 : appTheme.lightGray00,
|
||||
),
|
||||
child: isCompleted
|
||||
? Icon(Icons.check, size: 14, color: appTheme.whiteA700)
|
||||
: Text(
|
||||
'${index + 1}',
|
||||
style: TextStyles.textFS10FW500.copyWith(color: appTheme.gray),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PersonalDietStatusChip extends StatelessWidget {
|
||||
const PersonalDietStatusChip({super.key, required this.progress});
|
||||
|
||||
final ConsultationProgress progress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!progress.hasChip) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final s = S.of(context);
|
||||
final (label, background, foreground) = _chipStyle(s);
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyles.textXSSemibold.copyWith(color: foreground),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(String, Color, Color) _chipStyle(S s) {
|
||||
if (progress.isCanceled) {
|
||||
return (s.orderCanceled, appTheme.lightRed, appTheme.red);
|
||||
}
|
||||
if (progress.isPaymentFailed) {
|
||||
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 (
|
||||
label,
|
||||
appTheme.transparentGreen,
|
||||
appTheme.greenA700,
|
||||
);
|
||||
}
|
||||
return (
|
||||
s.personal_diet_not_proposed,
|
||||
appTheme.gray100,
|
||||
appTheme.gray,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
Set<ConsultationStep> _completedSteps(ChosenPlanStateEnum state) {
|
||||
switch (state) {
|
||||
case ChosenPlanStateEnum.stateInited:
|
||||
case ChosenPlanStateEnum.stateConfirmedByConsultant:
|
||||
case ChosenPlanStateEnum.stateInCoordination:
|
||||
case ChosenPlanStateEnum.stateCanceledByTheInitiator:
|
||||
return const {};
|
||||
case ChosenPlanStateEnum.stateStartDateHasBeenSet:
|
||||
case ChosenPlanStateEnum.statePendingPayment:
|
||||
case ChosenPlanStateEnum.statePaymentFailed:
|
||||
return const {ConsultationStep.startDate};
|
||||
case ChosenPlanStateEnum.statePaid:
|
||||
case ChosenPlanStateEnum.personalDietSet:
|
||||
return const {ConsultationStep.startDate, ConsultationStep.paid};
|
||||
case ChosenPlanStateEnum.stateStarted:
|
||||
case ChosenPlanStateEnum.stateInProgress:
|
||||
case ChosenPlanStateEnum.stateLastDayStarted:
|
||||
case ChosenPlanStateEnum.stateLastDayFinished:
|
||||
case ChosenPlanStateEnum.stateComplained:
|
||||
return const {
|
||||
ConsultationStep.startDate,
|
||||
ConsultationStep.paid,
|
||||
ConsultationStep.inProgress,
|
||||
};
|
||||
case ChosenPlanStateEnum.stateDone:
|
||||
case ChosenPlanStateEnum.stateChargeWasCollected:
|
||||
return const {
|
||||
ConsultationStep.startDate,
|
||||
ConsultationStep.paid,
|
||||
ConsultationStep.inProgress,
|
||||
ConsultationStep.finished,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
bool _showProgressPlank(ChosenPlanStateEnum state) {
|
||||
switch (state) {
|
||||
case ChosenPlanStateEnum.stateInited:
|
||||
case ChosenPlanStateEnum.stateConfirmedByConsultant:
|
||||
case ChosenPlanStateEnum.stateChargeWasCollected:
|
||||
case ChosenPlanStateEnum.stateCanceledByTheInitiator:
|
||||
case ChosenPlanStateEnum.statePaymentFailed:
|
||||
return false;
|
||||
case ChosenPlanStateEnum.stateInCoordination:
|
||||
case ChosenPlanStateEnum.stateStartDateHasBeenSet:
|
||||
case ChosenPlanStateEnum.statePendingPayment:
|
||||
case ChosenPlanStateEnum.statePaid:
|
||||
case ChosenPlanStateEnum.personalDietSet:
|
||||
case ChosenPlanStateEnum.stateStarted:
|
||||
case ChosenPlanStateEnum.stateInProgress:
|
||||
case ChosenPlanStateEnum.stateLastDayStarted:
|
||||
case ChosenPlanStateEnum.stateLastDayFinished:
|
||||
case ChosenPlanStateEnum.stateComplained:
|
||||
case ChosenPlanStateEnum.stateDone:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
PersonalDietStatus _dietStatus(
|
||||
ChosenPlanStateEnum state,
|
||||
bool hasPersonalDiet,
|
||||
) {
|
||||
if (!hasPersonalDiet) {
|
||||
return PersonalDietStatus.notRequired;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case ChosenPlanStateEnum.personalDietSet:
|
||||
case ChosenPlanStateEnum.stateStarted:
|
||||
case ChosenPlanStateEnum.stateInProgress:
|
||||
case ChosenPlanStateEnum.stateLastDayStarted:
|
||||
case ChosenPlanStateEnum.stateLastDayFinished:
|
||||
case ChosenPlanStateEnum.stateComplained:
|
||||
case ChosenPlanStateEnum.stateDone:
|
||||
case ChosenPlanStateEnum.stateChargeWasCollected:
|
||||
return PersonalDietStatus.proposed;
|
||||
case ChosenPlanStateEnum.stateInited:
|
||||
case ChosenPlanStateEnum.stateInCoordination:
|
||||
case ChosenPlanStateEnum.stateConfirmedByConsultant:
|
||||
case ChosenPlanStateEnum.stateStartDateHasBeenSet:
|
||||
case ChosenPlanStateEnum.statePendingPayment:
|
||||
case ChosenPlanStateEnum.statePaid:
|
||||
return PersonalDietStatus.notProposed;
|
||||
case ChosenPlanStateEnum.stateCanceledByTheInitiator:
|
||||
case ChosenPlanStateEnum.statePaymentFailed:
|
||||
return PersonalDietStatus.notRequired;
|
||||
}
|
||||
}
|
||||
|
||||
ConsultationPhase _phase(ChosenPlanStateEnum state) {
|
||||
switch (state) {
|
||||
case ChosenPlanStateEnum.stateCanceledByTheInitiator:
|
||||
return ConsultationPhase.canceled;
|
||||
case ChosenPlanStateEnum.statePaymentFailed:
|
||||
return ConsultationPhase.paymentFailed;
|
||||
default:
|
||||
return ConsultationPhase.active;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -19,9 +18,7 @@ class ChosenDietWidget extends StatelessWidget {
|
||||
if (state is DiaryStateLoaded) {
|
||||
if (state.diary.typeOfDiary.hasChosenDiet) {
|
||||
return ChosenDietWidgetContent(
|
||||
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
|
||||
dietName: state.diary.diet?.name ?? '',
|
||||
dietDuration: state.diary.diet?.duration ?? 0,
|
||||
dietId: state.diary.diet?.id ?? 0,
|
||||
);
|
||||
}
|
||||
@@ -33,9 +30,7 @@ class ChosenDietWidget extends StatelessWidget {
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 16),
|
||||
ChosenDietWidgetContent(
|
||||
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
|
||||
dietName: state.diary.diet?.name ?? '',
|
||||
dietDuration: state.diary.diet?.duration ?? 0,
|
||||
dietId: state.diary.diet?.id ?? 0,
|
||||
),
|
||||
],
|
||||
@@ -50,15 +45,11 @@ class ChosenDietWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class ChosenDietWidgetContent extends StatelessWidget {
|
||||
const ChosenDietWidgetContent({super.key, required this.currentDietDay, required this.dietName, required this.dietDuration, required this.dietId});
|
||||
final int currentDietDay;
|
||||
const ChosenDietWidgetContent({super.key, required this.dietName, required this.dietId});
|
||||
final String dietName;
|
||||
final int dietDuration;
|
||||
final int dietId;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final s = S.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': dietId, 'navigateToComments': false});
|
||||
@@ -67,19 +58,12 @@ class ChosenDietWidgetContent extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size.width * 0.4,
|
||||
Expanded(
|
||||
child: Text(
|
||||
dietName,
|
||||
style: TextStyles.textLMedium.copyWith(color: appTheme.black, decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -136,6 +136,11 @@ class MyDietsCubit extends Cubit<MyDietsState> {
|
||||
}
|
||||
}
|
||||
|
||||
void selectInitialTab(int index) {
|
||||
if (index < 0 || index > 2) return;
|
||||
emit(state.copyWith(selectedStateIndex: index));
|
||||
}
|
||||
|
||||
void changeSelectedStateIndex(int index) {
|
||||
emit(state.copyWith(selectedStateIndex: index));
|
||||
if (state.activeDiets.isEmpty) {
|
||||
|
||||
@@ -19,12 +19,19 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MyDietsScreen extends StatelessWidget {
|
||||
const MyDietsScreen({super.key});
|
||||
const MyDietsScreen({super.key, this.initialTabIndex = 0});
|
||||
static const name = 'myDietsScreen';
|
||||
static const path = '/my-diets-screen';
|
||||
final int initialTabIndex;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(create: (context) => sl<MyDietsCubit>()..loadAllDiets(), child: MyDietsScreenContent());
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
sl<MyDietsCubit>()
|
||||
..selectInitialTab(initialTabIndex)
|
||||
..loadAllDiets(),
|
||||
child: MyDietsScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,30 @@ class _PersonalDietIsDefinedToCustomerWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonalDietIsDefinedToAuthorWidget extends StatelessWidget {
|
||||
const _PersonalDietIsDefinedToAuthorWidget({required this.notification});
|
||||
final PersonalDietIsDefinedToAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
final userName = notification.userName.isNotEmpty ? notification.userName : s.default_user_name;
|
||||
final notificationsText = notification.dietName.isEmpty
|
||||
? s.personal_diet_defined_to_author(userName)
|
||||
: s.personal_diet_defined_to_author_named(userName, notification.dietName);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: notificationsText,
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cancel_plan_bottom_
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/widgets/collect_charge_banner.dart';
|
||||
@@ -106,14 +105,7 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
||||
),
|
||||
);
|
||||
}
|
||||
PacketOptions? chosenOption;
|
||||
if (plan.optionType == 'options1') {
|
||||
chosenOption = plan.plan.packet1Options;
|
||||
} else if (plan.optionType == 'options2') {
|
||||
chosenOption = plan.plan.packet2Options;
|
||||
} else {
|
||||
chosenOption = plan.plan.packet3Options;
|
||||
}
|
||||
final chosenOption = plan.chosenOption;
|
||||
|
||||
PlanStatusValues planStatus;
|
||||
if (widget.successfulPayment == true) {
|
||||
@@ -164,7 +156,7 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
||||
const SizedBox(height: 8),
|
||||
PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false),
|
||||
const SizedBox(height: 20),
|
||||
if (widget.successfulPayment == null)
|
||||
if (widget.successfulPayment == null && plan.chosenPlanState.canShowCancelAction)
|
||||
CustomOutlinedButton(
|
||||
text: s.cancelConsultation,
|
||||
height: 52,
|
||||
@@ -173,7 +165,7 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
||||
onPressed: () async {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isStateAfterPaidState(), planId: plan.id),
|
||||
builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isCancelAfterPaidState, planId: plan.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -9,38 +9,54 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
||||
class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
|
||||
final DietRepository dietRepository;
|
||||
final PersonalDietSuggestionRepository personalDietSuggestionService;
|
||||
late final int dietId;
|
||||
late int dietId;
|
||||
PersonalDietSuggestionCubit({required this.dietRepository, required this.personalDietSuggestionService}) : super(PersonalDietSuggestionState());
|
||||
|
||||
Future<void> setDietName(int dietId) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
}
|
||||
final diet = await dietRepository.getDiet(dietId);
|
||||
emit(state.copyWith(isLoading: false, dietName: diet.name));
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
dietName: diet.name,
|
||||
dietDuration: diet.duration,
|
||||
));
|
||||
}
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getConsultationWithPersonalDiets(int dietId) async {
|
||||
try {
|
||||
this.dietId = dietId;
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
setDietName(dietId);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
}
|
||||
await setDietName(dietId);
|
||||
final consultations = await personalDietSuggestionService.getConsultationWithPersonalDiets();
|
||||
final consultationWithDiet = consultations.where((consultation) => consultation.personalDietId != null).toList();
|
||||
final consultationWithoutDiet = consultations.where((consultation) => consultation.personalDietId == null).toList();
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
consultationsWithDiet: consultationWithDiet,
|
||||
consultationsWithoutDiet: consultationWithoutDiet,
|
||||
));
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
consultationsWithDiet: consultationWithDiet,
|
||||
consultationsWithoutDiet: consultationWithoutDiet,
|
||||
));
|
||||
}
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +65,7 @@ class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
|
||||
}) async {
|
||||
try {
|
||||
await personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
await getConsultationWithPersonalDiets(dietId);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
@@ -62,6 +79,7 @@ class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
|
||||
}) async {
|
||||
try {
|
||||
await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
await getConsultationWithPersonalDiets(dietId);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
|
||||
@@ -9,8 +9,8 @@ abstract class PersonalDietSuggestionState with _$PersonalDietSuggestionState {
|
||||
@Default([]) List<ConsultationModel> consultationsWithDiet,
|
||||
@Default([]) List<ConsultationModel> consultationsWithoutDiet,
|
||||
@Default('') String dietName,
|
||||
@Default(0) int dietDuration,
|
||||
@Default(false) bool isLoading,
|
||||
@Default(null) String? error,
|
||||
}) = _PersonalDietSuggestionState;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
@@ -11,6 +13,7 @@ import 'package:eatvio_app/features/consultations/models/consultation_model.dart
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -19,9 +22,15 @@ class ConsultationWidget extends StatelessWidget {
|
||||
super.key,
|
||||
required this.consultationModel,
|
||||
required this.dietName,
|
||||
this.dietDuration = 0,
|
||||
});
|
||||
final ConsultationModel consultationModel;
|
||||
final String dietName;
|
||||
final int dietDuration;
|
||||
|
||||
bool get _isDurationMismatch =>
|
||||
dietDuration > 0 && consultationModel.duration != dietDuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -64,7 +73,20 @@ class ConsultationWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Старт: ${consultationModel.userName}',
|
||||
s.plan_duration_label(
|
||||
consultationModel.duration,
|
||||
getDayForm(consultationModel.duration, s),
|
||||
),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: const Color(0xFF88949F),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.start_date_label(
|
||||
DateFormat('dd.MM.yyyy').format(
|
||||
consultationModel.startDate,
|
||||
),
|
||||
),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: const Color(0xFF88949F),
|
||||
),
|
||||
@@ -103,15 +125,47 @@ class ConsultationWidget extends StatelessWidget {
|
||||
color: const Color(0xFF88949F),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.plan_duration_label(
|
||||
consultationModel.duration,
|
||||
getDayForm(consultationModel.duration, s),
|
||||
),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: const Color(0xFF88949F),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_isDurationMismatch)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 18, color: appTheme.red),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
s.personal_diet_duration_mismatch(
|
||||
dietDuration,
|
||||
getDayForm(dietDuration, s),
|
||||
consultationModel.duration,
|
||||
getDayForm(consultationModel.duration, s),
|
||||
),
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (consultationModel.personalDietId != null)
|
||||
CustomOutlinedButton(
|
||||
text: s.changeDiet,
|
||||
height: 52,
|
||||
isDisabled: _isDurationMismatch,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(
|
||||
color: appTheme.gray,
|
||||
),
|
||||
@@ -128,35 +182,8 @@ class ConsultationWidget extends StatelessWidget {
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
onTap: () async {
|
||||
final result = await context
|
||||
.read<PersonalDietSuggestionCubit>()
|
||||
.attachDietToConsultation(
|
||||
consultationId: consultationModel.id,
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionSuccessBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionFailureBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
await _attachDiet(context, reattach: true);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -166,12 +193,17 @@ class ConsultationWidget extends StatelessWidget {
|
||||
CustomOutlinedButton(
|
||||
text: s.suggestDiet,
|
||||
height: 52,
|
||||
isDisabled: _isDurationMismatch,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
color: _isDurationMismatch ? appTheme.gray : appTheme.whiteA700,
|
||||
),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
_isDurationMismatch
|
||||
? appTheme.dividerColor
|
||||
: appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showBottomSheetWithBlur(
|
||||
@@ -183,34 +215,7 @@ class ConsultationWidget extends StatelessWidget {
|
||||
dietName: dietName,
|
||||
onTap: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
final result = await context
|
||||
.read<PersonalDietSuggestionCubit>()
|
||||
.attachDietToConsultation(
|
||||
consultationId: consultationModel.id,
|
||||
);
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionSuccessBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionFailureBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
await _attachDiet(context, reattach: false);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -220,6 +225,41 @@ class ConsultationWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _attachDiet(
|
||||
BuildContext context, {
|
||||
required bool reattach,
|
||||
}) async {
|
||||
final router = GoRouter.of(context);
|
||||
final chatId = consultationModel.chatId;
|
||||
final noChatMessage = S.of(context).msg_something_went_wrong;
|
||||
final cubit = context.read<PersonalDietSuggestionCubit>();
|
||||
final result = reattach
|
||||
? await cubit.reattachDietToConsultation(
|
||||
consultationId: consultationModel.id,
|
||||
)
|
||||
: await cubit.attachDietToConsultation(
|
||||
consultationId: consultationModel.id,
|
||||
);
|
||||
if (!result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionFailureBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (chatId == null || chatId.isEmpty) {
|
||||
showErrorToast(noChatMessage);
|
||||
return;
|
||||
}
|
||||
router.goNamed('chatScreen', queryParameters: {'uuid': chatId});
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestDietBottomSheet extends StatefulWidget {
|
||||
@@ -278,65 +318,6 @@ class _SuggestDietBottomSheet extends State<SuggestDietBottomSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestionSuccessBottomSheet extends StatefulWidget {
|
||||
const SuggestionSuccessBottomSheet({
|
||||
super.key,
|
||||
required this.userName,
|
||||
required this.dietName,
|
||||
});
|
||||
final String userName;
|
||||
final String dietName;
|
||||
|
||||
@override
|
||||
State<SuggestionSuccessBottomSheet> createState() =>
|
||||
_SuggestionSuccessBottomSheet();
|
||||
}
|
||||
|
||||
class _SuggestionSuccessBottomSheet
|
||||
extends State<SuggestionSuccessBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Предложение диеты',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'«${widget.dietName}» предложена пользователю ${widget.userName}',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomOutlinedButton(
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestionFailureBottomSheet extends StatefulWidget {
|
||||
const SuggestionFailureBottomSheet({
|
||||
super.key,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
|
||||
@@ -17,7 +18,9 @@ class PersonalDietSuggestionScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<PersonalDietSuggestionCubit>()..getConsultationWithPersonalDiets(dietId),
|
||||
create: (context) =>
|
||||
sl<PersonalDietSuggestionCubit>()
|
||||
..getConsultationWithPersonalDiets(dietId),
|
||||
child: const PersonalDietSuggestionScreenContent(),
|
||||
);
|
||||
}
|
||||
@@ -28,7 +31,10 @@ class PersonalDietSuggestionScreenContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<PersonalDietSuggestionCubit, PersonalDietSuggestionState>(
|
||||
return BlocBuilder<
|
||||
PersonalDietSuggestionCubit,
|
||||
PersonalDietSuggestionState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
@@ -36,45 +42,97 @@ class PersonalDietSuggestionScreenContent extends StatelessWidget {
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'${s.dietSuggestion}\n«${state.dietName}»',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(child: Loader())
|
||||
: (state.consultationsWithDiet.isEmpty && state.consultationsWithoutDiet.isEmpty)
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text(
|
||||
'Нет активных консультаций с персональной диетой',
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...List.generate(state.consultationsWithDiet.length, (index) {
|
||||
return ConsultationWidget(
|
||||
consultationModel: state.consultationsWithDiet[index],
|
||||
dietName: state.dietName,
|
||||
);
|
||||
}),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (state.dietDuration > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: _PersonalDietDurationHint(
|
||||
duration: state.dietDuration,
|
||||
),
|
||||
...List.generate(state.consultationsWithoutDiet.length, (index) {
|
||||
),
|
||||
if (state.consultationsWithDiet.isEmpty &&
|
||||
state.consultationsWithoutDiet.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
s.no_active_personal_diet_consultations,
|
||||
style: TextStyles.textMSemibold.copyWith(
|
||||
color: appTheme.gray,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
...List.generate(state.consultationsWithDiet.length, (
|
||||
index,
|
||||
) {
|
||||
return ConsultationWidget(
|
||||
consultationModel:
|
||||
state.consultationsWithDiet[index],
|
||||
dietName: state.dietName,
|
||||
dietDuration: state.dietDuration,
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 12),
|
||||
...List.generate(
|
||||
state.consultationsWithoutDiet.length,
|
||||
(index) {
|
||||
return ConsultationWidget(
|
||||
consultationModel: state.consultationsWithoutDiet[index],
|
||||
consultationModel:
|
||||
state.consultationsWithoutDiet[index],
|
||||
dietName: state.dietName,
|
||||
dietDuration: state.dietDuration,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonalDietDurationHint extends StatelessWidget {
|
||||
const _PersonalDietDurationHint({required this.duration});
|
||||
|
||||
final int duration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: appTheme.dividerColor),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 20, color: appTheme.blueA400),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
s.personal_diet_duration_hint(duration, getDayForm(duration, s)),
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.gray),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,23 @@ class PersonalDietSuggestionRepository {
|
||||
|
||||
}
|
||||
|
||||
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) async {
|
||||
return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
|
||||
Future<void> attachPersonalDiet({
|
||||
required int consultationId,
|
||||
required int dietId,
|
||||
}) async {
|
||||
await personalDietSuggestionService.attachPersonalDiet(
|
||||
consultationId,
|
||||
{'diet_id': dietId},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) async {
|
||||
return await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
|
||||
Future<void> reattachPersonalDiet({
|
||||
required int consultationId,
|
||||
required int dietId,
|
||||
}) async {
|
||||
await personalDietSuggestionService.reattachPersonalDiet(
|
||||
consultationId,
|
||||
{'diet_id': dietId},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
class PersonalDietSuggestionService {
|
||||
final Dio dio;
|
||||
PersonalDietSuggestionService(this.dio);
|
||||
part 'personal_diet_suggestion_service.g.dart';
|
||||
|
||||
Future<ConsultationPaginationModel> getConsultationWithPersonalDiets() async {
|
||||
try {
|
||||
final response = await dio.get('/api/consultations', queryParameters: {
|
||||
'filters[plan][has_personal_diet][\$personal_diet]': '1',
|
||||
'filters[last_state][\$notIn][]': ['done', 'charge_was_collected', 'complained', 'last_day_finished', 'canceled_by_the_initiator', 'inited'],
|
||||
'sort[]': 'start_date:desc',
|
||||
});
|
||||
return ConsultationPaginationModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@RestApi()
|
||||
abstract class PersonalDietSuggestionService {
|
||||
factory PersonalDietSuggestionService(Dio dio, {String? baseUrl}) =
|
||||
_PersonalDietSuggestionService;
|
||||
|
||||
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) async {
|
||||
try {
|
||||
await dio.post('/api/chosen_plans/$consultationId/attach_diet', data: {'diet_id': dietId});
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@GET('/api/consultations')
|
||||
Future<ConsultationPaginationModel> getConsultationWithPersonalDiets({
|
||||
@Query('filters[plan][has_personal_diet][\$personal_diet]')
|
||||
String hasPersonalDiet = '1',
|
||||
@Query('filters[last_state][\$notIn][]')
|
||||
List<String> notIn = const [
|
||||
'done',
|
||||
'charge_was_collected',
|
||||
'complained',
|
||||
'last_day_finished',
|
||||
'last_day_started',
|
||||
'canceled_by_the_initiator',
|
||||
'inited',
|
||||
],
|
||||
@Query('sort[]') List<String> sort = const ['start_date:desc'],
|
||||
});
|
||||
|
||||
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) async {
|
||||
try {
|
||||
await dio.post('/api/chosen_plans/$consultationId/re_attach_diet', data: {'diet_id': dietId});
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@POST('/api/chosen_plans/{id}/attach_diet')
|
||||
Future<void> attachPersonalDiet(
|
||||
@Path('id') int consultationId,
|
||||
@Body() Map<String, dynamic> body,
|
||||
);
|
||||
|
||||
@POST('/api/chosen_plans/{id}/re_attach_diet')
|
||||
Future<void> reattachPersonalDiet(
|
||||
@Path('id') int consultationId,
|
||||
@Body() Map<String, dynamic> body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,32 @@ enum ChosenPlanStateEnum {
|
||||
}
|
||||
}
|
||||
|
||||
bool get canShowCancelAction {
|
||||
switch (this) {
|
||||
case ChosenPlanStateEnum.stateInited:
|
||||
case ChosenPlanStateEnum.stateInCoordination:
|
||||
case ChosenPlanStateEnum.stateStartDateHasBeenSet:
|
||||
case ChosenPlanStateEnum.statePendingPayment:
|
||||
case ChosenPlanStateEnum.statePaid:
|
||||
case ChosenPlanStateEnum.stateStarted:
|
||||
case ChosenPlanStateEnum.stateInProgress:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isCancelAfterPaidState {
|
||||
switch (this) {
|
||||
case ChosenPlanStateEnum.statePaid:
|
||||
case ChosenPlanStateEnum.stateStarted:
|
||||
case ChosenPlanStateEnum.stateInProgress:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static List<ChosenPlanStateEnum> getActiveStates() {
|
||||
return [
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
|
||||
29
lib/features/plans/enums/plan_option_type_enum.dart
Normal file
29
lib/features/plans/enums/plan_option_type_enum.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
enum PlanOptionType {
|
||||
@JsonValue('options1')
|
||||
options1('options1'),
|
||||
@JsonValue('options2')
|
||||
options2('options2'),
|
||||
@JsonValue('options3')
|
||||
options3('options3');
|
||||
|
||||
const PlanOptionType(this.apiValue);
|
||||
|
||||
final String apiValue;
|
||||
|
||||
static PlanOptionType? tryParse(String? raw) {
|
||||
for (final option in values) {
|
||||
if (option.apiValue == raw) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static PlanOptionType fromIndex(int index) => switch (index) {
|
||||
1 => PlanOptionType.options2,
|
||||
2 => PlanOptionType.options3,
|
||||
_ => PlanOptionType.options1,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/plan_option_type_enum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chosen_plan_model.freezed.dart';
|
||||
@@ -7,6 +8,7 @@ part 'chosen_plan_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ChosenPlanModel with _$ChosenPlanModel {
|
||||
const ChosenPlanModel._();
|
||||
|
||||
factory ChosenPlanModel({
|
||||
@JsonKey() required int id,
|
||||
@@ -15,10 +17,24 @@ 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;
|
||||
|
||||
factory ChosenPlanModel.fromJson(Map<String, dynamic> json) => _$ChosenPlanModelFromJson(json);
|
||||
|
||||
PacketOptions? get chosenOption {
|
||||
switch (PlanOptionType.tryParse(optionType)) {
|
||||
case PlanOptionType.options1:
|
||||
return plan.packet1Options;
|
||||
case PlanOptionType.options2:
|
||||
return plan.packet2Options;
|
||||
case PlanOptionType.options3:
|
||||
return plan.packet3Options;
|
||||
case null:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChosenPlanModel? parseChosenPlan(dynamic json) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/plan_option_type_enum.dart';
|
||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||
@@ -133,7 +134,7 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
final slug = state.planModel?.slug;
|
||||
final optionIndex = state.chosenOptionIndex;
|
||||
if (slug != null) {
|
||||
final optionName = _getOptionNameFromIndex(optionIndex);
|
||||
final optionName = PlanOptionType.fromIndex(optionIndex).apiValue;
|
||||
final body = {'options_type': optionName};
|
||||
return (await planRepository.chosePlan(slug, body), null);
|
||||
}
|
||||
@@ -181,10 +182,4 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String _getOptionNameFromIndex(int index) => switch (index) {
|
||||
1 => 'options2',
|
||||
2 => 'options3',
|
||||
_ => 'options1',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,32 +22,50 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
|
||||
static String m0(date) => "Консультант прокомментировал день ${date}";
|
||||
|
||||
static String m1(planName) => "План «${planName}» начался";
|
||||
static String m1(userName) =>
|
||||
"You have proposed a personal diet to ${userName}";
|
||||
|
||||
static String m2(planName, name) =>
|
||||
static String m2(userName, dietName) =>
|
||||
"You have proposed a personal diet to ${userName}, selected diet: «${dietName}»";
|
||||
|
||||
static String m3(duration, dayWord) =>
|
||||
"The diet lasts ${duration} ${dayWord}. It can only be offered for a plan with the same duration.";
|
||||
|
||||
static String m4(dietDuration, dietDayWord, planDuration, planDayWord) =>
|
||||
"Not suitable: diet lasts ${dietDuration} ${dietDayWord}, plan lasts ${planDuration} ${planDayWord}.";
|
||||
|
||||
static String m5(name) => "Diet «${name}» proposed";
|
||||
|
||||
static String m6(planName) => "План «${planName}» начался";
|
||||
|
||||
static String m7(planName, name) =>
|
||||
"План «${planName}» с пользователем ${name} начался";
|
||||
|
||||
static String m3(percent, amount) =>
|
||||
static String m8(duration, dayWord) => "Plan for ${duration} ${dayWord}";
|
||||
|
||||
static String m9(percent, amount) =>
|
||||
"Discount ${percent}% — you save ${amount} ₽";
|
||||
|
||||
static String m4(step) => "шаг ${step} из 2";
|
||||
static String m10(date) => "Start: ${date}";
|
||||
|
||||
static String m5(planName, name) =>
|
||||
static String m11(step) => "шаг ${step} из 2";
|
||||
|
||||
static String m12(planName, name) =>
|
||||
"Завтра начинается план «${planName}» с пользователем ${name}";
|
||||
|
||||
static String m6(planName) =>
|
||||
static String m13(planName) =>
|
||||
"Завтра начнется прохождение плана «${planName}»";
|
||||
|
||||
static String m7(name, planName) =>
|
||||
static String m14(name, planName) =>
|
||||
"Пользователь ${name} отменил подписку на план «${planName}»";
|
||||
|
||||
static String m8(name, planName) =>
|
||||
static String m15(name, planName) =>
|
||||
"Пользователь ${name} оплатил план «${planName}»";
|
||||
|
||||
static String m9(name, planName) =>
|
||||
static String m16(name, planName) =>
|
||||
"Пользователь ${name} подписался на план «${planName}»";
|
||||
|
||||
static String m10(planName) => "Вы отменили подписку на план «${planName}»";
|
||||
static String m17(planName) => "Вы отменили подписку на план «${planName}»";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
@@ -170,6 +188,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
|
||||
"changeDiet": MessageLookupByLibrary.simpleMessage("Заменить диету"),
|
||||
"change_personal_diet": MessageLookupByLibrary.simpleMessage(
|
||||
"Change personal diet",
|
||||
),
|
||||
"chargeApproved": MessageLookupByLibrary.simpleMessage("Выплата одобрена"),
|
||||
"chargeCollectionFailed": MessageLookupByLibrary.simpleMessage(
|
||||
"Не удалось оформить выплату",
|
||||
@@ -317,6 +338,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"days": MessageLookupByLibrary.simpleMessage("дней"),
|
||||
"deception": MessageLookupByLibrary.simpleMessage("Обман"),
|
||||
"default_user_name": MessageLookupByLibrary.simpleMessage("User"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("Удалить"),
|
||||
"deleteComment": MessageLookupByLibrary.simpleMessage(
|
||||
"Удалить комментарий",
|
||||
@@ -604,6 +626,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"nicotine": MessageLookupByLibrary.simpleMessage("Никотин"),
|
||||
"no": MessageLookupByLibrary.simpleMessage("Нет"),
|
||||
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
|
||||
"no_active_personal_diet_consultations":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"No active consultations with a personal diet",
|
||||
),
|
||||
"no_default_cards": MessageLookupByLibrary.simpleMessage(
|
||||
"У вас нет привязанных карт. Что бы начать работу, привяжите карту",
|
||||
),
|
||||
@@ -635,6 +661,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
|
||||
"orderCanceled": MessageLookupByLibrary.simpleMessage("Заказ отменен"),
|
||||
"order_created": MessageLookupByLibrary.simpleMessage("Заказ создан"),
|
||||
"order_in_progress": MessageLookupByLibrary.simpleMessage(
|
||||
"Order in progress",
|
||||
),
|
||||
"order_paid": MessageLookupByLibrary.simpleMessage("Заказ оплачен"),
|
||||
"order_price": MessageLookupByLibrary.simpleMessage("Сумма заказа"),
|
||||
"order_status": MessageLookupByLibrary.simpleMessage("Статус заказа"),
|
||||
@@ -668,6 +697,20 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"personalDietCreating": MessageLookupByLibrary.simpleMessage(
|
||||
"Составление индивидуальной диеты",
|
||||
),
|
||||
"personal_diet_defined_to_author": m1,
|
||||
"personal_diet_defined_to_author_named": m2,
|
||||
"personal_diet_duration_hint": m3,
|
||||
"personal_diet_duration_mismatch": m4,
|
||||
"personal_diet_not_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Personal diet not proposed",
|
||||
),
|
||||
"personal_diet_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Personal diet proposed",
|
||||
),
|
||||
"personal_diet_proposed_named": m5,
|
||||
"personal_diet_replaces_tomorrow": MessageLookupByLibrary.simpleMessage(
|
||||
"The change takes effect tomorrow",
|
||||
),
|
||||
"personal_diet_set": MessageLookupByLibrary.simpleMessage(
|
||||
"Персональная диета установлена",
|
||||
),
|
||||
@@ -684,17 +727,18 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Физическая форма",
|
||||
),
|
||||
"place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"),
|
||||
"planStarted": m1,
|
||||
"planStarted": m6,
|
||||
"planWillBeConfirmedAfterThreeDays": MessageLookupByLibrary.simpleMessage(
|
||||
"Если вы не подтвердите, то план через три дня подтвердится автоматически",
|
||||
),
|
||||
"planWithUserStarted": m2,
|
||||
"planWithUserStarted": m7,
|
||||
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
|
||||
"plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"),
|
||||
"plan_desc_example": MessageLookupByLibrary.simpleMessage(
|
||||
"Большое подробное, привлекательное описание плана",
|
||||
),
|
||||
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
|
||||
"plan_duration_label": m8,
|
||||
"plan_label_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"План для поддержания формы",
|
||||
),
|
||||
@@ -731,7 +775,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Promo code applied!",
|
||||
),
|
||||
"promo_code": MessageLookupByLibrary.simpleMessage("Promo code"),
|
||||
"promo_discount_hint": m3,
|
||||
"promo_discount_hint": m9,
|
||||
"promo_invalid": MessageLookupByLibrary.simpleMessage(
|
||||
"Promo code not found or expired",
|
||||
),
|
||||
@@ -852,12 +896,13 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Выбрана дата старта",
|
||||
),
|
||||
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
|
||||
"start_date_label": m10,
|
||||
"statistic": MessageLookupByLibrary.simpleMessage("Статистика"),
|
||||
"step": MessageLookupByLibrary.simpleMessage("Шаг"),
|
||||
"step_by_step_photo_recipe": MessageLookupByLibrary.simpleMessage(
|
||||
"Пошаговый фоторецепт",
|
||||
),
|
||||
"step_widget": m4,
|
||||
"step_widget": m11,
|
||||
"successfully_published": MessageLookupByLibrary.simpleMessage(
|
||||
"успешно опубликован",
|
||||
),
|
||||
@@ -882,8 +927,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
|
||||
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
|
||||
"to_order": MessageLookupByLibrary.simpleMessage("Заказать"),
|
||||
"tommorowPlanWithUserWillStart": m5,
|
||||
"tomorrowYourPlanWillStart": m6,
|
||||
"tommorowPlanWithUserWillStart": m12,
|
||||
"tomorrowYourPlanWillStart": m13,
|
||||
"try_again": MessageLookupByLibrary.simpleMessage("Попробовать снова"),
|
||||
"try_again_2": MessageLookupByLibrary.simpleMessage("Попробуйте ещё раз"),
|
||||
"typeAmoutOfReturn": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -917,7 +962,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Верхние показатели давления:",
|
||||
),
|
||||
"upper": MessageLookupByLibrary.simpleMessage("Верхнее"),
|
||||
"userCanceldThePlanSubscription": m7,
|
||||
"userCanceldThePlanSubscription": m14,
|
||||
"userConfirmedPlanCompletion": MessageLookupByLibrary.simpleMessage(
|
||||
"Пользователь подтвердил прохождения плана. В ближайшее время Вы получите",
|
||||
),
|
||||
@@ -925,8 +970,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Пользователь подтвердил прохождения плана.",
|
||||
),
|
||||
"userPaidThePlan": m8,
|
||||
"userSubscribedToPlan": m9,
|
||||
"userPaidThePlan": m15,
|
||||
"userSubscribedToPlan": m16,
|
||||
"user_authorization": MessageLookupByLibrary.simpleMessage(
|
||||
"Аутентификация",
|
||||
),
|
||||
@@ -1056,7 +1101,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"youCanComplain": MessageLookupByLibrary.simpleMessage(
|
||||
"Если вас что-то не устроило, вы можете подать жалобу",
|
||||
),
|
||||
"youCanceledSubscriptionToPlan": m10,
|
||||
"youCanceledSubscriptionToPlan": m17,
|
||||
"youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage(
|
||||
"Вы уверены, что хотите отменить заказ?",
|
||||
),
|
||||
|
||||
@@ -20,9 +20,27 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'ru';
|
||||
|
||||
static String m3(percent, amount) =>
|
||||
static String m1(userName) =>
|
||||
"Вы предложили персональную диету пользователю ${userName}";
|
||||
|
||||
static String m2(userName, dietName) =>
|
||||
"Вы предложили персональную диету пользователю ${userName}, выбранная диета: «${dietName}»";
|
||||
|
||||
static String m3(duration, dayWord) =>
|
||||
"Диета рассчитана на ${duration} ${dayWord}. Предложить её можно только для плана с такой же длительностью.";
|
||||
|
||||
static String m4(dietDuration, dietDayWord, planDuration, planDayWord) =>
|
||||
"Не подходит: диета на ${dietDuration} ${dietDayWord}, план на ${planDuration} ${planDayWord}.";
|
||||
|
||||
static String m5(name) => "Диета «${name}» предложена";
|
||||
|
||||
static String m8(duration, dayWord) => "План на ${duration} ${dayWord}";
|
||||
|
||||
static String m9(percent, amount) =>
|
||||
"Скидка ${percent}% — вы экономите ${amount} ₽";
|
||||
|
||||
static String m10(date) => "Старт: ${date}";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"Moscow": MessageLookupByLibrary.simpleMessage("Москва"),
|
||||
@@ -66,6 +84,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
|
||||
),
|
||||
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
|
||||
"changeDiet": MessageLookupByLibrary.simpleMessage("Заменить диету"),
|
||||
"change_personal_diet": MessageLookupByLibrary.simpleMessage(
|
||||
"Заменить диету",
|
||||
),
|
||||
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
|
||||
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
||||
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
||||
@@ -78,6 +100,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"choose_plan_duration": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите срок плана",
|
||||
),
|
||||
"choose_plan_start_date_with_personal_diet":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старта плана питания с учетом составления персональной диеты",
|
||||
),
|
||||
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
|
||||
),
|
||||
@@ -110,6 +136,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"),
|
||||
"day": MessageLookupByLibrary.simpleMessage("День"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("дней"),
|
||||
"default_user_name": MessageLookupByLibrary.simpleMessage("Пользователь"),
|
||||
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
|
||||
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
|
||||
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
|
||||
@@ -126,6 +153,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"do_you_take_pills": MessageLookupByLibrary.simpleMessage(
|
||||
"Принимаете ли вы лекарства или бады?",
|
||||
),
|
||||
"done": MessageLookupByLibrary.simpleMessage("Завершен"),
|
||||
"down_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Нижние показатели давления:",
|
||||
),
|
||||
@@ -192,6 +220,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"),
|
||||
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
|
||||
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
|
||||
"goToMyDiets": MessageLookupByLibrary.simpleMessage(
|
||||
"Перейти к моим диетам",
|
||||
),
|
||||
"gr": MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"grams": MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"have_promo_code": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -262,6 +293,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"no": MessageLookupByLibrary.simpleMessage("Нет"),
|
||||
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
|
||||
"no_active_personal_diet_consultations":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Нет активных консультаций с персональной диетой",
|
||||
),
|
||||
"no_installed_banks": MessageLookupByLibrary.simpleMessage(
|
||||
"Нет установленных банков",
|
||||
),
|
||||
@@ -284,6 +319,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"),
|
||||
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
|
||||
"orderCanceled": MessageLookupByLibrary.simpleMessage("Заказ отменен"),
|
||||
"order_in_progress": MessageLookupByLibrary.simpleMessage("Заказ в работе"),
|
||||
"order_paid": MessageLookupByLibrary.simpleMessage("Заказ оплачен"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
|
||||
"password_confirmation": MessageLookupByLibrary.simpleMessage(
|
||||
"Подтвердите пароль",
|
||||
@@ -300,8 +338,26 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pay_by_qr_code": MessageLookupByLibrary.simpleMessage(
|
||||
"Оплатить по QR-коду",
|
||||
),
|
||||
"payment_failed": MessageLookupByLibrary.simpleMessage("Оплата не прошла"),
|
||||
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
|
||||
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
|
||||
"personal_diet_defined_to_author": m1,
|
||||
"personal_diet_defined_to_author_named": m2,
|
||||
"personal_diet_duration_hint": m3,
|
||||
"personal_diet_duration_mismatch": m4,
|
||||
"personal_diet_not_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Диета не предложена",
|
||||
),
|
||||
"personal_diet_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Диета предложена",
|
||||
),
|
||||
"personal_diet_proposed_named": m5,
|
||||
"personal_diet_replaces_tomorrow": MessageLookupByLibrary.simpleMessage(
|
||||
"Замена действует со следующего дня",
|
||||
),
|
||||
"personal_diet_set": MessageLookupByLibrary.simpleMessage(
|
||||
"Персональная диета установлена",
|
||||
),
|
||||
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
|
||||
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
|
||||
"place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"),
|
||||
@@ -311,6 +367,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Большое подробное, привлекательное описание плана",
|
||||
),
|
||||
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
|
||||
"plan_duration_label": m8,
|
||||
"plan_label_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"План для поддержания формы",
|
||||
),
|
||||
@@ -338,7 +395,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Промокод применён!",
|
||||
),
|
||||
"promo_code": MessageLookupByLibrary.simpleMessage("Промокод"),
|
||||
"promo_discount_hint": m3,
|
||||
"promo_discount_hint": m9,
|
||||
"promo_invalid": MessageLookupByLibrary.simpleMessage(
|
||||
"Промокод не найден или истёк",
|
||||
),
|
||||
@@ -395,7 +452,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
|
||||
),
|
||||
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
|
||||
"startDateHasBeenSet": MessageLookupByLibrary.simpleMessage(
|
||||
"Выбрана дата старта",
|
||||
),
|
||||
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
|
||||
"start_date_label": m10,
|
||||
"suggestDiet": MessageLookupByLibrary.simpleMessage("Предложить диету"),
|
||||
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
|
||||
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
|
||||
),
|
||||
|
||||
@@ -4553,6 +4553,149 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Order in progress`
|
||||
String get order_in_progress {
|
||||
return Intl.message(
|
||||
'Order in progress',
|
||||
name: 'order_in_progress',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Personal diet proposed`
|
||||
String get personal_diet_proposed {
|
||||
return Intl.message(
|
||||
'Personal diet proposed',
|
||||
name: 'personal_diet_proposed',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Diet «{name}» proposed`
|
||||
String personal_diet_proposed_named(String name) {
|
||||
return Intl.message(
|
||||
'Diet «$name» proposed',
|
||||
name: 'personal_diet_proposed_named',
|
||||
desc: '',
|
||||
args: [name],
|
||||
);
|
||||
}
|
||||
|
||||
/// `User`
|
||||
String get default_user_name {
|
||||
return Intl.message('User', name: 'default_user_name', desc: '', args: []);
|
||||
}
|
||||
|
||||
/// `You have proposed a personal diet to {userName}`
|
||||
String personal_diet_defined_to_author(String userName) {
|
||||
return Intl.message(
|
||||
'You have proposed a personal diet to $userName',
|
||||
name: 'personal_diet_defined_to_author',
|
||||
desc: '',
|
||||
args: [userName],
|
||||
);
|
||||
}
|
||||
|
||||
/// `You have proposed a personal diet to {userName}, selected diet: «{dietName}»`
|
||||
String personal_diet_defined_to_author_named(
|
||||
String userName,
|
||||
String dietName,
|
||||
) {
|
||||
return Intl.message(
|
||||
'You have proposed a personal diet to $userName, selected diet: «$dietName»',
|
||||
name: 'personal_diet_defined_to_author_named',
|
||||
desc: '',
|
||||
args: [userName, dietName],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Personal diet not proposed`
|
||||
String get personal_diet_not_proposed {
|
||||
return Intl.message(
|
||||
'Personal diet not proposed',
|
||||
name: 'personal_diet_not_proposed',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Change personal diet`
|
||||
String get change_personal_diet {
|
||||
return Intl.message(
|
||||
'Change personal diet',
|
||||
name: 'change_personal_diet',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The change takes effect tomorrow`
|
||||
String get personal_diet_replaces_tomorrow {
|
||||
return Intl.message(
|
||||
'The change takes effect tomorrow',
|
||||
name: 'personal_diet_replaces_tomorrow',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Start: {date}`
|
||||
String start_date_label(String date) {
|
||||
return Intl.message(
|
||||
'Start: $date',
|
||||
name: 'start_date_label',
|
||||
desc: '',
|
||||
args: [date],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The diet lasts {duration} {dayWord}. It can only be offered for a plan with the same duration.`
|
||||
String personal_diet_duration_hint(int duration, String dayWord) {
|
||||
return Intl.message(
|
||||
'The diet lasts $duration $dayWord. It can only be offered for a plan with the same duration.',
|
||||
name: 'personal_diet_duration_hint',
|
||||
desc: '',
|
||||
args: [duration, dayWord],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Not suitable: diet lasts {dietDuration} {dietDayWord}, plan lasts {planDuration} {planDayWord}.`
|
||||
String personal_diet_duration_mismatch(
|
||||
int dietDuration,
|
||||
String dietDayWord,
|
||||
int planDuration,
|
||||
String planDayWord,
|
||||
) {
|
||||
return Intl.message(
|
||||
'Not suitable: diet lasts $dietDuration $dietDayWord, plan lasts $planDuration $planDayWord.',
|
||||
name: 'personal_diet_duration_mismatch',
|
||||
desc: '',
|
||||
args: [dietDuration, dietDayWord, planDuration, planDayWord],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Plan for {duration} {dayWord}`
|
||||
String plan_duration_label(int duration, String dayWord) {
|
||||
return Intl.message(
|
||||
'Plan for $duration $dayWord',
|
||||
name: 'plan_duration_label',
|
||||
desc: '',
|
||||
args: [duration, dayWord],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No active consultations with a personal diet`
|
||||
String get no_active_personal_diet_consultations {
|
||||
return Intl.message(
|
||||
'No active consultations with a personal diet',
|
||||
name: 'no_active_personal_diet_consultations',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -537,6 +537,61 @@
|
||||
"video_call_close": "Close",
|
||||
"video_call_notification_in_progress": "Call in progress",
|
||||
"video_call_notification_return": "Return to call",
|
||||
"video_call_notification_end": "End call"
|
||||
"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"}
|
||||
}
|
||||
},
|
||||
"default_user_name": "User",
|
||||
"personal_diet_defined_to_author": "You have proposed a personal diet to {userName}",
|
||||
"@personal_diet_defined_to_author": {
|
||||
"placeholders": {
|
||||
"userName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_defined_to_author_named": "You have proposed a personal diet to {userName}, selected diet: «{dietName}»",
|
||||
"@personal_diet_defined_to_author_named": {
|
||||
"placeholders": {
|
||||
"userName": {"type": "String"},
|
||||
"dietName": {"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",
|
||||
"start_date_label": "Start: {date}",
|
||||
"@start_date_label": {
|
||||
"placeholders": {
|
||||
"date": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_duration_hint": "The diet lasts {duration} {dayWord}. It can only be offered for a plan with the same duration.",
|
||||
"@personal_diet_duration_hint": {
|
||||
"placeholders": {
|
||||
"duration": {"type": "int"},
|
||||
"dayWord": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_duration_mismatch": "Not suitable: diet lasts {dietDuration} {dietDayWord}, plan lasts {planDuration} {planDayWord}.",
|
||||
"@personal_diet_duration_mismatch": {
|
||||
"placeholders": {
|
||||
"dietDuration": {"type": "int"},
|
||||
"dietDayWord": {"type": "String"},
|
||||
"planDuration": {"type": "int"},
|
||||
"planDayWord": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"plan_duration_label": "Plan for {duration} {dayWord}",
|
||||
"@plan_duration_label": {
|
||||
"placeholders": {
|
||||
"duration": {"type": "int"},
|
||||
"dayWord": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"no_active_personal_diet_consultations": "No active consultations with a personal diet"
|
||||
}
|
||||
|
||||
|
||||
@@ -282,5 +282,70 @@
|
||||
"video_call_notification_in_progress": "Звонок идёт",
|
||||
"video_call_notification_return": "Вернуться к звонку",
|
||||
"video_call_notification_end": "Завершить звонок",
|
||||
"blog_post_image_min_size_error": "Минимальный размер изображения — 500×500 px"
|
||||
"blog_post_image_min_size_error": "Минимальный размер изображения — 500×500 px",
|
||||
"startDateHasBeenSet": "Выбрана дата старта",
|
||||
"order_paid": "Заказ оплачен",
|
||||
"done": "Завершен",
|
||||
"orderCanceled": "Заказ отменен",
|
||||
"payment_failed": "Оплата не прошла",
|
||||
"personal_diet_set": "Персональная диета установлена",
|
||||
"suggestDiet": "Предложить диету",
|
||||
"changeDiet": "Заменить диету",
|
||||
"goToMyDiets": "Перейти к моим диетам",
|
||||
"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"}
|
||||
}
|
||||
},
|
||||
"default_user_name": "Пользователь",
|
||||
"personal_diet_defined_to_author": "Вы предложили персональную диету пользователю {userName}",
|
||||
"@personal_diet_defined_to_author": {
|
||||
"placeholders": {
|
||||
"userName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_defined_to_author_named": "Вы предложили персональную диету пользователю {userName}, выбранная диета: «{dietName}»",
|
||||
"@personal_diet_defined_to_author_named": {
|
||||
"placeholders": {
|
||||
"userName": {"type": "String"},
|
||||
"dietName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_not_proposed": "Диета не предложена",
|
||||
"change_personal_diet": "Заменить диету",
|
||||
"personal_diet_replaces_tomorrow": "Замена действует со следующего дня",
|
||||
"start_date_label": "Старт: {date}",
|
||||
"@start_date_label": {
|
||||
"placeholders": {
|
||||
"date": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_duration_hint": "Диета рассчитана на {duration} {dayWord}. Предложить её можно только для плана с такой же длительностью.",
|
||||
"@personal_diet_duration_hint": {
|
||||
"placeholders": {
|
||||
"duration": {"type": "int"},
|
||||
"dayWord": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"personal_diet_duration_mismatch": "Не подходит: диета на {dietDuration} {dietDayWord}, план на {planDuration} {planDayWord}.",
|
||||
"@personal_diet_duration_mismatch": {
|
||||
"placeholders": {
|
||||
"dietDuration": {"type": "int"},
|
||||
"dietDayWord": {"type": "String"},
|
||||
"planDuration": {"type": "int"},
|
||||
"planDayWord": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"plan_duration_label": "План на {duration} {dayWord}",
|
||||
"@plan_duration_label": {
|
||||
"placeholders": {
|
||||
"duration": {"type": "int"},
|
||||
"dayWord": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"no_active_personal_diet_consultations": "Нет активных консультаций с персональной диетой"
|
||||
}
|
||||
|
||||
@@ -78,6 +78,66 @@ void main() {
|
||||
meta: const ChatMetaModel(lastPage: 1),
|
||||
);
|
||||
|
||||
const planForChosenPlanUpdates = PlanModel(
|
||||
id: 1,
|
||||
slug: 'test-plan',
|
||||
name: 'Test Plan',
|
||||
image: 'https://example.com/image.jpg',
|
||||
duration: 7,
|
||||
state: 'active',
|
||||
type: PlanType(
|
||||
fit: true,
|
||||
slim: false,
|
||||
behavior: false,
|
||||
wellness: false,
|
||||
),
|
||||
details: 'Test details',
|
||||
packetsCount: 3,
|
||||
author: Author(
|
||||
authorSlug: 'author',
|
||||
authorName: 'Author Name',
|
||||
authorProfileIcon: 'https://example.com/icon.jpg',
|
||||
authorIsMe: false,
|
||||
),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
isFavorite: false,
|
||||
minPriceOfOptions: 1000,
|
||||
maxPriceOfOptions: 2000,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
ChosenPlanModel buildChosenPlan(
|
||||
int id,
|
||||
ChosenPlanStateEnum chosenPlanState,
|
||||
) {
|
||||
return ChosenPlanModel(
|
||||
id: id,
|
||||
optionType: 'options1',
|
||||
startDate: DateTime.now(),
|
||||
chosenPlanState: chosenPlanState,
|
||||
chatId: 'chat-id',
|
||||
consultantId: 1,
|
||||
plan: planForChosenPlanUpdates,
|
||||
);
|
||||
}
|
||||
|
||||
void stubChatChannel({
|
||||
required Stream<ChannelReadEvent> chosenPlanStream,
|
||||
}) {
|
||||
when(mockWsManager.client).thenReturn(MockPusherChannelsClient());
|
||||
when(
|
||||
mockWsManager.directChatStream,
|
||||
).thenAnswer((_) => const Stream<ChannelReadEvent>.empty());
|
||||
when(
|
||||
mockWsManager.clientTypingStream,
|
||||
).thenAnswer((_) => const Stream<ChannelReadEvent>.empty());
|
||||
when(
|
||||
mockWsManager.chosenPlanStateChangedStream,
|
||||
).thenAnswer((_) => chosenPlanStream);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
mockChatRepository = MockChatRepository();
|
||||
mockCardsRepository = MockCardsRepository();
|
||||
@@ -721,6 +781,160 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'chosenPlanStateChangedStream - перезапрашивает план и обновляет state',
|
||||
() async {
|
||||
const uuid = 'room-uuid';
|
||||
const planId = 321;
|
||||
final chosenPlan = buildChosenPlan(
|
||||
planId,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
);
|
||||
final updatedPlan = chosenPlan.copyWith(
|
||||
chosenPlanState: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
final controller = StreamController<ChannelReadEvent>.broadcast();
|
||||
|
||||
when(
|
||||
mockChatRepository.getChatRoom(uuid),
|
||||
).thenAnswer((_) async => testChatRoom.copyWith(chosenPlanId: planId));
|
||||
when(
|
||||
mockChatRepository.getChatMessages(uuid, 1),
|
||||
).thenAnswer((_) async => testChatMessages);
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => chosenPlan);
|
||||
stubChatChannel(chosenPlanStream: controller.stream);
|
||||
|
||||
await chatRoomCubit.initChat(uuid);
|
||||
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
|
||||
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => updatedPlan);
|
||||
clearInteractions(mockChatRepository);
|
||||
|
||||
controller.add(_FakeChannelReadEvent());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 350));
|
||||
|
||||
verify(mockChatRepository.getChosenPlan(id: planId)).called(1);
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(
|
||||
state.chatRoom.chosenPlan?.chosenPlanState,
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
await controller.close();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'chosenPlanStateChangedStream - использует chosenPlan.id если chosenPlanId null',
|
||||
() async {
|
||||
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||
const conversationUuid = 'fbd81429-8019-499a-80e8-e3933cc4d872';
|
||||
const planId = 654;
|
||||
final chosenPlan = buildChosenPlan(
|
||||
planId,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
);
|
||||
final updatedPlan = chosenPlan.copyWith(
|
||||
chosenPlanState: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
final controller = StreamController<ChannelReadEvent>.broadcast();
|
||||
|
||||
when(
|
||||
mockChatRepository.resolveOrderIdToConversation(orderId: orderId),
|
||||
).thenAnswer(
|
||||
(_) async => OrderIdToConversationModel(
|
||||
chosenPlanId: planId,
|
||||
conversationUuid: conversationUuid,
|
||||
),
|
||||
);
|
||||
when(
|
||||
mockChatRepository.getChatRoom(conversationUuid),
|
||||
).thenAnswer((_) async => testChatRoom);
|
||||
when(
|
||||
mockChatRepository.getChatMessages(conversationUuid, 1),
|
||||
).thenAnswer((_) async => testChatMessages);
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => chosenPlan);
|
||||
stubChatChannel(chosenPlanStream: controller.stream);
|
||||
|
||||
await chatRoomCubit.initChat(null, orderId: orderId);
|
||||
final loaded = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(loaded.chatRoom.chosenPlanId, isNull);
|
||||
expect(loaded.chatRoom.chosenPlan?.id, planId);
|
||||
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => updatedPlan);
|
||||
clearInteractions(mockChatRepository);
|
||||
|
||||
controller.add(_FakeChannelReadEvent());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 350));
|
||||
|
||||
verify(mockChatRepository.getChosenPlan(id: planId)).called(1);
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(
|
||||
state.chatRoom.chosenPlan?.chosenPlanState,
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
await controller.close();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'chosenPlanStateChangedStream - дебаунсит повторные события',
|
||||
() async {
|
||||
const uuid = 'room-uuid';
|
||||
const planId = 987;
|
||||
final chosenPlan = buildChosenPlan(
|
||||
planId,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
);
|
||||
final updatedPlan = chosenPlan.copyWith(
|
||||
chosenPlanState: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
final controller = StreamController<ChannelReadEvent>.broadcast();
|
||||
|
||||
when(
|
||||
mockChatRepository.getChatRoom(uuid),
|
||||
).thenAnswer((_) async => testChatRoom.copyWith(chosenPlanId: planId));
|
||||
when(
|
||||
mockChatRepository.getChatMessages(uuid, 1),
|
||||
).thenAnswer((_) async => testChatMessages);
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => chosenPlan);
|
||||
stubChatChannel(chosenPlanStream: controller.stream);
|
||||
|
||||
await chatRoomCubit.initChat(uuid);
|
||||
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
|
||||
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => updatedPlan);
|
||||
clearInteractions(mockChatRepository);
|
||||
|
||||
controller
|
||||
..add(_FakeChannelReadEvent())
|
||||
..add(_FakeChannelReadEvent());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 350));
|
||||
|
||||
verify(mockChatRepository.getChosenPlan(id: planId)).called(1);
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(
|
||||
state.chatRoom.chosenPlan?.chosenPlanState,
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
await controller.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('choseStartPlanDate - устанавливает дату начала плана', () async {
|
||||
const uuid = 'room-uuid';
|
||||
const planId = 123;
|
||||
@@ -2578,3 +2792,8 @@ void main() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeChannelReadEvent extends Fake implements ChannelReadEvent {
|
||||
@override
|
||||
dynamic get data => null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import 'package:eatvio_app/features/chat/presentation/utils/chat_banner_resolver.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
ConsultationProgress _progress(
|
||||
ChosenPlanStateEnum state, {
|
||||
bool hasPersonalDiet = false,
|
||||
}) {
|
||||
return mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: hasPersonalDiet,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('resolveChatBanner — карта консультанта', () {
|
||||
test('in_coordination без карты просит добавить карту', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateInCoordination),
|
||||
amIConsultant: true,
|
||||
hasCard: false,
|
||||
state: ChosenPlanStateEnum.stateInCoordination,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.card);
|
||||
});
|
||||
|
||||
test('in_coordination с картой просит выбрать дату', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateInCoordination),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateInCoordination,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.date);
|
||||
});
|
||||
|
||||
test('paid + notProposed + диета просит предложить диету', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.setPersonalDiet);
|
||||
});
|
||||
|
||||
test(
|
||||
'personal_diet_setted/started/in_progress + proposed просят сменить диету',
|
||||
() {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(state, hasPersonalDiet: true),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: state,
|
||||
);
|
||||
|
||||
expect(
|
||||
banner,
|
||||
BannerType.changePersonalDiet,
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('last_day_finished не просит менять диету (нет баннера)', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateLastDayFinished,
|
||||
);
|
||||
|
||||
expect(banner, isNot(BannerType.changePersonalDiet));
|
||||
expect(banner, isNull);
|
||||
});
|
||||
|
||||
test('done не просит менять диету (collectCharge)', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateDone,
|
||||
);
|
||||
|
||||
expect(banner, isNot(BannerType.changePersonalDiet));
|
||||
expect(banner, BannerType.collectCharge);
|
||||
});
|
||||
});
|
||||
|
||||
group('resolveChatBanner — карта клиента', () {
|
||||
test('start_date_has_been_set просит оплатить', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateStartDateHasBeenSet),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateStartDateHasBeenSet,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.payment);
|
||||
});
|
||||
|
||||
test('pending_payment просит оплатить', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePendingPayment),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePendingPayment,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.payment);
|
||||
});
|
||||
|
||||
test('paid + notProposed не показывает diet-баннер клиенту', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
expect(banner, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'personal_diet_setted + proposed не показывает diet-баннер клиенту',
|
||||
() {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
);
|
||||
|
||||
expect(banner, isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('resolveChatBanner — терминальные фазы', () {
|
||||
test('canceled не показывает баннер', () {
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateCanceledByTheInitiator),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateCanceledByTheInitiator,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateCanceledByTheInitiator),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateCanceledByTheInitiator,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('payment_failed не показывает баннер', () {
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePaymentFailed),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaymentFailed,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePaymentFailed),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaymentFailed,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('нет progress или state — нет баннера', () {
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: null,
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePaid),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: null,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_consultation_header.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_progress_plank.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/personal_diet_status_chip.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Widget _wrap(Widget child) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('ru')],
|
||||
home: Scaffold(body: Center(child: child)),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('ChatConsultationHeader', () {
|
||||
testWidgets('вызывает onInfoTap по тапу на info', (tester) async {
|
||||
var tapped = 0;
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateInited,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
ChatConsultationHeader(
|
||||
progress: progress,
|
||||
showInfoButton: true,
|
||||
onInfoTap: () => tapped++,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.info_outline_rounded));
|
||||
await tester.pump();
|
||||
|
||||
expect(tapped, 1);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'personal_diet_setted показывает плашку и чип вместе с баннером замены',
|
||||
(tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
personalDietName: 'Индивидуальный план питания',
|
||||
);
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
expect(progress.dietStatus, PersonalDietStatus.proposed);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
ChatConsultationHeader(
|
||||
progress: progress,
|
||||
showInfoButton: false,
|
||||
onInfoTap: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(ChatConsultationHeader)));
|
||||
|
||||
expect(find.text(s.order_in_progress), findsNWidgets(2));
|
||||
expect(find.byType(PersonalDietStatusChip), findsOneWidget);
|
||||
expect(
|
||||
find.text(s.personal_diet_proposed_named('Индивидуальный план питания')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(ConsultationProgressPlank), findsOneWidget);
|
||||
expect(find.byIcon(Icons.check), findsNWidgets(2));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_progress_plank.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Widget _wrap(Widget child) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('ru')],
|
||||
home: Scaffold(body: Center(child: child)),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('ConsultationProgressPlank', () {
|
||||
testWidgets('не строит шаги при showProgressPlank=false', (tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateConfirmedByConsultant,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
expect(progress.showProgressPlank, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(ConsultationProgressPlank(progress: progress)),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ConsultationProgressPlank),
|
||||
matching: find.byType(SizedBox),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byIcon(Icons.check), findsNothing);
|
||||
expect(find.byType(Text), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'рисует 4 шага с иконками и подписями при showProgressPlank=true',
|
||||
(tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateDone,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(ConsultationProgressPlank(progress: progress)),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(ConsultationProgressPlank)));
|
||||
|
||||
expect(find.byIcon(Icons.check), findsNWidgets(4));
|
||||
expect(find.text(s.startDateHasBeenSet), findsOneWidget);
|
||||
expect(find.text(s.order_paid), findsOneWidget);
|
||||
expect(find.text(s.order_in_progress), findsOneWidget);
|
||||
expect(find.text(s.done), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('подписи шагов центрированы под своими кружками', (
|
||||
tester,
|
||||
) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateDone,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
SizedBox(
|
||||
width: 360,
|
||||
child: ConsultationProgressPlank(progress: progress),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (var index = 0; index < 4; index++) {
|
||||
final circle = tester.getCenter(
|
||||
find.byKey(ValueKey('consultation_step_circle_$index')),
|
||||
);
|
||||
final label = tester.getCenter(
|
||||
find.byKey(ValueKey('consultation_step_label_$index')),
|
||||
);
|
||||
expect(label.dx, closeTo(circle.dx, 0.5));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/personal_diet_status_chip.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Widget _wrap(Widget child) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('ru')],
|
||||
home: Scaffold(body: Center(child: child)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chip(
|
||||
ChosenPlanStateEnum state, {
|
||||
bool hasPersonalDiet = true,
|
||||
String? personalDietName,
|
||||
}) {
|
||||
return PersonalDietStatusChip(
|
||||
progress: mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: hasPersonalDiet,
|
||||
personalDietName: personalDietName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('PersonalDietStatusChip', () {
|
||||
testWidgets('notRequired + active ничего не показывает', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(_chip(ChosenPlanStateEnum.stateInited, hasPersonalDiet: false)),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(PersonalDietStatusChip),
|
||||
matching: find.byType(SizedBox),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(Text), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('proposed показывает personal_diet_proposed', (tester) async {
|
||||
await tester.pumpWidget(_wrap(_chip(ChosenPlanStateEnum.stateStarted)));
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
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 {
|
||||
await tester.pumpWidget(_wrap(_chip(ChosenPlanStateEnum.statePaid)));
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.personal_diet_not_proposed), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('canceled показывает orderCanceled', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(_chip(ChosenPlanStateEnum.stateCanceledByTheInitiator)),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.orderCanceled), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('paymentFailed показывает payment_failed', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(_chip(ChosenPlanStateEnum.statePaymentFailed)),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.payment_failed), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
typedef _Expected = ({
|
||||
Set<ConsultationStep> steps,
|
||||
bool plank,
|
||||
PersonalDietStatus diet,
|
||||
ConsultationPhase phase,
|
||||
});
|
||||
|
||||
const _startDate = ConsultationStep.startDate;
|
||||
const _paid = ConsultationStep.paid;
|
||||
const _inProgress = ConsultationStep.inProgress;
|
||||
const _finished = ConsultationStep.finished;
|
||||
|
||||
final Map<ChosenPlanStateEnum, _Expected> _expectations = {
|
||||
ChosenPlanStateEnum.stateInited: (
|
||||
steps: {},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateStartDateHasBeenSet: (
|
||||
steps: {_startDate},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateInCoordination: (
|
||||
steps: {},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateStarted: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateInProgress: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.statePendingPayment: (
|
||||
steps: {_startDate},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateConfirmedByConsultant: (
|
||||
steps: {},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.statePaid: (
|
||||
steps: {_startDate, _paid},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateCanceledByTheInitiator: (
|
||||
steps: {},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notRequired,
|
||||
phase: ConsultationPhase.canceled,
|
||||
),
|
||||
ChosenPlanStateEnum.stateLastDayStarted: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateLastDayFinished: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateComplained: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateDone: (
|
||||
steps: {_startDate, _paid, _inProgress, _finished},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.personalDietSet: (
|
||||
steps: {_startDate, _paid},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateChargeWasCollected: (
|
||||
steps: {_startDate, _paid, _inProgress, _finished},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.statePaymentFailed: (
|
||||
steps: {_startDate},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notRequired,
|
||||
phase: ConsultationPhase.paymentFailed,
|
||||
),
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('mapConsultationProgress — все состояния', () {
|
||||
test('таблица покрывает ровно все 16 состояний', () {
|
||||
expect(_expectations.length, 16);
|
||||
expect(_expectations.keys.toSet(), ChosenPlanStateEnum.values.toSet());
|
||||
});
|
||||
|
||||
for (final entry in _expectations.entries) {
|
||||
test('${entry.key.name} маппится во все поля ConsultationProgress', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: entry.key,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.completedSteps, unorderedEquals(entry.value.steps));
|
||||
expect(progress.showProgressPlank, entry.value.plank);
|
||||
expect(progress.dietStatus, entry.value.diet);
|
||||
expect(progress.phase, entry.value.phase);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('mapConsultationProgress — ключевые сценарии', () {
|
||||
test('in_coordination показывает плашку без завершённых шагов', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateInCoordination,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
expect(progress.completedSteps, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'personal_diet_setted показывает плашку и завершает startDate+paid',
|
||||
() {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
expect(progress.completedSteps, unorderedEquals([_startDate, _paid]));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'started/in_progress/last_day_started/last_day_finished/complained — 3 шага',
|
||||
() {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
ChosenPlanStateEnum.stateComplained,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.completedSteps,
|
||||
unorderedEquals([_startDate, _paid, _inProgress]),
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('done/charge_was_collected — 4 шага', () {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
ChosenPlanStateEnum.stateChargeWasCollected,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.completedSteps,
|
||||
unorderedEquals([_startDate, _paid, _inProgress, _finished]),
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'canceled — фаза canceled и dietStatus notRequired даже при диете',
|
||||
() {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateCanceledByTheInitiator,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.phase, ConsultationPhase.canceled);
|
||||
expect(progress.isCanceled, isTrue);
|
||||
expect(progress.dietStatus, PersonalDietStatus.notRequired);
|
||||
},
|
||||
);
|
||||
|
||||
test('payment_failed — фаза paymentFailed и только startDate', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.statePaymentFailed,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.phase, ConsultationPhase.paymentFailed);
|
||||
expect(progress.isPaymentFailed, isTrue);
|
||||
expect(progress.completedSteps, unorderedEquals([_startDate]));
|
||||
});
|
||||
|
||||
test('hasPersonalDiet=false даёт notRequired для любого состояния', () {
|
||||
for (final state in ChosenPlanStateEnum.values) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.dietStatus,
|
||||
PersonalDietStatus.notRequired,
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
expect(progress.hasDietChip, isFalse, reason: 'state=${state.name}');
|
||||
}
|
||||
});
|
||||
|
||||
test('paid с диетой даёт notProposed', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.dietStatus, PersonalDietStatus.notProposed);
|
||||
});
|
||||
|
||||
test('personal_diet_setted/started/proposed дают proposed', () {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.dietStatus,
|
||||
PersonalDietStatus.proposed,
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('personalDietName передаётся в ConsultationProgress', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
personalDietName: 'Индивидуальный план питания',
|
||||
);
|
||||
|
||||
expect(progress.personalDietName, 'Индивидуальный план питания');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -27,9 +27,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(
|
||||
const ChosenDietWidgetContent(
|
||||
currentDietDay: 5,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 14,
|
||||
dietId: 1,
|
||||
),
|
||||
),
|
||||
@@ -38,15 +36,14 @@ void main() {
|
||||
|
||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||
expect(find.text('Test Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 14'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('displays current day and duration', (tester) async {
|
||||
testWidgets('does not display current day and duration', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(
|
||||
const ChosenDietWidgetContent(
|
||||
currentDietDay: 7,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 21,
|
||||
dietId: 1,
|
||||
),
|
||||
),
|
||||
@@ -54,15 +51,14 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||
expect(find.textContaining('из 21'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('displays diet name with underline', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(
|
||||
const ChosenDietWidgetContent(
|
||||
currentDietDay: 1,
|
||||
dietName: 'My Diet',
|
||||
dietDuration: 7,
|
||||
dietId: 1,
|
||||
),
|
||||
),
|
||||
@@ -70,6 +66,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('My Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 7'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,6 +100,17 @@ void main() {
|
||||
await tester.pumpWidget(wrap(const ChosenDietWidget(), cubit));
|
||||
await tester.pump();
|
||||
expect(find.text('Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 7'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('ChosenDietWidget hides diet day while plan keeps its day', (tester) async {
|
||||
final cubit = FakeDiaryCubit(loadedState(hasDiet: true, hasPlan: true));
|
||||
await tester.pumpWidget(wrap(const Column(children: [ChosenPlanWidget(), ChosenDietWidget()]), cubit));
|
||||
await tester.pump();
|
||||
expect(find.text('Plan'), findsOneWidget);
|
||||
expect(find.textContaining('из 30'), findsOneWidget);
|
||||
expect(find.text('Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 7'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('AddProductButtonWidget renders for loaded state', (tester) async {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1212,6 +1212,89 @@ 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.text('Вы предложили персональную диету пользователю Client, выбранная диета: «Test Diet»'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'displays PersonalDietIsDefinedToAuthorNotification with fallback user name',
|
||||
(tester) async {
|
||||
final notification = PersonalDietIsDefinedToAuthorNotification(
|
||||
type: NotificationType.personalDietIsDefinedToAuthor,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: 'Plan',
|
||||
userName: '',
|
||||
consultantName: 'Consultant',
|
||||
dietName: 'Test Diet',
|
||||
role: 'consultant',
|
||||
peerSlug: 'client-slug',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text('Вы предложили персональную диету пользователю Пользователь, выбранная диета: «Test Diet»'),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'displays PersonalDietIsDefinedToAuthorNotification without diet name',
|
||||
(tester) async {
|
||||
final notification = PersonalDietIsDefinedToAuthorNotification(
|
||||
type: NotificationType.personalDietIsDefinedToAuthor,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: 'Plan',
|
||||
userName: 'Client',
|
||||
consultantName: 'Consultant',
|
||||
dietName: '',
|
||||
role: 'consultant',
|
||||
peerSlug: 'client-slug',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text('Вы предложили персональную диету пользователю Client'),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('displays UnknownNotification', (tester) async {
|
||||
final notification = UnknownNotification(
|
||||
type: NotificationType.unknown,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -243,12 +243,12 @@ void main() {
|
||||
expect(find.byType(InfoByOrderScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
ChosenPlanModel donePlan() {
|
||||
ChosenPlanModel donePlan({ChosenPlanStateEnum state = ChosenPlanStateEnum.stateDone}) {
|
||||
return ChosenPlanModel(
|
||||
id: 1,
|
||||
optionType: 'options1',
|
||||
startDate: DateTime(2024, 1, 1),
|
||||
chosenPlanState: ChosenPlanStateEnum.stateDone,
|
||||
chosenPlanState: state,
|
||||
chatId: 'chat-1',
|
||||
consultantId: 1,
|
||||
plan: const PlanModel(
|
||||
@@ -320,6 +320,7 @@ void main() {
|
||||
|
||||
expect(find.byType(CollectChargeBanner), findsOneWidget);
|
||||
expect(find.text('Получить выплату'), findsOneWidget);
|
||||
expect(find.text('Отменить консультацию'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -337,6 +338,22 @@ void main() {
|
||||
|
||||
expect(find.byType(CollectChargeBanner), findsNothing);
|
||||
expect(find.text('Получить выплату'), findsNothing);
|
||||
expect(find.text('Отменить консультацию'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows cancel action for cancellable post-paid plan', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: anyNamed('id')),
|
||||
).thenAnswer((_) async => donePlan(state: ChosenPlanStateEnum.stateStarted));
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(createTestWidget(planId: 1));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
expect(find.text('Отменить консультацию'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ void main() {
|
||||
expect(cubit.state.consultationsWithDiet, isEmpty);
|
||||
expect(cubit.state.consultationsWithoutDiet, isEmpty);
|
||||
expect(cubit.state.dietName, isEmpty);
|
||||
expect(cubit.state.dietDuration, 0);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
expect(cubit.state.error, isNull);
|
||||
});
|
||||
@@ -71,6 +72,7 @@ void main() {
|
||||
await cubit.setDietName(dietId);
|
||||
|
||||
expect(cubit.state.dietName, equals(dietName));
|
||||
expect(cubit.state.dietDuration, 30);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(1);
|
||||
});
|
||||
@@ -161,6 +163,7 @@ void main() {
|
||||
await cubit.getConsultationWithPersonalDiets(dietId);
|
||||
|
||||
expect(cubit.state.dietName, equals(dietName));
|
||||
expect(cubit.state.dietDuration, 30);
|
||||
expect(cubit.state.consultationsWithDiet.length, 1);
|
||||
expect(cubit.state.consultationsWithoutDiet.length, 1);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
@@ -216,6 +219,8 @@ void main() {
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockPersonalDietSuggestionRepository.getConsultationWithPersonalDiets()).called(2);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(2);
|
||||
});
|
||||
|
||||
test('reattachDietToConsultation - успешное переприкрепление диеты', () async {
|
||||
@@ -265,6 +270,8 @@ void main() {
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockPersonalDietSuggestionRepository.getConsultationWithPersonalDiets()).called(2);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ void main() {
|
||||
expect(state.consultationsWithDiet, isEmpty);
|
||||
expect(state.consultationsWithoutDiet, isEmpty);
|
||||
expect(state.dietName, '');
|
||||
expect(state.dietDuration, 0);
|
||||
expect(state.isLoading, isFalse);
|
||||
expect(state.error, isNull);
|
||||
});
|
||||
@@ -16,6 +17,7 @@ void main() {
|
||||
test('creates instance with custom values', () {
|
||||
final state = PersonalDietSuggestionState(
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 14,
|
||||
isLoading: true,
|
||||
error: 'Test error',
|
||||
);
|
||||
@@ -23,6 +25,7 @@ void main() {
|
||||
expect(state.consultationsWithDiet, isEmpty);
|
||||
expect(state.consultationsWithoutDiet, isEmpty);
|
||||
expect(state.dietName, 'Test Diet');
|
||||
expect(state.dietDuration, 14);
|
||||
expect(state.isLoading, isTrue);
|
||||
expect(state.error, 'Test error');
|
||||
});
|
||||
@@ -31,14 +34,15 @@ void main() {
|
||||
final original = PersonalDietSuggestionState();
|
||||
final updated = original.copyWith(
|
||||
dietName: 'New Diet',
|
||||
dietDuration: 7,
|
||||
isLoading: true,
|
||||
);
|
||||
|
||||
expect(updated.dietName, 'New Diet');
|
||||
expect(updated.dietDuration, 7);
|
||||
expect(updated.isLoading, isTrue);
|
||||
expect(updated.consultationsWithDiet, original.consultationsWithDiet);
|
||||
expect(updated.error, original.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
@@ -34,6 +35,7 @@ void main() {
|
||||
|
||||
final mockConsultation = ConsultationModel(
|
||||
id: 1,
|
||||
chatId: 'chat-1',
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
|
||||
planTitle: 'Test Plan',
|
||||
userName: 'Test User',
|
||||
@@ -68,6 +70,32 @@ void main() {
|
||||
path: '/personal-diet-suggestion',
|
||||
builder: (context, state) => const Scaffold(body: Text('Personal Diet')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat-select-screen',
|
||||
name: 'chatSelectScreen',
|
||||
builder: (context, state) => const Scaffold(body: Text('Chats')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat-screen',
|
||||
name: 'chatScreen',
|
||||
builder: (context, state) {
|
||||
final uuid = state.uri.queryParameters['uuid'];
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/chat-select-screen');
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Text('Chat $uuid'),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -201,7 +229,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('shows success bottom sheet after successful diet attachment', (tester) async {
|
||||
testWidgets('opens chat after successful diet attachment', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
|
||||
@@ -226,7 +254,99 @@ void main() {
|
||||
await tester.tap(offerButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('«Test Diet» предложена пользователю Test User'), findsOneWidget);
|
||||
expect(find.text('Chat chat-1'), findsOneWidget);
|
||||
verify(cubit.attachDietToConsultation(consultationId: 1)).called(1);
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Chats'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('opens chat after successful diet reattachment', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
final consultationWithDiet = ConsultationModel(
|
||||
id: 1,
|
||||
chatId: 'chat-2',
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
|
||||
planTitle: 'Test Plan',
|
||||
userName: 'Test User',
|
||||
userSlug: 'test-user',
|
||||
consultantPayment: 1000.0,
|
||||
duration: 7,
|
||||
currentDiaryDay: 3,
|
||||
userProfilePhotoUrl: '/avatar.jpg',
|
||||
consultationOptionPrice: 500,
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
endDate: DateTime(2025, 1, 8),
|
||||
consultantId: 1,
|
||||
diaries: [],
|
||||
personalDietId: 1,
|
||||
);
|
||||
|
||||
when(cubit.state).thenReturn(PersonalDietSuggestionState());
|
||||
when(cubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.reattachDietToConsultation(consultationId: 1)).thenAnswer((_) async => true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<PersonalDietSuggestionCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(ConsultationWidget(consultationModel: consultationWithDiet, dietName: 'Test Diet')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final changeButton = find.text(S.of(tester.element(find.byType(ConsultationWidget))).changeDiet);
|
||||
await tester.tap(changeButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final offerButton = find.text(S.of(tester.element(find.byType(ConsultationWidget))).toOffer);
|
||||
await tester.tap(offerButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Chat chat-2'), findsOneWidget);
|
||||
verify(cubit.reattachDietToConsultation(consultationId: 1)).called(1);
|
||||
verifyNever(cubit.attachDietToConsultation(consultationId: 1));
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Chats'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('stays on screen when chat id is missing after success', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
|
||||
when(cubit.state).thenReturn(PersonalDietSuggestionState());
|
||||
when(cubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.attachDietToConsultation(consultationId: 1)).thenAnswer((_) async => true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<PersonalDietSuggestionCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(
|
||||
ConsultationWidget(
|
||||
consultationModel: mockConsultation.copyWith(chatId: null),
|
||||
dietName: 'Test Diet',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Предложить диету'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(S.of(tester.element(find.byType(ConsultationWidget))).toOffer));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Chat '), findsNothing);
|
||||
expect(find.text('Test User'), findsOneWidget);
|
||||
verify(cubit.attachDietToConsultation(consultationId: 1)).called(1);
|
||||
});
|
||||
});
|
||||
@@ -279,7 +399,7 @@ void main() {
|
||||
|
||||
expect(find.text('Test User'), findsOneWidget);
|
||||
expect(find.text('Test Plan'), findsOneWidget);
|
||||
expect(find.text('Старт: Test User'), findsOneWidget);
|
||||
expect(find.text('Старт: 01.01.2025'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,7 +438,71 @@ void main() {
|
||||
|
||||
expect(find.text('Test User'), findsOneWidget);
|
||||
expect(find.text('Test Plan'), findsOneWidget);
|
||||
expect(find.text('Старт: Test User'), findsNothing);
|
||||
expect(find.text('Старт: 01.01.2025'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('disables CTA when plan duration differs from diet duration', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
final consultation = ConsultationModel(
|
||||
id: 1,
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
|
||||
planTitle: 'Test Plan',
|
||||
userName: 'Test User',
|
||||
userSlug: 'test-user',
|
||||
consultantPayment: 1000.0,
|
||||
duration: 14,
|
||||
currentDiaryDay: 3,
|
||||
userProfilePhotoUrl: '/avatar.jpg',
|
||||
consultationOptionPrice: 500,
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
endDate: DateTime(2025, 1, 15),
|
||||
consultantId: 1,
|
||||
diaries: [],
|
||||
personalDietId: null,
|
||||
);
|
||||
|
||||
when(cubit.state).thenReturn(PersonalDietSuggestionState());
|
||||
when(cubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<PersonalDietSuggestionCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(
|
||||
ConsultationWidget(
|
||||
consultationModel: consultation,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 7,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final s = S.of(tester.element(find.byType(ConsultationWidget)));
|
||||
expect(
|
||||
find.text(
|
||||
s.personal_diet_duration_mismatch(
|
||||
7,
|
||||
getDayForm(7, s),
|
||||
14,
|
||||
getDayForm(14, s),
|
||||
),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
final button = tester.widget<OutlinedButton>(find.byType(OutlinedButton));
|
||||
expect(button.onPressed, isNull);
|
||||
expect(
|
||||
button.style?.backgroundColor?.resolve({WidgetState.disabled}),
|
||||
appTheme.dividerColor,
|
||||
);
|
||||
expect(
|
||||
tester.widget<Text>(find.text(s.suggestDiet)).style?.color,
|
||||
appTheme.gray,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
|
||||
@@ -84,5 +85,31 @@ void main() {
|
||||
|
||||
expect(find.textContaining('Test Diet Name'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows diet duration hint and empty state', (tester) async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
const dietDuration = 7;
|
||||
final state = PersonalDietSuggestionState(
|
||||
isLoading: false,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: dietDuration,
|
||||
consultationsWithDiet: [],
|
||||
consultationsWithoutDiet: [],
|
||||
);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await tester.pumpWidget(wrap(const PersonalDietSuggestionScreenContent(), cubit));
|
||||
await tester.pump();
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietSuggestionScreenContent)));
|
||||
expect(
|
||||
find.text(s.personal_diet_duration_hint(dietDuration, getDayForm(dietDuration, s))),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text(s.no_active_personal_diet_consultations), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,41 +68,33 @@ void main() {
|
||||
test('attachPersonalDiet - успешное прикрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': dietId};
|
||||
|
||||
when(mockService.attachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).thenAnswer((_) async => {});
|
||||
when(mockService.attachPersonalDiet(consultationId, body))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
await repository.attachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
);
|
||||
|
||||
verify(mockService.attachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockService.attachPersonalDiet(consultationId, body)).called(1);
|
||||
});
|
||||
|
||||
test('reattachPersonalDiet - успешное переприкрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': dietId};
|
||||
|
||||
when(mockService.reattachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).thenAnswer((_) async => {});
|
||||
when(mockService.reattachPersonalDiet(consultationId, body))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
await repository.reattachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
);
|
||||
|
||||
verify(mockService.reattachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockService.reattachPersonalDiet(consultationId, body)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,55 +15,48 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
mockDio = MockDio();
|
||||
when(mockDio.options).thenReturn(BaseOptions(baseUrl: 'http://localhost'));
|
||||
service = PersonalDietSuggestionService(mockDio);
|
||||
});
|
||||
|
||||
test('getConsultationWithPersonalDiets - успешное получение консультаций', () async {
|
||||
final mockData = {
|
||||
'data': [],
|
||||
'links': {},
|
||||
'meta': {
|
||||
'current_page': 1,
|
||||
'last_page': 1,
|
||||
'total': 0,
|
||||
'per_page': 10,
|
||||
},
|
||||
};
|
||||
late RequestOptions capturedRequest;
|
||||
|
||||
when(mockDio.get(
|
||||
'/api/consultations',
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
)).thenAnswer((_) async => Response(
|
||||
requestOptions: RequestOptions(path: '/api/consultations'),
|
||||
statusCode: 200,
|
||||
data: mockData,
|
||||
));
|
||||
when(mockDio.fetch<Map<String, dynamic>>(any)).thenAnswer((invocation) async {
|
||||
capturedRequest = invocation.positionalArguments.first as RequestOptions;
|
||||
return Response<Map<String, dynamic>>(
|
||||
requestOptions: capturedRequest,
|
||||
statusCode: 200,
|
||||
data: mockData,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.getConsultationWithPersonalDiets();
|
||||
|
||||
expect(result, isA<ConsultationPaginationModel>());
|
||||
verify(mockDio.get(
|
||||
'/api/consultations',
|
||||
queryParameters: {
|
||||
'filters[plan][has_personal_diet][\$personal_diet]': '1',
|
||||
'filters[last_state][\$notIn][]': [
|
||||
'done',
|
||||
'charge_was_collected',
|
||||
'complained',
|
||||
'last_day_finished',
|
||||
'canceled_by_the_initiator',
|
||||
'inited'
|
||||
],
|
||||
'sort[]': 'start_date:desc',
|
||||
},
|
||||
)).called(1);
|
||||
expect(capturedRequest.path, '/api/consultations');
|
||||
expect(capturedRequest.method, 'GET');
|
||||
expect(capturedRequest.queryParameters, {
|
||||
r'filters[plan][has_personal_diet][$personal_diet]': '1',
|
||||
r'filters[last_state][$notIn][]': [
|
||||
'done',
|
||||
'charge_was_collected',
|
||||
'complained',
|
||||
'last_day_finished',
|
||||
'last_day_started',
|
||||
'canceled_by_the_initiator',
|
||||
'inited',
|
||||
],
|
||||
r'sort[]': ['start_date:desc'],
|
||||
});
|
||||
verify(mockDio.fetch<Map<String, dynamic>>(any)).called(1);
|
||||
});
|
||||
|
||||
test('getConsultationWithPersonalDiets - пробрасывает ошибку', () async {
|
||||
when(mockDio.get(
|
||||
any,
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
)).thenThrow(DioException(
|
||||
when(mockDio.fetch<Map<String, dynamic>>(any)).thenThrow(DioException(
|
||||
requestOptions: RequestOptions(path: '/api/consultations'),
|
||||
));
|
||||
|
||||
@@ -75,74 +68,70 @@ void main() {
|
||||
|
||||
test('attachPersonalDiet - успешное прикрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': 2};
|
||||
late RequestOptions capturedRequest;
|
||||
|
||||
when(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/attach_diet',
|
||||
data: anyNamed('data'),
|
||||
)).thenAnswer((_) async => Response(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/attach_diet'),
|
||||
statusCode: 200,
|
||||
));
|
||||
when(mockDio.fetch<void>(any)).thenAnswer((invocation) async {
|
||||
capturedRequest = invocation.positionalArguments.first as RequestOptions;
|
||||
return Response<void>(
|
||||
requestOptions: capturedRequest,
|
||||
statusCode: 200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
await service.attachPersonalDiet(consultationId, body);
|
||||
|
||||
verify(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/attach_diet',
|
||||
data: {'diet_id': dietId},
|
||||
)).called(1);
|
||||
expect(capturedRequest.path, '/api/chosen_plans/$consultationId/attach_diet');
|
||||
expect(capturedRequest.method, 'POST');
|
||||
expect(capturedRequest.data, body);
|
||||
verify(mockDio.fetch<void>(any)).called(1);
|
||||
});
|
||||
|
||||
test('attachPersonalDiet - пробрасывает ошибку', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': 2};
|
||||
|
||||
when(mockDio.post(
|
||||
any,
|
||||
data: anyNamed('data'),
|
||||
)).thenThrow(DioException(
|
||||
when(mockDio.fetch<void>(any)).thenThrow(DioException(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/attach_diet'),
|
||||
));
|
||||
|
||||
expect(
|
||||
() => service.attachPersonalDiet(consultationId: consultationId, dietId: dietId),
|
||||
() => service.attachPersonalDiet(consultationId, body),
|
||||
throwsA(isA<DioException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('reattachPersonalDiet - успешное повторное прикрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 3;
|
||||
final body = {'diet_id': 3};
|
||||
late RequestOptions capturedRequest;
|
||||
|
||||
when(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/re_attach_diet',
|
||||
data: anyNamed('data'),
|
||||
)).thenAnswer((_) async => Response(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/re_attach_diet'),
|
||||
statusCode: 200,
|
||||
));
|
||||
when(mockDio.fetch<void>(any)).thenAnswer((invocation) async {
|
||||
capturedRequest = invocation.positionalArguments.first as RequestOptions;
|
||||
return Response<void>(
|
||||
requestOptions: capturedRequest,
|
||||
statusCode: 200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
await service.reattachPersonalDiet(consultationId, body);
|
||||
|
||||
verify(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/re_attach_diet',
|
||||
data: {'diet_id': dietId},
|
||||
)).called(1);
|
||||
expect(capturedRequest.path, '/api/chosen_plans/$consultationId/re_attach_diet');
|
||||
expect(capturedRequest.method, 'POST');
|
||||
expect(capturedRequest.data, body);
|
||||
verify(mockDio.fetch<void>(any)).called(1);
|
||||
});
|
||||
|
||||
test('reattachPersonalDiet - пробрасывает ошибку', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 3;
|
||||
final body = {'diet_id': 3};
|
||||
|
||||
when(mockDio.post(
|
||||
any,
|
||||
data: anyNamed('data'),
|
||||
)).thenThrow(DioException(
|
||||
when(mockDio.fetch<void>(any)).thenThrow(DioException(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/re_attach_diet'),
|
||||
));
|
||||
|
||||
expect(
|
||||
() => service.reattachPersonalDiet(consultationId: consultationId, dietId: dietId),
|
||||
() => service.reattachPersonalDiet(consultationId, body),
|
||||
throwsA(isA<DioException>()),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,6 +61,47 @@ void main() {
|
||||
expect(ChosenPlanStateEnum.stateConfirmedByConsultant.isStateAfterPaidState(), isTrue);
|
||||
});
|
||||
|
||||
test('canShowCancelAction returns true for cancellable states', () {
|
||||
expect(ChosenPlanStateEnum.stateInited.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInCoordination.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.statePendingPayment.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.statePaid.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateStarted.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInProgress.canShowCancelAction, isTrue);
|
||||
});
|
||||
|
||||
test('canShowCancelAction returns false for non-cancellable states', () {
|
||||
expect(ChosenPlanStateEnum.personalDietSet.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayStarted.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayFinished.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateComplained.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateDone.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateChargeWasCollected.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateCanceledByTheInitiator.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.statePaymentFailed.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateConfirmedByConsultant.canShowCancelAction, isFalse);
|
||||
});
|
||||
|
||||
test('isCancelAfterPaidState returns true only for post-paid cancellable states', () {
|
||||
expect(ChosenPlanStateEnum.statePaid.isCancelAfterPaidState, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateStarted.isCancelAfterPaidState, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInProgress.isCancelAfterPaidState, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInited.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateInCoordination.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.statePendingPayment.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.statePaymentFailed.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateConfirmedByConsultant.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.personalDietSet.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayStarted.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayFinished.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateComplained.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateDone.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateChargeWasCollected.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateCanceledByTheInitiator.isCancelAfterPaidState, isFalse);
|
||||
});
|
||||
|
||||
test('getActiveStates returns correct states', () {
|
||||
final activeStates = ChosenPlanStateEnum.getActiveStates();
|
||||
|
||||
|
||||
@@ -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', () {
|
||||
@@ -198,6 +200,66 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('chosenOption', () {
|
||||
const plan = PlanModel(
|
||||
id: 1,
|
||||
slug: 'test-plan',
|
||||
name: 'Test Plan',
|
||||
image: 'https://example.com/image.jpg',
|
||||
duration: 7,
|
||||
state: 'active',
|
||||
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||
details: 'Test details',
|
||||
packetsCount: 3,
|
||||
packet1Options: PacketOptions(items: ['one'], price: 100, short: 'packet-1', hasPersonalDiet: false),
|
||||
packet2Options: PacketOptions(items: ['two'], price: 200, short: 'packet-2', hasPersonalDiet: true),
|
||||
packet3Options: PacketOptions(items: ['three'], price: 300, short: 'packet-3', hasPersonalDiet: false),
|
||||
author: Author(
|
||||
authorSlug: 'author',
|
||||
authorName: 'Author Name',
|
||||
authorProfileIcon: 'https://example.com/icon.jpg',
|
||||
authorIsMe: false,
|
||||
isEntitySubscribedByMe: null,
|
||||
authorPlaceInRating: null,
|
||||
),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
isFavorite: false,
|
||||
minPriceOfOptions: 100,
|
||||
maxPriceOfOptions: 300,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
ChosenPlanModel modelWith(String optionType) => ChosenPlanModel(
|
||||
id: 1,
|
||||
optionType: optionType,
|
||||
startDate: DateTime(2024, 1, 1),
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInited,
|
||||
chatId: 'chat-1',
|
||||
consultantId: 1,
|
||||
plan: plan,
|
||||
);
|
||||
|
||||
test('возвращает первую опцию для options1', () {
|
||||
expect(modelWith('options1').chosenOption?.short, 'packet-1');
|
||||
});
|
||||
|
||||
test('возвращает вторую опцию для options2', () {
|
||||
expect(modelWith('options2').chosenOption?.short, 'packet-2');
|
||||
});
|
||||
|
||||
test('возвращает третью опцию для options3', () {
|
||||
expect(modelWith('options3').chosenOption?.short, 'packet-3');
|
||||
});
|
||||
|
||||
test('возвращает null для неизвестного значения', () {
|
||||
expect(modelWith('unknown').chosenOption, isNull);
|
||||
expect(modelWith('1').chosenOption, isNull);
|
||||
expect(modelWith('').chosenOption, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseChosenPlan', () {
|
||||
test('parseChosenPlan создает ChosenPlanModel из Map', () {
|
||||
final json = {
|
||||
@@ -207,6 +269,7 @@ void main() {
|
||||
'last_state': 'initied',
|
||||
'chat_id': 'chat-1',
|
||||
'consultant_id': 1,
|
||||
'personal_diet_name': 'Индивидуальный план питания',
|
||||
'plan': {
|
||||
'id': 1,
|
||||
'slug': 'test-plan',
|
||||
|
||||
Reference in New Issue
Block a user