Merge pull request 'feat(diary): New state for future days with diet in diary' (#465) from misha/eatvio_app:diary into main

Reviewed-on: eatvio/eatvio_app#465
This commit is contained in:
mso7nueh
2025-05-28 13:10:50 +00:00
89 changed files with 413 additions and 19 deletions

View File

@@ -44,11 +44,50 @@ abstract class DiaryDayModel with _$DiaryDayModel {
@JsonKey(name: "state") required TypeOfDiaryModel typeOfDiary,
@JsonKey(name: "meals") required List<MealModel> meals,
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
@JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet,
}) = _DiaryDayModel;
factory DiaryDayModel.fromJson(Map<String, dynamic> json) => _$DiaryDayModelFromJson(json);
}
@freezed
abstract class DiaryFutureDietDayModel with _$DiaryFutureDietDayModel {
const factory DiaryFutureDietDayModel({
@JsonKey(name: "date") required String date,
@JsonKey(name: "state", fromJson: _parseDayTypeForFutureDay) required TypeOfDiaryModel typeOfDiary,
@JsonKey(name: "meals") required List<MealModel> meals,
@JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet,
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
}) = _DiaryFutureDietDayModel;
factory DiaryFutureDietDayModel.fromJson(Map<String, dynamic> json) => _$DiaryFutureDietDayModelFromJson(json);
}
DiaryDietModel? _parseDiet(json) {
if (json == null) return null;
return DiaryDietModel.fromJson(json['diet']);
}
@freezed
abstract class DiaryDietModel with _$DiaryDietModel {
const factory DiaryDietModel({
required int id,
required int duration,
required String name,
}) = _DiaryDietModel;
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
}
TypeOfDiaryModel _parseDayTypeForFutureDay(json) {
return TypeOfDiaryModel(
hasChosenDiet: json['has_chosen_diet'] as bool,
hasChosenPlan: json['has_chosen_plan'] as bool,
diaryState: DiaryStateEnum.init,
);
}
@freezed
abstract class MealModel with _$MealModel {
const factory MealModel({

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
@@ -21,12 +22,10 @@ class DiaryCubit extends Cubit<DiaryState> {
final DiaryRepository diaryRepository;
final GlossaryRepository glossaryRepository;
final ProfileRepository profileRepository;
final DietRepository dietRepository;
int? diaryId;
DiaryCubit({
required this.diaryRepository,
required this.glossaryRepository,
required this.profileRepository,
}) : super(const DiaryState.loading()) {
DiaryCubit({required this.diaryRepository, required this.glossaryRepository, required this.profileRepository, required this.dietRepository})
: super(const DiaryState.loading()) {
today = DateTime.now();
selectedDay = today;
diaryRepository.forceFetchStream.listen((_) {
@@ -36,19 +35,117 @@ class DiaryCubit extends Cubit<DiaryState> {
String comment = '';
setSelectedDay(DateTime day) async {
Future<bool?> setSelectedDay(DateTime day, {bool? forDietOnly}) async {
selectedDay = day;
if (forDietOnly ?? false) {
final result = await fetchDiaryForDietOnly();
return result;
}
await fetchDiary(true);
return null;
}
setSelectedMeal(int meal) {
selectedMeal = meal;
emit((state as DiaryStateLoaded).copyWith(selectedMeal: selectedMeal));
final currentState = state;
if (currentState is DiaryStateLoaded) {
emit(currentState.copyWith(selectedMeal: selectedMeal));
} else if (currentState is DiaryStateLoadedTomorrowWithDiet) {
emit(currentState.copyWith(selectedMeal: selectedMeal));
}
}
Future<bool> fetchDiaryForDietOnly() async {
final stringDate = selectedDay.format('yyyy-MM-dd');
final diaryDay = await diaryRepository.getDiaryByDateForDietOnly(stringDate);
if (!diaryDay.typeOfDiary.hasChosenDiet) {
return false;
}
emit(const DiaryState.loading());
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
final meals = diet.dailySchedule[diaryDay.dayOfChosenDiet];
final firstMealPortions = meals?[1]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final secondMealPortions = meals?[2]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final thirdMealPortions = meals?[3]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final fourthMealPortions = meals?[4]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final fifthMealPortions = meals?[5]?.values.map((elem) {
return PortionModel(
id: 0,
productName: elem.name,
quantity: elem.quantity,
portion: elem.portion.getValue(),
fromDiet: true,
portionState: PortionState.filledDiet,
);
}).toList();
final firstMeal = MealModel(id: 0, time: '0', typeNumber: 1, satiety: 0, mood: 0, portions: firstMealPortions ?? [], photos: []);
final secondMeal = MealModel(id: 0, time: '0', typeNumber: 2, satiety: 0, mood: 0, portions: secondMealPortions ?? [], photos: []);
final thirdMeal = MealModel(id: 0, time: '0', typeNumber: 3, satiety: 0, mood: 0, portions: thirdMealPortions ?? [], photos: []);
final fourthMeal = MealModel(id: 0, time: '0', typeNumber: 4, satiety: 0, mood: 0, portions: fourthMealPortions ?? [], photos: []);
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
emit(
DiaryState.loadedTomorrowWithDiet(
diary: diaryDay.copyWith(
meals: [
firstMeal,
secondMeal,
thirdMeal,
fourthMeal,
fifthMeal,
],
),
selectedDay: selectedDay,
selectedMeal: selectedMeal,
),
);
return true;
}
return false;
}
fetchDiary([bool needLoading = false, int? diaryId]) async {
this.diaryId ??= diaryId;
await profileRepository.getChosenDiets();
if (needLoading) {
emit(const DiaryState.loading());
selectedMeal = 1;
@@ -58,7 +155,7 @@ class DiaryCubit extends Cubit<DiaryState> {
if (diaryId != null) {
diaryDay = await diaryRepository.getDiaryDay(diaryId);
} else {
final stringDate = selectedDay.toUtc().format('yyyy-MM-dd');
final stringDate = selectedDay.format('yyyy-MM-dd');
diaryDay = await diaryRepository.getDiaryByDate(stringDate);
}
ConsultantReviewModel? consultantReview;

View File

@@ -21,8 +21,14 @@ abstract class DiaryState with _$DiaryState {
required DateTime selectedDay,
required int selectedMeal,
@Default(false) bool isConsultantView,
ConsultantReviewModel? consultantReview,
ConsultantReviewModel? consultantReview,
@Default(false) bool isDiaryFinished,
@Default(false) bool isDiaryCommented,
}) = DiaryStateLoaded;
const factory DiaryState.loadedTomorrowWithDiet({
required DiaryFutureDietDayModel diary,
required DateTime selectedDay,
required int selectedMeal,
}) = DiaryStateLoadedTomorrowWithDiet;
}

View File

@@ -96,6 +96,11 @@ class DiaryScreenContent extends StatelessWidget {
showMood: state.diary.typeOfDiary.hasChosenPlan,
showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented,
),
if (state is DiaryStateLoadedTomorrowWithDiet)
const MealWidget(
showMood: false,
showAddButton: false,
),
if (state is DiaryStateLoaded) const CaloriesChartWidget(),
const SizedBox(height: 12),
Builder(builder: (context) {

View File

@@ -49,12 +49,34 @@ class _CalendarWidgetState extends State<CalendarWidget> {
leftChevronVisible: false,
rightChevronVisible: false,
),
onDaySelected: (selectedDay, focusedDay) {
onDaySelected: (selectedDay, focusedDay) async {
if (!isSameDay(_selectedDay, selectedDay)) {
final today = DateTime.now();
if (selectedDay.isAfter(today)) {
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
return;
if ((state is DiaryStateLoaded)) {
final result = await context.read<DiaryCubit>().setSelectedDay(_selectedDay, forDietOnly: true);
if (result == false && context.mounted) {
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
return;
}
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
return;
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
final result = await context.read<DiaryCubit>().setSelectedDay(_selectedDay, forDietOnly: true);
if (result == false && context.mounted) {
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
return;
}
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
return;
}
}
setState(() {
_selectedDay = selectedDay;

View File

@@ -0,0 +1,82 @@
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/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';
class ChosenDietWidget extends StatelessWidget {
const ChosenDietWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(builder: (context, state) {
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,
);
}
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
if (state.diary.typeOfDiary.hasChosenDiet) {
return Column(
children: [
const CustomDivider(),
const SizedBox(height: 16),
ChosenDietWidgetContent(
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
dietName: state.diary.diet?.name ?? '',
dietDuration: state.diary.diet?.duration ?? 0,
),
],
);
}
}
return const SizedBox.shrink();
});
}
}
class ChosenDietWidgetContent extends StatelessWidget {
const ChosenDietWidgetContent({
super.key,
required this.currentDietDay,
required this.dietName,
required this.dietDuration,
});
final int currentDietDay;
final String dietName;
final int dietDuration;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final s = S.of(context);
return Column(
children: [
Row(
children: [
SizedBox(
width: size.width * 0.4,
child: Text(dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
),
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,
)
],
);
}
}

View File

@@ -59,6 +59,32 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
),
);
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
return InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100),
child: Ink(
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
height: 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
),
child: Center(
child: Text(
'${widget.mealNumber}',
style: TextStyle(
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
return const SizedBox.shrink();
},
);

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/theme/theme_helper.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/features/diary/presentation/widgets/add_product_button_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/chosen_diet_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/comment_container.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_button_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart';
@@ -57,7 +58,8 @@ class _MealWidgetState extends State<MealWidget> {
),
const SizedBox(height: 12),
const TimeOfMealPickerWidget(),
const SizedBox(height: 12),
const ChosenDietWidget(),
const ProductsListWidget(),
if (widget.showAddButton) ...[
const SizedBox(height: 12),

View File

@@ -64,6 +64,18 @@ class _ProductWidgetState extends State<ProductWidget> {
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoadedTomorrowWithDiet) {
return ListTile(
horizontalTitleGap: 0,
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null,
title: Text(widget.portion.productName),
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF),
);
}
if (state is DiaryStateLoaded) {
final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||

View File

@@ -28,6 +28,18 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
],
);
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
int mealIndex = state.selectedMeal - 1;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
],
);
}
return const SizedBox.shrink();
},
);

View File

@@ -124,6 +124,7 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
),
),
const CustomDivider(),
const SizedBox(height: 12),
],
);
}

View File

@@ -27,6 +27,14 @@ class DiaryRepository {
}
}
Future<DiaryFutureDietDayModel> getDiaryByDateForDietOnly(String date) async {
try {
return (await diaryService.getDiaryIndexByDateForDietOnly(date: date)).last;
} catch (e) {
rethrow;
}
}
Future<List<DiaryDayModel>> getDiaryIndexByDate(String date) async {
return (await diaryService.getDiaryIndexByDate(date: date));
}

View File

@@ -19,6 +19,11 @@ abstract class DiaryService {
@Path('date') required String date,
});
@GET('/api/diary/index/{date}')
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({
@Path('date') required String date,
});
@GET('/api/diary/{diary}/show')
Future<DiaryDayWrapperModel> getDiaryDay({
@Path('diary') required int diary,