diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index c82f70fb..15bb5270 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -122,7 +122,7 @@ Future init() async { sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); - sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl())); + sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); diff --git a/lib/features/create_plan/models/create_plan_model.dart b/lib/features/create_plan/models/create_plan_model.dart index 4b85ffba..623bcae4 100644 --- a/lib/features/create_plan/models/create_plan_model.dart +++ b/lib/features/create_plan/models/create_plan_model.dart @@ -1,7 +1,3 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -20,20 +16,5 @@ class CreatePlanModel with _$CreatePlanModel { required List type, XFile? image, required List options, - }) = _CreatePlanModel; - - Future> toCreationJson() async => { - 'name': name, - 'duration': duration, - 'state': 'active', - 'description_md': details, - 'type[slim]': type[0] ? '1' : '0', - 'type[fit]': type[1] ? '1' : '0', - 'type[wellness]': type[2] ? '1' : '0', - 'type[behavior]': type[3] ? '1' : '0', - 'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image!.path))).path, filename: image!.name) : null, - }; - - Map toOptionCreationJson() => {'packet1_options': options[0]}; - Map toOptionsCreationJson() => {'packet1_options': options[0], 'packet2_options': options[1], 'packet3_options': options[2]}; + }) = _CreatePlanModel; } diff --git a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart index 7239c657..96385904 100644 --- a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart +++ b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart @@ -1,4 +1,9 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -386,17 +391,63 @@ class CreatePlanCubit extends Cubit { } Future publishPlan() async { - if (validateAllFields()) { - emit(state.copyWith(isLoading: true)); - final planModel = await state.planData.toCreationJson(); - final plan = await createPlanRepository.createPlan(model: planModel); - if (state.planData.options[1].short.isNotEmpty) { - final options = state.planData.toOptionsCreationJson(); - await createPlanRepository.createPacketOptions(slug: plan.slug, model: options); - } else { - final option = state.planData.toOptionCreationJson(); - await createPlanRepository.createPacketOption(slug: plan.slug, model: option); + try { + if (validateAllFields()) { + emit(state.copyWith(isLoading: true)); + final planMap = {}; + + planMap.addAll({ + 'name': state.planData.name, + 'duration': state.planData.duration, + 'state': 'active', + 'description_md': state.planData.details, + 'type[slim]': state.planData.type[0] ? '1' : '0', + 'type[fit]': state.planData.type[1] ? '1' : '0', + 'type[wellness]': state.planData.type[2] ? '1' : '0', + 'type[behavior]': state.planData.type[3] ? '1' : '0', + 'image': state.planData.image != null + ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(state.planData.image!.path))).path, + filename: state.planData.image!.name) + : null + }); + if (state.planData.options[1].short.isNotEmpty) { + planMap.addAll({ + 'packets_count': 3, + 'packet1_options': { + 'short': state.planData.options[0].short, + 'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)), + 'description_md': state.planData.options[0].description, + 'items': Map.fromIterable(state.planData.options[0].items), + }, + 'packet2_options': { + 'short': state.planData.options[1].short, + 'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)), + 'description_md': state.planData.options[1].description, + 'items': Map.fromIterable(state.planData.options[1].items), + }, + 'packet3_options': { + 'short': state.planData.options[2].short, + 'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)), + 'description_md': state.planData.options[2].description, + 'items': Map.fromIterable(state.planData.options[2].items), + }, + }); + } else { + planMap.addAll({ + 'packets_count': 1, + 'packet1_options': { + 'short': state.planData.options[0].short, + 'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)), + 'description_md': state.planData.options[0].description, + 'items': Map.fromIterable(state.planData.options[0].items), + }, + }); + } + await createPlanRepository.createPlan(model: planMap); + emit(state.copyWith(isLoading: false, planCreated: true)); } + } catch (e) { + log(e.toString()); emit(state.copyWith(isLoading: false, planCreated: true)); } } diff --git a/lib/features/create_plan/repositories/create_plan_repository.dart b/lib/features/create_plan/repositories/create_plan_repository.dart index 09a4fcb6..96e961d7 100644 --- a/lib/features/create_plan/repositories/create_plan_repository.dart +++ b/lib/features/create_plan/repositories/create_plan_repository.dart @@ -1,5 +1,4 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/features/create_plan/models/plan_slug_model.dart'; import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; class CreatePlanRepository { @@ -7,24 +6,10 @@ class CreatePlanRepository { CreatePlanRepository({required this.createPlanService}); - Future createPlan({ + Future createPlan({ required Map model, }) async { final formData = FormData.fromMap(model); return await createPlanService.createPlan(formData: formData); - } - - Future createPacketOption({ - required String slug, - required Map model, - }) async { - await createPlanService.createPacketOption(slug: slug, model: model); - } - - Future createPacketOptions({ - required String slug, - required Map model, - }) async { - await createPlanService.createPacketOptions(slug: slug, model: model); - } + } } diff --git a/lib/features/create_plan/services/create_plan_service.dart b/lib/features/create_plan/services/create_plan_service.dart index 513d39b4..7236a956 100644 --- a/lib/features/create_plan/services/create_plan_service.dart +++ b/lib/features/create_plan/services/create_plan_service.dart @@ -1,5 +1,4 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/features/create_plan/models/plan_slug_model.dart'; import 'package:retrofit/retrofit.dart'; part 'create_plan_service.g.dart'; @@ -8,21 +7,10 @@ part 'create_plan_service.g.dart'; abstract class CreatePlanService { factory CreatePlanService(Dio dio, {String baseUrl}) = _CreatePlanService; - @POST('/api/plans/create') + @POST('/api/plans/create/published') @MultiPart() - Future createPlan({ + Future createPlan({ @Body() required FormData formData, }); - - @POST('/api/plans/create/{slug}/packet_option') - Future createPacketOption({ - @Path() required String slug, - @Body() required Map model, - }); - - @POST('/api/plans/create/{slug}/packet_options') - Future createPacketOptions({ - @Path() required String slug, - @Body() required Map model, - }); + } diff --git a/lib/features/create_recipe/models/ingredient.dart b/lib/features/create_recipe/models/ingredient.dart index 26c979e6..a891a529 100644 --- a/lib/features/create_recipe/models/ingredient.dart +++ b/lib/features/create_recipe/models/ingredient.dart @@ -10,6 +10,7 @@ class Ingredient with _$Ingredient { required String name, required String unit, required String portion, + @JsonKey(name: 'product_nutritional_id') required int productId, }) = _Ingredient; factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 0aaf687f..43273ae6 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -7,15 +7,18 @@ import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; class CreateRecipeCubit extends Cubit { final RecipeRepository recipeRepository; - + final GlossaryRepository glossaryRepository; CreateRecipeCubit({ required this.recipeRepository, + required this.glossaryRepository, }) : super( const CreateRecipeState( loading: true, @@ -63,6 +66,12 @@ class CreateRecipeCubit extends Cubit { emit(state.copyWith.entity(ingredients: ingredients)); } + void updateIngredient({required int index, required i.Ingredient ingredient}) { + final ingredients = [...state.entity.ingredients]; + ingredients[index] = ingredient; + emit(state.copyWith.entity(ingredients: ingredients)); + } + replaceIngredientPosition(int oldIndex, int newIndex) { List ingredients = [...state.entity.ingredients]; final item = ingredients.removeAt(oldIndex); @@ -201,7 +210,14 @@ class CreateRecipeCubit extends Cubit { description: recipe.description, image: file, videoUrl: recipe.videoUrl, - ingredients: recipe.ingredients!.map((e) => i.Ingredient(name: e.name, unit: e.unit!, portion: e.portion)).toList(), + ingredients: recipe.ingredients! + .map((e) => i.Ingredient( + name: e.name, + unit: e.unit!, + portion: e.portion, + productId: e.productId, + )) + .toList(), steps: steps, tags: recipe.tags.map((e) => (e.name)).toList(), ), @@ -306,4 +322,8 @@ class CreateRecipeCubit extends Cubit { return false; } } + + Future> getGlossaryListByQuery(String query) async { + return await glossaryRepository.getGlossaryListByQuery(query); + } } diff --git a/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart b/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart new file mode 100644 index 00000000..274ef55d --- /dev/null +++ b/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart @@ -0,0 +1,314 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; +import 'package:flutter/material.dart'; + +class AddPortionWidget extends StatefulWidget { + const AddPortionWidget({ + super.key, + required this.createRecipeCubit, + this.portionIndex, + this.productNameControllerText, + this.quantityControllerText, + }); + final CreateRecipeCubit createRecipeCubit; + final String? productNameControllerText; + final String? quantityControllerText; + final int? portionIndex; + @override + State createState() => _AddPortionWidgetState(); +} + +class _AddPortionWidgetState extends State { + GlossaryModel? selectedGlossary; + final productNameController = TextEditingController(); + final quantityController = TextEditingController(); + List glossariesList = []; + + @override + void initState() { + super.initState(); + productNameController.text = widget.productNameControllerText ?? ''; + quantityController.text = widget.quantityControllerText ?? ''; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16) + + EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Добавление блюда', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF323F4B), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.8 - 32, + child: CustomTextFormField( + controller: productNameController, + autofocus: true, + hintText: 'Введите название продукта/блюда', + onChanged: (query) { + EasyDebounce.debounce( + 'glossary-search', + const Duration(milliseconds: 250), + () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; + selectedGlossary = null; + setState(() {}); + return; + } + glossariesList = await widget.createRecipeCubit.getGlossaryListByQuery(query); + setState(() {}); + }, + ); + }, + ), + ), + const SizedBox(width: 4), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2 - 4, + child: CustomTextFormField( + controller: quantityController, + keyboardType: TextInputType.number, + hintText: selectedGlossary == null + ? '100' + : selectedGlossary!.grInPortion == 0 + ? '100' + : '1', + suffixText: selectedGlossary == null + ? 'гр' + : selectedGlossary?.grInPortion == 0 + ? 'гр' + : 'шт', + ), + ), + ], + ), + glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(), + Container( + constraints: const BoxConstraints( + maxHeight: 256, + ), + child: ListView.separated( + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, i) { + return ListTile( + onTap: () async { + setState(() { + selectedGlossary = glossariesList[i]; + productNameController.text = selectedGlossary!.name; + }); + glossariesList = []; + setState(() {}); + FocusManager.instance.primaryFocus?.nextFocus(); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: Text( + glossariesList[i].name, + style: const TextStyle( + height: 1, + ), + ), + subtitle: glossariesList[i].grInPortion == 0 + ? null + : Text( + '${glossariesList[i].grInPortion} г', + style: TextStyle( + height: 1, + color: Colors.grey.shade600, + ), + ), + trailing: Icon( + Icons.add_rounded, + color: Colors.grey.shade400, + ), + contentPadding: const EdgeInsets.only(left: 12), + ); + }, + itemCount: glossariesList.length, + separatorBuilder: (BuildContext context, int index) { + return const CustomDivider(); + }, + ), + ), + Row( + children: [ + if (widget.portionIndex != null) + InkWell( + onTap: () { + widget.createRecipeCubit.removeIngredient(widget.portionIndex!); + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(20), + child: Ink( + width: 52, + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: const Color(0xFFF6F6F6), + ), + child: const Icon( + Icons.delete_outline_rounded, + color: Colors.red, + ), + ), + ), + if (widget.portionIndex != null) const SizedBox(width: 8), + InkWell( + onTap: () => addPortion(context), + borderRadius: BorderRadius.circular(20), + child: Ink( + width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92), + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: appTheme.blueA400, + ), + child: Center( + child: Text( + widget.portionIndex == null ? 'Добавить' : 'Сохранить', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Future addPortion(BuildContext context) async { + final portionIndex = widget.portionIndex; + var measurementUnits = selectedGlossary?.grInPortion == null + ? Portion.pcs + : selectedGlossary?.grInPortion == 0 + ? Portion.grams + : Portion.pcs; + final glossary = selectedGlossary; + if (glossary != null) { + final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits); + final portion = DailySchedule( + dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), + quantity: int.parse(quantityController.text), + name: glossary.name, + productNutritionalValueId: glossary.id.toString(), + portion: measurementUnits, + ); + if (portionIndex == null) { + widget.createRecipeCubit.addIngredient( + Ingredient( + name: portion.name, + unit: portion.portion.getValue(), + portion: portion.quantity.toString(), + productId: glossary.id, + ), + ); + if (context.mounted) { + Navigator.of(context).pop(); + } + } else {} + } else { + Navigator.of(context).pop(); + final newGlossary = await showModalBottomSheet( + isScrollControlled: true, + useRootNavigator: true, + context: context, + builder: (context) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: GlossaryModerationRequestModalWidget( + productName: productNameController.text, + diaryCubit: sl(), + ), + ), + ); + if (newGlossary != null) { + measurementUnits = Portion.grams; + final [calories, carbohydrates, proteins, fats] = _getCPFC(newGlossary, measurementUnits); + final portion = DailySchedule( + dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), + quantity: int.parse(quantityController.text), + name: newGlossary.name, + productNutritionalValueId: newGlossary.id.toString(), + portion: measurementUnits, + ); + if (portionIndex == null) { + widget.createRecipeCubit.addIngredient( + Ingredient( + name: portion.name, + unit: portion.portion.getValue(), + portion: portion.quantity.toString(), + productId: newGlossary.id, + ), + ); + if (context.mounted) { + Navigator.of(context).pop(); + } + } else { + widget.createRecipeCubit.updateIngredient( + index: portionIndex, + ingredient: Ingredient( + name: portion.name, + unit: portion.portion.getValue(), + portion: portion.quantity.toString(), + productId: newGlossary.id, + ), + ); + } + } + } + } + + List _getCPFC(GlossaryModel glossary, Portion measurementUnits) { + final double calories; + final double carbohydrates; + final double proteins; + final double fats; + if (measurementUnits == Portion.grams) { + calories = (glossary.energyOn100Gr * int.parse(quantityController.text) / 100); + carbohydrates = (glossary.carbohydratesOn100Gr * int.parse(quantityController.text) / 100); + proteins = (glossary.proteinsOn100Gr * int.parse(quantityController.text) / 100); + fats = (glossary.fatsOn100Gr * int.parse(quantityController.text) / 100); + } else { + calories = (glossary.energyOnPortion * int.parse(quantityController.text)); + carbohydrates = (glossary.carbohydratesOnPortion * int.parse(quantityController.text)); + proteins = (glossary.proteinsOnPortion * int.parse(quantityController.text)); + fats = (glossary.fatsOnPortion * int.parse(quantityController.text)); + } + return [calories, carbohydrates, proteins, fats]; + } +} diff --git a/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart b/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart index 53a2c7fc..633bd2e7 100644 --- a/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart @@ -1,8 +1,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; -import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/add_portion_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -87,116 +86,8 @@ class _IngredientsWidgetState extends State { nameController.clear(); gramsController.clear(); pcsController.clear(); - showModalBottomSheet( - useRootNavigator: true, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(12)), - ), - isScrollControlled: true, - builder: (context) { - return StatefulBuilder( - builder: (BuildContext context, void Function(void Function()) setStateInner) { - return Padding( - padding: const EdgeInsets.all(16) + - EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Добавление ингредиента', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF323F4B), - ), - ), - const SizedBox(height: 16), - SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: CustomTextFormField( - autofocus: true, - controller: nameController, - hintText: 'Введите название продукта', - onChanged: (query) {}, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 20, - child: CustomTextFormField( - enabled: pcsController.text.isEmpty ? true : false, - keyboardType: TextInputType.number, - controller: gramsController, - hintText: pcsController.text.isEmpty ? '100' : null, - suffixText: 'гр', - onChanged: (value) { - setStateInner(() {}); - }, - ), - ), - const SizedBox(width: 8), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 20, - child: CustomTextFormField( - enabled: gramsController.text.isEmpty ? true : false, - keyboardType: TextInputType.number, - controller: pcsController, - onChanged: (value) { - setStateInner(() {}); - }, - hintText: gramsController.text.isEmpty ? '1' : null, - suffixText: 'шт', - ), - ), - ], - ), - const SizedBox(height: 12), - InkWell( - onTap: () { - if (nameController.text.isNotEmpty && (pcsController.text.isNotEmpty || gramsController.text.isNotEmpty)) { - createRecipeCubit.addIngredient( - Ingredient( - name: nameController.text, - unit: gramsController.text.isEmpty ? 'pcs' : 'gr', - portion: gramsController.text.isEmpty ? pcsController.text : gramsController.text, - ), - ); - Navigator.of(context).pop(); - setState(() {}); - } - }, - borderRadius: BorderRadius.circular(20), - child: Ink( - width: MediaQuery.of(context).size.width - 32, - height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), - child: const Center( - child: Text( - 'Добавить', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - }, + _modalBottomSheet( + createRecipeCubit: createRecipeCubit, ); }, shape: RoundedRectangleBorder( @@ -220,4 +111,28 @@ class _IngredientsWidgetState extends State { }, ); } + + _modalBottomSheet({ + required CreateRecipeCubit createRecipeCubit, + int? portionIndex, + String? productNameControllerText, + String? quantityControllerText, + }) { + return showModalBottomSheet( + useRootNavigator: true, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + isScrollControlled: true, + builder: (context) { + return AddPortionWidget( + createRecipeCubit: createRecipeCubit, + portionIndex: portionIndex, + productNameControllerText: productNameControllerText, + quantityControllerText: quantityControllerText, + ); + }, + ); + } } diff --git a/lib/features/feed/models/diets_pagination_model.dart b/lib/features/feed/models/diets_pagination_model.dart index 14538a30..e78f9c1d 100644 --- a/lib/features/feed/models/diets_pagination_model.dart +++ b/lib/features/feed/models/diets_pagination_model.dart @@ -132,6 +132,12 @@ enum Portion { return null; } } + + String getValue() => switch (this) { + Portion.grams => 'gr', + Portion.milliliters => 'milliliters', + Portion.pcs => 'pcs', + }; } @freezed diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 8723f490..e87d32c4 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -63,7 +63,7 @@ PlanType planTypeFromJson(dynamic json) { @freezed class PacketOptions with _$PacketOptions { const factory PacketOptions({ - @JsonKey(name: "items") required List items, + @JsonKey(name: "items", fromJson: _parseItems) required List items, @JsonKey(name: "price") required dynamic price, @JsonKey(name: "short") required String short, @JsonKey(name: "description_md") required String description, @@ -71,7 +71,14 @@ class PacketOptions with _$PacketOptions { factory PacketOptions.fromJson(Map json) => _$PacketOptionsFromJson(json); - factory PacketOptions.empty() => const PacketOptions(items: [], price: 1200, short: '', description: ''); + factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: ''); +} + +List _parseItems(json) { + if (json is Map) { + return json.values.map((e) => e as String).toList(); + } + return (json as List).map((e) => e as String).toList(); } @freezed diff --git a/lib/features/feed/models/recipes_model.dart b/lib/features/feed/models/recipes_model.dart index b999af60..fb627ca3 100644 --- a/lib/features/feed/models/recipes_model.dart +++ b/lib/features/feed/models/recipes_model.dart @@ -44,11 +44,21 @@ class Tag with _$Tag { @freezed class Ingredient with _$Ingredient { - const factory Ingredient({required String name, String? unit, required String portion}) = _Ingredient; + const factory Ingredient({ + required String name, + String? unit, + required String portion, + @JsonKey(name: 'product_nutritional_id', fromJson: _parseProductId) required int productId, + }) = _Ingredient; factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); } - +int _parseProductId(json) { + if (json is String) { + return int.parse(json); + } + return json as int; +} @freezed class Step with _$Step { const factory Step({required String image, @JsonKey(name: 'description_md') required String description}) = _Step; diff --git a/lib/features/recipe/presentation/widgets/ingredients_widget.dart b/lib/features/recipe/presentation/widgets/ingredients_widget.dart index 140237b0..87eea49d 100644 --- a/lib/features/recipe/presentation/widgets/ingredients_widget.dart +++ b/lib/features/recipe/presentation/widgets/ingredients_widget.dart @@ -35,19 +35,21 @@ class IngredientsWidget extends StatelessWidget { ), ), const SizedBox(width: 4), - Builder( - builder: (context) { - final portion = Portion.getPortionFromValue(recipe.ingredients![index].unit ?? ''); + Expanded( + child: Builder( + builder: (context) { + final portion = Portion.getPortionFromValue(recipe.ingredients![index].unit ?? ''); - String? portionName; - if (portion != null) { - portionName = getLocalizedMeasurementUnits(s, portion); - } - return Text( - '${recipe.ingredients![index].name} - ${recipe.ingredients![index].portion} $portionName', - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ); - }, + String? portionName; + if (portion != null) { + portionName = getLocalizedMeasurementUnits(s, portion); + } + return Text( + '${recipe.ingredients![index].name} - ${recipe.ingredients![index].portion} $portionName', + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ); + }, + ), ), ], ),