Files
eatvio_app/lib/features/personal_diet_suggestion/presentation/consultation_widget.dart
alex-chevski 57536aba06 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
2026-11-05 18:28:16 +03:00

379 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/utils/url_utils.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
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';
class ConsultationWidget extends StatelessWidget {
const ConsultationWidget({
super.key,
required this.consultationModel,
required this.dietName,
this.dietDuration = 0,
});
final ConsultationModel consultationModel;
final String dietName;
final int dietDuration;
bool get _isDurationMismatch =>
dietDuration > 0 && consultationModel.duration != dietDuration;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
),
child: Column(
children: [
if (consultationModel.personalDietId == null)
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(
joinBaseUrl(
sl<AppConfig>().baseUrl,
consultationModel.userProfilePhotoUrl,
),
),
),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultationModel.userName,
style: TextStyles.textLSemibold.copyWith(
color: appTheme.black,
),
),
Text(
consultationModel.planTitle,
style: TextStyles.textMMedium.copyWith(
color: const Color(0xFF88949F),
),
),
Text(
s.plan_duration_label(
consultationModel.duration,
getDayForm(consultationModel.duration, s),
),
style: TextStyles.textMMedium.copyWith(
color: const Color(0xFF88949F),
),
),
Text(
s.start_date_label(
DateFormat('dd.MM.yyyy').format(
consultationModel.startDate,
),
),
style: TextStyles.textMMedium.copyWith(
color: const Color(0xFF88949F),
),
),
],
),
],
)
else
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(
joinBaseUrl(
sl<AppConfig>().baseUrl,
consultationModel.userProfilePhotoUrl,
),
),
),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultationModel.userName,
style: TextStyles.textLSemibold.copyWith(
color: appTheme.black,
),
),
Text(
consultationModel.planTitle,
style: TextStyles.textMMedium.copyWith(
color: const Color(0xFF88949F),
),
),
Text(
s.plan_duration_label(
consultationModel.duration,
getDayForm(consultationModel.duration, s),
),
style: TextStyles.textMMedium.copyWith(
color: const Color(0xFF88949F),
),
),
],
),
],
),
const SizedBox(height: 12),
if (_isDurationMismatch)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.error_outline, size: 18, color: appTheme.red),
const SizedBox(width: 8),
Expanded(
child: Text(
s.personal_diet_duration_mismatch(
dietDuration,
getDayForm(dietDuration, s),
consultationModel.duration,
getDayForm(consultationModel.duration, s),
),
style: TextStyles.textSMedium.copyWith(color: appTheme.red),
),
),
],
),
),
if (consultationModel.personalDietId != null)
CustomOutlinedButton(
text: s.changeDiet,
height: 52,
isDisabled: _isDurationMismatch,
buttonTextStyle: TextStyles.textMSemibold.copyWith(
color: appTheme.gray,
),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dividerColor),
),
onPressed: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestDietBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
AppRouter.navigatorKey.currentContext?.pop();
await _attachDiet(context, reattach: true);
},
),
);
},
)
else
CustomOutlinedButton(
text: s.suggestDiet,
height: 52,
isDisabled: _isDurationMismatch,
buttonTextStyle: TextStyles.textMSemibold.copyWith(
color: _isDurationMismatch ? appTheme.gray : appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(
_isDurationMismatch
? appTheme.dividerColor
: appTheme.blueA400,
),
),
onPressed: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestDietBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
AppRouter.navigatorKey.currentContext?.pop();
await _attachDiet(context, reattach: false);
},
),
);
},
),
],
),
);
}
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 {
const SuggestDietBottomSheet({
super.key,
required this.userName,
required this.dietName,
required this.onTap,
});
final String userName;
final String dietName;
final Future<void> Function() onTap;
@override
State<SuggestDietBottomSheet> createState() => _SuggestDietBottomSheet();
}
class _SuggestDietBottomSheet extends State<SuggestDietBottomSheet> {
@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.userName} «${widget.dietName}»',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
CustomOutlinedButton(
text: s.toOffer,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: widget.onTap,
),
const SizedBox(height: 40),
],
),
);
}
}
class SuggestionFailureBottomSheet extends StatefulWidget {
const SuggestionFailureBottomSheet({
super.key,
required this.userName,
required this.dietName,
});
final String userName;
final String dietName;
@override
State<SuggestionFailureBottomSheet> createState() =>
_SuggestionFailureBottomSheet();
}
class _SuggestionFailureBottomSheet
extends State<SuggestionFailureBottomSheet> {
@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),
],
),
);
}
}