From 55e3abec99518051fca5190f38c35069afcf3fb6 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 14 Jul 2025 12:15:26 +0700 Subject: [PATCH] feat(personal diets): added personal diet option to plans --- lib/core/const.dart | 2 + lib/core/theme/theme_helper.dart | 4 +- lib/core/widgets/option_widget.dart | 52 ++++++++++++++++-- .../presentation/cubit/create_plan_cubit.dart | 11 ++++ .../presentation/widgets/option_widget.dart | 54 +++++++++---------- .../feed/models/plan_pagination_model.dart | 10 +++- .../presentation/info_by_order_screen.dart | 1 + .../presentation/pages/about_plan_screen.dart | 3 ++ lib/generated/intl/messages_en.dart | 3 ++ lib/generated/l10n.dart | 10 ++++ lib/l10n/intl_en.arb | 3 +- 11 files changed, 118 insertions(+), 35 deletions(-) diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..9a1275d6 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,6 @@ String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://dev2.eatvio.com'; + const List durationValues = [7, 14, 21]; diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 3b2a063d..81087ec9 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -101,7 +101,7 @@ class PrimaryColors { // Blue Color get blue400 => const Color(0XFF43A6F2); - Color get blueA400 => const Color(0XFF2F80ED); + Color get blueA400 => const Color(0XFF2F80ED); // BlueGray Color get blueGray400 => const Color(0XFF7B8794); @@ -148,6 +148,8 @@ class PrimaryColors { Color get green => const Color(0XFF0BB450); + Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63); + // LightGreen Color get lightGreen300 => const Color(0XFFB2DF8A); diff --git a/lib/core/widgets/option_widget.dart b/lib/core/widgets/option_widget.dart index 779f6c38..bed09c27 100644 --- a/lib/core/widgets/option_widget.dart +++ b/lib/core/widgets/option_widget.dart @@ -9,9 +9,10 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; class OptionWidget extends StatelessWidget { - const OptionWidget({super.key, required this.option, this.disableDuration = false}); + const OptionWidget({super.key, required this.option, this.disableDuration = false, required this.duration}); final PacketOptions? option; final bool disableDuration; + final int duration; @override Widget build(BuildContext context) { if (option == null) { @@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget { ), const SizedBox(height: 12), if (!disableDuration) ...[ - const _OptionDurationWidget(duration: 7), - const SizedBox(height: 16), + _OptionDurationWidget(duration: duration), + SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12), + ], + if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[ + const _OptionHasPersonalDietWidget(), + const SizedBox(height: 12), ], const CustomDivider(), const SizedBox(height: 16), @@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget { } } - class _OptionDurationWidget extends StatelessWidget { const _OptionDurationWidget({required this.duration}); final int duration; @@ -92,3 +96,43 @@ class _OptionDurationWidget extends StatelessWidget { ); } } + +class _OptionHasPersonalDietWidget extends StatelessWidget { + const _OptionHasPersonalDietWidget(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: appTheme.transparentGreen), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: appTheme.green, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(100), + bottomRight: Radius.circular(100), + ), + ), + ), + ), + const SizedBox( + width: 12, + ), + Text( + s.personalDietCreating, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + const SizedBox( + width: 12, + ), + ], + ), + ); + } +} 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 96385904..2289b31f 100644 --- a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart +++ b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart @@ -67,6 +67,13 @@ class CreatePlanCubit extends Cubit { validateSelectedOptionItems(); } + void togglePersonalizedDietOptionState() { + final options = [...state.planData.options]; + options[state.selectedOptionIndex ?? 0] = + options[state.selectedOptionIndex ?? 0].copyWith(hasPersonalDiet: !options[state.selectedOptionIndex ?? 0].hasPersonalDiet); + emit(state.copyWith(planData: state.planData.copyWith(options: options))); + } + void setDuration(int index) { emit( state.copyWith( @@ -418,18 +425,21 @@ class CreatePlanCubit extends Cubit { '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), + 'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0', }, '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), + 'has_personal_diet': state.planData.options[1].hasPersonalDiet ? '1' : '0', }, '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), + 'has_personal_diet': state.planData.options[2].hasPersonalDiet ? '1' : '0', }, }); } else { @@ -440,6 +450,7 @@ class CreatePlanCubit extends Cubit { '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), + 'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0', }, }); } diff --git a/lib/features/create_plan/presentation/widgets/option_widget.dart b/lib/features/create_plan/presentation/widgets/option_widget.dart index 42362e96..24cde52b 100644 --- a/lib/features/create_plan/presentation/widgets/option_widget.dart +++ b/lib/features/create_plan/presentation/widgets/option_widget.dart @@ -90,7 +90,7 @@ class _OptionWidgetState extends State { 2 => thirdOptionCustomerPriceController, _ => secondOptionCustomerPriceController, }; - + final price = double.tryParse(customerPriceController.text.replaceAll('₽', '')); creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽'; firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short; @@ -255,33 +255,31 @@ class _OptionWidgetState extends State { ), const SizedBox(height: 16), const CustomDivider(), - // const SizedBox(height: 16), - // Row( - // children: [ - // Checkbox( - // value: isChecked, - // activeColor: appTheme.gray, - // checkColor: appTheme.gray, - // fillColor: const WidgetStatePropertyAll(Colors.transparent), - // side: WidgetStateBorderSide.resolveWith( - // (Set states) { - // return BorderSide(width: 1, color: appTheme.gray); - // }, - // ), - // onChanged: (bool? value) { - // setState(() { - // isChecked = value ?? false; - // }); - // }, - // ), - // Expanded( - // child: Text( - // s.option_personal_diet_desc, - // style: TextStyles.textMMedium.copyWith(color: appTheme.black), - // ), - // ), - // ], - // ), + const SizedBox(height: 16), + Row( + children: [ + Checkbox( + value: state.planData.options[selectedOptionIndex].hasPersonalDiet, + activeColor: appTheme.gray, + checkColor: appTheme.gray, + fillColor: const WidgetStatePropertyAll(Colors.transparent), + side: WidgetStateBorderSide.resolveWith( + (Set states) { + return BorderSide(width: 1, color: appTheme.gray); + }, + ), + onChanged: (bool? value) { + context.read().togglePersonalizedDietOptionState(); + }, + ), + Expanded( + child: Text( + s.option_personal_diet_desc, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + ], + ), ], ), ); diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 728ff0b7..6c948685 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -67,11 +67,19 @@ abstract class PacketOptions with _$PacketOptions { @JsonKey(name: "price") required dynamic price, @JsonKey(name: "short") required String short, @JsonKey(name: "description_md") required String description, + @JsonKey(name: "has_personal_diet", fromJson: _parseHasPersonalDietField) required bool hasPersonalDiet, }) = _PacketOptions; factory PacketOptions.fromJson(Map json) => _$PacketOptionsFromJson(json); - factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: ''); + factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '', hasPersonalDiet: false); +} + +bool _parseHasPersonalDietField(dynamic json) { + if (json == null) return false; + if (json == '1') return true; + if (json == '0') return false; + return json as bool; } List _parseItems(dynamic json) { diff --git a/lib/features/order_info/presentation/info_by_order_screen.dart b/lib/features/order_info/presentation/info_by_order_screen.dart index a1d94580..0aa5e6e7 100644 --- a/lib/features/order_info/presentation/info_by_order_screen.dart +++ b/lib/features/order_info/presentation/info_by_order_screen.dart @@ -169,6 +169,7 @@ class _InfoByOrderScreenContentState extends State { OptionWidget( option: chosenOption, disableDuration: true, + duration: plan.plan.duration, ), const SizedBox(height: 8), PlanState( diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index d2984b68..41610a38 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -505,12 +505,15 @@ class _AboutPlanScreenState extends State { switch (state.chosenOptionIndex) { 1 => OptionWidget( option: state.planModel?.packet2Options, + duration: state.planModel?.duration ?? 0, ), 2 => OptionWidget( option: state.planModel?.packet3Options, + duration: state.planModel?.duration ?? 0, ), _ => OptionWidget( option: state.planModel?.packet1Options, + duration: state.planModel?.duration ?? 0, ), }, const SizedBox( diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index e318c962..700d4b48 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -542,6 +542,9 @@ class MessageLookup extends MessageLookupByLibrary { "pcs": MessageLookupByLibrary.simpleMessage("шт"), "pending_payment": MessageLookupByLibrary.simpleMessage("Ожидается оплата"), "personal": MessageLookupByLibrary.simpleMessage("Персональные"), + "personalDietCreating": MessageLookupByLibrary.simpleMessage( + "Составление индивидуальной диеты", + ), "personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"), "photo": MessageLookupByLibrary.simpleMessage("Фото"), "physical_fitness": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 9f338062..585a3154 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3394,6 +3394,16 @@ class S { args: [], ); } + + /// `Составление индивидуальной диеты` + String get personalDietCreating { + return Intl.message( + 'Составление индивидуальной диеты', + name: 'personalDietCreating', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 96ddc4be..08ad7704 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -402,6 +402,7 @@ "allowAcessToGaleryDescription": "Предоставление доступа позволит вам добавлять фото к записям", "allowAccessToCamera": "Разрешите доступ к камере", "allowAccessToCameraDescription": "Предоставление доступа позволит вам делать фотографии и добавлять к записям", - "allowAccess": "Разрешить доступ" + "allowAccess": "Разрешить доступ", + "personalDietCreating": "Составление индивидуальной диеты" }