feat(personalDiets): preapared chats for consultations with personal diets

This commit is contained in:
Username1548
2025-07-14 20:24:40 +07:00
parent 5616bedd25
commit a07d085ba7
10 changed files with 251 additions and 86 deletions

View File

@@ -110,25 +110,6 @@ class _ChatScreenState extends State<ChatScreen> {
ChosenPlanStateEnum.statePendingPayment,
ChosenPlanStateEnum.stateInCoordination,
].contains(chosenPlan?.chosenPlanState);
final showAboutOrderButton =
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
BannerType? bannerType;
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
bannerType = null;
} else if (amIConsultant && !state.hasCard) {
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;
}
PacketOptions? chosenOption;
if (chosenPlan?.optionType == 'options1') {
chosenOption = chosenPlan?.plan.packet1Options;
@@ -137,6 +118,27 @@ class _ChatScreenState extends State<ChatScreen> {
} else if (chosenPlan?.optionType == 'options3') {
chosenOption = chosenPlan?.plan.packet3Options;
}
final showAboutOrderButton =
state.chatRoom.chosenPlan != null && state.chatRoom.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 (chosenOption?.hasPersonalDiet == true && amIConsultant) {
bannerType = BannerType.setPersonalDiet;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) {
bannerType = BannerType.collectCharge;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
bannerType = BannerType.finishPlan;
}
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
@@ -330,6 +332,7 @@ class _ChatScreenState extends State<ChatScreen> {
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
),
),
],

View File

@@ -9,17 +9,28 @@ 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_outlined_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
class ChosePlanDateWidget extends StatefulWidget {
const ChosePlanDateWidget({super.key, required this.chatCubit});
const ChosePlanDateWidget({super.key, required this.chatCubit, required this.hasPersonalDiet});
final ChatRoomCubit chatCubit;
final bool hasPersonalDiet;
@override
State<ChosePlanDateWidget> createState() => _ChosePlanDateWidget();
}
class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
final startDate = [DateTime.now()];
late final List<DateTime> startDate;
bool visible = false;
@override
void initState() {
super.initState();
if (widget.hasPersonalDiet) {
startDate = [DateTime.now().add(const Duration(days: 2))];
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -27,64 +38,129 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
if (widget.chatCubit.state is ChatRoomStateLoaded) {
planName = (widget.chatCubit.state as ChatRoomStateLoaded).chatRoom.chosenPlan?.plan.name;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
const BottomSheetHandlerBar(),
const SizedBox(height: 12),
Text(
planName ?? '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
return GestureDetector(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
const BottomSheetHandlerBar(),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
planName ?? '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
),
if (widget.hasPersonalDiet) ...[
const SizedBox(
width: 8,
),
PortalTarget(
closeDuration: const Duration(milliseconds: 20),
anchor: const Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,
target: Alignment.bottomLeft,
),
portalFollower: Padding(
padding: const EdgeInsetsGeometry.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: visible ? appTheme.whiteA700 : Colors.transparent,
boxShadow: visible
? [
BoxShadow(
color: appTheme.black900.withValues(alpha: 0.15),
blurRadius: 4,
offset: const Offset(0, 4),
),
]
: null,
),
child: AnimatedDefaultTextStyle(
style: TextStyles.textMMediumAuto.copyWith(
color: visible ? appTheme.black : Colors.transparent,
),
duration: const Duration(milliseconds: 300),
child: Text(
s.chosenPlanStartDateDescr,
),
),
),
),
child: GestureDetector(
onTap: () {
setState(() {
visible = !visible;
});
},
child: SvgPicture.asset(
'assets/icons/question_mark.svg',
width: 22,
height: 22,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
)
]
],
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Text(
s.chose_plan_start_date_2,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Text(
s.chose_plan_start_date_2,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
),
),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: CustomDivider(),
),
CalendarWidget(
startDate: startDate,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
child: CustomOutlinedButton(
onPressed: () async {
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
if (result) {
showSuccessToast('Дата установлена');
if (!context.mounted) return;
Navigator.pop(context);
} else {
showErrorToast('Не удалось установить дату');
if (!context.mounted) return;
Navigator.pop(context);
}
},
height: 60,
text: s.confirm,
buttonTextStyle: TextStyles.textLSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: CustomDivider(),
),
CalendarWidget(
startDate: startDate,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
child: CustomOutlinedButton(
onPressed: () async {
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
if (result) {
showSuccessToast('Дата установлена');
if (!context.mounted) return;
Navigator.pop(context);
} else {
showErrorToast('Не удалось установить дату');
if (!context.mounted) return;
Navigator.pop(context);
}
},
height: 60,
text: s.confirm,
buttonTextStyle: TextStyles.textLSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
),
],
],
),
);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'
import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -17,12 +18,12 @@ import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class NotificationBanner extends StatefulWidget {
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId});
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId, this.hasPersonalDiet = false});
final BannerType bannerType;
final double? planPrice;
final String? planName;
final int? chosenPlanId;
final bool hasPersonalDiet;
@override
State<NotificationBanner> createState() => _NotificationBannerState();
}
@@ -68,7 +69,8 @@ class _NotificationBannerState extends State<NotificationBanner> {
BannerType.date => [
SizedBox(
width: size.width * 0.6,
child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
child: Text(widget.hasPersonalDiet == true ? s.choose_plan_start_date_with_personal_diet : s.choose_plan_start_date,
style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
@@ -78,6 +80,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
context: context,
builder: (context) => ChosePlanDateWidget(
chatCubit: cubit,
hasPersonalDiet: widget.hasPersonalDiet,
),
);
},
@@ -155,6 +158,20 @@ class _NotificationBannerState extends State<NotificationBanner> {
),
const SizedBox(height: 8),
],
BannerType.setPersonalDiet => [
SizedBox(
width: size.width * 0.6,
child: Text(s.createPersonalDiet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(MyDietsScreen.name);
},
child: Text(s.goToMyDiets, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
},
),
),
@@ -242,4 +259,4 @@ class _StarIcon extends StatelessWidget {
}
}
enum BannerType { date, card, payment, collectCharge, finishPlan }
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet }

View File

@@ -13,8 +13,15 @@ class CalendarWidget extends StatefulWidget {
class _CalendarWidgetState extends State<CalendarWidget> {
CalendarFormat _calendarFormat = CalendarFormat.week;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay = DateTime.now();
late DateTime _focusedDay;
DateTime? _selectedDay;
@override
void initState() {
super.initState();
_selectedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
_focusedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
}
@override
Widget build(BuildContext context) {
return Column(
@@ -23,7 +30,7 @@ class _CalendarWidgetState extends State<CalendarWidget> {
Padding(
padding: const EdgeInsets.only(top: 5),
child: TableCalendar(
firstDay: DateTime.now(),
firstDay: widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0],
lastDay: DateTime.utc(2030, 3, 14),
locale: 'ru_RU',
startingDayOfWeek: StartingDayOfWeek.monday,

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.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/widgets/custom_outlined_button.dart';
@@ -25,7 +26,7 @@ class ConsultationWidget extends StatelessWidget {
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
child: Image.network(consultationModel.userProfilePhotoUrl),
child: Image.network('$baseUrl/${consultationModel.userProfilePhotoUrl}'),
),
),
const SizedBox(

View File

@@ -8,7 +8,7 @@ class PersonalDietSuggestionService {
Future<ConsultationPaginationModel> getConsultationWithPersonalDiets() async {
try {
final response =
await dio.get('/api/consultations', queryParameters: {'filters[plan][has_personal_diet][\$personal_diet]': '1', 'sort[]': 'start_date:desc'});
await dio.get('/api/consultations', queryParameters: {'filters[plan][has_personal_diet]': '1', 'sort[]': 'start_date:desc'});
return ConsultationPaginationModel.fromJson(response.data);
} catch (e) {
rethrow;

View File

@@ -27,6 +27,8 @@ enum ChosenPlanStateEnum {
stateComplained,
@JsonValue('done')
stateDone,
@JsonValue('personal_diet_setted')
personalDietSet,
@JsonValue('charge_was_collected')
stateChargeWasCollected;
@@ -40,6 +42,7 @@ enum ChosenPlanStateEnum {
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.personalDietSet,
];
}
@@ -64,7 +67,8 @@ enum ChosenPlanStateEnum {
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateComplained,
ChosenPlanStateEnum.stateDone
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.personalDietSet
];
}
}

View File

@@ -150,6 +150,10 @@ class MessageLookup extends MessageLookupByLibrary {
"choose_plan_start_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старта плана питания",
),
"choose_plan_start_date_with_personal_diet":
MessageLookupByLibrary.simpleMessage(
"Выберите дату старта плана питания с учетом составления персональной диеты",
),
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
),
@@ -157,6 +161,9 @@ class MessageLookup extends MessageLookupByLibrary {
"chose_plan_start_date_2": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты плана. Она отобразиться в дневнике пользователя",
),
"chosenPlanStartDateDescr": MessageLookupByLibrary.simpleMessage(
"Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты",
),
"circumference_empty": MessageLookupByLibrary.simpleMessage(
"Все замеры обязательны к заполнению",
),
@@ -237,6 +244,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Не удалось обновить диету",
),
"country": MessageLookupByLibrary.simpleMessage("Страна"),
"createPersonalDiet": MessageLookupByLibrary.simpleMessage(
"Составьте персональную диету",
),
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
"cunsutation_was_paid_successfully": MessageLookupByLibrary.simpleMessage(
"Поздравляем, консультация успешно оплачена",
@@ -379,6 +389,9 @@ class MessageLookup extends MessageLookupByLibrary {
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
"general_info": MessageLookupByLibrary.simpleMessage("Общая информация"),
"goToMyDiets": MessageLookupByLibrary.simpleMessage(
"Перейти к моим диетам",
),
"go_to_diary": MessageLookupByLibrary.simpleMessage("Перейти в дневник"),
"gr": MessageLookupByLibrary.simpleMessage("гр"),
"grams": MessageLookupByLibrary.simpleMessage("гр"),

View File

@@ -3454,6 +3454,46 @@ class S {
args: [],
);
}
/// `Выберите дату старта плана питания с учетом составления персональной диеты`
String get choose_plan_start_date_with_personal_diet {
return Intl.message(
'Выберите дату старта плана питания с учетом составления персональной диеты',
name: 'choose_plan_start_date_with_personal_diet',
desc: '',
args: [],
);
}
/// `Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты`
String get chosenPlanStartDateDescr {
return Intl.message(
'Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты',
name: 'chosenPlanStartDateDescr',
desc: '',
args: [],
);
}
/// `Составьте персональную диету`
String get createPersonalDiet {
return Intl.message(
'Составьте персональную диету',
name: 'createPersonalDiet',
desc: '',
args: [],
);
}
/// `Перейти к моим диетам`
String get goToMyDiets {
return Intl.message(
'Перейти к моим диетам',
name: 'goToMyDiets',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -410,6 +410,10 @@
"personalDietCreating": "Составление индивидуальной диеты",
"dietSuggestion": "Предложение диеты",
"suggestDiet": "Предложить диету",
"changeDiet": "Заменить диету"
"changeDiet": "Заменить диету",
"choose_plan_start_date_with_personal_diet": "Выберите дату старта плана питания с учетом составления персональной диеты",
"chosenPlanStartDateDescr": "Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты",
"createPersonalDiet": "Составьте персональную диету",
"goToMyDiets": "Перейти к моим диетам"
}