From 0fcb2c1a0d526d69f3ee9df8a85695b5e4f65c52 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 29 Apr 2025 14:44:33 +0700 Subject: [PATCH] feat(plans): plan creating has been completed --- .DS_Store | Bin 10244 -> 10244 bytes assets/icons/star.svg | 3 + lib/core/app_router.dart | 8 +- lib/core/locator_service.dart | 6 +- lib/core/theme/theme_helper.dart | 2 + lib/core/widgets/custom_text_form_field.dart | 13 +- .../success_published_diet_bottom_sheet.dart | 7 +- .../bloc/chat_room_cubit/chat_room_cubit.dart | 8 +- .../chat/presentation/pages/chat_screen.dart | 31 +- .../widgets/notification_banner.dart | 219 ++++++---- .../presentation/create_diet_screen.dart | 8 +- .../create_plan/models/create_plan_model.dart | 11 +- .../create_plan/models/plan_slug_model.dart | 11 + .../presentation/create_plan_screen.dart | 391 ++++++++++-------- .../presentation/cubit/create_plan_cubit.dart | 343 ++++++++++++++- .../presentation/cubit/create_plan_state.dart | 52 ++- .../presentation/widgets/add_item_widget.dart | 75 ++-- .../presentation/widgets/option_widget.dart | 110 ++--- .../plan_duration_drop_down_widget.dart | 54 ++- .../widgets/plan_image_picker_widget.dart | 140 ++++--- .../widgets/plan_type_choice_widget.dart | 83 ++-- .../repositories/create_plan_repository.dart | 7 +- .../services/create_plan_service.dart | 7 +- .../diary/models/consultant_review_model.dart | 3 +- .../consultant_review_model.freezed.dart | 47 ++- .../models/consultant_review_model.g.dart | 6 +- .../widgets/meal_button_widget.dart | 16 +- .../presentation/widgets/meal_widget.dart | 9 +- .../diary/repositories/diary_repository.dart | 2 +- .../feed/models/plan_pagination_model.dart | 9 +- .../presentation/widgets/my_diet_widget.dart | 2 +- .../my_finance/models/card_model.dart | 3 +- .../user_cards_cubit/user_cards_cubit.dart | 25 +- .../presentation/pages/card_settings.dart | 273 ++++++++---- .../repositories/cards_repository.dart | 8 + .../my_finance/services/cards_service.dart | 5 + .../presentation/info_by_order_screen.dart | 16 +- .../widgets/collect_charge_banner.dart | 72 ++++ .../presentation/widgets/payment_banner.dart | 1 + .../presentation/pages/about_plan_screen.dart | 1 + lib/generated/intl/messages_en.dart | 27 ++ lib/generated/l10n.dart | 110 +++++ lib/l10n/intl_en.arb | 13 +- 43 files changed, 1602 insertions(+), 635 deletions(-) create mode 100644 assets/icons/star.svg create mode 100644 lib/features/create_plan/models/plan_slug_model.dart create mode 100644 lib/features/order_info/presentation/widgets/collect_charge_banner.dart diff --git a/.DS_Store b/.DS_Store index 1ea62397377fa3c9d7e919b69fb702fa9bb34479..097b747a3befbb4b0b1709860d28a9e8dc21a8f5 100644 GIT binary patch delta 36 scmZn(XbG6$&*-o*U^hRb!{h`3iOnhkg1nnm*NFhiOnhkg1q%S40#Os3@Hq$4Dk%PU{)zZ4v<&unUkNK zl#`#tz`!5?#Ew8b?>`s-Squy~_2r}+1}Ep|7JyYTu;}Aco135O;sUjV!|v72{~Ec+ eKo;RNJU2guB=rRu$c|CiY$*GTaWlKZUv>bOS44yW diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 00000000..6a077d0e --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 58ccde6e..ce10badc 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -136,7 +136,10 @@ class AppRouter { GoRoute( path: CardSettingsScreen.path, name: CardSettingsScreen.name, - builder: (context, state) => const CardSettingsScreen(), + builder: (context, state) { + final chosenPlanId = state.extra as int?; + return CardSettingsScreen(chosenPlanId: chosenPlanId); + }, ), GoRoute( path: '/about-plan-screen', @@ -182,7 +185,8 @@ class AppRouter { builder: (context, state) { final data = state.extra as Map; final chatId = data['chatId'] as String; - return ChatScreen(chatId: chatId); + final sendInitialMessage = data['sendInitialMessage'] as bool; + return ChatScreen(chatId: chatId, sendInitialMessage: sendInitialMessage,); }, ), GoRoute( diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 8794ce95..ebd8d539 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -19,6 +19,8 @@ import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_c import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; +import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart'; @@ -117,7 +119,7 @@ Future init() async { sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => CreatePlanCubit()); + sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl())); @@ -158,6 +160,7 @@ Future init() async { sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); + sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -184,6 +187,7 @@ Future init() async { sl.registerLazySingleton(() => CardsService(sl())); sl.registerLazySingleton(() => ConsultationService(sl())); sl.registerLazySingleton(() => CancelPlanService(sl())); + sl.registerLazySingleton(() => CreatePlanService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 9bed83fd..aa4e30a6 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -147,6 +147,8 @@ class PrimaryColors { Color get greenA70001 => const Color(0XFF00BE13); + Color get green => const Color(0XFF0BB450); + // LightGreen Color get lightGreen300 => const Color(0XFFB2DF8A); diff --git a/lib/core/widgets/custom_text_form_field.dart b/lib/core/widgets/custom_text_form_field.dart index aedfda0f..041bee70 100644 --- a/lib/core/widgets/custom_text_form_field.dart +++ b/lib/core/widgets/custom_text_form_field.dart @@ -99,7 +99,7 @@ class CustomTextFormField extends StatelessWidget { controller: controller, focusNode: focusNode, autofocus: autofocus, - style: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w500), + style: textStyle ?? const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w500), obscureText: obscureText!, textInputAction: textInputAction, maxLines: maxLines, @@ -115,20 +115,17 @@ class CustomTextFormField extends StatelessWidget { prefixIconConstraints: prefixConstraints, suffixIcon: suffix, floatingLabelBehavior: FloatingLabelBehavior.always, - suffixStyle: textStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), + suffixStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), suffixIconConstraints: suffixConstraints, isDense: false, contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), fillColor: const Color(0xFFF6F6F6), filled: true, - border: - borderDecoration ?? + border: borderDecoration ?? OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none), - enabledBorder: - borderDecoration ?? + enabledBorder: borderDecoration ?? OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none), - focusedBorder: - borderDecoration ?? + focusedBorder: borderDecoration ?? OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none), ), keyboardType: keyboardType, diff --git a/lib/core/widgets/success_published_diet_bottom_sheet.dart b/lib/core/widgets/success_published_diet_bottom_sheet.dart index 0145d041..223ed220 100644 --- a/lib/core/widgets/success_published_diet_bottom_sheet.dart +++ b/lib/core/widgets/success_published_diet_bottom_sheet.dart @@ -7,9 +7,10 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -class SuccessPublishedDietBottomSheet extends StatelessWidget { - const SuccessPublishedDietBottomSheet({super.key, required this.title}); +class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget { + const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false}); final String title; + final bool isPlan; @override Widget build(BuildContext context) { final s = S.of(context); @@ -27,7 +28,7 @@ class SuccessPublishedDietBottomSheet extends StatelessWidget { style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), Text( - 'опубликованна', + isPlan ? 'опубликован' : 'опубликованна', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), const SizedBox( diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index bb8e6d9f..bd53c0ac 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -72,7 +72,7 @@ class ChatRoomCubit extends Cubit { } } - initChat(String chatId) async { + initChat(String chatId, {bool sendInitialMessage = false}) async { try { final chatRoom = await chatRepository.getChatRoom(chatId); ChosenPlanModel? chosenPlan; @@ -92,10 +92,14 @@ class ChatRoomCubit extends Cubit { hasCard: false, ), ); - + if (chatRoom.chosenPlan?.consultantId == chatRoom.users.receiver.id) { await checkCards(); } + + if (sendInitialMessage) { + await chatRepository.sendMessage(chatId: chatId, data: 'Здравствуйте, хочу заказать у вас: ${chosenPlan?.plan.name ?? 'план'}'); + } } catch (e) { log(e.toString()); typingSubscription?.cancel(); diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 5f14e0bf..a9403849 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -15,6 +15,7 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; @@ -28,7 +29,8 @@ import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String chatId; - const ChatScreen({required this.chatId, super.key}); + final bool sendInitialMessage; + const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage}); @override State createState() => _ChatScreenState(); @@ -46,7 +48,7 @@ class _ChatScreenState extends State { void initState() { super.initState(); scrollController.addListener(_onScroll); - chatRoomCubit.initChat(widget.chatId); + chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage); } _onScroll() { @@ -96,13 +98,24 @@ class _ChatScreenState extends State { BannerType? bannerType; if (chosenPlan == null) { bannerType = null; - } else if (amIConsultant && state.hasCard) { + } else if (!amIConsultant && !state.hasCard) { bannerType = BannerType.card; } else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) { bannerType = BannerType.date; } else if (!amIConsultant && (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) { bannerType = BannerType.payment; + } else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone) { + bannerType = BannerType.collectCharge; + } + + PacketOptions? chosenOption; + if (chosenPlan?.optionType == 'options1') { + chosenOption = chosenPlan?.plan.packet1Options; + } else if (chosenPlan?.optionType == 'options2') { + chosenOption = chosenPlan?.plan.packet2Options; + } else if (chosenPlan?.optionType == 'options3') { + chosenOption = chosenPlan?.plan.packet3Options; } return Scaffold( resizeToAvoidBottomInset: true, @@ -262,7 +275,7 @@ class _ChatScreenState extends State { itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0), ), const SizedBox(height: 16), - if (showUserDetailsButton) ...[ + if (!showUserDetailsButton) ...[ // OrderCard( // plan: chosenPlan.plan, // isCustomerView: !amIConsultant, @@ -288,7 +301,15 @@ class _ChatScreenState extends State { ), ), if (bannerType != null) - Padding(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: NotificationBanner(bannerType: bannerType)), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: NotificationBanner( + bannerType: bannerType, + chosenPlanId: chosenPlan?.id, + planName: chosenPlan?.plan.name, + planPrice: chosenOption?.price.toDouble(), + ), + ), ], ], ), diff --git a/lib/features/chat/presentation/widgets/notification_banner.dart b/lib/features/chat/presentation/widgets/notification_banner.dart index 0059f7cc..d4eaf577 100644 --- a/lib/features/chat/presentation/widgets/notification_banner.dart +++ b/lib/features/chat/presentation/widgets/notification_banner.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; @@ -13,8 +14,11 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; class NotificationBanner extends StatefulWidget { - const NotificationBanner({super.key, required this.bannerType}); + const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId}); final BannerType bannerType; + final double? planPrice; + final String? planName; + final int? chosenPlanId; @override State createState() => _NotificationBannerState(); @@ -31,96 +35,145 @@ class _NotificationBannerState extends State { color: appTheme.gray50, borderRadius: BorderRadius.circular(20), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(top: 12, right: 8, left: 12), - child: SvgPicture.asset('assets/icons/reversed_exclamation_mark.svg', height: 68, width: 68), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.bannerType != BannerType.collectCharge) + Padding( + padding: const EdgeInsets.only(top: 12, right: 8, left: 12), + child: SvgPicture.asset('assets/icons/reversed_exclamation_mark.svg', height: 68, width: 68), + ) + else + const Padding( + padding: EdgeInsets.only(top: 12, right: 8, left: 12), + child: _StarIcon(), + ), + if (isLoading) + const Padding( + padding: EdgeInsets.only(left: 40, top: 24), + child: Loader(), + ) + else + Padding( + padding: const EdgeInsets.only(top: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: switch (widget.bannerType) { + BannerType.date => [ + SizedBox( + width: size.width * 0.6, + child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + final cubit = context.read(); + showModalBottomSheet( + context: context, + builder: (context) => ChosePlanDateWidget( + chatCubit: cubit, + ), + ); + }, + child: Text(s.choose_date, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), + ), + const SizedBox(height: 16), + ], + BannerType.card => [ + SizedBox( + width: size.width * 0.6, + child: Text(s.can_not_choose_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + GoRouter.of(context).pushReplacement( + CardSettingsScreen.path, + ); + }, + child: Text(s.add_card, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), + ), + const SizedBox(height: 16), + ], + BannerType.payment => [ + SizedBox( + width: size.width * 0.6, + child: Text(s.tap_on_link_to_pay, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () async { + final planId = (context.read().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; + setState(() { + isLoading = true; + }); + final paymentLink = await context.read().getPaymentLink(); + setState(() { + isLoading = false; + }); + if (paymentLink != null && planId != null) { + if (!context.mounted) return; + GoRouter.of(context).pushReplacement( + PaymentScreen.path, + extra: {'url': paymentLink, 'planId': planId}, + ); + } + }, + child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), + ), + const SizedBox(height: 16), + ], + BannerType.collectCharge => [ + SizedBox( + width: size.width * 0.6, + child: + Text('${s.congratsYouHaveFinishedPlan} «${widget.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 8), + SizedBox( + width: size.width * 0.6, + child: Text('${s.userConfirmedPlanCompletion} ${widget.planPrice}₽', style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 8), + ], + }, + ), + ), + ], ), - if (isLoading) - const Padding( - padding: EdgeInsets.only(left: 40, top: 24), - child: Loader(), - ) - else + if (widget.bannerType == BannerType.collectCharge) Padding( - padding: const EdgeInsets.only(top: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: switch (widget.bannerType) { - BannerType.date => [ - SizedBox( - width: size.width * 0.6, - child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () { - final cubit = context.read(); - showModalBottomSheet( - context: context, - builder: (context) => ChosePlanDateWidget( - chatCubit: cubit, - ), - ); - }, - child: Text(s.choose_date, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), - ), - const SizedBox(height: 16), - ], - BannerType.card => [ - SizedBox( - width: size.width * 0.6, - child: Text(s.can_not_choose_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () { - GoRouter.of(context).pushReplacement( - CardSettingsScreen.path, - ); - }, - child: Text(s.add_card, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), - ), - const SizedBox(height: 16), - ], - BannerType.payment => [ - SizedBox( - width: size.width * 0.6, - child: Text(s.tap_on_link_to_pay, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () async { - final planId = (context.read().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; - setState(() { - isLoading = true; - }); - final paymentLink = await context.read().getPaymentLink(); - setState(() { - isLoading = false; - }); - if (paymentLink != null && planId != null) { - if (!context.mounted) return; - GoRouter.of(context).pushReplacement( - PaymentScreen.path, - extra: {'url': paymentLink, 'planId': planId}, - ); - } - }, - child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), - ), - const SizedBox(height: 16), - ], + padding: const EdgeInsets.symmetric(horizontal: 12), + child: CustomOutlinedButton( + text: s.collectCharge, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () async { + GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.chosenPlanId); }, ), - ), + ) ], ), ); } } -enum BannerType { date, card, payment } +class _StarIcon extends StatelessWidget { + const _StarIcon(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: appTheme.whiteA700, shape: BoxShape.circle), + child: SvgPicture.asset('assets/icons/star.svg'), + ); + } +} + +enum BannerType { date, card, payment, collectCharge } diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index a8d2e019..48d1d27e 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -72,7 +72,7 @@ class _CreateDietScreenContentState extends State { showBottomSheetWithBlur( userRootNavigator: true, context: context, - child: SuccessPublishedDietBottomSheet( + child: SuccessPublishedDietOrPlanBottomSheet( title: state.dietData.name ?? 'Диета', ), ); @@ -83,7 +83,7 @@ class _CreateDietScreenContentState extends State { context.pop(); showBottomSheetWithBlur( context: context, - child: SuccessPublishedDietBottomSheet( + child: SuccessPublishedDietOrPlanBottomSheet( title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', ), ); @@ -193,8 +193,8 @@ class _CreateDietScreenContentState extends State { context.read().setDietState(DietState.draft); if (widget.diet != null) { final result = await context.read().updateDiet( - widget.diet!.id, - ); + widget.diet!.id, + ); if (result == 'error') { if (!context.mounted) return; showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}'); diff --git a/lib/features/create_plan/models/create_plan_model.dart b/lib/features/create_plan/models/create_plan_model.dart index 75187284..4b85ffba 100644 --- a/lib/features/create_plan/models/create_plan_model.dart +++ b/lib/features/create_plan/models/create_plan_model.dart @@ -15,8 +15,8 @@ class CreatePlanModel with _$CreatePlanModel { const factory CreatePlanModel({ String? name, String? details, - String? state, - int? duration, + @Default('active') String? state, + int? duration, required List type, XFile? image, required List options, @@ -25,9 +25,12 @@ class CreatePlanModel with _$CreatePlanModel { Future> toCreationJson() async => { 'name': name, 'duration': duration, - 'state': state, - 'type': type, + '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, }; diff --git a/lib/features/create_plan/models/plan_slug_model.dart b/lib/features/create_plan/models/plan_slug_model.dart new file mode 100644 index 00000000..b440cb07 --- /dev/null +++ b/lib/features/create_plan/models/plan_slug_model.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'plan_slug_model.freezed.dart'; +part 'plan_slug_model.g.dart'; + +@freezed +class PlanSlugModel with _$PlanSlugModel { + factory PlanSlugModel({@JsonKey(name: 'slug') required String slug}) = _PlanSlugModel; + + factory PlanSlugModel.fromJson(Map json) => _$PlanSlugModelFromJson(json); +} diff --git a/lib/features/create_plan/presentation/create_plan_screen.dart b/lib/features/create_plan/presentation/create_plan_screen.dart index ab921880..c9ac8302 100644 --- a/lib/features/create_plan/presentation/create_plan_screen.dart +++ b/lib/features/create_plan/presentation/create_plan_screen.dart @@ -1,9 +1,13 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/widgets/option_widget.dart'; @@ -15,6 +19,7 @@ import 'package:eatvio_app/features/create_plan/presentation/widgets/select_exac import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class CreatePlanScreen extends StatelessWidget { static const name = 'createPlanScreen'; @@ -36,177 +41,229 @@ class CreatePlanScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Scaffold( - backgroundColor: appTheme.gray100, - appBar: AppBar( - centerTitle: true, - title: Text( - s.adding_plan, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - ), - body: Stack( - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - MainBackgroundBouble( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - Text( - s.label, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), - const SizedBox(height: 4), - CustomTextFormField( - hintText: s.plan_label_desc, - onChanged: (value) { - context.read().setName(value); - }, - maxLength: 150, - ), - const SizedBox(height: 12), - Text( - s.plan_duration, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), - const SizedBox(height: 4), - const PlanDurationDropDownWidget(), - const SizedBox(height: 20), - const PlanImagePickerWidget(), - const SizedBox(height: 16), - const PlanTypeChoiceWidget(), - const SizedBox(height: 16), - Text( - s.plan_desc, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), - const SizedBox(height: 4), - SizedBox( - height: 132, - child: CustomTextFormField( - textAlignVertical: TextAlignVertical.top, - maxLength: 1000, - maxLines: null, - minLines: null, - expands: true, - hintText: s.plan_desc_example, - onChanged: (value) { - context.read().setDetails(value); - }, - ), - ), - const SizedBox(height: 16), - ], - ), - ), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - s.services, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.blueGray400), - ), - ), - const SizedBox(height: 8), - const OptionsCountButton(), - const SizedBox(height: 20), - BlocBuilder( - buildWhen: (previous, current) => previous.selectedOptionIndex != current.selectedOptionIndex, - builder: (context, state) { - if (state.selectedOptionIndex == null) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - '${s.option} ${(state.selectedOptionIndex ?? 0) + 1}', - style: TextStyles.textLSemibold.copyWith(color: appTheme.blueGray400), - ), - ); - }, - ), - const SizedBox(height: 8), - const OptionWidget(), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - CustomOutlinedButton( - onPressed: () {}, - text: s.to_draft, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), - ), - ), - const SizedBox(height: 12), - CustomOutlinedButton( - onPressed: () async {}, - text: s.preview, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), - ), - ), - const SizedBox(height: 12), - CustomOutlinedButton( - onPressed: () {}, - text: s.publish, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ], - ), - ), - ], + return BlocBuilder( + buildWhen: (previous, current) => previous.planCreated != current.planCreated || previous.isLoading != current.isLoading, + builder: (context, state) { + if (state.planCreated) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; + context.pop(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + child: SuccessPublishedDietOrPlanBottomSheet( + title: state.planData.name ?? 'План', + isPlan: true, + ), + ); + }); + } + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.adding_plan, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), ), - BlocBuilder( - builder: (context, state) { - if (state.selectedOptionIndex == null) { - return const SizedBox.shrink(); - } - return const Padding( - padding: EdgeInsets.only(bottom: 24.0), - child: Align( - alignment: Alignment.bottomCenter, - child: SelectExactOptionWidget(), + body: Stack( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + MainBackgroundBouble( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + s.label, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + const SizedBox(height: 4), + BlocBuilder( + buildWhen: (previous, current) => previous.validationState != current.validationState, + builder: (context, state) { + return Column( + children: [ + CustomTextFormField( + hintText: s.plan_label_desc, + hasError: state.validationState.titleHasError, + onChanged: (value) { + context.read().setName(value); + }, + maxLength: 150, + ), + if (state.validationState.titleHasError) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.atLeast3Symbols), + ], + ], + ); + }, + ), + const SizedBox(height: 12), + Text( + s.plan_duration, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + const SizedBox(height: 4), + const PlanDurationDropDownWidget(), + const SizedBox(height: 20), + const PlanImagePickerWidget(), + const SizedBox(height: 16), + const PlanTypeChoiceWidget(), + const SizedBox(height: 16), + Text( + s.plan_desc, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + const SizedBox(height: 4), + BlocBuilder( + buildWhen: (previous, current) => previous.validationState != current.validationState, + builder: (context, state) { + return Column( + children: [ + SizedBox( + height: 132, + child: CustomTextFormField( + hasError: state.validationState.descriptionHasError, + textAlignVertical: TextAlignVertical.top, + maxLength: 1000, + maxLines: null, + minLines: null, + expands: true, + hintText: s.plan_desc_example, + onChanged: (value) { + context.read().setDetails(value); + }, + ), + ), + if (state.validationState.descriptionHasError) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.atLeast3Symbols), + ], + ], + ); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + s.services, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.blueGray400), + ), + ), + const SizedBox(height: 8), + const OptionsCountButton(), + const SizedBox(height: 20), + BlocBuilder( + buildWhen: (previous, current) => previous.selectedOptionIndex != current.selectedOptionIndex, + builder: (context, state) { + if (state.selectedOptionIndex == null) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + '${s.option} ${(state.selectedOptionIndex ?? 0) + 1}', + style: TextStyles.textLSemibold.copyWith(color: appTheme.blueGray400), + ), + ); + }, + ), + const SizedBox(height: 8), + const OptionWidget(), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + // CustomOutlinedButton( + // onPressed: () {}, + // text: s.to_draft, + // height: 52, + // buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), + // buttonStyle: ButtonStyle( + // side: const WidgetStatePropertyAll(BorderSide.none), + // backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + // ), + // ), + // const SizedBox(height: 12), + // CustomOutlinedButton( + // onPressed: () async {}, + // text: s.preview, + // height: 52, + // buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), + // buttonStyle: ButtonStyle( + // side: const WidgetStatePropertyAll(BorderSide.none), + // backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + // ), + // ), + // const SizedBox(height: 12), + state.isLoading + ? const Loader() + : CustomOutlinedButton( + onPressed: () async { + await context.read().publishPlan(); + }, + text: s.publish, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ], + ), + ), + const SizedBox(height: 80), + ], ), - ); - }, + ), + BlocBuilder( + builder: (context, state) { + if (state.selectedOptionIndex == null) { + return const SizedBox.shrink(); + } + return const Padding( + padding: EdgeInsets.only(bottom: 24.0), + child: Align( + alignment: Alignment.bottomCenter, + child: SelectExactOptionWidget(), + ), + ); + }, + ), + ], ), - ], - ), + ); + }, ); } } 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 8d406a71..7239c657 100644 --- a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart +++ b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart @@ -1,56 +1,65 @@ import 'package:eatvio_app/core/const.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'; import 'package:flutter_image_compress/flutter_image_compress.dart'; class CreatePlanCubit extends Cubit { - CreatePlanCubit() : super(CreatePlanState.initial()); + final CreatePlanRepository createPlanRepository; + CreatePlanCubit({required this.createPlanRepository}) : super(CreatePlanState.initial()); void setDetails(String text) { emit(state.copyWith(planData: state.planData.copyWith(details: text))); + validateDetails(); } void setName(String name) { emit(state.copyWith(planData: state.planData.copyWith(name: name))); + validateName(); } void setOptionTitle(String title) { final options = [...state.planData.options]; - options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(short: title); + options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(short: title); emit(state.copyWith(planData: state.planData.copyWith(options: options))); + validateSelectedOptionTitle(); } - void setPrice(double price){ + void setPrice(double price) { final selectedOptionIndex = state.selectedOptionIndex ?? 0; - final options = [...state.planData.options]; - options[selectedOptionIndex] = options[selectedOptionIndex].copyWith(price: price); + final options = [...state.planData.options]; + options[selectedOptionIndex] = options[selectedOptionIndex].copyWith(price: price); emit(state.copyWith(planData: state.planData.copyWith(options: options))); + validateSelectedOptionPrice(); } void setOptionDescription(String description) { final options = [...state.planData.options]; - options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(description: description); + options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(description: description); emit(state.copyWith(planData: state.planData.copyWith(options: options))); + validateSelectedOptionDescription(); } - void setOptionContent(String content, index) { - final options = [...state.planData.options]; + void setOptionContent(String content, index) { + final options = [...state.planData.options]; final items = [...options[state.selectedOptionIndex ?? 0].items]; if (items.length <= index) { items.add(content); - }else { + } else { items[index] = content; } - options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: items); + options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: items); emit(state.copyWith(planData: state.planData.copyWith(options: options))); + validateSelectedOptionItems(); } void removeOptionContentItem(int index) { final options = [...state.planData.options]; final updatedItems = [...options[state.selectedOptionIndex ?? 0].items]; updatedItems.removeAt(index); - options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: updatedItems); + options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: updatedItems); emit(state.copyWith(planData: state.planData.copyWith(options: options))); + validateSelectedOptionItems(); } void setDuration(int index) { @@ -62,10 +71,12 @@ class CreatePlanCubit extends Cubit { ), ), ); + validateDuration(); } void setImage(XFile? image) { emit(state.copyWith(planData: state.planData.copyWith(image: image))); + validateImage(); } void setPlanState(String planState) { @@ -74,9 +85,319 @@ class CreatePlanCubit extends Cubit { void setPlanType(List type) { emit(state.copyWith(planData: state.planData.copyWith(type: type))); + if (type.any((elem) => elem)) { + validateType(); + } } void setSelectedOptionIndex(int? index) { emit(state.copyWith(selectedOptionIndex: index)); } + + bool validateName() { + final name = state.planData.name; + if (name == null || name.length < 3) { + emit(state.copyWith.validationState(titleHasError: true)); + return false; + } + emit(state.copyWith.validationState(titleHasError: false)); + return true; + } + + bool validateDetails() { + final details = state.planData.details; + if (details == null || details.length < 3) { + emit(state.copyWith.validationState(descriptionHasError: true)); + return false; + } + emit(state.copyWith.validationState(descriptionHasError: false)); + return true; + } + + bool validateDuration() { + if (state.planData.duration == null) { + emit(state.copyWith.validationState(durationHasError: true)); + return false; + } + emit(state.copyWith.validationState(durationHasError: false)); + return true; + } + + bool validateType() { + final type = state.planData.type; + if (type.isEmpty || type.every((elem) => elem)) { + emit(state.copyWith.validationState(typeHasError: true)); + return false; + } + emit(state.copyWith.validationState(typeHasError: false)); + return true; + } + + bool validateImage() { + if (state.planData.image == null) { + emit(state.copyWith.validationState(imageHasError: true)); + return false; + } + emit(state.copyWith.validationState(imageHasError: false)); + return true; + } + + bool validateSelectedOptionPrice() { + final option = state.planData.options[state.selectedOptionIndex ?? 0]; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + if (option.price < 1200) { + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(priceHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return false; + } + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(priceHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return true; + } + + bool validatePriceOfAllFilledOptions() { + final options = state.planData.options; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + bool result = true; + if (options[0].price < 1200) { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(priceHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(priceHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + + if (options[1].price < 1200) { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(priceHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(priceHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + + if (options[2].price < 1200) { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(priceHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(priceHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + return result; + } + + bool validateSelectedOptionItems() { + final option = state.planData.options[state.selectedOptionIndex ?? 0]; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + bool result = true; + if (option.items.isEmpty) { + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(isItemsEmpty: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return false; + } + final itemsErrors = Map.from(optionsValidationsStates[state.selectedOptionIndex ?? 0].itemsErrors); + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(isItemsEmpty: false); + for (final (index, item) in option.items.indexed) { + if (item.length < 3) { + itemsErrors[index] = true; + result = false; + } else { + itemsErrors[index] = false; + } + } + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(itemsErrors: itemsErrors); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return result; + } + + bool validateItemsOfAllFilledOptions() { + final options = state.planData.options; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + bool result = true; + + if (options[0].items.isEmpty) { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(isItemsEmpty: true); + result = false; + } else { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(isItemsEmpty: false); + } + + final option1ItemsErrors = Map.from(optionsValidationsStates[0].itemsErrors); + for (final (index, item) in options[0].items.indexed) { + if (item.length < 3) { + option1ItemsErrors[index] = true; + result = false; + } else { + option1ItemsErrors[index] = false; + } + } + + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(itemsErrors: option1ItemsErrors); + if (state.selectedOptionIndex == null) { + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return result; + } + if (options[1].items.isEmpty) { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(isItemsEmpty: true); + result = false; + } else { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(isItemsEmpty: false); + } + + final option2ItemsErrors = Map.from(optionsValidationsStates[1].itemsErrors); + for (final (index, item) in options[1].items.indexed) { + if (item.length < 3) { + option2ItemsErrors[index] = true; + result = false; + } else { + option2ItemsErrors[index] = false; + } + } + + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(itemsErrors: option2ItemsErrors); + + if (options[2].items.isEmpty) { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(isItemsEmpty: true); + result = false; + } else { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(isItemsEmpty: false); + } + + final option3ItemsErrors = Map.from(optionsValidationsStates[2].itemsErrors); + for (final (index, item) in options[2].items.indexed) { + if (item.length < 3) { + option3ItemsErrors[index] = true; + result = false; + } else { + option3ItemsErrors[index] = false; + } + } + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(itemsErrors: option3ItemsErrors); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return result; + } + + bool validateSelectedOptionTitle() { + final option = state.planData.options[state.selectedOptionIndex ?? 0]; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + if (option.short.length < 3) { + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(titleHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return false; + } + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(titleHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return true; + } + + bool validateTitlesOfAllFilledOptions() { + final options = state.planData.options; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + bool result = true; + if (options[0].short.length < 3) { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(titleHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(titleHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + if (state.selectedOptionIndex == null) return result; + + if (options[1].short.length < 3) { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(titleHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(titleHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + + if (options[2].short.length < 3) { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(titleHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(titleHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + return result; + } + + bool validateSelectedOptionDescription() { + final option = state.planData.options[state.selectedOptionIndex ?? 0]; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + if (option.description.length < 3) { + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(descriptionHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return false; + } + optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(descriptionHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + return true; + } + + bool validateDescriptionsOfAllOptions() { + final options = state.planData.options; + final optionsValidationsStates = [...state.validationState.optionsValidationState]; + bool result = true; + + if (options[0].description.length < 3) { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(descriptionHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(descriptionHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + if (state.selectedOptionIndex == null) return result; + if (options[1].description.length < 3) { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(descriptionHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(descriptionHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + + if (options[2].description.length < 3) { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(descriptionHasError: true); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + result = false; + } else { + optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(descriptionHasError: false); + emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates)); + } + return result; + } + + bool validateAllFields() { + final isNameValid = validateName(); + final isDetailsValid = validateDetails(); + final isImageValid = validateImage(); + final isDurationValid = validateDuration(); + final areTitlesValid = validateTitlesOfAllFilledOptions(); + final arePricesValid = validatePriceOfAllFilledOptions(); + final areDescriptionsValid = validateDescriptionsOfAllOptions(); + final areItemsValid = validateItemsOfAllFilledOptions(); + return isNameValid && isDetailsValid && isImageValid && isDurationValid && areTitlesValid && arePricesValid && areDescriptionsValid && areItemsValid; + } + + 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); + } + emit(state.copyWith(isLoading: false, planCreated: true)); + } + } } diff --git a/lib/features/create_plan/presentation/cubit/create_plan_state.dart b/lib/features/create_plan/presentation/cubit/create_plan_state.dart index 684df959..7a70be54 100644 --- a/lib/features/create_plan/presentation/cubit/create_plan_state.dart +++ b/lib/features/create_plan/presentation/cubit/create_plan_state.dart @@ -9,7 +9,9 @@ class CreatePlanState with _$CreatePlanState { const factory CreatePlanState({ required bool isLoading, required String? errorMessage, - required CreatePlanModel planData, + required CreatePlanModel planData, + required PlanValidationState validationState, + required bool planCreated, int? durationIndex, int? selectedOptionIndex, }) = _CreatePlanState; @@ -17,9 +19,55 @@ class CreatePlanState with _$CreatePlanState { factory CreatePlanState.initial() => CreatePlanState( isLoading: false, errorMessage: null, + planCreated: false, + validationState: PlanValidationState.empty(), planData: CreatePlanModel( type: [false, false, false, false], options: [PacketOptions.empty(), PacketOptions.empty(), PacketOptions.empty()], - ), + ), + ); +} + +@freezed +class PlanValidationState with _$PlanValidationState { + factory PlanValidationState({ + required bool titleHasError, + required bool durationHasError, + required bool typeHasError, + required bool descriptionHasError, + required bool imageHasError, + required List optionsValidationState, + }) = _PlanValidationState; + + factory PlanValidationState.empty() => PlanValidationState( + titleHasError: false, + durationHasError: false, + typeHasError: false, + descriptionHasError: false, + imageHasError: false, + optionsValidationState: [ + OptionValidationState.empty(), + OptionValidationState.empty(), + OptionValidationState.empty(), + ], + ); +} + +@freezed +class OptionValidationState with _$OptionValidationState { + factory OptionValidationState({ + required bool titleHasError, + required bool priceHasError, + required bool descriptionHasError, + required Map itemsErrors, + required bool isItemsEmpty, + }) = _OptionValidationState; + + factory OptionValidationState.empty() => OptionValidationState( + descriptionHasError: false, + titleHasError: false, + priceHasError: false, + isItemsEmpty: false, + itemsErrors: {}, ); } diff --git a/lib/features/create_plan/presentation/widgets/add_item_widget.dart b/lib/features/create_plan/presentation/widgets/add_item_widget.dart index 16a79d26..e51cf977 100644 --- a/lib/features/create_plan/presentation/widgets/add_item_widget.dart +++ b/lib/features/create_plan/presentation/widgets/add_item_widget.dart @@ -1,6 +1,8 @@ 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_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -39,34 +41,53 @@ class _AddItemWidgetState extends State { if (widget.items.length > widget.index && controller.text != widget.items[widget.index]) { controller.text = widget.items[widget.index]; } - return Row( - children: [ - Expanded( - child: CustomTextFormField( - controller: controller, - hintText: widget.enableHint ? s.option_content_hint_text : null, - onChanged: (value) { - context.read().setOptionContent(value, widget.index); - }, - ), - ), - if (widget.canBeDeleted) ...[ - const SizedBox(width: 8), - GestureDetector( - onTap: () { - context.read().removeOptionContentItem(widget.index); - }, - child: Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: appTheme.lightGray0, - borderRadius: BorderRadius.circular(20), - ), - child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24), + return BlocBuilder( + buildWhen: (previous, current) => previous.validationState != current.validationState, + builder: (context, state) { + final hasError = state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty || + (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false); + return Column( + children: [ + Row( + children: [ + Expanded( + child: CustomTextFormField( + controller: controller, + hasError: hasError, + hintText: widget.enableHint ? s.option_content_hint_text : null, + onChanged: (value) { + context.read().setOptionContent(value, widget.index); + }, + ), + ), + if (widget.canBeDeleted) ...[ + const SizedBox(width: 8), + GestureDetector( + onTap: () { + context.read().removeOptionContentItem(widget.index); + }, + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: appTheme.lightGray0, + borderRadius: BorderRadius.circular(20), + ), + child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24), + ), + ), + ], + ], ), - ), - ], - ], + if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.addAtLeastOneItem) + ] else if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.atLeast3Symbols) + ] + ], + ); + }, ); } } diff --git a/lib/features/create_plan/presentation/widgets/option_widget.dart b/lib/features/create_plan/presentation/widgets/option_widget.dart index 42d52977..fffa43b7 100644 --- a/lib/features/create_plan/presentation/widgets/option_widget.dart +++ b/lib/features/create_plan/presentation/widgets/option_widget.dart @@ -3,6 +3,7 @@ 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/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/widgets/add_item_widget.dart'; @@ -32,35 +33,9 @@ class _OptionWidgetState extends State { super.initState(); customerPriceController.addListener(() { if (!customerPriceController.text.endsWith('₽')) { - final textWithoutCurrency = customerPriceController.text.replaceAll('₽', ''); - customerPriceController.value = TextEditingValue( - text: "$textWithoutCurrency₽", - selection: TextSelection.collapsed(offset: textWithoutCurrency.length), - ); - } - final price = double.tryParse(customerPriceController.text.replaceAll('₽', '')); - if (price != null && price < 1200) { - customerPriceController.value = const TextEditingValue( - text: "1200₽", - selection: TextSelection.collapsed(offset: 4), - ); - } - }); - - creatorPriceController.addListener(() { - if (!creatorPriceController.text.endsWith('₽')) { - final textWithoutCurrency = creatorPriceController.text.replaceAll('₽', ''); - creatorPriceController.value = TextEditingValue( - text: "$textWithoutCurrency₽", - selection: TextSelection.collapsed(offset: textWithoutCurrency.length), - ); - } - final price = double.tryParse(creatorPriceController.text.replaceAll('₽', '')); - if (price != null && price < 1000) { - creatorPriceController.value = const TextEditingValue( - text: "1000₽", - selection: TextSelection.collapsed(offset: 4), - ); + final text = customerPriceController.text.replaceAll('₽', ''); + customerPriceController.text = '$text₽'; + customerPriceController.selection = TextSelection.collapsed(offset: text.length); } }); } @@ -94,12 +69,17 @@ class _OptionWidgetState extends State { const SizedBox(height: 4), CustomTextFormField( hintText: optionNameHintText, + hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError, controller: optionTitleController, onChanged: (value) { context.read().setOptionTitle(value); }, maxLength: 150, ), + if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.atLeast3Symbols), + ], const SizedBox(height: 12), Text( s.option_desc, @@ -114,6 +94,7 @@ class _OptionWidgetState extends State { child: CustomTextFormField( controller: optionDescriptionController, textAlignVertical: TextAlignVertical.top, + hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError, maxLength: 1000, minLines: null, maxLines: null, @@ -124,6 +105,10 @@ class _OptionWidgetState extends State { }, ), ), + if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.atLeast3Symbols), + ], const SizedBox(height: 12), Row( children: [ @@ -141,14 +126,8 @@ class _OptionWidgetState extends State { const SizedBox(height: 4), CustomTextFormField( controller: creatorPriceController, - onChanged: (value) { - double? price = double.tryParse(value.replaceAll('₽', '')); - if (price != null) { - price = price * 1.2; - customerPriceController.text = price.toStringAsFixed(2); - context.read().setPrice(price); - } - }, + readOnly: true, + textStyle: TextStyle(color: appTheme.gray), maxLength: 20, ), const SizedBox(height: 12), @@ -169,10 +148,11 @@ class _OptionWidgetState extends State { CustomTextFormField( controller: customerPriceController, textInputType: const TextInputType.numberWithOptions(decimal: true), + hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError, onChanged: (value) { final price = double.tryParse(value.replaceAll('₽', '')); + creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽'; if (price != null) { - creatorPriceController.text = (price * 0.8).toStringAsFixed(2); context.read().setPrice(price); } }, @@ -235,33 +215,33 @@ 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: 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), + // ), + // ), + // ], + // ), ], ), ); diff --git a/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart b/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart index 9e06a8e7..ad4100c0 100644 --- a/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart +++ b/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -36,28 +37,37 @@ class _PlanDurationDropDownWidgetState extends State final s = S.of(context); return BlocBuilder( builder: (context, state) { - return CustomDropDownButton( - hint: Text( - s.choose_plan_duration, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - onChanged: (value) { - if (value != null) { - context.read().setDuration(durationDropDownTitles.indexOf(value)); - } - }, - items: durationDropDownTitles - .mapIndexed( - (index, value) => DropdownMenuItem( - value: durationDropDownTitles[index], - child: Text( - durationDropDownTitles[index], - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - ) - .toList(), - value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], + return Column( + children: [ + CustomDropDownButton( + hasError: state.validationState.durationHasError, + hint: Text( + s.choose_plan_duration, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + context.read().setDuration(durationDropDownTitles.indexOf(value)); + } + }, + items: durationDropDownTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + value: durationDropDownTitles[index], + child: Text( + durationDropDownTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + ) + .toList(), + value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], + ), + if (state.validationState.durationHasError) ...[ + const SizedBox(height: 4), + ValidationErrorWidget(error: s.choose_plan_duration), + ], + ], ); }, ); diff --git a/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart b/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart index c834d5a7..0e744a37 100644 --- a/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart +++ b/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:dotted_border/dotted_border.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -72,74 +73,79 @@ class _PlanImagePickerWidgetState extends State { final s = S.of(context); return BlocBuilder( builder: (context, state) { - return GestureDetector( - onTap: () { - _showImagePickerOptions(s, context.read()); - }, - child: Container( - height: 240, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null, - color: appTheme.gray50, + return Column( + children: [ + GestureDetector( + onTap: () { + _showImagePickerOptions(s, context.read()); + }, + child: Container( + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null, + color: appTheme.gray50, + ), + width: MediaQuery.sizeOf(context).width, + padding: state.planData.image != null ? null : const EdgeInsets.all(8), + child: state.planData.image != null + ? Padding( + padding: const EdgeInsets.only(top: 6, right: 6), + child: GestureDetector( + onTap: () { + context.read().setImage(null); + }, + child: Align( + alignment: Alignment.topRight, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appTheme.dividerColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close_rounded, + color: Colors.grey, + size: 16, + ), + ), + ), + ), + ) + : Stack( + children: [ + DottedBorder( + stackFit: StackFit.expand, + dashPattern: const [14, 14], + color: state.validationState.imageHasError ? appTheme.red : appTheme.dottedBorder, + borderType: BorderType.RRect, + radius: const Radius.circular(12), + child: const SizedBox.expand(), + ), + Center( + child: Column( + children: [ + const SizedBox( + height: 48, + ), + SvgPicture.asset('assets/icons/eatvio_logo.svg'), + const SizedBox( + height: 24, + ), + Text( + s.add_cover, + style: TextStyles.textMMedium.copyWith(color: appTheme.gray), + ), + ], + ), + ), + ], + ), + ), ), - width: MediaQuery.sizeOf(context).width, - padding: state.planData.image != null ? null : const EdgeInsets.all(8), - child: state.planData.image != null - ? Padding( - padding: const EdgeInsets.only(top: 6, right: 6), - child: GestureDetector( - onTap: () { - context.read().setImage(null); - }, - child: Align( - alignment: Alignment.topRight, - child: Container( - width: 34, - height: 34, - decoration: BoxDecoration( - color: appTheme.dividerColor, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.close_rounded, - color: Colors.grey, - size: 16, - ), - ), - ), - ), - ) - : Stack( - children: [ - DottedBorder( - stackFit: StackFit.expand, - dashPattern: const [14, 14], - color: appTheme.dottedBorder, - borderType: BorderType.RRect, - radius: const Radius.circular(12), - child: const SizedBox.expand(), - ), - Center( - child: Column( - children: [ - const SizedBox( - height: 48, - ), - SvgPicture.asset('assets/icons/eatvio_logo.svg'), - const SizedBox( - height: 24, - ), - Text( - s.add_cover, - style: TextStyles.textMMedium.copyWith(color: appTheme.gray), - ), - ], - ), - ), - ], - ), - ), + if (state.validationState.imageHasError) ...[const SizedBox(height: 8), ValidationErrorWidget(error: s.add_cover)], + ], ); }, ); diff --git a/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart b/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart index cff69d5e..a50d461d 100644 --- a/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart +++ b/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -50,49 +51,55 @@ class _PlanTypeChoiceWidgetState extends State { @override Widget build(BuildContext context) { + final s = S.of(context); return BlocBuilder( builder: (context, state) { - return Row( + return Column( children: [ - ...List.generate(4, (index) { - return Expanded( - child: GestureDetector( - onTap: () { - final planTypes = [...state.planData.type]; - planTypes[index] = !planTypes[index]; - context.read().setPlanType(planTypes); - }, - child: Container( - margin: EdgeInsets.only(right: index == 3 ? 0 : 4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700, - border: Border.all( - width: 1, - color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00, + Row( + children: [ + ...List.generate(4, (index) { + return Expanded( + child: GestureDetector( + onTap: () { + final planTypes = [...state.planData.type]; + planTypes[index] = !planTypes[index]; + context.read().setPlanType(planTypes); + }, + child: Container( + margin: EdgeInsets.only(right: index == 3 ? 0 : 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700, + border: Border.all( + width: 1, + color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00, + ), + ), + height: 104, + child: Column( + children: [ + const SizedBox(height: 16), + Image.asset( + choiceContent[index].$1, + height: 30, + ), + const SizedBox(height: 8), + Text( + choiceContent[index].$2, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + style: TextStyles.textFS10FW500.copyWith(color: appTheme.black), + ), + ], + ), ), ), - height: 104, - child: Column( - children: [ - const SizedBox(height: 16), - Image.asset( - choiceContent[index].$1, - height: 30, - ), - const SizedBox(height: 8), - Text( - choiceContent[index].$2, - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - style: TextStyles.textFS10FW500.copyWith(color: appTheme.black), - ), - ], - ), - ), - ), - ); - }), + ); + }), + ], + ), + if (state.validationState.typeHasError) ...[const SizedBox(height: 8), ValidationErrorWidget(error: s.chosePlanType)] ], ); }, diff --git a/lib/features/create_plan/repositories/create_plan_repository.dart b/lib/features/create_plan/repositories/create_plan_repository.dart index 0f01edbc..09a4fcb6 100644 --- a/lib/features/create_plan/repositories/create_plan_repository.dart +++ b/lib/features/create_plan/repositories/create_plan_repository.dart @@ -1,3 +1,5 @@ +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 { @@ -5,10 +7,11 @@ class CreatePlanRepository { CreatePlanRepository({required this.createPlanService}); - Future createPlan({ + Future createPlan({ required Map model, }) async { - createPlanService.createPlan(model: model); + final formData = FormData.fromMap(model); + return await createPlanService.createPlan(formData: formData); } Future createPacketOption({ diff --git a/lib/features/create_plan/services/create_plan_service.dart b/lib/features/create_plan/services/create_plan_service.dart index f5d1891e..513d39b4 100644 --- a/lib/features/create_plan/services/create_plan_service.dart +++ b/lib/features/create_plan/services/create_plan_service.dart @@ -1,4 +1,5 @@ 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'; @@ -9,8 +10,8 @@ abstract class CreatePlanService { @POST('/api/plans/create') @MultiPart() - Future createPlan({ - @Body() required Map model, + Future createPlan({ + @Body() required FormData formData, }); @POST('/api/plans/create/{slug}/packet_option') @@ -19,7 +20,7 @@ abstract class CreatePlanService { @Body() required Map model, }); - @POST('/api/plans/create/{slug}/packet_options') + @POST('/api/plans/create/{slug}/packet_options') Future createPacketOptions({ @Path() required String slug, @Body() required Map model, diff --git a/lib/features/diary/models/consultant_review_model.dart b/lib/features/diary/models/consultant_review_model.dart index 95b12813..f31b21c9 100644 --- a/lib/features/diary/models/consultant_review_model.dart +++ b/lib/features/diary/models/consultant_review_model.dart @@ -36,6 +36,7 @@ class MealComment with _$MealComment { factory MealComment({ required String comment, + required int typeNo, }) = _MealComment; factory MealComment.fromJson(Map json) => _$MealCommentFromJson(json); @@ -45,7 +46,7 @@ class MealComment with _$MealComment { class PortionComment with _$PortionComment { factory PortionComment({ - @JsonKey(name: 'commentable_id') required int portionId, + @JsonKey(name: 'id') required int portionId, required String comment, }) = _PortionComment; diff --git a/lib/features/diary/models/consultant_review_model.freezed.dart b/lib/features/diary/models/consultant_review_model.freezed.dart index 96310b65..44d803d4 100644 --- a/lib/features/diary/models/consultant_review_model.freezed.dart +++ b/lib/features/diary/models/consultant_review_model.freezed.dart @@ -479,6 +479,7 @@ MealComment _$MealCommentFromJson(Map json) { /// @nodoc mixin _$MealComment { String get comment => throw _privateConstructorUsedError; + int get typeNo => throw _privateConstructorUsedError; /// Serializes this MealComment to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -496,7 +497,7 @@ abstract class $MealCommentCopyWith<$Res> { MealComment value, $Res Function(MealComment) then) = _$MealCommentCopyWithImpl<$Res, MealComment>; @useResult - $Res call({String comment}); + $Res call({String comment, int typeNo}); } /// @nodoc @@ -515,12 +516,17 @@ class _$MealCommentCopyWithImpl<$Res, $Val extends MealComment> @override $Res call({ Object? comment = null, + Object? typeNo = null, }) { return _then(_value.copyWith( comment: null == comment ? _value.comment : comment // ignore: cast_nullable_to_non_nullable as String, + typeNo: null == typeNo + ? _value.typeNo + : typeNo // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } } @@ -533,7 +539,7 @@ abstract class _$$MealCommentImplCopyWith<$Res> __$$MealCommentImplCopyWithImpl<$Res>; @override @useResult - $Res call({String comment}); + $Res call({String comment, int typeNo}); } /// @nodoc @@ -550,12 +556,17 @@ class __$$MealCommentImplCopyWithImpl<$Res> @override $Res call({ Object? comment = null, + Object? typeNo = null, }) { return _then(_$MealCommentImpl( comment: null == comment ? _value.comment : comment // ignore: cast_nullable_to_non_nullable as String, + typeNo: null == typeNo + ? _value.typeNo + : typeNo // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -563,17 +574,19 @@ class __$$MealCommentImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$MealCommentImpl implements _MealComment { - _$MealCommentImpl({required this.comment}); + _$MealCommentImpl({required this.comment, required this.typeNo}); factory _$MealCommentImpl.fromJson(Map json) => _$$MealCommentImplFromJson(json); @override final String comment; + @override + final int typeNo; @override String toString() { - return 'MealComment(comment: $comment)'; + return 'MealComment(comment: $comment, typeNo: $typeNo)'; } @override @@ -581,12 +594,13 @@ class _$MealCommentImpl implements _MealComment { return identical(this, other) || (other.runtimeType == runtimeType && other is _$MealCommentImpl && - (identical(other.comment, comment) || other.comment == comment)); + (identical(other.comment, comment) || other.comment == comment) && + (identical(other.typeNo, typeNo) || other.typeNo == typeNo)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, comment); + int get hashCode => Object.hash(runtimeType, comment, typeNo); /// Create a copy of MealComment /// with the given fields replaced by the non-null parameter values. @@ -605,13 +619,17 @@ class _$MealCommentImpl implements _MealComment { } abstract class _MealComment implements MealComment { - factory _MealComment({required final String comment}) = _$MealCommentImpl; + factory _MealComment( + {required final String comment, + required final int typeNo}) = _$MealCommentImpl; factory _MealComment.fromJson(Map json) = _$MealCommentImpl.fromJson; @override String get comment; + @override + int get typeNo; /// Create a copy of MealComment /// with the given fields replaced by the non-null parameter values. @@ -627,7 +645,7 @@ PortionComment _$PortionCommentFromJson(Map json) { /// @nodoc mixin _$PortionComment { - @JsonKey(name: 'commentable_id') + @JsonKey(name: 'id') int get portionId => throw _privateConstructorUsedError; String get comment => throw _privateConstructorUsedError; @@ -647,7 +665,7 @@ abstract class $PortionCommentCopyWith<$Res> { PortionComment value, $Res Function(PortionComment) then) = _$PortionCommentCopyWithImpl<$Res, PortionComment>; @useResult - $Res call({@JsonKey(name: 'commentable_id') int portionId, String comment}); + $Res call({@JsonKey(name: 'id') int portionId, String comment}); } /// @nodoc @@ -689,7 +707,7 @@ abstract class _$$PortionCommentImplCopyWith<$Res> __$$PortionCommentImplCopyWithImpl<$Res>; @override @useResult - $Res call({@JsonKey(name: 'commentable_id') int portionId, String comment}); + $Res call({@JsonKey(name: 'id') int portionId, String comment}); } /// @nodoc @@ -725,14 +743,13 @@ class __$$PortionCommentImplCopyWithImpl<$Res> @JsonSerializable() class _$PortionCommentImpl implements _PortionComment { _$PortionCommentImpl( - {@JsonKey(name: 'commentable_id') required this.portionId, - required this.comment}); + {@JsonKey(name: 'id') required this.portionId, required this.comment}); factory _$PortionCommentImpl.fromJson(Map json) => _$$PortionCommentImplFromJson(json); @override - @JsonKey(name: 'commentable_id') + @JsonKey(name: 'id') final int portionId; @override final String comment; @@ -775,14 +792,14 @@ class _$PortionCommentImpl implements _PortionComment { abstract class _PortionComment implements PortionComment { factory _PortionComment( - {@JsonKey(name: 'commentable_id') required final int portionId, + {@JsonKey(name: 'id') required final int portionId, required final String comment}) = _$PortionCommentImpl; factory _PortionComment.fromJson(Map json) = _$PortionCommentImpl.fromJson; @override - @JsonKey(name: 'commentable_id') + @JsonKey(name: 'id') int get portionId; @override String get comment; diff --git a/lib/features/diary/models/consultant_review_model.g.dart b/lib/features/diary/models/consultant_review_model.g.dart index f08faeb1..fd223d76 100644 --- a/lib/features/diary/models/consultant_review_model.g.dart +++ b/lib/features/diary/models/consultant_review_model.g.dart @@ -48,22 +48,24 @@ Map _$$ConsultantCommentsImplToJson( _$MealCommentImpl _$$MealCommentImplFromJson(Map json) => _$MealCommentImpl( comment: json['comment'] as String, + typeNo: (json['typeNo'] as num).toInt(), ); Map _$$MealCommentImplToJson(_$MealCommentImpl instance) => { 'comment': instance.comment, + 'typeNo': instance.typeNo, }; _$PortionCommentImpl _$$PortionCommentImplFromJson(Map json) => _$PortionCommentImpl( - portionId: (json['commentable_id'] as num).toInt(), + portionId: (json['id'] as num).toInt(), comment: json['comment'] as String, ); Map _$$PortionCommentImplToJson( _$PortionCommentImpl instance) => { - 'commentable_id': instance.portionId, + 'id': instance.portionId, 'comment': instance.comment, }; diff --git a/lib/features/diary/presentation/widgets/meal_button_widget.dart b/lib/features/diary/presentation/widgets/meal_button_widget.dart index 6ac80f20..afeef711 100644 --- a/lib/features/diary/presentation/widgets/meal_button_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_button_widget.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; 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'; @@ -6,7 +7,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class MealButtonWidget extends StatefulWidget { final int mealNumber; - const MealButtonWidget({ super.key, required this.mealNumber, @@ -22,6 +22,18 @@ 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 Color bgColor; + if (state.selectedMeal == widget.mealNumber) { + bgColor = appTheme.blueA400; + } else if (consultantComment != null) { + bgColor = appTheme.commentBg; + } else { + bgColor = const Color(0xFFF1F2F6); + } return InkWell( onTap: () { context.read().setSelectedMeal(widget.mealNumber); @@ -32,7 +44,7 @@ class _MealButtonWidgetState extends State { height: 32, decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), - color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), + color: bgColor, ), child: Center( child: Text( diff --git a/lib/features/diary/presentation/widgets/meal_widget.dart b/lib/features/diary/presentation/widgets/meal_widget.dart index 591d1a65..93f9fb2d 100644 --- a/lib/features/diary/presentation/widgets/meal_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_widget.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; @@ -81,8 +82,10 @@ class _MealWidgetState extends State { BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded) { - final mealsComments = state.consultantReview?.consultantComments.mealsComments; - if (mealsComments != null && (state.selectedMeal - 1 < mealsComments.length)) { + final consultantCommentOfMeal = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull( + (element) => element.typeNo == state.selectedMeal, + ); + if (consultantCommentOfMeal != null) { return Column( children: [ Text( @@ -90,7 +93,7 @@ class _MealWidgetState extends State { style: TextStyles.textLSemibold.copyWith(color: appTheme.black), ), const SizedBox(height: 12), - CommentContainer(comment: mealsComments[state.selectedMeal - 1].comment), + CommentContainer(comment: consultantCommentOfMeal.comment), ], ); } diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 5a9df1da..880e0df1 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -11,7 +11,7 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; class DiaryRepository { final DiaryService diaryService; - final StreamController _diaryForceFetchController = StreamController(); + final StreamController _diaryForceFetchController = StreamController.broadcast(); Stream get forceFetchStream => _diaryForceFetchController.stream; diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index b835effc..63e283c6 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/models/pagination_model.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'plan_pagination_model.freezed.dart'; @@ -70,10 +71,10 @@ class PacketOptions with _$PacketOptions { class PlanType with _$PlanType { const PlanType._(); const factory PlanType({ - @JsonKey(name: "fit") required bool? fit, - @JsonKey(name: "slim") required bool? slim, - @JsonKey(name: "behavior") required bool? behavior, - @JsonKey(name: "wellness") required bool? wellness, + @JsonKey(name: "fit", fromJson: parseBool) required bool? fit, + @JsonKey(name: "slim", fromJson: parseBool) required bool? slim, + @JsonKey(name: "behavior", fromJson: parseBool) required bool? behavior, + @JsonKey(name: "wellness", fromJson: parseBool) required bool? wellness, }) = _PlanType; factory PlanType.fromJson(Map json) => _$PlanTypeFromJson(json); diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index e2a687fb..cfbd39d5 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -118,7 +118,7 @@ class _MyDietWidgetState extends State { if (!context.mounted) return; showBottomSheetWithBlur( context: context, - child: SuccessPublishedDietBottomSheet(title: widget.diet.name), + child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name), ); await context.read().loadAllDiets(); } else { diff --git a/lib/features/my_finance/models/card_model.dart b/lib/features/my_finance/models/card_model.dart index c923519a..a6f0b159 100644 --- a/lib/features/my_finance/models/card_model.dart +++ b/lib/features/my_finance/models/card_model.dart @@ -5,7 +5,6 @@ part 'card_model.g.dart'; @freezed class CardModel with _$CardModel { - factory CardModel({ required int id, @JsonKey(name: 'bank_pan') required String cardNumber, @@ -16,7 +15,7 @@ class CardModel with _$CardModel { } bool parseBool(json) { - if (json == 1 || json == true) { + if (json == 1 || json == true || json == '1') { return true; } return false; diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart index c35b8ed1..184cd44f 100644 --- a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart @@ -28,20 +28,19 @@ class UserCardsCubit extends Cubit { emit(state.copyWith(cards: cards, state: BaseState.success)); } catch (e, st) { log(e.toString(), stackTrace: st); - emit(state.copyWith(state: BaseState.error, error: e.toString())); + emit(state.copyWith(state: BaseState.error, error: e.toString())); } } Future markAsDefault({required int id}) async { final oldCards = [...state.cards]; if (isClosed) return; - final cards = - oldCards.map((card) { - if (card.id == id) { - return card.copyWith(isDefault: true); - } - return card.copyWith(isDefault: false); - }).toList(); + final cards = oldCards.map((card) { + if (card.id == id) { + return card.copyWith(isDefault: true); + } + return card.copyWith(isDefault: false); + }).toList(); emit(state.copyWith(cards: cards)); EasyDebounce.debounce('markAsDefault', Duration(milliseconds: id), () async { @@ -66,4 +65,14 @@ class UserCardsCubit extends Cubit { log(e.toString(), stackTrace: st); } } + + Future collectCharge({required int chosenPlaId}) async { + try { + await cardsRepository.collectCharge(chosenPlaId: chosenPlaId); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return false; + } } diff --git a/lib/features/my_finance/presentation/pages/card_settings.dart b/lib/features/my_finance/presentation/pages/card_settings.dart index 5d690995..fa271da6 100644 --- a/lib/features/my_finance/presentation/pages/card_settings.dart +++ b/lib/features/my_finance/presentation/pages/card_settings.dart @@ -1,7 +1,11 @@ +import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; @@ -11,104 +15,221 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:collection/collection.dart'; +import 'package:go_router/go_router.dart'; class CardSettingsScreen extends StatelessWidget { static const path = '/card-settings-screen'; static const name = 'cardSettingsScreen'; - const CardSettingsScreen({super.key}); - + const CardSettingsScreen({super.key, this.chosenPlanId}); + final int? chosenPlanId; @override Widget build(BuildContext context) { - return BlocProvider(create: (context) => sl()..getCards(), child: const CardsSettingsContent()); + return BlocProvider( + create: (context) => sl()..getCards(), + child: CardsSettingsContent( + chosenPlanId: chosenPlanId, + )); } } class CardsSettingsContent extends StatelessWidget { - const CardsSettingsContent({super.key}); + const CardsSettingsContent({super.key, this.chosenPlanId}); + final int? chosenPlanId; @override Widget build(BuildContext context) { final s = S.of(context); - return Scaffold( - backgroundColor: const Color(0xFFF0F2F5), - appBar: AppBar( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), - title: Text(s.my_cards), - centerTitle: true, - ), - body: Column( - children: [ - const SizedBox(height: 16), - Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('Привязанные карты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w600, height: 1)), + return BlocBuilder( + builder: (context, state) { + return Scaffold( + backgroundColor: const Color(0xFFF0F2F5), + appBar: AppBar( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), + title: Text(s.my_cards), + centerTitle: true, + ), + body: Column( + children: [ + const SizedBox(height: 16), + Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text('Привязанные карты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w600, height: 1)), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Builder( + builder: (context) { + final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber; + if (defaultCardNumber != null) { + return Text( + 'Выплату будут производиться\nна карту ${defaultCardNumber.substring(defaultCardNumber.length - 7)}', + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, height: 1), + ); + } + return Text(s.no_default_cards, style: TextStyles.textMMedium.copyWith(color: appTheme.black)); + }, + ), + ), + const SizedBox(height: 16), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Builder( + builder: (context) { + switch (state.state) { + case BaseState.error: + return SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Center( + child: DataErrorWidget( + onReload: () async { + context.read().getCards(); + }, + ), + ), + ); + case BaseState.success: + return Padding( + padding: const EdgeInsets.only(left: 12), + child: Row( + spacing: 12, + children: List.generate(state.cards.length + 1, (index) { + if (index < state.cards.length) { + final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7); + return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber); + } + return const CardWidget(enabledAddCard: true); + }), + ), + ); + + default: + return SizedBox(width: MediaQuery.sizeOf(context).width, height: 200, child: const Center(child: Loader())); + } + }, + ), + ), + const SizedBox(height: 16), + ], ), - const SizedBox(height: 8), + ), + if (chosenPlanId != null && state.cards.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: BlocBuilder( - builder: (context, state) { + child: CustomOutlinedButton( + text: s.collectCharge, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () async { final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber; - if (defaultCardNumber != null) { - return Text( - 'Выплату будут производиться\nна карту ${defaultCardNumber.substring(defaultCardNumber.length - 7)}', - style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, height: 1), + final result = await context.read().collectCharge(chosenPlaId: chosenPlanId!); + if (context.mounted) { + if (!result) { + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text( + s.chargeCollectionFailed, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + const SizedBox( + height: 8, + ), + Text( + s.contactSupport, + style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + AppRouter.navigatorKey.currentContext?.pop(); + }, + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + ], + ), + ); + return; + } + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text( + s.chargeApproved, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + const SizedBox( + height: 8, + ), + Text( + '${s.moneySentToCard} ${defaultCardNumber?.substring(defaultCardNumber.length - 7)}', + textAlign: TextAlign.center, + style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black), + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + AppRouter.navigatorKey.currentContext?.pop(); + }, + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + ], + ), ); } - return Text(s.no_default_cards, style: TextStyles.textMMedium.copyWith(color: appTheme.black)); }, ), - ), - const SizedBox(height: 16), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: BlocBuilder( - builder: (context, state) { - switch (state.state) { - case BaseState.error: - return SizedBox( - width: MediaQuery.sizeOf(context).width, - child: Center( - child: DataErrorWidget( - onReload: () async { - context.read().getCards(); - }, - ), - ), - ); - case BaseState.success: - return Padding( - padding: const EdgeInsets.only(left: 12), - child: Row( - spacing: 12, - children: List.generate(state.cards.length + 1, (index) { - if (index < state.cards.length) { - final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7); - return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber); - } - return const CardWidget(enabledAddCard: true); - }), - ), - ); - - default: - return SizedBox(width: MediaQuery.sizeOf(context).width, height: 200, child: const Center(child: Loader())); - } - }, - ), - ), - const SizedBox(height: 16), - ], - ), + ) + ] + ], ), - ], - ), + ); + }, ); } } diff --git a/lib/features/my_finance/repositories/cards_repository.dart b/lib/features/my_finance/repositories/cards_repository.dart index 221a3376..42534348 100644 --- a/lib/features/my_finance/repositories/cards_repository.dart +++ b/lib/features/my_finance/repositories/cards_repository.dart @@ -36,4 +36,12 @@ class CardsRepository { rethrow; } } + + Future collectCharge({required int chosenPlaId}) async { + try { + await cardsService.collectCharge(chosenPlanId: chosenPlaId); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/my_finance/services/cards_service.dart b/lib/features/my_finance/services/cards_service.dart index b42a8ed7..c653d1a5 100644 --- a/lib/features/my_finance/services/cards_service.dart +++ b/lib/features/my_finance/services/cards_service.dart @@ -24,5 +24,10 @@ abstract class CardsService { Future deleteCard({ @Path('id') required int id, }); + + @POST('/api/chosen_plans/{id}/take_payin') + Future collectCharge({ + @Path('id') required int chosenPlanId, + }); } 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 2198b589..a8168917 100644 --- a/lib/features/order_info/presentation/info_by_order_screen.dart +++ b/lib/features/order_info/presentation/info_by_order_screen.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart' import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart'; +import 'package:eatvio_app/features/order_info/presentation/widgets/collect_charge_banner.dart'; import 'package:eatvio_app/features/order_info/presentation/widgets/payment_banner.dart'; import 'package:eatvio_app/features/order_info/presentation/widgets/plan_info.dart'; import 'package:eatvio_app/features/order_info/presentation/widgets/plan_props.dart'; @@ -61,7 +62,7 @@ class _InfoByOrderScreenContentState extends State { builder: (context, state) { final plan = state.chosenPlan; if (state.isLoading) { - Scaffold( + return Scaffold( appBar: AppBar( title: Text( s.infoAboutOrder, @@ -113,7 +114,7 @@ class _InfoByOrderScreenContentState extends State { planStatus = PlanStatusValues.paid; } else if (widget.successfulPayment == false) { planStatus = PlanStatusValues.paymentFailed; - } else if (plan.chosenPlanState == ChosenPlanStateEnum.statePaid) { + } else if (plan.chosenPlanState.isStateAfterPaidState()) { planStatus = PlanStatusValues.paid; } else { planStatus = PlanStatusValues.pendingPayment; @@ -134,6 +135,12 @@ class _InfoByOrderScreenContentState extends State { body: SingleChildScrollView( child: Column( children: [ + if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[ + CollectChargeBanner( + chosenPlan: plan, + price: chosenOption?.price ?? 0, + ) + ], if (widget.successfulPayment != null) ...[ const SizedBox(height: 8), PaymentBanner( @@ -148,7 +155,10 @@ class _InfoByOrderScreenContentState extends State { backgroundColor: appTheme.whiteA700, ), const SizedBox(height: 8), - PlanProps(planDuration: plan.plan.duration, planPrice: chosenOption?.price.toDouble() ?? 0, planStatus: planStatus), + PlanProps( + planDuration: plan.plan.duration, + planPrice: chosenOption?.price ?? 0, + planStatus: planStatus), const SizedBox(height: 8), PlanInfo( title: plan.plan.name, diff --git a/lib/features/order_info/presentation/widgets/collect_charge_banner.dart b/lib/features/order_info/presentation/widgets/collect_charge_banner.dart new file mode 100644 index 00000000..cb1eaf1d --- /dev/null +++ b/lib/features/order_info/presentation/widgets/collect_charge_banner.dart @@ -0,0 +1,72 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class CollectChargeBanner extends StatelessWidget { + const CollectChargeBanner({super.key, required this.chosenPlan, required this.price}); + final ChosenPlanModel chosenPlan; + final double price; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return MainBackgroundBouble( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Stack( + children: [ + Positioned( + left: 0, + top: 0, + bottom: 0, + child: Container( + width: 4, + decoration: BoxDecoration( + color: appTheme.blueA400, borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20))), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(right: 80), + child: Text( + '${s.congratsYouHaveFinishedPlan} «${chosenPlan.plan.name}»', + style: TextStyles.textMSemibold.copyWith( + color: appTheme.black, + ), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(right: 80), + child: Text( + '${s.userConfirmedPlanCompletion} $price₽', + style: TextStyles.textSMedium.copyWith( + color: appTheme.black, + ), + ), + ), + const SizedBox(height: 12), + CustomOutlinedButton( + text: s.collectCharge, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () async { + GoRouter.of(context).push(CardSettingsScreen.path, extra: chosenPlan.id); + }, + ) + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/features/order_info/presentation/widgets/payment_banner.dart b/lib/features/order_info/presentation/widgets/payment_banner.dart index 316a318d..bd570581 100644 --- a/lib/features/order_info/presentation/widgets/payment_banner.dart +++ b/lib/features/order_info/presentation/widgets/payment_banner.dart @@ -14,6 +14,7 @@ class PaymentBanner extends StatefulWidget { const PaymentBanner({ super.key, required this.successfulPayment, + }); final bool successfulPayment; diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index 2935310f..f905901a 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -237,6 +237,7 @@ class _AboutPlanScreenState extends State { 'chatScreen', extra: { 'chatId': chosenPlan.chatId, + 'sendInitialMessage': true, }, ); }, diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 96602566..d27fa8ee 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -33,6 +33,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), "add": MessageLookupByLibrary.simpleMessage("Добавить"), + "addAtLeastOneItem": MessageLookupByLibrary.simpleMessage( + "Добавьте хотя бы одину опцию", + ), "add_card": MessageLookupByLibrary.simpleMessage("Привязать карту"), "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), @@ -55,6 +58,9 @@ class MessageLookup extends MessageLookupByLibrary { "another_form_of_stage": MessageLookupByLibrary.simpleMessage("этапа"), "answer": MessageLookupByLibrary.simpleMessage("Ответить"), "archived": MessageLookupByLibrary.simpleMessage("В архиве"), + "atLeast3Symbols": MessageLookupByLibrary.simpleMessage( + "Минимум 3 символа", + ), "authentificate": MessageLookupByLibrary.simpleMessage("Войти"), "average_pressure": MessageLookupByLibrary.simpleMessage( "Средние показатели давления:", @@ -86,6 +92,10 @@ class MessageLookup extends MessageLookupByLibrary { "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", ), "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), + "chargeApproved": MessageLookupByLibrary.simpleMessage("Выплата одобрена"), + "chargeCollectionFailed": MessageLookupByLibrary.simpleMessage( + "Не удалось оформить выплату", + ), "chat": MessageLookupByLibrary.simpleMessage("Чат"), "check_out_video_recipe": MessageLookupByLibrary.simpleMessage( "Посмотреть видео рецепт", @@ -105,6 +115,7 @@ class MessageLookup extends MessageLookupByLibrary { "choose_start_diet_date": MessageLookupByLibrary.simpleMessage( "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", ), + "chosePlanType": MessageLookupByLibrary.simpleMessage("Выберите тип плана"), "chose_plan_start_date_2": MessageLookupByLibrary.simpleMessage( "Выберите дату старты плана. Она отобразиться в дневнике пользователя", ), @@ -119,6 +130,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "city": MessageLookupByLibrary.simpleMessage("Город"), "city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"), + "collectCharge": MessageLookupByLibrary.simpleMessage("Получить выплату"), "comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), "comment": MessageLookupByLibrary.simpleMessage("комментарий"), "commentForMeal": MessageLookupByLibrary.simpleMessage( @@ -138,9 +150,15 @@ class MessageLookup extends MessageLookupByLibrary { "Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса.", ), "confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"), + "congratsYouHaveFinishedPlan": MessageLookupByLibrary.simpleMessage( + "Поздравляем, Вы закончили план", + ), "consultations_count": MessageLookupByLibrary.simpleMessage( "Кол-во проведенных консулдьтаций", ), + "contactSupport": MessageLookupByLibrary.simpleMessage( + "Обратитесь в поддержку", + ), "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), @@ -382,6 +400,9 @@ class MessageLookup extends MessageLookupByLibrary { "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.", ), "milliliter": MessageLookupByLibrary.simpleMessage("мл"), + "minPlanPrice": MessageLookupByLibrary.simpleMessage( + "Минимальная сумма 1200₽", + ), "minimal_customer_price": MessageLookupByLibrary.simpleMessage( "Минимальная цена для покупателя: 1200₽", ), @@ -393,6 +414,9 @@ class MessageLookup extends MessageLookupByLibrary { "misrepresentation_description": MessageLookupByLibrary.simpleMessage( "Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение.", ), + "moneySentToCard": MessageLookupByLibrary.simpleMessage( + "Средства отправлены на карту", + ), "more_about_order": MessageLookupByLibrary.simpleMessage( "Подробнее о заказе", ), @@ -628,6 +652,9 @@ class MessageLookup extends MessageLookupByLibrary { "Верхние показатели давления:", ), "upper": MessageLookupByLibrary.simpleMessage("Верхнее"), + "userConfirmedPlanCompletion": MessageLookupByLibrary.simpleMessage( + "Пользователь подтвердил прохождения плана. В ближайшее время Вы получите", + ), "user_authorization": MessageLookupByLibrary.simpleMessage( "Аутентификация", ), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 260c15f5..441ca764 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3064,6 +3064,116 @@ class S { args: [], ); } + + /// `Поздравляем, Вы закончили план` + String get congratsYouHaveFinishedPlan { + return Intl.message( + 'Поздравляем, Вы закончили план', + name: 'congratsYouHaveFinishedPlan', + desc: '', + args: [], + ); + } + + /// `Пользователь подтвердил прохождения плана. В ближайшее время Вы получите` + String get userConfirmedPlanCompletion { + return Intl.message( + 'Пользователь подтвердил прохождения плана. В ближайшее время Вы получите', + name: 'userConfirmedPlanCompletion', + desc: '', + args: [], + ); + } + + /// `Получить выплату` + String get collectCharge { + return Intl.message( + 'Получить выплату', + name: 'collectCharge', + desc: '', + args: [], + ); + } + + /// `Выплата одобрена` + String get chargeApproved { + return Intl.message( + 'Выплата одобрена', + name: 'chargeApproved', + desc: '', + args: [], + ); + } + + /// `Средства отправлены на карту` + String get moneySentToCard { + return Intl.message( + 'Средства отправлены на карту', + name: 'moneySentToCard', + desc: '', + args: [], + ); + } + + /// `Не удалось оформить выплату` + String get chargeCollectionFailed { + return Intl.message( + 'Не удалось оформить выплату', + name: 'chargeCollectionFailed', + desc: '', + args: [], + ); + } + + /// `Обратитесь в поддержку` + String get contactSupport { + return Intl.message( + 'Обратитесь в поддержку', + name: 'contactSupport', + desc: '', + args: [], + ); + } + + /// `Минимум 3 символа` + String get atLeast3Symbols { + return Intl.message( + 'Минимум 3 символа', + name: 'atLeast3Symbols', + desc: '', + args: [], + ); + } + + /// `Выберите тип плана` + String get chosePlanType { + return Intl.message( + 'Выберите тип плана', + name: 'chosePlanType', + desc: '', + args: [], + ); + } + + /// `Минимальная сумма 1200₽` + String get minPlanPrice { + return Intl.message( + 'Минимальная сумма 1200₽', + name: 'minPlanPrice', + desc: '', + args: [], + ); + } + + /// `Добавьте хотя бы одину опцию` + String get addAtLeastOneItem { + return Intl.message( + 'Добавьте хотя бы одину опцию', + name: 'addAtLeastOneItem', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b0f45e86..c8e5a2c6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -369,6 +369,17 @@ "orderCanceled": "Заказ отменен", "couldNotCancelOrder": "Не удалось отменить заказ", "dayComment": "Комментарий ко всему дню", - "mealComment": "Комментарий к приему пищи" + "mealComment": "Комментарий к приему пищи", + "congratsYouHaveFinishedPlan": "Поздравляем, Вы закончили план", + "userConfirmedPlanCompletion": "Пользователь подтвердил прохождения плана. В ближайшее время Вы получите", + "collectCharge": "Получить выплату", + "chargeApproved": "Выплата одобрена", + "moneySentToCard": "Средства отправлены на карту", + "chargeCollectionFailed": "Не удалось оформить выплату", + "contactSupport": "Обратитесь в поддержку", + "atLeast3Symbols": "Минимум 3 символа", + "chosePlanType": "Выберите тип плана", + "minPlanPrice": "Минимальная сумма 1200₽", + "addAtLeastOneItem": "Добавьте хотя бы одину опцию" }