fix(personal-diet): open client chat after diet assignment

- navigate to the client chat after a successful attach or reattach
- reset the stack so back returns to the chat list
- keep the diet screen on missing chat id and cover the flow with tests
This commit is contained in:
2026-11-05 18:28:16 +03:00
parent 16084ee94c
commit 57536aba06
2 changed files with 113 additions and 119 deletions

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
@@ -181,35 +182,8 @@ class ConsultationWidget extends StatelessWidget {
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
final result = await context
.read<PersonalDietSuggestionCubit>()
.reattachDietToConsultation(
consultationId: consultationModel.id,
);
AppRouter.navigatorKey.currentContext?.pop();
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionSuccessBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
await _attachDiet(context, reattach: true);
},
),
);
@@ -241,34 +215,7 @@ class ConsultationWidget extends StatelessWidget {
dietName: dietName,
onTap: () async {
AppRouter.navigatorKey.currentContext?.pop();
final result = await context
.read<PersonalDietSuggestionCubit>()
.attachDietToConsultation(
consultationId: consultationModel.id,
);
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionSuccessBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
await _attachDiet(context, reattach: false);
},
),
);
@@ -278,6 +225,41 @@ class ConsultationWidget extends StatelessWidget {
),
);
}
Future<void> _attachDiet(
BuildContext context, {
required bool reattach,
}) async {
final router = GoRouter.of(context);
final chatId = consultationModel.chatId;
final noChatMessage = S.of(context).msg_something_went_wrong;
final cubit = context.read<PersonalDietSuggestionCubit>();
final result = reattach
? await cubit.reattachDietToConsultation(
consultationId: consultationModel.id,
)
: await cubit.attachDietToConsultation(
consultationId: consultationModel.id,
);
if (!result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (chatId == null || chatId.isEmpty) {
showErrorToast(noChatMessage);
return;
}
router.goNamed('chatScreen', queryParameters: {'uuid': chatId});
}
}
class SuggestDietBottomSheet extends StatefulWidget {
@@ -336,65 +318,6 @@ class _SuggestDietBottomSheet extends State<SuggestDietBottomSheet> {
}
}
class SuggestionSuccessBottomSheet extends StatefulWidget {
const SuggestionSuccessBottomSheet({
super.key,
required this.userName,
required this.dietName,
});
final String userName;
final String dietName;
@override
State<SuggestionSuccessBottomSheet> createState() =>
_SuggestionSuccessBottomSheet();
}
class _SuggestionSuccessBottomSheet
extends State<SuggestionSuccessBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(
'Предложение диеты',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 16),
Text(
'«${widget.dietName}» предложена пользователю ${widget.userName}',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
CustomOutlinedButton(
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.pop();
},
),
const SizedBox(height: 40),
],
),
);
}
}
class SuggestionFailureBottomSheet extends StatefulWidget {
const SuggestionFailureBottomSheet({
super.key,

View File

@@ -35,6 +35,7 @@ void main() {
final mockConsultation = ConsultationModel(
id: 1,
chatId: 'chat-1',
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
planTitle: 'Test Plan',
userName: 'Test User',
@@ -69,6 +70,32 @@ void main() {
path: '/personal-diet-suggestion',
builder: (context, state) => const Scaffold(body: Text('Personal Diet')),
),
GoRoute(
path: '/chat-select-screen',
name: 'chatSelectScreen',
builder: (context, state) => const Scaffold(body: Text('Chats')),
),
GoRoute(
path: '/chat-screen',
name: 'chatScreen',
builder: (context, state) {
final uuid = state.uri.queryParameters['uuid'];
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/chat-select-screen');
},
),
),
body: Text('Chat $uuid'),
);
},
),
],
),
);
@@ -202,7 +229,7 @@ void main() {
});
});
testWidgets('shows success bottom sheet after successful diet attachment', (tester) async {
testWidgets('opens chat after successful diet attachment', (tester) async {
await mockNetworkImagesFor(() async {
final cubit = MockPersonalDietSuggestionCubit();
@@ -227,16 +254,22 @@ void main() {
await tester.tap(offerButton);
await tester.pumpAndSettle();
expect(find.text('«Test Diet» предложена пользователю Test User'), findsOneWidget);
expect(find.text('Chat chat-1'), findsOneWidget);
verify(cubit.attachDietToConsultation(consultationId: 1)).called(1);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Chats'), findsOneWidget);
});
});
testWidgets('shows success bottom sheet after successful diet reattachment', (tester) async {
testWidgets('opens chat after successful diet reattachment', (tester) async {
await mockNetworkImagesFor(() async {
final cubit = MockPersonalDietSuggestionCubit();
final consultationWithDiet = ConsultationModel(
id: 1,
chatId: 'chat-2',
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
planTitle: 'Test Plan',
userName: 'Test User',
@@ -274,9 +307,47 @@ void main() {
await tester.tap(offerButton);
await tester.pumpAndSettle();
expect(find.text('«Test Diet» предложена пользователю Test User'), findsOneWidget);
expect(find.text('Chat chat-2'), findsOneWidget);
verify(cubit.reattachDietToConsultation(consultationId: 1)).called(1);
verifyNever(cubit.attachDietToConsultation(consultationId: 1));
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Chats'), findsOneWidget);
});
});
testWidgets('stays on screen when chat id is missing after success', (tester) async {
await mockNetworkImagesFor(() async {
final cubit = MockPersonalDietSuggestionCubit();
when(cubit.state).thenReturn(PersonalDietSuggestionState());
when(cubit.stream).thenAnswer((_) => const Stream.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.attachDietToConsultation(consultationId: 1)).thenAnswer((_) async => true);
await tester.pumpWidget(
BlocProvider<PersonalDietSuggestionCubit>.value(
value: cubit,
child: wrap(
ConsultationWidget(
consultationModel: mockConsultation.copyWith(chatId: null),
dietName: 'Test Diet',
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Предложить диету'));
await tester.pumpAndSettle();
await tester.tap(find.text(S.of(tester.element(find.byType(ConsultationWidget))).toOffer));
await tester.pumpAndSettle();
expect(find.textContaining('Chat '), findsNothing);
expect(find.text('Test User'), findsOneWidget);
verify(cubit.attachDietToConsultation(consultationId: 1)).called(1);
});
});