From 6a31880a85f0c841059cb4ae7f439b08fde10ff8 Mon Sep 17 00:00:00 2001 From: alex-chevski Date: Thu, 29 Oct 2026 17:27:12 +0300 Subject: [PATCH] fix(consultations): open chat from consultation card The chat icon on the consultant consultation card had an empty onTap handler. Resolve the conversation with the client via the chat API and navigate to it. related to #1106 --- lib/core/di/cubit_registrations.dart | 2 +- .../cubit/consultations_cubit.dart | 15 +++++- .../widgets/consultation_card_widget.dart | 18 ++++++- .../cubit/consultations_cubit_test.dart | 38 ++++++++++++++- .../consultation_card_widget_test.dart | 48 ++++++++++++++++++- 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index 05bfd7d1..19a1962b 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -263,7 +263,7 @@ class CubitRegistrations { ), ); sl.registerFactory( - () => ConsultationsCubit(consultationsRepository: sl()), + () => ConsultationsCubit(consultationsRepository: sl(), chatRepository: sl()), ); sl.registerFactory( () => ConsultantCommentsCubit(diaryRepository: sl()), diff --git a/lib/features/consultations/presentation/cubit/consultations_cubit.dart b/lib/features/consultations/presentation/cubit/consultations_cubit.dart index 80f8b206..ea830c88 100644 --- a/lib/features/consultations/presentation/cubit/consultations_cubit.dart +++ b/lib/features/consultations/presentation/cubit/consultations_cubit.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,7 +9,8 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultationsCubit extends Cubit { final ConsultationsRepository consultationsRepository; - ConsultationsCubit({required this.consultationsRepository}) : super(ConsultationsState()); + final ChatRepository chatRepository; + ConsultationsCubit({required this.consultationsRepository, required this.chatRepository}) : super(ConsultationsState()); Future loadConsultations() async { try { @@ -20,4 +22,15 @@ class ConsultationsCubit extends Cubit { emit(state.copyWith(state: BaseState.error)); } } + + Future getChatIdByUserSlug(String slug) async { + try { + final chatIdOrError = await chatRepository.getChatByUserSlug(slug); + return chatIdOrError == '404' ? null : chatIdOrError; + } catch (e, st) { + await reportFrom(e, st, this); + log(e.toString(), stackTrace: st); + return null; + } + } } diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 7efa25f7..e579c9b3 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -3,14 +3,17 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class ConsultationCardWidget extends StatefulWidget { @@ -34,6 +37,7 @@ class _ConsultationCardWidgetState extends State { final List legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)]; late final List<(int, DiaryModel)> givenDaysWithDayNumber; + bool _isOpeningChat = false; final List> legendStates = [ [DiaryStateEnum.init], [DiaryStateEnum.started, DiaryStateEnum.lastDayStarted], @@ -279,7 +283,7 @@ class _ConsultationCardWidgetState extends State { const SizedBox(width: 8), InkWell( borderRadius: BorderRadius.circular(20), - onTap: () {}, + onTap: _isOpeningChat ? null : _openChat, child: Ink( height: 52, width: 52, @@ -308,6 +312,18 @@ class _ConsultationCardWidgetState extends State { ); } + Future _openChat() async { + setState(() => _isOpeningChat = true); + final chatId = await context.read().getChatIdByUserSlug(widget.consultationsModel.userSlug); + if (!mounted) return; + setState(() => _isOpeningChat = false); + if (chatId == null) { + showErrorToast(S.of(context).msg_something_went_wrong); + return; + } + GoRouter.of(context).pushNamed('chatScreen', extra: {'uuid': chatId}); + } + Row _legendTitle(String text, Color color) { return Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/test/features/consultations/presentation/cubit/consultations_cubit_test.dart b/test/features/consultations/presentation/cubit/consultations_cubit_test.dart index 26ab5974..c9fe20b5 100644 --- a/test/features/consultations/presentation/cubit/consultations_cubit_test.dart +++ b/test/features/consultations/presentation/cubit/consultations_cubit_test.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart'; @@ -8,14 +9,21 @@ import 'package:mocktail/mocktail.dart'; class MockConsultationsRepository extends Mock implements ConsultationsRepository {} +class MockChatRepository extends Mock implements ChatRepository {} + void main() { group('ConsultationsCubit', () { late ConsultationsCubit cubit; late MockConsultationsRepository mockRepository; + late MockChatRepository mockChatRepository; setUp(() { mockRepository = MockConsultationsRepository(); - cubit = ConsultationsCubit(consultationsRepository: mockRepository); + mockChatRepository = MockChatRepository(); + cubit = ConsultationsCubit( + consultationsRepository: mockRepository, + chatRepository: mockChatRepository, + ); }); tearDown(() { @@ -47,5 +55,33 @@ void main() { expect(cubit.state.state, BaseState.error); }); + + test('getChatIdByUserSlug - возвращает id чата', () async { + when(() => mockChatRepository.getChatByUserSlug('john-doe')) + .thenAnswer((_) async => 'chat-uuid'); + + final chatId = await cubit.getChatIdByUserSlug('john-doe'); + + expect(chatId, 'chat-uuid'); + verify(() => mockChatRepository.getChatByUserSlug('john-doe')).called(1); + }); + + test('getChatIdByUserSlug - возвращает null при ошибке', () async { + when(() => mockChatRepository.getChatByUserSlug(any())) + .thenThrow(Exception('Error')); + + final chatId = await cubit.getChatIdByUserSlug('john-doe'); + + expect(chatId, isNull); + }); + + test('getChatIdByUserSlug - возвращает null когда чат не найден', () async { + when(() => mockChatRepository.getChatByUserSlug(any())) + .thenAnswer((_) async => '404'); + + final chatId = await cubit.getChatIdByUserSlug('john-doe'); + + expect(chatId, isNull); + }); }); } diff --git a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart index c897d8e2..c170f224 100644 --- a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart +++ b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart @@ -3,16 +3,21 @@ import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.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_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:network_image_mock/network_image_mock.dart'; +class MockConsultationsCubit extends Mock implements ConsultationsCubit {} + void main() { group('ConsultationCardWidget smoke', () { setUpAll(() async { @@ -64,7 +69,8 @@ void main() { personalDietId: null, ); - Widget wrap(Widget child) { + Widget wrap(Widget child, {ConsultationsCubit? cubit}) { + final consultationsCubit = cubit ?? MockConsultationsCubit(); return MaterialApp.router( theme: ThemeHelper().themeData(), localizationsDelegates: const [ @@ -77,7 +83,20 @@ void main() { routerConfig: GoRouter( initialLocation: '/', routes: [ - GoRoute(path: '/', builder: (context, state) => child), + GoRoute( + path: '/', + builder: (context, state) => BlocProvider.value( + value: consultationsCubit, + child: child, + ), + ), + GoRoute( + path: '/chat-screen', + name: 'chatScreen', + builder: (context, state) => Scaffold( + body: Text('chat:${(state.extra as Map)['uuid']}'), + ), + ), GoRoute( path: '/profile-screen', name: 'profileScreen', @@ -163,5 +182,30 @@ void main() { }); }, ); + + testWidgets('opens chat with consultation user when chat icon tapped', ( + tester, + ) async { + final cubit = MockConsultationsCubit(); + when(() => cubit.stream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.getChatIdByUserSlug('john-doe')) + .thenAnswer((_) async => 'chat-uuid'); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + wrap( + ConsultationCardWidget(consultationsModel: mockConsultation), + cubit: cubit, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.chat_outlined)); + await tester.pumpAndSettle(); + + expect(find.text('chat:chat-uuid'), findsOneWidget); + verify(() => cubit.getChatIdByUserSlug('john-doe')).called(1); + }); + }); }); }