feat(personalDiets): diets suggestions

This commit is contained in:
Username1548
2025-07-14 13:44:44 +07:00
parent 0a3d9e577d
commit 5616bedd25
14 changed files with 340 additions and 14 deletions

View File

@@ -46,6 +46,7 @@ import 'package:eatvio_app/features/finances_and_consultations_archive/presentat
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
@@ -515,6 +516,12 @@ class AppRouter {
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
GoRoute(
path: PersonalDietSuggestionScreen.path,
name: PersonalDietSuggestionScreen.name,
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
),
],
);
}

View File

@@ -1,5 +1,5 @@
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
// String baseUrl = 'https://eatvio.ru';
String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];

View File

@@ -81,6 +81,9 @@ import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
@@ -170,6 +173,7 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -201,6 +205,7 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -234,6 +239,7 @@ Future<void> init() async {
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -22,6 +22,7 @@ abstract class ConsultationModel with _$ConsultationModel {
@JsonKey(name: 'end_date', fromJson: parseDate) required DateTime endDate,
@JsonKey(name: 'consultant_id') required int consultantId,
required List<DiaryModel> diaries,
@JsonKey(name: 'personal_diet_id') required int? personalDietId,
}) = _ConsultationModel;
factory ConsultationModel.fromJson(Map<String, dynamic> json) => _$ConsultationModelFromJson(json);

View File

@@ -16,6 +16,7 @@ import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.d
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -89,7 +90,9 @@ class _MyDietWidgetState extends State<MyDietWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
data: widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.diet.details.replaceAll('\n', '<br>'),
style: {
"*": Style(
margin: Margins.zero,
@@ -128,6 +131,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
}
} else if (state.selectedStateIndex == 2) {
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
} else {
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
}

View File

@@ -0,0 +1,67 @@
import 'dart:developer';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
final DietRepository dietRepository;
final PersonalDietSuggestionRepository personalDietSuggestionService;
late final int dietId;
PersonalDietSuggestionCubit({required this.dietRepository, required this.personalDietSuggestionService}) : super(PersonalDietSuggestionState());
Future<void> setDietName(int dietId) async {
try {
emit(state.copyWith(isLoading: true, error: null));
final diet = await dietRepository.getDiet(dietId);
emit(state.copyWith(isLoading: false, dietName: diet.name));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
Future<void> getConsultationWithPersonalDiets(int dietId) async {
try {
this.dietId = dietId;
emit(state.copyWith(isLoading: true, error: null));
setDietName(dietId);
final consultations = await personalDietSuggestionService.getConsultationWithPersonalDiets();
final consultationWithDiet = consultations.where((consultation) => consultation.personalDietId != null).toList();
final consultationWithoutDiet = consultations.where((consultation) => consultation.personalDietId == null).toList();
emit(state.copyWith(
isLoading: false,
consultationsWithDiet: consultationWithDiet,
consultationsWithoutDiet: consultationWithoutDiet,
));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
Future<bool> attachDietToConsultation({
required int consultationId,
}) async {
try {
await personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
}
}
Future<bool> reattachDietToConsultation({
required int consultationId,
}) async {
try {
await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'personal_diet_suggestion_state.freezed.dart';
@freezed
abstract class PersonalDietSuggestionState with _$PersonalDietSuggestionState {
factory PersonalDietSuggestionState({
@Default([]) List<ConsultationModel> consultationsWithDiet,
@Default([]) List<ConsultationModel> consultationsWithoutDiet,
@Default('') String dietName,
@Default(false) bool isLoading,
@Default(null) String? error,
}) = _PersonalDietSuggestionState;
}

View File

@@ -0,0 +1,58 @@
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: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:flutter_bloc/flutter_bloc.dart';
class ConsultationWidget extends StatelessWidget {
const ConsultationWidget({super.key, required this.consultationModel});
final ConsultationModel consultationModel;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: appTheme.whiteA700,
),
child: Column(
children: [
ListTile(
leading: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
child: Image.network(consultationModel.userProfilePhotoUrl),
),
),
const SizedBox(
height: 12,
),
if (consultationModel.personalDietId != null)
CustomOutlinedButton(
text: s.changeDiet,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.dividerColor)),
onPressed: () async {
context.read<PersonalDietSuggestionCubit>().reattachDietToConsultation(consultationId: consultationModel.id);
},
)
else
CustomOutlinedButton(
text: s.suggestDiet,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
},
)
],
),
);
}
}

View File

@@ -0,0 +1,66 @@
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/widgets/loader.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/consultation_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalDietSuggestionScreen extends StatelessWidget {
final int dietId;
const PersonalDietSuggestionScreen({super.key, required this.dietId});
static const path = '/personal-diet-suggestion';
static const name = 'personalDietSuggestion';
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<PersonalDietSuggestionCubit>()..getConsultationWithPersonalDiets(dietId),
child: const PersonalDietSuggestionScreenContent(),
);
}
}
class PersonalDietSuggestionScreenContent extends StatelessWidget {
const PersonalDietSuggestionScreenContent({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<PersonalDietSuggestionCubit, PersonalDietSuggestionState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(
'${s.dietSuggestion}\n«${state.dietName}»',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: state.isLoading
? const Center(child: Loader())
: SingleChildScrollView(
child: Column(
children: [
...List.generate(state.consultationsWithDiet.length, (index) {
return ConsultationWidget(
consultationModel: state.consultationsWithDiet[index],
);
}),
const SizedBox(
height: 12,
),
...List.generate(state.consultationsWithoutDiet.length, (index) {
return ConsultationWidget(
consultationModel: state.consultationsWithoutDiet[index],
);
})
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
class PersonalDietSuggestionRepository {
final PersonalDietSuggestionService personalDietSuggestionService;
PersonalDietSuggestionRepository({required this.personalDietSuggestionService});
Future<List<ConsultationModel>> getConsultationWithPersonalDiets() async{
try {
return (await personalDietSuggestionService.getConsultationWithPersonalDiets()).data;
} catch (e) {
rethrow;
}
}
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) {
try {
return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
} catch (e) {
rethrow;
}
}
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) {
try {
return personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,33 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
class PersonalDietSuggestionService {
final Dio dio;
PersonalDietSuggestionService(this.dio);
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'});
return ConsultationPaginationModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) async {
try {
await dio.post('api/chosen_plans/$consultationId/attach_diet', data: {'diet_id': dietId});
} catch (e) {
rethrow;
}
}
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) async {
try {
await dio.post('api/chosen_plans/$consultationId/re_attach_diet', data: {'diet_id': dietId});
} catch (e) {
rethrow;
}
}
}

View File

@@ -129,6 +129,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
),
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
"changeDiet": MessageLookupByLibrary.simpleMessage("Заменить диету"),
"chargeApproved": MessageLookupByLibrary.simpleMessage("Выплата одобрена"),
"chargeCollectionFailed": MessageLookupByLibrary.simpleMessage(
"Не удалось оформить выплату",
@@ -262,6 +263,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
"dietSuggestion": MessageLookupByLibrary.simpleMessage("Предложение диеты"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_has_been_sent_to_draft": MessageLookupByLibrary.simpleMessage(
"снята с публикации и перенесена в черновик",
@@ -677,6 +679,7 @@ class MessageLookup extends MessageLookupByLibrary {
"successfully_published": MessageLookupByLibrary.simpleMessage(
"успешно опубликован",
),
"suggestDiet": MessageLookupByLibrary.simpleMessage("Предложить диету"),
"suicide": MessageLookupByLibrary.simpleMessage("Склонение к самоубийству"),
"takes_pills": MessageLookupByLibrary.simpleMessage("Принимает лекарства"),
"tap_on_link_to_pay": MessageLookupByLibrary.simpleMessage(

View File

@@ -3395,16 +3395,6 @@ class S {
);
}
/// `Составление индивидуальной диеты`
String get personalDietCreating {
return Intl.message(
'Составление индивидуальной диеты',
name: 'personalDietCreating',
desc: '',
args: [],
);
}
/// `Скопировать`
String get copy {
return Intl.message('Скопировать', name: 'copy', desc: '', args: []);
@@ -3424,6 +3414,46 @@ class S {
args: [],
);
}
/// `Составление индивидуальной диеты`
String get personalDietCreating {
return Intl.message(
'Составление индивидуальной диеты',
name: 'personalDietCreating',
desc: '',
args: [],
);
}
/// `Предложение диеты`
String get dietSuggestion {
return Intl.message(
'Предложение диеты',
name: 'dietSuggestion',
desc: '',
args: [],
);
}
/// `Предложить диету`
String get suggestDiet {
return Intl.message(
'Предложить диету',
name: 'suggestDiet',
desc: '',
args: [],
);
}
/// `Заменить диету`
String get changeDiet {
return Intl.message(
'Заменить диету',
name: 'changeDiet',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -407,6 +407,9 @@
"reply": "Ответить",
"deleteComment": "Удалить комментарий"
,
"personalDietCreating": "Составление индивидуальной диеты"
"personalDietCreating": "Составление индивидуальной диеты",
"dietSuggestion": "Предложение диеты",
"suggestDiet": "Предложить диету",
"changeDiet": "Заменить диету"
}