forked from eatvio/eatvio_app
feat(chat): add consultation progress and personal diet status
related to #1115
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,11 +23,14 @@ 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/plans/models/chosen_plan_model.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/pages/video_call_screen.dart';
|
||||
@@ -63,8 +65,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 +188,23 @@ 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,
|
||||
);
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -142,12 +142,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)),
|
||||
),
|
||||
@@ -240,4 +265,4 @@ class _StarIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet }
|
||||
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet, changePersonalDiet }
|
||||
|
||||
@@ -20,7 +20,7 @@ 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,
|
||||
@@ -40,6 +40,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();
|
||||
|
||||
|
||||
31
lib/features/consultations/models/consultation_progress.dart
Normal file
31
lib/features/consultations/models/consultation_progress.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
});
|
||||
|
||||
final Set<ConsultationStep> completedSteps;
|
||||
final bool showProgressPlank;
|
||||
final PersonalDietStatus dietStatus;
|
||||
final ConsultationPhase phase;
|
||||
|
||||
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,25 @@ 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(
|
||||
s.personal_diet_proposed,
|
||||
style: TextStyles.textXSSemibold.copyWith(
|
||||
color: appTheme.greenA700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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 Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
for (var index = 0; index < _steps.length; index++) ...[
|
||||
_ProgressStepCircle(
|
||||
index: index,
|
||||
isCompleted: progress.isStepCompleted(_steps[index]),
|
||||
),
|
||||
if (index != _steps.length - 1)
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
color: progress.isStepCompleted(_steps[index])
|
||||
? appTheme.greenA700
|
||||
: appTheme.lightGray00,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final step in _steps)
|
||||
Expanded(
|
||||
child: Text(
|
||||
_stepLabel(S.of(context), step),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyles.textFS10FW500.copyWith(
|
||||
color: progress.isStepCompleted(step)
|
||||
? 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({
|
||||
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,57 @@
|
||||
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) {
|
||||
return (
|
||||
s.personal_diet_proposed,
|
||||
appTheme.transparentGreen,
|
||||
appTheme.greenA700,
|
||||
);
|
||||
}
|
||||
return (
|
||||
s.personal_diet_not_proposed,
|
||||
appTheme.gray100,
|
||||
appTheme.gray,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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,
|
||||
}) {
|
||||
return ConsultationProgress(
|
||||
completedSteps: _completedSteps(state),
|
||||
showProgressPlank: _showProgressPlank(state),
|
||||
dietStatus: _dietStatus(state, hasPersonalDiet),
|
||||
phase: _phase(state),
|
||||
);
|
||||
}
|
||||
|
||||
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.personalDietSet:
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,38 +9,50 @@ 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));
|
||||
}
|
||||
} 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 +61,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 +75,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);
|
||||
|
||||
@@ -11,6 +11,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';
|
||||
|
||||
@@ -64,7 +65,11 @@ class ConsultationWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Старт: ${consultationModel.userName}',
|
||||
s.start_date_label(
|
||||
DateFormat('dd.MM.yyyy').format(
|
||||
consultationModel.startDate,
|
||||
),
|
||||
),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: const Color(0xFF88949F),
|
||||
),
|
||||
@@ -130,7 +135,7 @@ class ConsultationWidget extends StatelessWidget {
|
||||
onTap: () async {
|
||||
final result = await context
|
||||
.read<PersonalDietSuggestionCubit>()
|
||||
.attachDietToConsultation(
|
||||
.reattachDietToConsultation(
|
||||
consultationId: consultationModel.id,
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,21 @@ abstract class ChosenPlanModel with _$ChosenPlanModel {
|
||||
factory ChosenPlanModel.fromJson(Map<String, dynamic> json) => _$ChosenPlanModelFromJson(json);
|
||||
}
|
||||
|
||||
extension ChosenPlanModelX on ChosenPlanModel {
|
||||
PacketOptions? get chosenOption {
|
||||
switch (optionType) {
|
||||
case 'options1':
|
||||
return plan.packet1Options;
|
||||
case 'options2':
|
||||
return plan.packet2Options;
|
||||
case 'options3':
|
||||
return plan.packet3Options;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChosenPlanModel? parseChosenPlan(dynamic json) {
|
||||
if (json is Map<String, dynamic>) {
|
||||
return ChosenPlanModel.fromJson(json);
|
||||
|
||||
@@ -30,24 +30,26 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
static String m3(percent, amount) =>
|
||||
"Discount ${percent}% — you save ${amount} ₽";
|
||||
|
||||
static String m4(step) => "шаг ${step} из 2";
|
||||
static String m4(date) => "Start: ${date}";
|
||||
|
||||
static String m5(planName, name) =>
|
||||
static String m5(step) => "шаг ${step} из 2";
|
||||
|
||||
static String m6(planName, name) =>
|
||||
"Завтра начинается план «${planName}» с пользователем ${name}";
|
||||
|
||||
static String m6(planName) =>
|
||||
static String m7(planName) =>
|
||||
"Завтра начнется прохождение плана «${planName}»";
|
||||
|
||||
static String m7(name, planName) =>
|
||||
static String m8(name, planName) =>
|
||||
"Пользователь ${name} отменил подписку на план «${planName}»";
|
||||
|
||||
static String m8(name, planName) =>
|
||||
static String m9(name, planName) =>
|
||||
"Пользователь ${name} оплатил план «${planName}»";
|
||||
|
||||
static String m9(name, planName) =>
|
||||
static String m10(name, planName) =>
|
||||
"Пользователь ${name} подписался на план «${planName}»";
|
||||
|
||||
static String m10(planName) => "Вы отменили подписку на план «${planName}»";
|
||||
static String m11(planName) => "Вы отменили подписку на план «${planName}»";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
@@ -170,6 +172,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(
|
||||
"Не удалось оформить выплату",
|
||||
@@ -635,6 +640,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("Статус заказа"),
|
||||
@@ -665,6 +673,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"personalDietCreating": MessageLookupByLibrary.simpleMessage(
|
||||
"Составление индивидуальной диеты",
|
||||
),
|
||||
"personal_diet_not_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Personal diet not proposed",
|
||||
),
|
||||
"personal_diet_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Personal diet proposed",
|
||||
),
|
||||
"personal_diet_replaces_tomorrow": MessageLookupByLibrary.simpleMessage(
|
||||
"The change takes effect tomorrow",
|
||||
),
|
||||
"personal_diet_set": MessageLookupByLibrary.simpleMessage(
|
||||
"Персональная диета установлена",
|
||||
),
|
||||
@@ -821,12 +838,13 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Выбрана дата старта",
|
||||
),
|
||||
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
|
||||
"start_date_label": m4,
|
||||
"statistic": MessageLookupByLibrary.simpleMessage("Статистика"),
|
||||
"step": MessageLookupByLibrary.simpleMessage("Шаг"),
|
||||
"step_by_step_photo_recipe": MessageLookupByLibrary.simpleMessage(
|
||||
"Пошаговый фоторецепт",
|
||||
),
|
||||
"step_widget": m4,
|
||||
"step_widget": m5,
|
||||
"successfully_published": MessageLookupByLibrary.simpleMessage(
|
||||
"успешно опубликован",
|
||||
),
|
||||
@@ -851,8 +869,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
|
||||
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
|
||||
"to_order": MessageLookupByLibrary.simpleMessage("Заказать"),
|
||||
"tommorowPlanWithUserWillStart": m5,
|
||||
"tomorrowYourPlanWillStart": m6,
|
||||
"tommorowPlanWithUserWillStart": m6,
|
||||
"tomorrowYourPlanWillStart": m7,
|
||||
"try_again": MessageLookupByLibrary.simpleMessage("Попробовать снова"),
|
||||
"try_again_2": MessageLookupByLibrary.simpleMessage("Попробуйте ещё раз"),
|
||||
"typeAmoutOfReturn": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -886,7 +904,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Верхние показатели давления:",
|
||||
),
|
||||
"upper": MessageLookupByLibrary.simpleMessage("Верхнее"),
|
||||
"userCanceldThePlanSubscription": m7,
|
||||
"userCanceldThePlanSubscription": m8,
|
||||
"userConfirmedPlanCompletion": MessageLookupByLibrary.simpleMessage(
|
||||
"Пользователь подтвердил прохождения плана. В ближайшее время Вы получите",
|
||||
),
|
||||
@@ -894,8 +912,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Пользователь подтвердил прохождения плана.",
|
||||
),
|
||||
"userPaidThePlan": m8,
|
||||
"userSubscribedToPlan": m9,
|
||||
"userPaidThePlan": m9,
|
||||
"userSubscribedToPlan": m10,
|
||||
"user_authorization": MessageLookupByLibrary.simpleMessage(
|
||||
"Аутентификация",
|
||||
),
|
||||
@@ -1025,7 +1043,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"youCanComplain": MessageLookupByLibrary.simpleMessage(
|
||||
"Если вас что-то не устроило, вы можете подать жалобу",
|
||||
),
|
||||
"youCanceledSubscriptionToPlan": m10,
|
||||
"youCanceledSubscriptionToPlan": m11,
|
||||
"youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage(
|
||||
"Вы уверены, что хотите отменить заказ?",
|
||||
),
|
||||
|
||||
@@ -23,6 +23,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
static String m3(percent, amount) =>
|
||||
"Скидка ${percent}% — вы экономите ${amount} ₽";
|
||||
|
||||
static String m4(date) => "Старт: ${date}";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"Moscow": MessageLookupByLibrary.simpleMessage("Москва"),
|
||||
@@ -66,6 +68,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 +84,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(
|
||||
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
|
||||
),
|
||||
@@ -126,6 +136,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"do_you_take_pills": MessageLookupByLibrary.simpleMessage(
|
||||
"Принимаете ли вы лекарства или бады?",
|
||||
),
|
||||
"done": MessageLookupByLibrary.simpleMessage("Завершен"),
|
||||
"down_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Нижние показатели давления:",
|
||||
),
|
||||
@@ -192,6 +203,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(
|
||||
@@ -284,6 +298,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(
|
||||
"Подтвердите пароль",
|
||||
@@ -297,8 +314,21 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"password_short": MessageLookupByLibrary.simpleMessage(
|
||||
"В пароле должно быть минимум 6 символов",
|
||||
),
|
||||
"payment_failed": MessageLookupByLibrary.simpleMessage("Оплата не прошла"),
|
||||
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
|
||||
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
|
||||
"personal_diet_not_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Диета не предложена",
|
||||
),
|
||||
"personal_diet_proposed": MessageLookupByLibrary.simpleMessage(
|
||||
"Диета предложена",
|
||||
),
|
||||
"personal_diet_replaces_tomorrow": MessageLookupByLibrary.simpleMessage(
|
||||
"Замена действует со следующего дня",
|
||||
),
|
||||
"personal_diet_set": MessageLookupByLibrary.simpleMessage(
|
||||
"Персональная диета установлена",
|
||||
),
|
||||
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
|
||||
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
|
||||
"place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"),
|
||||
@@ -364,7 +394,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
|
||||
),
|
||||
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
|
||||
"startDateHasBeenSet": MessageLookupByLibrary.simpleMessage(
|
||||
"Выбрана дата старта",
|
||||
),
|
||||
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
|
||||
"start_date_label": m4,
|
||||
"suggestDiet": MessageLookupByLibrary.simpleMessage("Предложить диету"),
|
||||
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
|
||||
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
|
||||
),
|
||||
|
||||
@@ -4443,6 +4443,66 @@ 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: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -526,6 +526,17 @@
|
||||
"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_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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,5 +271,26 @@
|
||||
"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_not_proposed": "Диета не предложена",
|
||||
"change_personal_diet": "Заменить диету",
|
||||
"personal_diet_replaces_tomorrow": "Замена действует со следующего дня",
|
||||
"start_date_label": "Старт: {date}",
|
||||
"@start_date_label": {
|
||||
"placeholders": {
|
||||
"date": {"type": "String"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,82 @@
|
||||
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(
|
||||
'при showProgressPlank=false и proposed видны заголовок и чип, плашки нет',
|
||||
(tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
expect(progress.showProgressPlank, isFalse);
|
||||
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.more_about_order), findsOneWidget);
|
||||
expect(find.byType(PersonalDietStatusChip), findsOneWidget);
|
||||
expect(find.text(s.personal_diet_proposed), findsOneWidget);
|
||||
expect(find.byType(ConsultationProgressPlank), findsNothing);
|
||||
expect(find.byIcon(Icons.check), findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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.personalDietSet,
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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}) {
|
||||
return PersonalDietStatusChip(
|
||||
progress: mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: hasPersonalDiet,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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('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,279 @@
|
||||
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: false,
|
||||
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, isFalse);
|
||||
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}',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -216,6 +216,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 +267,8 @@ void main() {
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockPersonalDietSuggestionRepository.getConsultationWithPersonalDiets()).called(2);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,6 +231,54 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('shows success bottom sheet after successful diet reattachment', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
final consultationWithDiet = ConsultationModel(
|
||||
id: 1,
|
||||
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('«Test Diet» предложена пользователю Test User'), findsOneWidget);
|
||||
verify(cubit.reattachDietToConsultation(consultationId: 1)).called(1);
|
||||
verifyNever(cubit.attachDietToConsultation(consultationId: 1));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('shows failure bottom sheet after failed diet attachment', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
@@ -279,7 +327,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 +366,7 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>()),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user