diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart index 6e08f6d0..e1ad4572 100644 --- a/lib/features/diary/models/diary_day_model.dart +++ b/lib/features/diary/models/diary_day_model.dart @@ -22,12 +22,7 @@ enum PortionState { @freezed abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel { - const factory DiaryDayWrapperModel({ - @JsonKey( - name: "diary", - ) - required DiaryDayModel diary, - }) = _DiaryDayWrapperModel; + const factory DiaryDayWrapperModel({@JsonKey(name: "diary") required DiaryDayModel diary}) = _DiaryDayWrapperModel; factory DiaryDayWrapperModel.fromJson(Map json) => _$DiaryDayWrapperModelFromJson(json); } @@ -71,21 +66,13 @@ DiaryDietModel? _parseDiet(dynamic json) { @freezed abstract class DiaryDietModel with _$DiaryDietModel { - const factory DiaryDietModel({ - required int id, - required int duration, - required String name, - }) = _DiaryDietModel; + const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel; factory DiaryDietModel.fromJson(Map json) => _$DiaryDietModelFromJson(json); } TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) { - return TypeOfDiaryModel( - hasChosenDiet: json['has_chosen_diet'] as bool, - hasChosenPlan: json['has_chosen_plan'] as bool, - diaryState: DiaryStateEnum.init, - ); + return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init); } @freezed @@ -115,11 +102,30 @@ abstract class DiaryPhotoModel with _$DiaryPhotoModel { factory DiaryPhotoModel.fromJson(Map json) => _$DiaryPhotoModelFromJson(json); } +@freezed +abstract class ProductCaloriesInfoModel with _$ProductCaloriesInfoModel { + const factory ProductCaloriesInfoModel({ + @JsonKey(name: "id") required int id, + @JsonKey(name: "product_name") required String productName, + @JsonKey(name: "energy_on_100gr") required double calories, + @JsonKey(name: "proteins_on_100gr") required double proteins, + @JsonKey(name: "fats_on_100gr") required double fats, + @JsonKey(name: "carbohydrates_on_100gr") required double carbohydrates, + @JsonKey(name: "energy_on_portion") required double energyOnPortion, + @JsonKey(name: "proteins_on_portion") required double proteinsOnPortion, + @JsonKey(name: "fats_on_portion") required double fatsOnPortion, + @JsonKey(name: "carbohydrates_on_portion") required double carbohydratesOnPortion, + }) = _ProductCaloriesInfoModel; + + factory ProductCaloriesInfoModel.fromJson(Map json) => _$ProductCaloriesInfoModelFromJson(json); +} + @freezed abstract class PortionModel with _$PortionModel { const factory PortionModel({ @JsonKey(name: "id") required int id, @JsonKey(name: "product_nutritional_value_id") int? productNutritionalValueId, + ProductCaloriesInfoModel? productCaloriesInfo, @JsonKey(name: "product_name") required String productName, @JsonKey(name: "quantity") required int quantity, @JsonKey(name: "portion") required String portion, diff --git a/lib/features/diary/presentation/widgets/meal_button_widget.dart b/lib/features/diary/presentation/widgets/meal_button_widget.dart index d8937b14..9e17c217 100644 --- a/lib/features/diary/presentation/widgets/meal_button_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_button_widget.dart @@ -1,16 +1,15 @@ import 'package:collection/collection.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.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/meal_summary_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MealButtonWidget extends StatefulWidget { final int mealNumber; - const MealButtonWidget({ - super.key, - required this.mealNumber, - }); + const MealButtonWidget({super.key, required this.mealNumber}); @override State createState() => _MealButtonWidgetState(); @@ -22,9 +21,7 @@ class _MealButtonWidgetState extends State { return BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded) { - final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull( - (element) => element.typeNo == widget.mealNumber, - ); + final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull((element) => element.typeNo == widget.mealNumber); final Color bgColor; if (state.selectedMeal == widget.mealNumber) { @@ -34,55 +31,84 @@ class _MealButtonWidgetState extends State { } else { bgColor = const Color(0xFFF1F2F6); } - return InkWell( - onTap: () { - context.read().setSelectedMeal(widget.mealNumber); - }, - borderRadius: BorderRadius.circular(100), - child: Ink( - width: MediaQuery.of(context).size.width / 5 - (96 / 5), - height: 32, - decoration: BoxDecoration( + + final mealIndex = widget.mealNumber - 1; + final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : []; + final totals = MealSummaryWidget.calculateMealTotals(portions); + final calories = totals['calories']!.round(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + context.read().setSelectedMeal(widget.mealNumber); + }, borderRadius: BorderRadius.circular(100), - color: bgColor, - ), - child: Center( - child: Text( - '${widget.mealNumber}', - style: TextStyle( - color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), - fontSize: 16, - fontWeight: FontWeight.w600, + child: Ink( + width: MediaQuery.of(context).size.width / 5 - (96 / 5), + height: 32, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: bgColor), + child: Center( + child: Text( + '${widget.mealNumber}', + style: TextStyle( + color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ), - ), + const SizedBox(height: 4), + Text( + calories > 0 ? '$calories ккал' : '— ккал', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ); } if (state is DiaryStateLoadedTomorrowWithDiet) { - return InkWell( - onTap: () { - context.read().setSelectedMeal(widget.mealNumber); - }, - borderRadius: BorderRadius.circular(100), - child: Ink( - width: MediaQuery.of(context).size.width / 5 - (96 / 5), - height: 32, - decoration: BoxDecoration( + + final mealIndex = widget.mealNumber - 1; + final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : []; + final totals = MealSummaryWidget.calculateMealTotals(portions); + final calories = totals['calories']!.round(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + context.read().setSelectedMeal(widget.mealNumber); + }, 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, + 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, + ), + ), ), ), ), - ), + const SizedBox(height: 4), + Text( + calories > 0 ? '$calories ккал' : '— ккал', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ); } return const SizedBox.shrink(); diff --git a/lib/features/diary/presentation/widgets/meal_summary_widget.dart b/lib/features/diary/presentation/widgets/meal_summary_widget.dart new file mode 100644 index 00000000..dd7108e6 --- /dev/null +++ b/lib/features/diary/presentation/widgets/meal_summary_widget.dart @@ -0,0 +1,112 @@ +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:flutter/material.dart'; + +class MealSummaryWidget extends StatelessWidget { + final List portions; + final int mealNumber; + + const MealSummaryWidget({super.key, required this.portions, required this.mealNumber}); + + @override + Widget build(BuildContext context) { + final totals = _calculateTotals(); + + if (totals['calories'] == 0 && totals['proteins'] == 0 && totals['fats'] == 0 && totals['carbohydrates'] == 0) { + return const SizedBox.shrink(); + } + + return Container( + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFF2F80ED), width: 2), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Прием $mealNumber', + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600), + ), + Row( + children: [ + _buildNutritionChip('К', totals['calories']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('Б', totals['proteins']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('Ж', totals['fats']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('У', totals['carbohydrates']!.round()), + ], + ), + ], + ), + ); + } + + Widget _buildNutritionChip(String label, int value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$label ', + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w600), + ), + TextSpan( + text: '$value', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + + Map _calculateTotals() { + return calculateMealTotals(portions); + } + + static Map calculateMealTotals(List portions) { + double totalCalories = 0; + double totalProteins = 0; + double totalFats = 0; + double totalCarbohydrates = 0; + + for (final portion in portions) { + if (portion.portionState == PortionState.eatenDeleted) { + continue; + } + + final caloriesInfo = portion.productCaloriesInfo; + if (caloriesInfo == null) { + continue; + } + + double calories, proteins, fats, carbohydrates; + + if (portion.portion == 'gr') { + calories = (caloriesInfo.calories * portion.quantity) / 100; + proteins = (caloriesInfo.proteins * portion.quantity) / 100; + fats = (caloriesInfo.fats * portion.quantity) / 100; + carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100; + } else { + calories = caloriesInfo.energyOnPortion; + proteins = caloriesInfo.proteinsOnPortion; + fats = caloriesInfo.fatsOnPortion; + carbohydrates = caloriesInfo.carbohydratesOnPortion; + } + + totalCalories += calories; + totalProteins += proteins; + totalFats += fats; + totalCarbohydrates += carbohydrates; + } + + return {'calories': totalCalories, 'proteins': totalProteins, 'fats': totalFats, 'carbohydrates': totalCarbohydrates}; + } +} diff --git a/lib/features/diary/presentation/widgets/meal_widget.dart b/lib/features/diary/presentation/widgets/meal_widget.dart index ce71ef66..21b7fe20 100644 --- a/lib/features/diary/presentation/widgets/meal_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_widget.dart @@ -40,11 +40,7 @@ class _MealWidgetState extends State { children: [ const Text( 'Прием пищи', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 18, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), const Row( @@ -57,23 +53,17 @@ class _MealWidgetState extends State { MealButtonWidget(mealNumber: 5), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), const TimeOfMealPickerWidget(), const ChosenDietWidget(), const ChosenPlanWidget(), const ProductsListWidget(), - if (widget.showAddButton) ...[ - const SizedBox(height: 12), - const AddProductButtonWidget(), - ], + if (widget.showAddButton) ...[const SizedBox(height: 12), const AddProductButtonWidget()], const SizedBox(height: 12), const PhotoGalleryWidget(), const SizedBox(height: 12), const SatietyRatingWidget(), - if (widget.showMood) ...[ - const SizedBox(height: 12), - const MoodRatingWidget(), - ], + if (widget.showMood) ...[const SizedBox(height: 12), const MoodRatingWidget()], BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) { @@ -91,10 +81,7 @@ class _MealWidgetState extends State { if (consultantCommentOfMeal != null) { return Column( children: [ - Text( - '${s.mealComment} ${state.selectedMeal}', - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + Text('${s.mealComment} ${state.selectedMeal}', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), const SizedBox(height: 12), CommentContainer(comment: consultantCommentOfMeal.comment), ], @@ -103,7 +90,7 @@ class _MealWidgetState extends State { } return const SizedBox.shrink(); }, - ) + ), ], ), ), diff --git a/lib/features/diary/presentation/widgets/product_widget.dart b/lib/features/diary/presentation/widgets/product_widget.dart index a221ffc6..cdf34f28 100644 --- a/lib/features/diary/presentation/widgets/product_widget.dart +++ b/lib/features/diary/presentation/widgets/product_widget.dart @@ -1,3 +1,4 @@ + import 'package:collection/collection.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -15,10 +16,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class ProductWidget extends StatefulWidget { final PortionModel portion; - const ProductWidget({ - super.key, - required this.portion, - }); + const ProductWidget({super.key, required this.portion}); @override State createState() => _ProductWidgetState(); @@ -37,6 +35,60 @@ class _ProductWidgetState extends State { isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; } + Widget _buildNutritionalInfo() { + if (widget.portion.productNutritionalValueId == null) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), + child: const Text( + 'Продукт отправлен на модерацию', + style: TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + } + + final caloriesInfo = widget.portion.productCaloriesInfo; + if (caloriesInfo == null) { + return const SizedBox.shrink(); + } + double calories, proteins, fats, carbohydrates; + + if (widget.portion.portion == 'gr') { + calories = (caloriesInfo.calories * widget.portion.quantity) / 100; + proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100; + fats = (caloriesInfo.fats * widget.portion.quantity) / 100; + carbohydrates = (caloriesInfo.carbohydrates * widget.portion.quantity) / 100; + } else { + calories = caloriesInfo.energyOnPortion; + proteins = caloriesInfo.proteinsOnPortion; + fats = caloriesInfo.fatsOnPortion; + carbohydrates = caloriesInfo.carbohydratesOnPortion; + } + + return Row( + children: [ + _buildNutritionChip('К', calories.round()), + const SizedBox(width: 4), + _buildNutritionChip('Б', proteins.round()), + const SizedBox(width: 4), + _buildNutritionChip('Ж', fats.round()), + const SizedBox(width: 4), + _buildNutritionChip('У', carbohydrates.round()), + ], + ); + } + + Widget _buildNutritionChip(String label, int value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), + child: Text( + '$label $value', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + } + @override void didUpdateWidget(ProductWidget oldWidget) { super.didUpdateWidget(oldWidget); @@ -48,15 +100,9 @@ class _ProductWidgetState extends State { return showModalBottomSheet( useRootNavigator: true, context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))), isScrollControlled: true, - builder: (context) => AddProductModal( - state: state, - portion: portion, - diaryCubit: diaryCubit, - ), + builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit), ); } @@ -69,15 +115,21 @@ class _ProductWidgetState extends State { 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), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), tileColor: const Color(0xFFF6FAFF), ); } if (state is DiaryStateLoaded) { - final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || + final isDiaryCommented = + state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; final isDiaryFinished = @@ -99,21 +151,20 @@ class _ProductWidgetState extends State { color: Color(0xFF6D6F71), ), child: ListTile( - onTap: () => diaryCubit.restore( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ), - trailing: SvgPicture.asset( - 'assets/icons/revert_product.svg', - ), + onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id), + trailing: SvgPicture.asset('assets/icons/revert_product.svg'), horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), // ignore: use_full_hex_values_for_flutter_colors tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01), ), @@ -122,12 +173,13 @@ class _ProductWidgetState extends State { Widget? trailing; if (state.isConsultantView && !isDiaryCommented) { trailing = GestureDetector( - onTap: () { - setState(() { - showCommentWidget = !showCommentWidget; - }); - }, - child: SvgPicture.asset('assets/icons/edit.svg')); + onTap: () { + setState(() { + showCommentWidget = !showCommentWidget; + }); + }, + child: SvgPicture.asset('assets/icons/edit.svg'), + ); } else if (!isDiaryCommented && !widget.portion.fromDiet) { trailing = const Icon(Icons.more_vert_rounded); } @@ -147,47 +199,36 @@ class _ProductWidgetState extends State { isEaten = value!; }); if (isEaten) { - diaryCubit.markEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); + diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); } else { - diaryCubit.unmarkEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); + diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); } } : null, activeColor: appTheme.blueA400, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - side: const BorderSide( - color: Color(0xFF809CB9), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + side: const BorderSide(color: Color(0xFF809CB9)), ), ) : null, horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), tileColor: const Color(0xFFF6FAFF), ), Padding( padding: const EdgeInsets.only(top: 8.0), - child: PortionCommentWidget( - enabled: showCommentWidget, - portionId: widget.portion.id, - comment: portionComment?.comment, - ), - ) + child: PortionCommentWidget(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment), + ), ], ); } diff --git a/lib/features/diary/presentation/widgets/products_list_widget.dart b/lib/features/diary/presentation/widgets/products_list_widget.dart index 6882af08..d3d7ac10 100644 --- a/lib/features/diary/presentation/widgets/products_list_widget.dart +++ b/lib/features/diary/presentation/widgets/products_list_widget.dart @@ -1,5 +1,6 @@ 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/meal_summary_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart'; import 'package:flutter/material.dart'; @@ -16,33 +17,33 @@ class _ProductsListWidgetState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (BuildContext context, DiaryState state) { + builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { int mealIndex = state.selectedMeal - 1; + final portions = state.diary.meals[mealIndex].portions; 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(), + for (int i = 0; i < portions.length; i++) ...[ + ProductWidget(portion: portions[i]), + i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], - if (state.isLoadingAddingProductInMeal) - const Padding( - padding: EdgeInsets.only(top: 8), - child: ProductLoaderWidget(), - ), + if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal), + if (state.isLoadingAddingProductInMeal) const Padding(padding: EdgeInsets.only(top: 8), child: ProductLoaderWidget()), ], ); } if (state is DiaryStateLoadedTomorrowWithDiet) { int mealIndex = state.selectedMeal - 1; + final portions = state.diary.meals[mealIndex].portions; 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(), + for (int i = 0; i < portions.length; i++) ...[ + ProductWidget(portion: portions[i]), + i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], + if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal), ], ); } diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 2ac30803..0165ad7f 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -15,13 +15,55 @@ class DiaryRepository { Stream get forceFetchStream => _diaryForceFetchController.stream; - DiaryRepository({ - required this.diaryService, - }); + DiaryRepository({required this.diaryService}); Future getDiaryByDate(String date) async { try { - return (await getDiaryIndexByDate(date)).last; + Set ids = {}; + final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last; + for (var meal in diaryDay.meals) { + for (var portion in meal.portions) { + if (portion.productNutritionalValueId != null) { + ids.add(portion.productNutritionalValueId!); + } + } + } + if (ids.isNotEmpty) { + final glossary = await getGlossariesIndex(ids: ids.toList()); + final List updatedMeals = List.from(diaryDay.meals); + for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) { + final meal = updatedMeals[mealIndex]; + final updatedPortions = meal.portions.map((portion) { + if (portion.productNutritionalValueId != null) { + try { + final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId); + return portion.copyWith( + productCaloriesInfo: ProductCaloriesInfoModel( + id: glossaryItem.id, + productName: glossaryItem.name, + calories: glossaryItem.energyOn100Gr, + proteins: glossaryItem.proteinsOn100Gr, + fats: glossaryItem.fatsOn100Gr, + carbohydrates: glossaryItem.carbohydratesOn100Gr, + energyOnPortion: glossaryItem.energyOnPortion, + proteinsOnPortion: glossaryItem.proteinsOnPortion, + fatsOnPortion: glossaryItem.fatsOnPortion, + carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion, + ), + ); + } catch (e) { + return portion; + } + } + return portion; + }).toList(); + + updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions); + } + final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals); + return updatedDiaryDay; + } + return diaryDay; } catch (e) { rethrow; } @@ -75,109 +117,47 @@ class DiaryRepository { int? productNutritionalValueId, required PortionState portionState, }) async { - Map data = ProductInMeal( - productName: productName, - portion: portion, - quantity: quantity, - ).toJson(); + Map data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson(); data['product_nutritional_value_id'] = productNutritionalValueId; data['state'] = portionState.name; - return await diaryService.updateProductInMeal( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - productInMeal: data, - ); + return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data); } - Future markEaten({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.markEaten( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future markEaten({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future unmarkEaten({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.unmarkEaten( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future restore({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.restore( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future restore({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future deleteProductFromMeal({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.deleteProductFromMeal( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future uploadPhotos({ - required int diaryId, - required int mealId, - required List files, - }) async { + Future uploadPhotos({required int diaryId, required int mealId, required List files}) async { try { for (int i = 0; i < files.length; i++) { - await diaryService.uploadPhotoOfMeal( - diary: diaryId, - typeNo: mealId, - photo: files[i], - ); + await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]); } } catch (e) { rethrow; } } - Future deletePhoto({ - required int diaryId, - required int mealId, - required DiaryPhotoModel photo, - }) async { + Future deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async { try { - await diaryService.deletePhotoOfMeal( - diaryId: diaryId, - typeNo: mealId, - userAttachment: photo.id, - ); + await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id); } catch (e) { rethrow; } } - Future finishDiaryDay({ - required int diaryId, - required String wakeUp, - required String retire, - String? comment, - }) async { + Future finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async { return await diaryService.finishDiaryDay( diary: diaryId, finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment), @@ -192,87 +172,45 @@ class DiaryRepository { required double carbohydrates, }) async { return await diaryService.sendProductToModeration( - data: ProductModerationModel( - name: name, - energy: energy, - proteins: proteins, - fats: fats, - carbohydrates: carbohydrates, - ).toJson(), + data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(), ); } - Future fillSatiety({ - required int diaryId, - required int mealId, - required int satiety, - }) async { + Future fillSatiety({required int diaryId, required int mealId, required int satiety}) async { try { - return await diaryService.fillSatiety( - diary: diaryId, - typeNo: mealId, - satiety: satiety, - ); + return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety); } catch (e) { rethrow; } } - Future fillMood({ - required int diaryId, - required int mealId, - required int mood, - }) async { + Future fillMood({required int diaryId, required int mealId, required int mood}) async { try { - return await diaryService.fillMood( - diary: diaryId, - typeNo: mealId, - mood: mood, - ); + return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood); } catch (e) { rethrow; } } - Future fillTime({ - required int diaryId, - required int mealId, - required String time, - }) async { + Future fillTime({required int diaryId, required int mealId, required String time}) async { try { - return await diaryService.fillTime( - diary: diaryId, - typeNo: mealId, - time: time, - ); + return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time); } catch (e) { rethrow; } } - Future fillWakeup({ - required int diaryId, - required String time, - }) async { + Future fillWakeup({required int diaryId, required String time}) async { try { - return await diaryService.fillWakeup( - diary: diaryId, - wakeUp: time, - ); + return await diaryService.fillWakeup(diary: diaryId, wakeUp: time); } catch (e) { rethrow; } } - Future fillRetire({ - required int diaryId, - required String time, - }) async { + Future fillRetire({required int diaryId, required String time}) async { try { - return await diaryService.fillRetire( - diary: diaryId, - retire: time, - ); + return await diaryService.fillRetire(diary: diaryId, retire: time); } catch (e) { rethrow; } @@ -287,10 +225,7 @@ class DiaryRepository { } } - Future getDiaryComments({ - required int planId, - required int diaryId, - }) async { + Future getDiaryComments({required int planId, required int diaryId}) async { try { return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); } catch (e) { @@ -305,4 +240,18 @@ class DiaryRepository { rethrow; } } + + Future getGlossariesIndex({required List ids}) async { + Map filters = {}; + if (ids.isNotEmpty) { + for (int i = 0; i < ids.length; i++) { + filters['filters[id][\$in][$i]'] = ids[i]; + } + } + try { + return await diaryService.getGlossariesIndex(filters: filters); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index d9967200..8cba6f33 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -15,32 +15,20 @@ abstract class DiaryService { factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService; @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDate({ - @Path('date') required String date, - }); + Future> getDiaryIndexByDate({@Path('date') required String date}); @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDateForDietOnly({ - @Path('date') required String date, - }); + Future> getDiaryIndexByDateForDietOnly({@Path('date') required String date}); @GET('/api/diary/{diary}/show') - Future getDiaryDay({ - @Path('diary') required int diary, - }); + Future getDiaryDay({@Path('diary') required int diary}); @GET('/api/diary/{diary}/dailyNutritionData') - Future getNutrition({ - @Path('diary') required int diary, - }); + Future getNutrition({@Path('diary') required int diary}); @POST('/api/diary/{diary}/meals/{type_no}/photo/upload') @MultiPart() - Future uploadPhotoOfMeal({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Part(name: 'photo') required File photo, - }); + Future uploadPhotoOfMeal({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Part(name: 'photo') required File photo}); @DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete') Future deletePhotoOfMeal({ @@ -68,25 +56,13 @@ abstract class DiaryService { }); @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten') - Future markEaten({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future markEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten') - Future unmarkEaten({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future unmarkEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore') - Future restore({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future restore({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete') Future deleteProductFromMeal({ @@ -96,63 +72,35 @@ abstract class DiaryService { }); @POST('/api/diary/{diary}/finishDay') - Future finishDiaryDay({ - @Path('diary') required int diary, - @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel, - }); + Future finishDiaryDay({@Path('diary') required int diary, @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel}); @POST('/api/diary/{diary}/meals/{type_no}/satiety/fill') - Future fillSatiety({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required int satiety, - }); + Future fillSatiety({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int satiety}); @POST('/api/diary/{diary}/meals/{type_no}/mood/fill') - Future fillMood({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required int mood, - }); + Future fillMood({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int mood}); @POST('/api/diary/{diary}/meals/{type_no}/time/fill') - Future fillTime({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required String time, - }); + Future fillTime({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required String time}); @POST('/api/diary/{diary}/wakeUp/fill') - Future fillWakeup({ - @Path('diary') required int diary, - @Field() required String wakeUp, - }); + Future fillWakeup({@Path('diary') required int diary, @Field() required String wakeUp}); @POST('/api/diary/{diary}/retire/fill') - Future fillRetire({ - @Path('diary') required int diary, - @Field() required String retire, - }); + Future fillRetire({@Path('diary') required int diary, @Field() required String retire}); @POST('/api/diary/product_nutritional_values/sendToModeration') - Future sendProductToModeration({ - @Body() required Map data, - }); + Future sendProductToModeration({@Body() required Map data}); @POST('/api/diary/{id}/consultant/comment') - Future commentUserDay({ - @Path('id') required int id, - @Body() required ConsultantCommentModel body, - }); + Future commentUserDay({@Path('id') required int id, @Body() required ConsultantCommentModel body}); @GET('/api/chosen_plans/{planId}/{diaryId}/comments/show') - Future getDiaryComments({ - @Path() required int planId, - @Path() required int diaryId, - }); + Future getDiaryComments({@Path() required int planId, @Path() required int diaryId}); @POST('/api/diary/{diaryId}/consultant/comment/view') - Future viewCommentOfDiaryDayByConsultant({ - @Path() required int diaryId, - }); + Future viewCommentOfDiaryDayByConsultant({@Path() required int diaryId}); + + @GET('/api/glossaries/index') + Future getGlossariesIndex({@Queries() Map? filters}); } diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index d124a9f9..d3ba615a 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -12,70 +12,57 @@ class FeedCubit extends Cubit { bool isLoading = false; String? _query; - FeedCubit({ - required this.feedRepository, - }) : super(const FeedState.loading()); + FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { - if (feedBlogPostModel == null) return; + if (feedBlogPostModel == null || isClosed) return; List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; final int index = feedBlogPostList.indexOf(feedBlogPostModel); feedBlogPostList[index] = feedBlogPostModel.copyWith( wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, ); - emit( - FeedState.loaded( - feedBlogPostList: feedBlogPostList, - isLoading: isLoading, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading)); + } } - Future loadFeedBlogPostList({ - String? query, - }) async { + Future loadFeedBlogPostList({String? query}) async { try { _query = query; final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query); lastPage = feedBlogPosts.meta.lastPage; - emit( - FeedState.loaded( - feedBlogPostList: feedBlogPosts.feedBlogPostList, - isLoading: false, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false)); + } } catch (e) { - emit(FeedState.error(errorMessage: e.toString())); + if (!isClosed) { + emit(FeedState.error(errorMessage: e.toString())); + } } } Future loadNextPage() async { - if (currentPage < lastPage && !isLoading) { + if (currentPage < lastPage && !isLoading && !isClosed) { isLoading = true; - emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); + if (!isClosed) { + emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); + } try { - if (currentPage < lastPage) { + if (currentPage < lastPage && !isClosed) { currentPage++; - final feedBlogPosts = await feedRepository.getFeedBlogPosts( - page: currentPage, - query: _query, - ); + final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage, query: _query); isLoading = false; - emit( - FeedState.loaded( - feedBlogPostList: [ - ...(state as FeedStateLoaded).feedBlogPostList, - ...feedBlogPosts.feedBlogPostList, - ], - isLoading: isLoading, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading)); + } } } catch (e) { - emit(FeedState.error(errorMessage: e.toString())); + if (!isClosed) { + emit(FeedState.error(errorMessage: e.toString())); + } } } } diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 9f30ca33..b00df3ea 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -40,6 +40,12 @@ class _RegistrationScreenState extends State { final pageController = PageController(); final RegistrationCubit registrationCubit = sl(); + @override + void initState() { + super.initState(); + RegistrationAnalyticsService.reportRegistrationStep(step); + } + @override Widget build(BuildContext context) { return BlocProvider(