From 3b4ed501fb0b7cc62d2c00d4ffe1190844dad462 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 30 Apr 2025 21:25:23 +0700 Subject: [PATCH] feat(chats,diary): added logic for chosen diet portions and added banner to finish plan to chats --- ios/Runner.xcodeproj/project.pbxproj | 33 ++-- lib/core/app_router.dart | 2 +- lib/core/const.dart | 4 +- lib/core/locator_service.dart | 4 +- lib/core/widgets/bottom_sheet_with_blur.dart | 2 +- .../chat/models/plan_complain_model.dart | 14 ++ .../bloc/chat_room_cubit/chat_room_cubit.dart | 27 ++- .../chat/presentation/pages/chat_screen.dart | 8 +- .../widgets/complete_plan_bottom_sheet.dart | 102 ++++++++++++ .../widgets/notification_banner.dart | 72 +++++++- .../widgets/plan_complain_bottom_sheet.dart | 154 ++++++++++++++++++ .../chat/repositories/chat_repository.dart | 22 +++ lib/features/chat/services/chat_service.dart | 12 ++ .../models/consultation_model.dart | 5 +- .../services/consultation_service.dart | 3 +- .../diary/models/diary_day_model.dart | 5 +- .../diary/presentation/bloc/diary_cubit.dart | 16 +- .../presentation/pages/diary_screen.dart | 7 +- .../presentation/widgets/product_widget.dart | 49 +++--- .../feed/models/plan_pagination_model.dart | 13 +- .../internet_connection_checker_screen.dart | 2 +- .../presentation/info_by_order_screen.dart | 2 +- .../plans/enums/chosen_plan_state_enum.dart | 1 + .../presentation/pages/welcome_screen.dart | 4 +- lib/generated/intl/messages_en.dart | 12 ++ lib/generated/l10n.dart | 40 +++++ lib/l10n/intl_en.arb | 6 +- pubspec.yaml | 2 +- 28 files changed, 552 insertions(+), 71 deletions(-) create mode 100644 lib/features/chat/models/plan_complain_model.dart create mode 100644 lib/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart create mode 100644 lib/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c67b16dc..c2ccb23a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -386,12 +386,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 7; - DEVELOPMENT_TEAM = "VL68C36HYS"; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = VL68C36HYS; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -411,8 +409,7 @@ MARKETING_VERSION = 1.0.50; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Distribution"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Distribution; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -539,12 +536,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 7; - DEVELOPMENT_TEAM = "VL68C36HYS"; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = VL68C36HYS; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -564,8 +559,7 @@ MARKETING_VERSION = 1.0.50; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Distribution"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Distribution; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -580,12 +574,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 7; - DEVELOPMENT_TEAM = "VL68C36HYS"; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = VL68C36HYS; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -605,8 +597,7 @@ MARKETING_VERSION = 1.0.50; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Distribution"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Distribution; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index e596c98e..63ec1094 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -223,7 +223,7 @@ class AppRouter { builder: (context, state) { final data = state.extra as Map; final chatId = data['chatId'] as String; - final sendInitialMessage = data['sendInitialMessage'] as bool; + final sendInitialMessage = data['sendInitialMessage'] as bool?; return ChatScreen(chatId: chatId, sendInitialMessage: sendInitialMessage,); }, ), diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..4f38a54e 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index ebd8d539..c2c27015 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -98,11 +98,11 @@ Future init() async { sl.registerLazySingleton(() => WsManager(wsService: sl())); sl.registerLazySingleton(() => NewMessagesCubit()); sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl())); - sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); // Cubit factories + sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); @@ -114,7 +114,7 @@ Future init() async { sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl())); + sl.registerFactory(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl(), profileRepository: sl())); sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); diff --git a/lib/core/widgets/bottom_sheet_with_blur.dart b/lib/core/widgets/bottom_sheet_with_blur.dart index f7cdf3df..57dc9026 100644 --- a/lib/core/widgets/bottom_sheet_with_blur.dart +++ b/lib/core/widgets/bottom_sheet_with_blur.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; Future showBottomSheetWithBlur({ required BuildContext context, required Widget child, - double height = 200, + double? height = 200, bool hasBlur = true, bool enableDrag = true, bool userRootNavigator = false, diff --git a/lib/features/chat/models/plan_complain_model.dart b/lib/features/chat/models/plan_complain_model.dart new file mode 100644 index 00000000..63e2af8e --- /dev/null +++ b/lib/features/chat/models/plan_complain_model.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'plan_complain_model.freezed.dart'; +part 'plan_complain_model.g.dart'; + +@freezed +class PlanComplainModel with _$PlanComplainModel { + + factory PlanComplainModel({ + @JsonKey(name: 'description') required String description, + }) = _PlanComplainModel; + + factory PlanComplainModel.fromJson(Map json) => _$PlanComplainModelFromJson(json); +} \ No newline at end of file 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 bd53c0ac..3c522edb 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 @@ -10,6 +10,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/models/plan_complain_model.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/my_finance/models/card_model.dart'; @@ -92,7 +93,7 @@ class ChatRoomCubit extends Cubit { hasCard: false, ), ); - + if (chatRoom.chosenPlan?.consultantId == chatRoom.users.receiver.id) { await checkCards(); } @@ -209,6 +210,30 @@ class ChatRoomCubit extends Cubit { } } + Future complainAboutConsultation({required String description}) async { + try { + final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; + if (planId == null) return false; + await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description)); + return true; + } catch (e) { + log(e.toString()); + return false; + } + } + + Future finishChosenPlan() async { + try { + final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; + if (planId == null) return false; + await chatRepository.finishChosenPlan(id: planId); + return true; + } catch (e) { + log(e.toString()); + return false; + } + } + @override Future close() async { wsManager.unsubscribeDirectChatChannel(); diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 250c86fe..d90aaa93 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -29,7 +29,7 @@ import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String chatId; - final bool sendInitialMessage; + final bool? sendInitialMessage; const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage}); @override @@ -48,7 +48,7 @@ class _ChatScreenState extends State { void initState() { super.initState(); scrollController.addListener(_onScroll); - chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage); + chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false); } _onScroll() { @@ -98,7 +98,7 @@ 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; @@ -107,6 +107,8 @@ class _ChatScreenState extends State { bannerType = BannerType.payment; } else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone) { bannerType = BannerType.collectCharge; + } else if (!(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished) && !amIConsultant) { + bannerType = BannerType.finishPlan; } PacketOptions? chosenOption; diff --git a/lib/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart b/lib/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart new file mode 100644 index 00000000..ef950bc9 --- /dev/null +++ b/lib/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart @@ -0,0 +1,102 @@ +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/custom_outlined_button.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class CompletePlanBottomSheet extends StatefulWidget { + const CompletePlanBottomSheet({super.key, required this.cubit}); + final ChatRoomCubit cubit; + + @override + State createState() => _CompletePlanBottomSheetState(); +} + +class _CompletePlanBottomSheetState extends State { + bool? isFailure; + @override + Widget build(BuildContext context) { + final s = S.of(context); + if (isFailure == true) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text('Произошла ошибка, попробуйте позже', style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + context.pop(); + }, + ), + const SizedBox( + height: 40, + ), + ], + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text('Подтверждение плана', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox( + height: 16, + ), + Text( + 'Вы уверенны что хотите подтвердить прохождения плана?', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.confirm, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + final result = await widget.cubit.finishChosenPlan(); + if (result) { + if (context.mounted) { + context.pop(); + return; + } + } + setState(() { + isFailure = true; + }); + }, + ), + const SizedBox( + height: 40, + ), + ], + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/notification_banner.dart b/lib/features/chat/presentation/widgets/notification_banner.dart index d4eaf577..8828863f 100644 --- a/lib/features/chat/presentation/widgets/notification_banner.dart +++ b/lib/features/chat/presentation/widgets/notification_banner.dart @@ -1,11 +1,14 @@ 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/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'; import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart'; import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -40,7 +43,7 @@ class _NotificationBannerState extends State { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.bannerType != BannerType.collectCharge) + if (widget.bannerType != BannerType.collectCharge && widget.bannerType != BannerType.finishPlan) Padding( padding: const EdgeInsets.only(top: 12, right: 8, left: 12), child: SvgPicture.asset('assets/icons/reversed_exclamation_mark.svg', height: 68, width: 68), @@ -139,6 +142,19 @@ class _NotificationBannerState extends State { ), const SizedBox(height: 8), ], + BannerType.finishPlan => [ + SizedBox( + width: size.width * 0.6, + child: Text('${s.congratsYouHaveFinishedPlan} «${widget.planName}». ${s.pleaseConfirmPlan}', + style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 8), + SizedBox( + width: size.width * 0.7, + child: Text(s.planWillBeConfirmedAfterThreeDays, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + ), + const SizedBox(height: 8), + ], }, ), ), @@ -156,7 +172,57 @@ class _NotificationBannerState extends State { GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.chosenPlanId); }, ), - ) + ), + if (widget.bannerType == BannerType.finishPlan) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: CustomOutlinedButton( + text: s.confirm, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + final cubit = context.read(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + height: null, + child: CompletePlanBottomSheet( + cubit: cubit, + ), + ); + }, + ), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + child: Text(s.youCanComplain, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + ), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GestureDetector( + onTap: () { + final cubit = context.read(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + height: null, + child: PlanComplainBottomSheet( + cubit: cubit, + ), + ); + }, + child: Text(s.leaveComplainRequest, style: TextStyles.textSMedium.copyWith(color: appTheme.red))), + ), + ), + const SizedBox(height: 8), + ] ], ), ); @@ -176,4 +242,4 @@ class _StarIcon extends StatelessWidget { } } -enum BannerType { date, card, payment, collectCharge } +enum BannerType { date, card, payment, collectCharge, finishPlan } diff --git a/lib/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart b/lib/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart new file mode 100644 index 00000000..34f500f7 --- /dev/null +++ b/lib/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart @@ -0,0 +1,154 @@ +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/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class PlanComplainBottomSheet extends StatefulWidget { + const PlanComplainBottomSheet({super.key, required this.cubit}); + final ChatRoomCubit cubit; + + @override + State createState() => _PlanComplainBottomSheetState(); +} + +class _PlanComplainBottomSheetState extends State { + final controller = TextEditingController(); + bool? result; + int? textLength; + @override + Widget build(BuildContext context) { + final s = S.of(context); + if (result == true) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text('Подача жалобы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox( + height: 16, + ), + Text('Жалоба отправлена на рассмотрение. Обычно это занимает до трёх рабочих дней.', style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + context.pop(); + }, + ), + const SizedBox( + height: 40, + ), + ], + ); + } else if (result == false) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text('Подача жалобы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox( + height: 16, + ), + Text('Произошла ошибка, попробуйте позже', style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + context.pop(); + }, + ), + const SizedBox( + height: 40, + ), + ], + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text('Подача жалобы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox( + height: 16, + ), + Align(alignment: Alignment.centerLeft, child: Text('Опишите причину жалобы', style: TextStyles.textSMedium.copyWith(color: appTheme.black))), + const SizedBox( + height: 8, + ), + CustomTextFormField( + controller: controller, + hasError: textLength != null && textLength! < 3, + onChanged: (p0) { + setState(() { + textLength = p0.length; + }); + }, + ), + if (textLength != null && textLength! < 3) ...[ + const SizedBox( + height: 4, + ), + ValidationErrorWidget(error: s.atLeast3Symbols), + ], + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.confirm, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + if (controller.text.length < 3) { + return; + } + final result = await widget.cubit.complainAboutConsultation(description: controller.text); + setState(() { + this.result = result; + }); + }, + ), + const SizedBox( + height: 40, + ), + ], + ), + ); + } +} diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 30c34586..28f80a48 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart'; import 'package:eatvio_app/features/chat/models/payment_model.dart'; +import 'package:eatvio_app/features/chat/models/plan_complain_model.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:http_parser/http_parser.dart'; @@ -132,4 +133,25 @@ class ChatRepository { rethrow; } } + + Future finishChosenPlan({ + required int id, + }) async { + try { + await chatService.finishChosenPlan(id: id); + } catch (e) { + rethrow; + } + } + + Future complainAboutConsultation({ + required int id, + required PlanComplainModel body, + }) async { + try { + await chatService.complainAboutConsultation(id: id, body: body); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index 2e2a127e..6d4c992e 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart'; import 'package:eatvio_app/features/chat/models/payment_model.dart'; +import 'package:eatvio_app/features/chat/models/plan_complain_model.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:retrofit/retrofit.dart'; @@ -68,4 +69,15 @@ abstract class ChatService { Future getChosenPlan({ @Path() required int id, }); + + @POST('/api/chosen_plans/{id}/confirm_finish') + Future finishChosenPlan({ + @Path() required int id, + }); + + @POST('/api/chosen_plans/{id}/complain') + Future complainAboutConsultation({ + @Path() required int id, + @Body() required PlanComplainModel body, + }); } diff --git a/lib/features/consultations/models/consultation_model.dart b/lib/features/consultations/models/consultation_model.dart index 5e96cb68..4c9464bf 100644 --- a/lib/features/consultations/models/consultation_model.dart +++ b/lib/features/consultations/models/consultation_model.dart @@ -37,8 +37,9 @@ class DiaryModel with _$DiaryModel { } DateTime parseUnformattedDate(String date) { - final dateChunks = date.split('/').map(int.parse).toList(); - return DateTime(dateChunks.last, dateChunks.first, dateChunks[1]); + final dateChunks = date.split('-').map(int.parse).toList(); + + return DateTime(dateChunks.first, dateChunks[1], dateChunks.last); } enum DiaryStateEnum { diff --git a/lib/features/consultations/services/consultation_service.dart b/lib/features/consultations/services/consultation_service.dart index 25719f8a..24881685 100644 --- a/lib/features/consultations/services/consultation_service.dart +++ b/lib/features/consultations/services/consultation_service.dart @@ -8,7 +8,6 @@ part 'consultation_service.g.dart'; abstract class ConsultationService { factory ConsultationService(Dio dio, {String baseUrl}) = _ConsultationService; - @GET('/api/consultations') + @GET('/api/consultations/in_progress') Future getConsultations(); - } diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart index dbed9b6f..f099987e 100644 --- a/lib/features/diary/models/diary_day_model.dart +++ b/lib/features/diary/models/diary_day_model.dart @@ -23,7 +23,10 @@ enum PortionState { @freezed class DiaryDayWrapperModel with _$DiaryDayWrapperModel { const factory DiaryDayWrapperModel({ - @JsonKey(name: "diary") required DiaryDayModel diary, + @JsonKey( + name: "diary", + ) + required DiaryDayModel diary, }) = _DiaryDayWrapperModel; factory DiaryDayWrapperModel.fromJson(Map json) => _$DiaryDayWrapperModelFromJson(json); diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 3cfe66c7..36934c50 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -11,6 +11,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DiaryCubit extends Cubit { @@ -19,8 +20,13 @@ class DiaryCubit extends Cubit { late int selectedMeal; final DiaryRepository diaryRepository; final GlossaryRepository glossaryRepository; + final ProfileRepository profileRepository; int? diaryId; - DiaryCubit({required this.diaryRepository, required this.glossaryRepository}) : super(const DiaryState.loading()) { + DiaryCubit({ + required this.diaryRepository, + required this.glossaryRepository, + required this.profileRepository, + }) : super(const DiaryState.loading()) { today = DateTime.now(); selectedDay = today; diaryRepository.forceFetchStream.listen((_) { @@ -42,6 +48,7 @@ class DiaryCubit extends Cubit { fetchDiary([bool needLoading = false, int? diaryId]) async { this.diaryId ??= diaryId; + await profileRepository.getChosenDiets(); if (needLoading) { emit(const DiaryState.loading()); selectedMeal = 1; @@ -88,7 +95,12 @@ class DiaryCubit extends Cubit { required String wakeUp, required String retire, }) async { - await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment); + await diaryRepository.finishDiaryDay( + diaryId: diaryId, + wakeUp: wakeUp, + retire: retire, + comment: comment.isEmpty ? null : comment, + ); await fetchDiary(); } diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 1e05e832..7ab05a60 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -79,7 +79,12 @@ class DiaryScreenContent extends StatelessWidget { child: Column( children: [ if (diaryId == null) const CalendarWidget(), - if (state is DiaryStateLoading) const Loader(), + if (state is DiaryStateLoading) ...[ + const SizedBox( + height: 60, + ), + const Loader() + ], if (state is DiaryStateLoaded) WakeupAndRetireWidget( showRetire: state.diary.typeOfDiary.hasChosenPlan, diff --git a/lib/features/diary/presentation/widgets/product_widget.dart b/lib/features/diary/presentation/widgets/product_widget.dart index bc9020cb..0ae071ee 100644 --- a/lib/features/diary/presentation/widgets/product_widget.dart +++ b/lib/features/diary/presentation/widgets/product_widget.dart @@ -34,7 +34,13 @@ class _ProductWidgetState extends State { super.initState(); diaryCubit = context.read(); portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт'; + isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; + } + @override + void didUpdateWidget(ProductWidget oldWidget) { + super.didUpdateWidget(oldWidget); + portionName = widget.portion.portion == 'gr' ? 'гр' : 'шт'; isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; } @@ -64,8 +70,9 @@ class _ProductWidgetState extends State { state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; final isDiaryFinished = state.diary.typeOfDiary.diaryState == DiaryStateEnum.finished || state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished; - final canEditPortion = !state.isConsultantView && !isDiaryCommented && !isDiaryFinished; + final canEditPortion = !state.isConsultantView && !isDiaryCommented && !isDiaryFinished && !widget.portion.fromDiet; final consultantComments = state.consultantReview?.consultantComments; + final canMarkAsEaten = !state.isConsultantView && !isDiaryCommented && !isDiaryFinished; PortionComment? portionComment; if (consultantComments != null) { portionComment = consultantComments.portionsComments.firstWhereOrNull((elem) => elem.portionId == widget.portion.id); @@ -108,7 +115,7 @@ class _ProductWidgetState extends State { }); }, child: SvgPicture.asset('assets/icons/edit.svg')); - } else if (!isDiaryCommented) { + } else if (!isDiaryCommented && !widget.portion.fromDiet) { trailing = const Icon(Icons.more_vert_rounded); } return Column( @@ -121,24 +128,26 @@ class _ProductWidgetState extends State { scale: 1.2, child: Checkbox( value: isEaten, - onChanged: (value) { - setState(() { - isEaten = value!; - }); - if (isEaten) { - diaryCubit.markEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); - } else { - diaryCubit.unmarkEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); - } - }, + onChanged: canMarkAsEaten + ? (value) { + setState(() { + isEaten = value!; + }); + if (isEaten) { + diaryCubit.markEaten( + diaryId: state.diary.id, + mealId: state.selectedMeal, + portionId: widget.portion.id, + ); + } else { + diaryCubit.unmarkEaten( + diaryId: state.diary.id, + mealId: state.selectedMeal, + portionId: widget.portion.id, + ); + } + } + : null, activeColor: appTheme.blueA400, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 63e283c6..8723f490 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -30,9 +30,9 @@ class PlanModel with _$PlanModel { @JsonKey(name: "type", fromJson: planTypeFromJson) required PlanType type, @JsonKey(name: "description_md") required String details, @JsonKey(name: "packets_count") required int packetsCount, - @JsonKey(name: "packet1_options") PacketOptions? packet1Options, - @JsonKey(name: "packet2_options") PacketOptions? packet2Options, - @JsonKey(name: "packet3_options") PacketOptions? packet3Options, + @JsonKey(name: "packet1_options", fromJson: parseOption) PacketOptions? packet1Options, + @JsonKey(name: "packet2_options", fromJson: parseOption) PacketOptions? packet2Options, + @JsonKey(name: "packet3_options", fromJson: parseOption) PacketOptions? packet3Options, @JsonKey(name: "author") required Author author, @JsonKey(name: "total_likes") required int totalLikes, @JsonKey(name: "total_views") required int totalViews, @@ -46,6 +46,13 @@ class PlanModel with _$PlanModel { factory PlanModel.fromJson(Map json) => _$PlanModelFromJson(json); } +PacketOptions? parseOption(json) { + if (json is Map) { + return PacketOptions.fromJson(json); + } + return null; +} + PlanType planTypeFromJson(dynamic json) { if (json is List) { return const PlanType(fit: false, slim: false, behavior: false, wellness: false); diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart index a8e65420..0f7f6e2c 100644 --- a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -68,7 +68,7 @@ class _InternetConnectionCheckerScreenState extends State { const SizedBox(height: 8), PlanProps( planDuration: plan.plan.duration, - planPrice: chosenOption?.price ?? 0, + planPrice: chosenOption?.price.toDouble() ?? 0, planStatus: planStatus), const SizedBox(height: 8), PlanInfo( diff --git a/lib/features/plans/enums/chosen_plan_state_enum.dart b/lib/features/plans/enums/chosen_plan_state_enum.dart index 5bb92efd..308df73c 100644 --- a/lib/features/plans/enums/chosen_plan_state_enum.dart +++ b/lib/features/plans/enums/chosen_plan_state_enum.dart @@ -45,6 +45,7 @@ enum ChosenPlanStateEnum { bool isStateAfterPaidState() { switch (this) { + case ChosenPlanStateEnum.stateCanceledByTheInitiator: case ChosenPlanStateEnum.stateInited: case ChosenPlanStateEnum.stateInCoordination: case ChosenPlanStateEnum.stateStartDateHasBeenSet: diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 6ede4081..df82f23b 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -42,7 +42,7 @@ class _WelcomeScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox(height: 40), + const SizedBox(height: 80), SvgPicture.asset('assets/images/logo_welcome.svg'), const Spacer(), Padding( @@ -70,7 +70,7 @@ class _WelcomeScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - height: 1, + height: 1.15, ), textAlign: TextAlign.center, ), diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 84b4d90e..dc980241 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -379,6 +379,9 @@ class MessageLookup extends MessageLookupByLibrary { "Нарушение законодательства", ), "lbl_english": MessageLookupByLibrary.simpleMessage("English"), + "leaveComplainRequest": MessageLookupByLibrary.simpleMessage( + "Подать жалобу", + ), "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), "light_type_desc": MessageLookupByLibrary.simpleMessage( "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.", @@ -501,6 +504,9 @@ class MessageLookup extends MessageLookupByLibrary { "Физическая форма", ), "place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"), + "planWillBeConfirmedAfterThreeDays": MessageLookupByLibrary.simpleMessage( + "Если вы не подтвердите, то план через три дня подтвердится автоматически", + ), "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), "plan_desc_example": MessageLookupByLibrary.simpleMessage( @@ -513,6 +519,9 @@ class MessageLookup extends MessageLookupByLibrary { "plan_seller": MessageLookupByLibrary.simpleMessage("Продавец плана"), "plan_type": MessageLookupByLibrary.simpleMessage("Тип плана"), "plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"), + "pleaseConfirmPlan": MessageLookupByLibrary.simpleMessage( + "Просим вас подтвердить прохождение плана", + ), "please_try_again_later": MessageLookupByLibrary.simpleMessage( "Пожалуйста, попробуйте еще раз позже", ), @@ -704,6 +713,9 @@ class MessageLookup extends MessageLookupByLibrary { "wrong_credentials": MessageLookupByLibrary.simpleMessage( "Неверные данные пользователя", ), + "youCanComplain": MessageLookupByLibrary.simpleMessage( + "Если вас что-то не устроило, вы можете подать жалобу", + ), "youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage( "Вы уверены, что хотите отменить заказ?", ), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 92f85b98..8da944a3 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3184,6 +3184,46 @@ class S { args: [], ); } + + /// `Просим вас подтвердить прохождение плана` + String get pleaseConfirmPlan { + return Intl.message( + 'Просим вас подтвердить прохождение плана', + name: 'pleaseConfirmPlan', + desc: '', + args: [], + ); + } + + /// `Если вы не подтвердите, то план через три дня подтвердится автоматически` + String get planWillBeConfirmedAfterThreeDays { + return Intl.message( + 'Если вы не подтвердите, то план через три дня подтвердится автоматически', + name: 'planWillBeConfirmedAfterThreeDays', + desc: '', + args: [], + ); + } + + /// `Если вас что-то не устроило, вы можете подать жалобу` + String get youCanComplain { + return Intl.message( + 'Если вас что-то не устроило, вы можете подать жалобу', + name: 'youCanComplain', + desc: '', + args: [], + ); + } + + /// `Подать жалобу` + String get leaveComplainRequest { + return Intl.message( + 'Подать жалобу', + name: 'leaveComplainRequest', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 7d09e039..d6038cb7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -381,6 +381,10 @@ "atLeast3Symbols": "Минимум 3 символа", "chosePlanType": "Выберите тип плана", "minPlanPrice": "Минимальная сумма 1200₽", - "addAtLeastOneItem": "Добавьте хотя бы одину опцию" + "addAtLeastOneItem": "Добавьте хотя бы одину опцию", + "pleaseConfirmPlan": "Просим вас подтвердить прохождение плана", + "planWillBeConfirmedAfterThreeDays": "Если вы не подтвердите, то план через три дня подтвердится автоматически", + "youCanComplain": "Если вас что-то не устроило, вы можете подать жалобу", + "leaveComplainRequest": "Подать жалобу" } diff --git a/pubspec.yaml b/pubspec.yaml index dfb0838a..46ddf5d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.0.50+6 +version: 1.0.51+6 publish_to: 'none' environment: