From 0241265b3cf333fff3e68ba36bcab69d8aee4e9f Mon Sep 17 00:00:00 2001 From: alex-chevski Date: Fri, 6 Nov 2026 13:33:44 +0300 Subject: [PATCH] fix: align diary diet row and hide terminal cancel - show only the personal diet name in the diary and keep the plan day chip - hide consultation cancellation for terminal and non-cancellable states - enable the refund form only for paid, started and in-progress plans - cover diary rows and order info cancel visibility with tests --- .../widgets/chosen_diet_widget.dart | 20 +-------- .../presentation/info_by_order_screen.dart | 4 +- .../plans/enums/chosen_plan_state_enum.dart | 26 ++++++++++++ .../chosen_diet_widget_content_test.dart | 11 ++--- .../diary_chosen_widgets_smoke_test.dart | 11 +++++ .../info_by_order_screen_test.dart | 21 +++++++++- .../enums/chosen_plan_state_enum_test.dart | 41 +++++++++++++++++++ 7 files changed, 105 insertions(+), 29 deletions(-) diff --git a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart index 622e0e5e..19411307 100644 --- a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart +++ b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart @@ -4,7 +4,6 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; -import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -19,9 +18,7 @@ class ChosenDietWidget extends StatelessWidget { if (state is DiaryStateLoaded) { if (state.diary.typeOfDiary.hasChosenDiet) { return ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, dietId: state.diary.diet?.id ?? 0, ); } @@ -33,9 +30,7 @@ class ChosenDietWidget extends StatelessWidget { const CustomDivider(), const SizedBox(height: 16), ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, dietId: state.diary.diet?.id ?? 0, ), ], @@ -50,15 +45,11 @@ class ChosenDietWidget extends StatelessWidget { } class ChosenDietWidgetContent extends StatelessWidget { - const ChosenDietWidgetContent({super.key, required this.currentDietDay, required this.dietName, required this.dietDuration, required this.dietId}); - final int currentDietDay; + const ChosenDietWidgetContent({super.key, required this.dietName, required this.dietId}); final String dietName; - final int dietDuration; final int dietId; @override Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - final s = S.of(context); return GestureDetector( onTap: () { GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': dietId, 'navigateToComments': false}); @@ -67,19 +58,12 @@ class ChosenDietWidgetContent extends StatelessWidget { children: [ Row( children: [ - SizedBox( - width: size.width * 0.4, + Expanded( child: Text( dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black, decoration: TextDecoration.underline), ), ), - const Spacer(), - Container( - decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), - ), ], ), const SizedBox(height: 16), diff --git a/lib/features/order_info/presentation/info_by_order_screen.dart b/lib/features/order_info/presentation/info_by_order_screen.dart index c37913f5..40a5f8c2 100644 --- a/lib/features/order_info/presentation/info_by_order_screen.dart +++ b/lib/features/order_info/presentation/info_by_order_screen.dart @@ -164,7 +164,7 @@ class _InfoByOrderScreenContentState extends State { const SizedBox(height: 8), PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false), const SizedBox(height: 20), - if (widget.successfulPayment == null) + if (widget.successfulPayment == null && plan.chosenPlanState.canShowCancelAction) CustomOutlinedButton( text: s.cancelConsultation, height: 52, @@ -173,7 +173,7 @@ class _InfoByOrderScreenContentState extends State { onPressed: () async { showModalBottomSheet( context: context, - builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isStateAfterPaidState(), planId: plan.id), + builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isCancelAfterPaidState, planId: plan.id), ); }, ), diff --git a/lib/features/plans/enums/chosen_plan_state_enum.dart b/lib/features/plans/enums/chosen_plan_state_enum.dart index 2e867f38..fad8736c 100644 --- a/lib/features/plans/enums/chosen_plan_state_enum.dart +++ b/lib/features/plans/enums/chosen_plan_state_enum.dart @@ -62,6 +62,32 @@ enum ChosenPlanStateEnum { } } + bool get canShowCancelAction { + switch (this) { + case ChosenPlanStateEnum.stateInited: + case ChosenPlanStateEnum.stateInCoordination: + case ChosenPlanStateEnum.stateStartDateHasBeenSet: + case ChosenPlanStateEnum.statePendingPayment: + case ChosenPlanStateEnum.statePaid: + case ChosenPlanStateEnum.stateStarted: + case ChosenPlanStateEnum.stateInProgress: + return true; + default: + return false; + } + } + + bool get isCancelAfterPaidState { + switch (this) { + case ChosenPlanStateEnum.statePaid: + case ChosenPlanStateEnum.stateStarted: + case ChosenPlanStateEnum.stateInProgress: + return true; + default: + return false; + } + } + static List getActiveStates() { return [ ChosenPlanStateEnum.statePaid, diff --git a/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart b/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart index 0ef9081a..5481b27f 100644 --- a/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart +++ b/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart @@ -27,9 +27,7 @@ void main() { await tester.pumpWidget( createTestWidget( const ChosenDietWidgetContent( - currentDietDay: 5, dietName: 'Test Diet', - dietDuration: 14, dietId: 1, ), ), @@ -38,15 +36,14 @@ void main() { expect(find.byType(ChosenDietWidgetContent), findsOneWidget); expect(find.text('Test Diet'), findsOneWidget); + expect(find.textContaining('из 14'), findsNothing); }); - testWidgets('displays current day and duration', (tester) async { + testWidgets('does not display current day and duration', (tester) async { await tester.pumpWidget( createTestWidget( const ChosenDietWidgetContent( - currentDietDay: 7, dietName: 'Test Diet', - dietDuration: 21, dietId: 1, ), ), @@ -54,15 +51,14 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(ChosenDietWidgetContent), findsOneWidget); + expect(find.textContaining('из 21'), findsNothing); }); testWidgets('displays diet name with underline', (tester) async { await tester.pumpWidget( createTestWidget( const ChosenDietWidgetContent( - currentDietDay: 1, dietName: 'My Diet', - dietDuration: 7, dietId: 1, ), ), @@ -70,6 +66,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('My Diet'), findsOneWidget); + expect(find.textContaining('из 7'), findsNothing); }); }); } diff --git a/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart b/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart index dd7bc0f9..a5cbfaa1 100644 --- a/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart +++ b/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart @@ -100,6 +100,17 @@ void main() { await tester.pumpWidget(wrap(const ChosenDietWidget(), cubit)); await tester.pump(); expect(find.text('Diet'), findsOneWidget); + expect(find.textContaining('из 7'), findsNothing); + }); + + testWidgets('ChosenDietWidget hides diet day while plan keeps its day', (tester) async { + final cubit = FakeDiaryCubit(loadedState(hasDiet: true, hasPlan: true)); + await tester.pumpWidget(wrap(const Column(children: [ChosenPlanWidget(), ChosenDietWidget()]), cubit)); + await tester.pump(); + expect(find.text('Plan'), findsOneWidget); + expect(find.textContaining('из 30'), findsOneWidget); + expect(find.text('Diet'), findsOneWidget); + expect(find.textContaining('из 7'), findsNothing); }); testWidgets('AddProductButtonWidget renders for loaded state', (tester) async { diff --git a/test/features/order_info/presentation/info_by_order_screen_test.dart b/test/features/order_info/presentation/info_by_order_screen_test.dart index ea426152..2a34f6da 100644 --- a/test/features/order_info/presentation/info_by_order_screen_test.dart +++ b/test/features/order_info/presentation/info_by_order_screen_test.dart @@ -243,12 +243,12 @@ void main() { expect(find.byType(InfoByOrderScreen), findsOneWidget); }); - ChosenPlanModel donePlan() { + ChosenPlanModel donePlan({ChosenPlanStateEnum state = ChosenPlanStateEnum.stateDone}) { return ChosenPlanModel( id: 1, optionType: 'options1', startDate: DateTime(2024, 1, 1), - chosenPlanState: ChosenPlanStateEnum.stateDone, + chosenPlanState: state, chatId: 'chat-1', consultantId: 1, plan: const PlanModel( @@ -320,6 +320,7 @@ void main() { expect(find.byType(CollectChargeBanner), findsOneWidget); expect(find.text('Получить выплату'), findsOneWidget); + expect(find.text('Отменить консультацию'), findsNothing); }, ); @@ -337,6 +338,22 @@ void main() { expect(find.byType(CollectChargeBanner), findsNothing); expect(find.text('Получить выплату'), findsNothing); + expect(find.text('Отменить консультацию'), findsNothing); + }); + + testWidgets('shows cancel action for cancellable post-paid plan', ( + WidgetTester tester, + ) async { + when( + mockChatRepository.getChosenPlan(id: anyNamed('id')), + ).thenAnswer((_) async => donePlan(state: ChosenPlanStateEnum.stateStarted)); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(createTestWidget(planId: 1)); + await tester.pumpAndSettle(); + }); + + expect(find.text('Отменить консультацию'), findsOneWidget); }); }); } diff --git a/test/features/plans/enums/chosen_plan_state_enum_test.dart b/test/features/plans/enums/chosen_plan_state_enum_test.dart index 3c963836..a82ef455 100644 --- a/test/features/plans/enums/chosen_plan_state_enum_test.dart +++ b/test/features/plans/enums/chosen_plan_state_enum_test.dart @@ -61,6 +61,47 @@ void main() { expect(ChosenPlanStateEnum.stateConfirmedByConsultant.isStateAfterPaidState(), isTrue); }); + test('canShowCancelAction returns true for cancellable states', () { + expect(ChosenPlanStateEnum.stateInited.canShowCancelAction, isTrue); + expect(ChosenPlanStateEnum.stateInCoordination.canShowCancelAction, isTrue); + expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.canShowCancelAction, isTrue); + expect(ChosenPlanStateEnum.statePendingPayment.canShowCancelAction, isTrue); + expect(ChosenPlanStateEnum.statePaid.canShowCancelAction, isTrue); + expect(ChosenPlanStateEnum.stateStarted.canShowCancelAction, isTrue); + expect(ChosenPlanStateEnum.stateInProgress.canShowCancelAction, isTrue); + }); + + test('canShowCancelAction returns false for non-cancellable states', () { + expect(ChosenPlanStateEnum.personalDietSet.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateLastDayStarted.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateLastDayFinished.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateComplained.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateDone.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateChargeWasCollected.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateCanceledByTheInitiator.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.statePaymentFailed.canShowCancelAction, isFalse); + expect(ChosenPlanStateEnum.stateConfirmedByConsultant.canShowCancelAction, isFalse); + }); + + test('isCancelAfterPaidState returns true only for post-paid cancellable states', () { + expect(ChosenPlanStateEnum.statePaid.isCancelAfterPaidState, isTrue); + expect(ChosenPlanStateEnum.stateStarted.isCancelAfterPaidState, isTrue); + expect(ChosenPlanStateEnum.stateInProgress.isCancelAfterPaidState, isTrue); + expect(ChosenPlanStateEnum.stateInited.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateInCoordination.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.statePendingPayment.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.statePaymentFailed.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateConfirmedByConsultant.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.personalDietSet.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateLastDayStarted.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateLastDayFinished.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateComplained.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateDone.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateChargeWasCollected.isCancelAfterPaidState, isFalse); + expect(ChosenPlanStateEnum.stateCanceledByTheInitiator.isCancelAfterPaidState, isFalse); + }); + test('getActiveStates returns correct states', () { final activeStates = ChosenPlanStateEnum.getActiveStates();