From 96e80ced38eb26db4aaa8e1724a2d4f56319a515 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 17 Jun 2025 13:33:41 +0700 Subject: [PATCH 01/34] feat(consultantRating): added api requests and state to consultant rating page --- lib/core/const.dart | 1 + lib/core/locator_service.dart | 8 +- .../models/rating_model.dart | 36 +++++- .../models/statistics_model.dart | 19 ++++ .../consultant_rating_screen.dart | 103 ++++++++++-------- .../cubit/consultant_rating_cubit.dart | 32 ++++++ .../cubit/consultant_rating_state.dart | 16 +++ .../widgets/period_filter_widget.dart | 72 +++++++----- .../widgets/rating_history_item.dart | 3 +- .../widgets/rating_history_widget.dart | 98 +++++++++-------- .../widgets/statistic_widget.dart | 25 +++-- .../consultant_rating_repository.dart | 26 +++++ .../services/consultant_rating_service.dart | 18 +++ .../menu/presentation/pages/menu_screen.dart | 10 ++ 14 files changed, 335 insertions(+), 132 deletions(-) create mode 100644 lib/features/consultant_rating/models/statistics_model.dart create mode 100644 lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart create mode 100644 lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart create mode 100644 lib/features/consultant_rating/repositories/consultant_rating_repository.dart create mode 100644 lib/features/consultant_rating/services/consultant_rating_service.dart diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..31c47e9c 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ 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 accb729d..82eac777 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -14,6 +14,9 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; +import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; @@ -158,6 +161,7 @@ Future init() async { sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantRatingCubit(sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -186,6 +190,7 @@ Future init() async { sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ConsultantRatingRepository(sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -214,8 +219,9 @@ Future init() async { sl.registerLazySingleton(() => CancelPlanService(sl())); sl.registerLazySingleton(() => CreatePlanService(sl())); sl.registerLazySingleton(() => NotificationsService(sl())); - sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => MyRecipesService(sl())); + sl.registerLazySingleton(() => ConsultantRatingService(sl())); // External sl.registerLazySingleton( diff --git a/lib/features/consultant_rating/models/rating_model.dart b/lib/features/consultant_rating/models/rating_model.dart index 164008dd..870d1c82 100644 --- a/lib/features/consultant_rating/models/rating_model.dart +++ b/lib/features/consultant_rating/models/rating_model.dart @@ -1,16 +1,42 @@ +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'rating_model.freezed.dart'; part 'rating_model.g.dart'; @freezed -abstract class RatingModel with _$RatingModel { +abstract class RatingPaginationModel with _$RatingPaginationModel { + factory RatingPaginationModel({ + required List data, + }) = _RatingPaginationModel; + factory RatingPaginationModel.fromJson(Map json) => _$RatingPaginationModelFromJson(json); +} + +@freezed +abstract class RatingModel with _$RatingModel { factory RatingModel({ - required DateTime date, - required String title, - required int amount, + @JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date, + @JsonKey(name: 'log_data') required RatingEnum type, + @JsonKey(name: 'log_scores') required int amount, }) = _RatingModel; factory RatingModel.fromJson(Map json) => _$RatingModelFromJson(json); -} \ No newline at end of file +} + +enum RatingEnum { + @JsonValue('ADDED_RECIPE') + addedRecipe, + @JsonValue('ADDED_PUBLIC_DIET') + addedPublicDiet, + @JsonValue('ADDED_BLOG_POST') + addedBlogPost, + @JsonValue('INVITED_A_NEW_USER') + invitedUser, + @JsonValue('HAS_MEDICAL_DEGREE') + hasMedicalDegree, + @JsonValue('CONSULTED') + consulted, + @JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED') + addedGlossary; +} diff --git a/lib/features/consultant_rating/models/statistics_model.dart b/lib/features/consultant_rating/models/statistics_model.dart new file mode 100644 index 00000000..bbac675b --- /dev/null +++ b/lib/features/consultant_rating/models/statistics_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'statistics_model.freezed.dart'; +part 'statistics_model.g.dart'; + +@freezed +abstract class StatisticsModel with _$StatisticsModel { + + factory StatisticsModel({ + @JsonKey(name: 'count_referal') int? countReferal, + @JsonKey(name: 'count_blog_posts') int? blogPostsCount, + @JsonKey(name: 'count_recipes') int? recipesCount, + @JsonKey(name: 'count_diets') int? dietsCount, + @JsonKey(name: 'count_consultants') int? consultationsCount, + @JsonKey(name: 'count_products_nutritional_values') int? qwe, + }) = _StatisticsModel; + + factory StatisticsModel.fromJson(Map json) => _$StatisticsModelFromJson(json); +} \ No newline at end of file diff --git a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart index 285e5d63..8c6cb3d4 100644 --- a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart +++ b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart @@ -1,12 +1,16 @@ +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/custom_outlined_button.dart'; -import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingScreen extends StatelessWidget { static const name = 'consultantRatingScreen'; @@ -15,7 +19,10 @@ class ConsultantRatingScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return const ConsultantRatingPageContent(); + return BlocProvider( + create: (context) => sl()..getConsultantStatistics()..getScoreLogs(), + child: const ConsultantRatingPageContent(), + ); } } @@ -25,52 +32,56 @@ class ConsultantRatingPageContent extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Scaffold( - backgroundColor: appTheme.gray100, - appBar: AppBar( - centerTitle: true, - title: Text( - s.rating_and_statistics, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - ), - body: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 16), - const RatingWidget( - rating: 1058, + return BlocBuilder( + builder: (context, state) { + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.rating_and_statistics, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: CustomOutlinedButton( - text: s.copy_referral_link, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + body: state.error != null + ? DataErrorWidget(onReload: () async { + await context.read().getConsultantStatistics(); + if (!context.mounted) return; + await context.read().getScoreLogs(); + }) + : SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 16), + const RatingWidget( + rating: 1058, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomOutlinedButton( + text: s.copy_referral_link, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () {}, + ), + ), + const SizedBox(height: 16), + StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,), + const SizedBox(height: 16), + RatingHistoryWidget( + isLoading: state.isRatingLoading, + ratingHistory: state.ratings, + ), + ], + ), ), - onPressed: () {}, - ), - ), - const SizedBox(height: 16), - const StatisticWidget(), - const SizedBox(height: 16), - RatingHistoryWidget( - ratingHistory: [ - RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300), - RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323), - RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20), - RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212), - RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323), - RatingModel(date: DateTime.now(), title: 'ee', amount: 1), - ], - ), - ], - ), - ), + ); + }, ); } } diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart new file mode 100644 index 00000000..291f450c --- /dev/null +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart @@ -0,0 +1,32 @@ +import 'dart:developer'; + +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart'; +import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ConsultantRatingCubit extends Cubit { + final ConsultantRatingRepository repository; + ConsultantRatingCubit(this.repository) : super(ConsultantRatingState()); + + Future getConsultantStatistics({String? date}) async { + try { + emit(state.copyWith(isStatisticsLoading: true)); + final statistics = await repository.getConsultantStatistics(date: date); + emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null)); + } catch (e, st) { + emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); + log(e.toString(), stackTrace: st); + } + } + + Future getScoreLogs({String? date}) async { + try { + emit(state.copyWith(isRatingLoading: true)); + final scoreLogs = await repository.getScoreLogs(date: date); + emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null)); + } catch (e, st) { + emit(state.copyWith(isRatingLoading: true, error: e.toString())); + log(e.toString(), stackTrace: st); + } + } +} diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart new file mode 100644 index 00000000..20cc458f --- /dev/null +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'consultant_rating_state.freezed.dart'; + +@freezed +abstract class ConsultantRatingState with _$ConsultantRatingState { + factory ConsultantRatingState({ + @Default(false) bool isStatisticsLoading, + @Default(false) bool isRatingLoading, + @Default(null) StatisticsModel? statistics, + @Default(null) String? error, + @Default([]) List ratings, + }) = _ConsultantRatingState; +} diff --git a/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart b/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart index 7ad0fa7e..3f15be8e 100644 --- a/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart @@ -1,42 +1,29 @@ 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/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class PeriodFilterWidget extends StatefulWidget { - const PeriodFilterWidget({super.key}); - + const PeriodFilterWidget({super.key, required this.isStatistic}); + final bool isStatistic; @override State createState() => _PeriodFilterWidgetState(); } class _PeriodFilterWidgetState extends State { - final List periodDropDownTitles = []; - int periodDropDownIndex = 0; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final s = S.of(context); - if (periodDropDownTitles.isEmpty) { - periodDropDownTitles.addAll([ - s.for_whole_period_of_time, - s.for_a_year, - s.for_a_month, - s.for_a_week, - ]); - } - } - + PeriodFilterStatus _currentState = PeriodFilterStatus.all; @override Widget build(BuildContext context) { final s = S.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomDropDownButton( + CustomDropDownButton( hint: Text( s.choose_diet_duration, style: TextStyles.textSMedium.copyWith(color: appTheme.black), @@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State { onChanged: (value) { if (value != null) { setState(() { - periodDropDownIndex = periodDropDownTitles.indexOf(value); + _currentState = value; }); + if (widget.isStatistic) { + if (value == PeriodFilterStatus.all) { + context.read().getConsultantStatistics(); + return; + } + context.read().getConsultantStatistics(date: value.value); + return; + } + if (value == PeriodFilterStatus.all) { + context.read().getScoreLogs(); + return; + } + context.read().getScoreLogs(date: value.value); + return; } }, - items: periodDropDownTitles + items: PeriodFilterStatus.values .mapIndexed( (index, value) => DropdownMenuItem( - value: periodDropDownTitles[index], + value: PeriodFilterStatus.values[index], child: Text( - periodDropDownTitles[index], + PeriodFilterStatus.values[index].name, style: TextStyles.textMMedium.copyWith(color: appTheme.black), ), ), ) .toList(), - value: periodDropDownTitles[periodDropDownIndex], + value: _currentState, ), ], ); } } + +enum PeriodFilterStatus { + all, + month, + week, + year; + + String get name => switch (this) { + PeriodFilterStatus.all => 'Все периоды', + PeriodFilterStatus.month => 'За месяц', + PeriodFilterStatus.week => 'За неделю', + PeriodFilterStatus.year => 'За год', + }; + + String? get value => switch (this) { + PeriodFilterStatus.all => null, + PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'), + PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'), + PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'), + }; +} diff --git a/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart b/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart index 3a3cff6e..4bafd1b3 100644 --- a/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart +++ b/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart @@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: 80, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: appTheme.gray100, @@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget { date.format('dd.MM.yyyy'), style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray), ), - const Spacer(), + const SizedBox(height: 12), Text( title, style: TextStyles.textMMedium.copyWith(color: appTheme.black), diff --git a/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart b/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart index 0413dda5..d6f8969b 100644 --- a/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/rating_history_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/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; @@ -9,7 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class RatingHistoryWidget extends StatelessWidget { - const RatingHistoryWidget({super.key, required this.ratingHistory}); + const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading}); + final bool isLoading; final List ratingHistory; @override Widget build(BuildContext context) { @@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget { right: 16, top: 16, ), - child: Column( - children: [ - Row( - children: [ - Text( - s.rating_history, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const Spacer(), - SvgPicture.asset( - 'assets/icons/question_mark.svg', - height: 22, - width: 22, - colorFilter: ColorFilter.mode( - appTheme.blueA400, - BlendMode.srcIn, + child: isLoading + ? const Center(child: Loader()) + : Column( + children: [ + Row( + children: [ + Text( + s.rating_history, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + const Spacer(), + SvgPicture.asset( + 'assets/icons/question_mark.svg', + height: 22, + width: 22, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ], ), - ), - ], - ), - const SizedBox( - height: 16, - ), - const PeriodFilterWidget(), - const SizedBox( - height: 12, - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Padding( - padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12), - child: RatingHistoryItem( - title: ratingHistory[index].title, - amount: ratingHistory[index].amount, - date: ratingHistory[index].date, + const SizedBox( + height: 16, ), - ); - }, - itemCount: ratingHistory.length, - shrinkWrap: true, - ), - ], - ), + const PeriodFilterWidget(isStatistic: false), + const SizedBox( + height: 12, + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12), + child: RatingHistoryItem( + title: switch (ratingHistory[index].type) { + RatingEnum.addedRecipe => 'Добавление рецепта', + RatingEnum.addedPublicDiet => 'Добавление публичной диеты', + RatingEnum.addedBlogPost => 'Добавление поста', + RatingEnum.invitedUser => 'Регистрация по реф. ссылке', + RatingEnum.hasMedicalDegree => 'Получение мед. степени', + RatingEnum.consulted => 'Консультация', + RatingEnum.addedGlossary => 'Добавление продукта в глоссарий', + }, + amount: ratingHistory[index].amount, + date: ratingHistory[index].date, + ), + ); + }, + itemCount: ratingHistory.length, + shrinkWrap: true, + ), + ], + ), ); } } diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart index b7f97e2b..3be44129 100644 --- a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart @@ -1,6 +1,8 @@ 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/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -8,14 +10,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class StatisticWidget extends StatelessWidget { - const StatisticWidget({super.key}); - + const StatisticWidget({super.key, required this.isLoading, required this.statistics}); + final bool isLoading; + final StatisticsModel? statistics; @override Widget build(BuildContext context) { final s = S.of(context); return MainBackgroundBouble( padding: const EdgeInsets.all(16), - child: Column( + child: isLoading ? const Center(child: Loader()) : Column( children: [ Row( children: [ @@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget { const SizedBox( height: 16, ), - const PeriodFilterWidget(), + const PeriodFilterWidget( + isStatistic: true, + ), const SizedBox( height: 12, ), Row( children: [ - Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)), + Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)), + Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)), ], ), const SizedBox( @@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget { ), Row( children: [ - Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)), + Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)), + Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)), ], ), const SizedBox( @@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget { ), Row( children: [ - Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), ], ), const SizedBox( diff --git a/lib/features/consultant_rating/repositories/consultant_rating_repository.dart b/lib/features/consultant_rating/repositories/consultant_rating_repository.dart new file mode 100644 index 00000000..b4ac3770 --- /dev/null +++ b/lib/features/consultant_rating/repositories/consultant_rating_repository.dart @@ -0,0 +1,26 @@ +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; +import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; + +class ConsultantRatingRepository { + final ConsultantRatingService service; + + ConsultantRatingRepository(this.service); + + Future getConsultantStatistics({String? date}) async { + try { + return await service.getConsultantStatistics(date: date); + } catch (e) { + rethrow; + } + } + + Future> getScoreLogs({String? date}) async { + try { + final data = (await service.getScoreLogs(date: date)).data; + return data; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/consultant_rating/services/consultant_rating_service.dart b/lib/features/consultant_rating/services/consultant_rating_service.dart new file mode 100644 index 00000000..c4412afb --- /dev/null +++ b/lib/features/consultant_rating/services/consultant_rating_service.dart @@ -0,0 +1,18 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; + +import 'package:retrofit/retrofit.dart'; + +part 'consultant_rating_service.g.dart'; + +@RestApi() +abstract class ConsultantRatingService { + factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService; + + @GET('/api/consultants/rating/statistics') + Future getConsultantStatistics({@Query('filters[duration][\$gte]') String? date}); + + @GET('/api/consultants/score/logs') + Future getScoreLogs({@Query('filters[duration][\$gte]') String? date}); +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index f657c6a7..7409a3a5 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; @@ -49,6 +50,15 @@ class MenuScreen extends StatelessWidget { GoRouter.of(context).push(UserPurchasesScreen.path); }, ), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile( + leading: 'assets/icons/finance.svg', + title: 'Мой рейтинг', + onTap: () { + GoRouter.of(context).push(ConsultantRatingScreen.path); + }, + ), _divider(context), _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), From 766d06fc4d4a946cc99ea33b6f8d921b0bd2acf9 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 22 Aug 2025 14:17:11 +0300 Subject: [PATCH 02/34] fix: Issue-592 --- .../feed/presentation/widgets/blog_post_widget.dart | 6 +++--- lib/features/feed/presentation/widgets/recipes_widget.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 6479d0d9..23eaeded 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -245,9 +245,9 @@ class _BlogPostWidgetState extends State { const SizedBox(width: 8), FeedIconButton( svgIcon: SvgPicture.asset( - height: 20, - width: 20, - 'assets/icons/share.svg', + height: 15, + width: 12, + 'assets/icons/repost.svg', colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), ), onTap: () { diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index 7720c6cd..c0dbeb39 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -231,7 +231,7 @@ class _RecipesWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${widget.recipe.id}'), + uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); From a416912d2a694a35b28c7d9734fd65305513bf10 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 11:26:14 +0300 Subject: [PATCH 03/34] fix: add product modal and auth error --- .../auth/presentation/pages/auth_screen.dart | 14 ++- .../widgets/add_product_modal.dart | 118 +++++------------- .../presentation/widgets/diets_widget.dart | 2 +- 3 files changed, 45 insertions(+), 89 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 20b0e26c..73162bd2 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -47,12 +47,20 @@ class _AuthScreenState extends State { super.initState(); var keyboardVisibilityController = KeyboardVisibilityController(); keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) { - setState(() { - isFocused = visible; - }); + if (mounted) { + setState(() { + isFocused = visible; + }); + } }); } + @override + void dispose() { + keyboardSubscription.cancel(); + super.dispose(); + } + Future _reconnectWebSocket(BuildContext context) async { try { await sl().clear(); diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index b55a979f..9b6c2536 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget { final DiaryStateLoaded state; final DiaryCubit diaryCubit; - const AddProductModal({ - super.key, - this.portion, - required this.state, - required this.diaryCubit, - }); + const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit}); @override State createState() => _AddProductModalState(); @@ -58,75 +53,62 @@ class _AddProductModalState extends State { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return Padding( - padding: const EdgeInsets.all(16) + - EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), + padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Добавление блюда', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF323F4B), - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( - width: MediaQuery.of(context).size.width * 0.8 - 32, + width: MediaQuery.of(context).size.width * 0.76 - 32, child: CustomTextFormField( controller: productNameController, autofocus: true, hintText: 'Введите название продукта/блюда', onChanged: (query) { - EasyDebounce.debounce( - 'glossary-search', - const Duration(milliseconds: 250), - () async { - selectedGlossary = null; - if (query.isEmpty) { - glossariesList = []; - setState(() {}); - return; - } - glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query); + EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; setState(() {}); - }, - ); + return; + } + glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query); + setState(() {}); + }); }, ), ), const SizedBox(width: 4), SizedBox( - width: MediaQuery.of(context).size.width * 0.2 - 4, + width: MediaQuery.of(context).size.width * 0.24 - 4, child: CustomTextFormField( controller: quantityController, keyboardType: TextInputType.number, hintText: selectedGlossary == null ? '100' : selectedGlossary!.grInPortion == 0 - ? '100' - : '1', + ? '100' + : '1', suffixText: selectedGlossary == null ? 'гр' : selectedGlossary?.grInPortion == 0 - ? 'гр' - : 'шт', + ? 'гр' + : 'шт', ), ), ], ), glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(), Container( - constraints: const BoxConstraints( - maxHeight: 256, - ), + constraints: const BoxConstraints(maxHeight: 256), child: ListView.separated( physics: const BouncingScrollPhysics(), shrinkWrap: true, @@ -141,28 +123,12 @@ class _AddProductModalState extends State { setState(() {}); FocusManager.instance.primaryFocus?.nextFocus(); }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: Text( - glossariesList[i].name, - style: const TextStyle( - height: 1, - ), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text(glossariesList[i].name, style: const TextStyle(height: 1)), subtitle: glossariesList[i].grInPortion == 0 ? null - : Text( - '${glossariesList[i].grInPortion} г', - style: TextStyle( - height: 1, - color: Colors.grey.shade600, - ), - ), - trailing: Icon( - Icons.add_rounded, - color: Colors.grey.shade400, - ), + : Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)), + trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400), contentPadding: const EdgeInsets.only(left: 12), ); }, @@ -188,14 +154,8 @@ class _AddProductModalState extends State { child: Ink( width: 52, height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), - child: const Icon( - Icons.delete_outline_rounded, - color: Colors.red, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), + child: const Icon(Icons.delete_outline_rounded, color: Colors.red), ), ), if (widget.portion != null) const SizedBox(width: 8), @@ -209,8 +169,8 @@ class _AddProductModalState extends State { portion: selectedGlossary == null ? 'gr' : selectedGlossary!.grInPortion == 0 - ? 'gr' - : 'pcs', + ? 'gr' + : 'pcs', quantity: int.parse(quantityController.text), productNutritionalValueId: selectedGlossary?.id, ); @@ -223,8 +183,8 @@ class _AddProductModalState extends State { portion: selectedGlossary == null ? 'gr' : selectedGlossary!.grInPortion == 0 - ? 'gr' - : 'pcs', + ? 'gr' + : 'pcs', quantity: int.parse(quantityController.text), productNutritionalValueId: selectedGlossary?.id, portionState: widget.portion!.portionState, @@ -237,13 +197,8 @@ class _AddProductModalState extends State { useRootNavigator: true, context: context, builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: GlossaryModerationRequestModalWidget( - productName: productNameController.text, - diaryCubit: widget.diaryCubit, - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit), ), ); } @@ -252,18 +207,11 @@ class _AddProductModalState extends State { child: Ink( width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92), height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: Center( child: Text( widget.portion == null ? 'Добавить' : 'Сохранить', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), ), ), ), diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index 34964d1a..d91cdd74 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -17,7 +17,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -class DietsWidget extends StatefulWidget { +class DietsWidget extends StatefulWidget { final Diet diet; final int index; const DietsWidget({ From 886cfa8f1c386fde94a340fe866364a5b2523b9f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 13:05:53 +0300 Subject: [PATCH 04/34] feat: AppMetrica --- lib/core/env_config.dart | 3 +++ lib/main.dart | 4 ++++ pubspec.yaml | 1 + 3 files changed, 8 insertions(+) diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index b363ad70..a3404aeb 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -7,6 +7,9 @@ class EnvConfig { // Sentry Configuration static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? ''; + // AppMetrica Configuration + static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? ''; + // Firebase Configuration static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? ''; static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? ''; diff --git a/lib/main.dart b/lib/main.dart index 6b44348c..468de90f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); @@ -38,6 +39,9 @@ Future main() async { io.HttpClient.enableTimelineLogging = true; } + AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); + AppMetrica.reportEvent('Application started'); + await SentryFlutter.init((options) { options.dsn = EnvConfig.sentryDsn; options.tracesSampleRate = 1.0; diff --git a/pubspec.yaml b/pubspec.yaml index 3b33303b..10fafb98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,7 @@ dependencies: qr_code_scanner_plus: ^2.0.10+1 flutter_dotenv: ^5.2.1 uuid: ^4.5.1 + appmetrica_plugin: ^3.3.0 dev_dependencies: build_runner: ^2.7.0 From 5a8fa7089ce7da0f492ca6916c8c95699b6bf6be Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 14:24:06 +0300 Subject: [PATCH 05/34] feat: appmetrica --- .../registration_cubit.dart | 73 +++++---- .../verify_email_cubit.dart | 6 +- .../pages/registration_screen.dart | 145 ++++-------------- .../pages/verify_email_screen.dart | 5 +- .../presentation/pages/verify_screen.dart | 44 ++---- .../pages/webview_registration_screen.dart | 18 +-- .../registration_analytics_service.dart | 30 ++++ 7 files changed, 120 insertions(+), 201 deletions(-) create mode 100644 lib/features/registration/services/registration_analytics_service.dart diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index c9426674..ccf9cae4 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -8,62 +8,61 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class RegistrationCubit extends Cubit { final RegistrationRepository registrationRepository; - RegistrationCubit({ - required this.registrationRepository, - }) : super( - const RegistrationState( - sheet: UserRegistrationSheet( - firstName: '', - lastName: '', - email: '', - password: '', - passwordConfirmation: '', - birthdate: '', - gender: '', - location: '', - responseBody: '{}', - lang: 'Русский', - weight: 0, - height: 0, - chestCircumference: 0, - waistCircumference: 0, - hipCircumference: 0, - bloodPressureUp: 0, - bloodPressureDown: 0, - hasHypo: 0, - hasHyper: 0, - aboutAllergy: '', - aboutTakingPils: '', - aboutSportHobbies: '', - favoriteFoods: '', - unfavoriteFoods: '', - additionalInfo: '', - phone: '', - ), + RegistrationCubit({required this.registrationRepository}) + : super( + const RegistrationState( + sheet: UserRegistrationSheet( + firstName: '', + lastName: '', + email: '', + password: '', + passwordConfirmation: '', + birthdate: '', + gender: '', + location: '', + responseBody: '{}', + lang: 'Русский', + weight: 0, + height: 0, + chestCircumference: 0, + waistCircumference: 0, + hipCircumference: 0, + bloodPressureUp: 0, + bloodPressureDown: 0, + hasHypo: 0, + hasHyper: 0, + aboutAllergy: '', + aboutTakingPils: '', + aboutSportHobbies: '', + favoriteFoods: '', + unfavoriteFoods: '', + additionalInfo: '', + phone: '', ), - ); + ), + ); void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet)); Future tryRegistration() async { try { final token = await registrationRepository.tryRegister( - state.sheet.copyWith( - phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), - responseBody: '{}', - ), + state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'), ); await TokenStorage.saveTokenInStorage(token); await sl().registerPushToken(); + RegistrationAnalyticsService.reportRegistrationComplete(); AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); } catch (e, st) { log(e.toString(), stackTrace: st); + RegistrationAnalyticsService.reportRegistrationError(e.toString()); emit(RegistrationState(sheet: state.sheet, error: e.toString())); } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index 649c1fb8..d1f547f8 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -1,13 +1,12 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class VerifyEmailCubit extends Cubit { final ProfileRepository profileRepository; - VerifyEmailCubit({ - required this.profileRepository, - }) : super(const VerifyEmailState.loading()); + VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading()); Future sendEmailCode() async { try { @@ -24,6 +23,7 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.verifyLoading()); final result = await profileRepository.tryVerifyEmailByCode(code); if (result == true) { + RegistrationAnalyticsService.reportEmailVerification(); emit(const VerifyEmailState.verifySuccess()); } else { emit(const VerifyEmailState.verifyError()); diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 86514be8..9f30ca33 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -17,6 +17,7 @@ import 'package:eatvio_app/features/registration/presentation/pages/medicine_int import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -51,12 +52,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка при регистрации'), content: Text(state.error!), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); } @@ -76,22 +72,14 @@ class _RegistrationScreenState extends State { highlightElevation: 0, focusElevation: 0, elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), backgroundColor: const Color(0xFFEAF2FD), onPressed: () async { setState(() { - pageController.previousPage( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); + pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut); }); }, - label: Icon( - Icons.arrow_back_ios_new_rounded, - color: appTheme.blueA400, - ), + label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400), ), ), Container( @@ -103,24 +91,16 @@ class _RegistrationScreenState extends State { highlightElevation: 0, elevation: 0, focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), backgroundColor: appTheme.blueA400, onPressed: () async { if (await canGoNext(state, context)) { setState(() { - pageController.nextPage( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); + pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut); }); } }, - label: Text( - 'Продолжить', - style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), - ), + label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)), ), ), ], @@ -128,23 +108,11 @@ class _RegistrationScreenState extends State { appBar: AppBar( automaticallyImplyLeading: false, leading: Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => GoRouter.of(context).pop(), - ), + builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()), ), centerTitle: true, - title: SvgPicture.asset( - ImageConstant.imgLogoEatvio, - height: 48, - fit: BoxFit.contain, - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: Text('шаг $step из 15'), - ), - ], + title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain), + actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))], bottom: PreferredSize( preferredSize: Size(MediaQuery.of(context).size.width, 32), child: Padding( @@ -152,10 +120,7 @@ class _RegistrationScreenState extends State { child: TweenAnimationBuilder( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, - tween: Tween( - begin: 1 / 15, - end: progressValue, - ), + tween: Tween(begin: 1 / 15, end: progressValue), builder: (context, value, _) => LinearProgressIndicator( value: value, borderRadius: BorderRadius.circular(16), @@ -175,6 +140,8 @@ class _RegistrationScreenState extends State { step = index + 1; progressValue = step / 15; }); + // Report analytics for step completion + RegistrationAnalyticsService.reportRegistrationStep(step); }, children: [ // LanguageSelectScreen(sheet: state.sheet), @@ -211,12 +178,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: const Text('Введите корректный номер телефона'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -224,12 +186,7 @@ class _RegistrationScreenState extends State { return true; } case 2: - List errors = validateCredentialsFields( - state.sheet.email, - state.sheet.password, - state.sheet.passwordConfirmation, - context, - ); + List errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context); if (errors.isEmpty) { return true; } else { @@ -244,22 +201,13 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; } case 3: - List errors = validateNameFields( - state.sheet.firstName.toString(), - state.sheet.lastName.toString(), - context, - ); + List errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context); if (errors.isEmpty) { return true; } else { @@ -274,12 +222,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -293,12 +236,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: const Text('Выберете пол'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -312,12 +250,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: const Text('Заполните все поля'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -343,12 +276,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -359,10 +287,7 @@ class _RegistrationScreenState extends State { barrierDismissible: false, useSafeArea: false, context: context, - builder: (context) => const PopScope( - canPop: false, - child: Loader(), - ), + builder: (context) => const PopScope(canPop: false, child: Loader()), ); await registrationCubit.tryRegistration(); if (context.mounted) { @@ -373,9 +298,7 @@ class _RegistrationScreenState extends State { Navigator.pop(context); showDialog( context: context, - builder: (context) => const AlertDialog( - title: Text('Ошибка при попытке регистрации'), - ), + builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')), ); } } @@ -385,12 +308,7 @@ class _RegistrationScreenState extends State { } } - List validateCircumferencesFields( - String chestCircumference, - String waistCircumference, - String hipCircumference, - BuildContext context, - ) { + List validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) { final s = S.of(context); List errors = []; @@ -406,12 +324,7 @@ class _RegistrationScreenState extends State { return errors; } - List validateCredentialsFields( - String email, - String password, - String passwordConfirmation, - BuildContext context, - ) { + List validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) { final s = S.of(context); List errors = []; @@ -435,11 +348,7 @@ class _RegistrationScreenState extends State { return errors; } - List validateNameFields( - String firstName, - String lastName, - BuildContext context, - ) { + List validateNameFields(String firstName, String lastName, BuildContext context) { List errors = []; if (firstName.isEmpty) { diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index f50d4960..e91b9e9e 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -29,8 +30,10 @@ class _VerifyEmailScreenState extends State { @override void initState() { super.initState(); - goRouter = GoRouter.of(context); + goRouter = GoRouter.of(context); _startTimer(); + // Report email verification screen opened + RegistrationAnalyticsService.reportRegistrationStep(16); // Email verification step } @override diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 637dba5b..8d5df9df 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; @@ -9,10 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart'; class VerifyScreen extends StatefulWidget { final String passKey; - const VerifyScreen({ - super.key, - required this.passKey, - }); + const VerifyScreen({super.key, required this.passKey}); @override State createState() => _VerifyScreenState(); @@ -25,6 +23,8 @@ class _VerifyScreenState extends State { @override void initState() { super.initState(); + // Report phone verification screen opened + RegistrationAnalyticsService.reportRegistrationStep(17); // Phone verification step _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( @@ -36,6 +36,8 @@ class _VerifyScreenState extends State { }, onNavigationRequest: (NavigationRequest request) async { if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) { + // Report phone verification success + RegistrationAnalyticsService.reportPhoneVerification(); GoRouter.of(context).pop(); await showModalBottomSheet( useRootNavigator: true, @@ -50,27 +52,15 @@ class _VerifyScreenState extends State { const Text( 'Номер телефона\nуспешно привязан', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - height: 1, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), - Lottie.asset( - 'assets/lottie_animations/success.json', - width: 160, - height: 160, - ), + Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160), const SizedBox(height: 16), const Text( 'Теперь Вам доступен весь функционал!', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - height: 1, - fontWeight: FontWeight.w500, - ), + style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), InkWell( @@ -80,18 +70,11 @@ class _VerifyScreenState extends State { child: Ink( width: MediaQuery.of(context).size.width - 32, height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text( 'Закрыть', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), ), ), @@ -117,10 +100,7 @@ class _VerifyScreenState extends State { child: Stack( children: [ WebViewWidget(controller: _controller), - if (_isLoading) - const Center( - child: Loader(), - ), + if (_isLoading) const Center(child: Loader()), ], ), ), diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index abb6537d..05c0ffc1 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -24,6 +25,8 @@ class _WebViewRegistrationScreenState extends State { void initState() { super.initState(); authCubit = sl(); + // Report webview registration started + RegistrationAnalyticsService.reportRegistrationStep(18); // Webview registration step _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( @@ -43,10 +46,9 @@ class _WebViewRegistrationScreenState extends State { NavigationDelegate( onNavigationRequest: (NavigationRequest request) { if (request.url.contains('profile')) { - authCubit.getTokenByCredentials( - email!, - password!, - ); + // Report webview registration completion + RegistrationAnalyticsService.reportRegistrationComplete(); + authCubit.getTokenByCredentials(email!, password!); } return NavigationDecision.navigate; }, @@ -86,12 +88,8 @@ class _WebViewRegistrationScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - toolbarHeight: 0, - ), - body: SafeArea( - child: WebViewWidget(controller: _controller), - ), + appBar: AppBar(toolbarHeight: 0), + body: SafeArea(child: WebViewWidget(controller: _controller)), ); } } diff --git a/lib/features/registration/services/registration_analytics_service.dart b/lib/features/registration/services/registration_analytics_service.dart new file mode 100644 index 00000000..3bfc69d1 --- /dev/null +++ b/lib/features/registration/services/registration_analytics_service.dart @@ -0,0 +1,30 @@ +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; + +class RegistrationAnalyticsService { + static const String _eventPrefix = 'registration_step'; + + /// Reports registration step completion + static void reportRegistrationStep(int stepNumber) { + AppMetrica.reportEvent('$_eventPrefix $stepNumber'); + } + + /// Reports email verification completion + static void reportEmailVerification() { + AppMetrica.reportEvent('registration_email_verify'); + } + + /// Reports phone verification completion + static void reportPhoneVerification() { + AppMetrica.reportEvent('registration_phone_verify'); + } + + /// Reports registration completion + static void reportRegistrationComplete() { + AppMetrica.reportEvent('registration_complete'); + } + + /// Reports registration error + static void reportRegistrationError(String error) { + AppMetrica.reportEvent('registration_error'); + } +} From 6061e413b2203eb1ef20a88e8ecd0c83ffe127d7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 14:40:50 +0300 Subject: [PATCH 06/34] fix: upd ver to 71 --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d6e7401a..4447f176 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e814e8a..f0baee2b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 69 + 70 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index 10fafb98..1505f232 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.10+70 +version: 1.2.11+71 publish_to: 'none' environment: From bae79b60fdabbefa2682aadd48e2090d78bd7098 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 17:29:07 +0300 Subject: [PATCH 07/34] fix: testflight --- ios/fastlane/Fastfile | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index ebd745be..7ccc8b7a 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -54,7 +54,6 @@ platform :ios do scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", - xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'" ) From b5edf13bd9857a394cb98c7efac0208b3ba83872 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 17:42:05 +0300 Subject: [PATCH 08/34] fix: testflight --- ios/Runner.xcodeproj/project.pbxproj | 1 - ios/fastlane/Fastfile | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4447f176..824c4906 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -569,7 +569,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 7ccc8b7a..588f4fc9 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -54,6 +54,7 @@ platform :ios do scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", + xcargs: "--dart-define=HIDE_PLANS=false --dart-define=BUILD_DATE='#{build_date}'" ) From bc6469a6aec42abefee691c7a90060add438cb4d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 17:55:34 +0300 Subject: [PATCH 09/34] fix: testflight --- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9c12df59..1a6f2553 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -41,7 +41,7 @@ Date: Mon, 25 Aug 2025 18:11:04 +0300 Subject: [PATCH 10/34] fix: testflight --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 19 ++++++++++--------- ios/Runner/Info.plist | 2 +- ios/fastlane/Fastfile | 3 --- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 86343d86..8c6e5614 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 16.0 + 12.0 diff --git a/ios/Podfile b/ios/Podfile index 4fe34ec9..53a8d390 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '16.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 824c4906..1da8a682 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 70; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -405,7 +405,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 70; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -555,7 +555,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -569,6 +569,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; @@ -583,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 70; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -592,7 +593,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f0baee2b..5cba2fb5 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 70 + 71 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 588f4fc9..9d9b239d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -46,15 +46,12 @@ platform :ios do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") - - build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S") build_app( workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", - xcargs: "--dart-define=HIDE_PLANS=false --dart-define=BUILD_DATE='#{build_date}'" ) From 997da4f94055bc20e8be697d4df53f3bf11263f4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:15:49 +0300 Subject: [PATCH 11/34] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index ac54d483..181b9362 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -60,7 +60,8 @@ jobs: flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs cd ios - pod update + rm -rf Pods Podfile.lock + pod install fastlane beta env: LANG: en_US.UTF-8 From 964550a32582ca33bc77a75788161141d29e9a02 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:19:18 +0300 Subject: [PATCH 12/34] fix: testflight --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 8c6e5614..86343d86 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 16.0 diff --git a/ios/Podfile b/ios/Podfile index 53a8d390..4fe34ec9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '16.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1da8a682..080fb053 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; From dd5692407670effabaaeb7a060fffc537a60609f Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:28:15 +0300 Subject: [PATCH 13/34] fix: test-flight --- .gitea/workflows/test-flight-cd.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 181b9362..a987ba88 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -61,6 +61,11 @@ jobs: flutter pub run build_runner build --delete-conflicting-outputs cd ios rm -rf Pods Podfile.lock + rm -rf .symlinks + rm -rf Flutter/Flutter.framework + rm -rf Flutter/Flutter.podspec + rm -rf Flutter/Generated.xcconfig + rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-* pod install fastlane beta env: From 020682ae8045a630302de21d3ecb0fd194c00d13 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:31:37 +0300 Subject: [PATCH 14/34] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index a987ba88..ac54d483 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -60,13 +60,7 @@ jobs: flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs cd ios - rm -rf Pods Podfile.lock - rm -rf .symlinks - rm -rf Flutter/Flutter.framework - rm -rf Flutter/Flutter.podspec - rm -rf Flutter/Generated.xcconfig - rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-* - pod install + pod update fastlane beta env: LANG: en_US.UTF-8 From d8324c1635c52e85d242d3362fbb70f199ebaaf7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 27 Aug 2025 11:33:06 +0300 Subject: [PATCH 15/34] feat(verify_email): Issue-645 --- .../repositories/profile_repository.dart | 8 +- .../profile/services/profile_service.dart | 66 ++----- .../verify_email_cubit.dart | 12 ++ .../verify_email_state.dart | 14 +- .../pages/verify_email_screen.dart | 125 ++++++++----- .../widgets/change_email_dialog.dart | 174 ++++++++++++++++++ 6 files changed, 293 insertions(+), 106 deletions(-) create mode 100644 lib/features/registration/presentation/widgets/change_email_dialog.dart diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 531cf589..4867d85a 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -164,8 +164,12 @@ class ProfileRepository { return (jsonDecode(await profileService.getMyPhone())['phone']); } - Future sendEmailCode() async { - return await profileService.sendEmailCode(); + Future sendEmailCode({String? newEmail}) async { + if (newEmail != null) { + return await profileService.sendEmailCode(data: {'new_email': newEmail}); + } else { + return await profileService.sendEmailCode(); + } } Future tryVerifyEmailByCode(String code) async { diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index bddd46f8..b313f27f 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -16,9 +16,7 @@ abstract class ProfileService { factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService; @GET('/api/profile/show/{slug}') - Future getProfileBySlug({ - @Path() required String slug, - }); + Future getProfileBySlug({@Path() required String slug}); @GET('/api/profile/index') Future getMyProfile(); @@ -27,9 +25,7 @@ abstract class ProfileService { Future getMyProfileDetails(); @GET('/api/profile/detailz/{slug}') - Future getProfileDetailsBySlug({ - @Path() required String slug, - }); + Future getProfileDetailsBySlug({@Path() required String slug}); @POST('/api/profile/addCircumference') Future addCircumference({ @@ -39,64 +35,38 @@ abstract class ProfileService { }); @POST('/api/profile/addWeight') - Future addWeight({ - @Part() required double weight, - }); + Future addWeight({@Part() required double weight}); @GET('/api/blogPosts/search_by_author/{slug}') - Future> getBlogPostByAuthorSlug({ - @Path() required String slug, - }); + Future> getBlogPostByAuthorSlug({@Path() required String slug}); @GET('/api/profile/measure/weight/{period}') - Future> getWeight({ - @Path() required String period, - }); + Future> getWeight({@Path() required String period}); @GET('/api/profile/measure/weight/{period}/{slug}') - Future> getWeightBySlug({ - @Path() required String period, - @Path() required String slug, - }); + Future> getWeightBySlug({@Path() required String period, @Path() required String slug}); @GET('/api/profile/measure/circumferences/{period}') - Future> getCircumferences({ - @Path() required String period, - }); + Future> getCircumferences({@Path() required String period}); @GET('/api/profile/measure/circumferences/{period}/{slug}') - Future> getCircumferencesBySlug({ - @Path() required String period, - @Path() required String slug, - }); + Future> getCircumferencesBySlug({@Path() required String period, @Path() required String slug}); @GET('/api/profile/{slug}/subscribers') - Future getSubscribers({ - @Path() required String slug, - @Query('filters[name][\$startsWith]') String? query, - }); + Future getSubscribers({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query}); @GET('/api/profile/{slug}/subscriptions') - Future getSubscriptions({ - @Path() required String slug, - @Query('filters[name][\$startsWith]') String? query, - }); + Future getSubscriptions({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query}); @POST('/api/profile/subscribe/{slug}') - Future subscribe({ - @Path() required String slug, - }); + Future subscribe({@Path() required String slug}); @POST('/api/profile/unsubscribe/{slug}') - Future unsubscribe({ - @Path() required String slug, - }); + Future unsubscribe({@Path() required String slug}); @POST('/api/profile/photo/upload') @MultiPart() - Future uploadPhoto({ - @Part(name: 'photo') required File photo, - }); + Future uploadPhoto({@Part(name: 'photo') required File photo}); @DELETE('/api/profile/photo/delete') Future deletePhoto(); @@ -105,20 +75,16 @@ abstract class ProfileService { Future getChosenDiets(); @POST('/api/profile/make_pass_key') - Future getPassKey({ - @Body() required String json, - }); + Future getPassKey({@Body() required String json}); @GET('/api/profile/my_phone') Future getMyPhone(); @POST('/api/profile/send_email_code') - Future sendEmailCode(); + Future sendEmailCode({@Body() Map data = const {}}); @POST('/api/profile/verificate_email_code') - Future tryVerifyEmailByCode({ - @Body() required String code, - }); + Future tryVerifyEmailByCode({@Body() required String code}); @DELETE('/api/profile/delete_my_account') Future deleteAccount(); diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index d1f547f8..57554130 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -32,4 +32,16 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.verifyError()); } } + + Future changeEmail(String newEmail) async { + try { + emit(const VerifyEmailState.changeEmailLoading()); + await profileRepository.sendEmailCode(newEmail: newEmail); + emit(const VerifyEmailState.changeEmailSuccess()); + // Обновляем email в состоянии после успешной смены + emit(VerifyEmailState.loaded(email: newEmail)); + } catch (e) { + emit(const VerifyEmailState.changeEmailError()); + } + } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart index 718b2cce..6cb91a32 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart @@ -6,13 +6,9 @@ part 'verify_email_state.freezed.dart'; abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.loading() = VerifyEmailStateLoading; - const factory VerifyEmailState.loaded({ - required String email, - }) = VerifyEmailStateLoaded; + const factory VerifyEmailState.loaded({required String email}) = VerifyEmailStateLoaded; - const factory VerifyEmailState.error({ - required String errorMessage, - }) = VerifyEmailStateError; + const factory VerifyEmailState.error({required String errorMessage}) = VerifyEmailStateError; const factory VerifyEmailState() = _VerifyEmailState; @@ -21,4 +17,10 @@ abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess; const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError; + + const factory VerifyEmailState.changeEmailLoading() = VerifyEmailStateChangeEmailLoading; + + const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess; + + const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError; } diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index e91b9e9e..8d1ef619 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,6 +28,7 @@ class _VerifyEmailScreenState extends State { late Timer _timer; int _secondsRemaining = 60; bool _canSendAgain = false; + @override void initState() { super.initState(); @@ -75,55 +77,82 @@ class _VerifyEmailScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (_) => sl()..sendEmailCode(), - child: BlocConsumer( - listener: (context, state) { - if (state is VerifyEmailStateLoaded) { - setState(() { - email = state.email; - }); - } else if (state is VerifyEmailStateVerifySuccess) { - showSuccessToast('Почта успешно подтверждена'); - goRouter.pushNamed('feedScreen'); - } else if (state is VerifyEmailStateVerifyError) { - showErrorToast("Неверный код. Попробуйте снова."); - for (var controller in _controllers) { - controller.clear(); - } - FocusScope.of(context).requestFocus(_focusNodes[0]); - } - }, - builder: (context, state) { - return Scaffold( - appBar: AppBar(title: const Text("Подтвердите почту")), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), - const SizedBox(height: 20), - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), - const SizedBox(height: 40), - CustomOutlinedButton( - text: 'Запросить код повторно', - height: 52, - isDisabled: !_canSendAgain, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), - ), - onPressed: () async { - await context.read().sendEmailCode(); - _startTimer(); - }, + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state is VerifyEmailStateLoaded) { + setState(() { + email = state.email; + }); + _startTimer(); + } else if (state is VerifyEmailStateVerifySuccess) { + showSuccessToast('Почта успешно подтверждена'); + goRouter.pushNamed('feedScreen'); + } else if (state is VerifyEmailStateVerifyError) { + showErrorToast("Неверный код. Попробуйте снова."); + for (var controller in _controllers) { + controller.clear(); + } + FocusScope.of(context).requestFocus(_focusNodes[0]); + } else if (state is VerifyEmailStateChangeEmailSuccess) { + showSuccessToast('Код подтверждения отправлен на новый email'); + // Диалог закроется автоматически в ChangeEmailDialog при успешной смене + } else if (state is VerifyEmailStateChangeEmailError) { + showErrorToast('Ошибка при смене email. Попробуйте снова.'); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar(title: const Text("Подтвердите почту")), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), + const SizedBox(height: 20), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), + const SizedBox(height: 40), + CustomOutlinedButton( + text: 'Запросить код повторно', + height: 52, + isDisabled: !_canSendAgain, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), + ), + onPressed: () async { + await context.read().sendEmailCode(); + _startTimer(); + }, + ), + const SizedBox(height: 8), + if (!_canSendAgain) + Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), + const SizedBox(height: 16), + CustomOutlinedButton( + text: 'Сменить email', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.blueA400), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide(color: appTheme.blueA400)), + backgroundColor: const WidgetStatePropertyAll(Colors.transparent), + ), + onPressed: () { + final verifyEmailCubit = context.read(); + showDialog( + context: context, + builder: (context) => ChangeEmailDialog(currentEmail: email, verifyEmailCubit: verifyEmailCubit), + ); + }, + ), + ], ), - const SizedBox(height: 8), - if (!_canSendAgain) - Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), - ], - ), - ), + ), + ); + }, ); }, ), diff --git a/lib/features/registration/presentation/widgets/change_email_dialog.dart b/lib/features/registration/presentation/widgets/change_email_dialog.dart new file mode 100644 index 00000000..266e9f04 --- /dev/null +++ b/lib/features/registration/presentation/widgets/change_email_dialog.dart @@ -0,0 +1,174 @@ +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/toasts.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChangeEmailDialog extends StatefulWidget { + final String currentEmail; + final VerifyEmailCubit verifyEmailCubit; + + const ChangeEmailDialog({super.key, required this.currentEmail, required this.verifyEmailCubit}); + + @override + State createState() => _ChangeEmailDialogState(); +} + +class _ChangeEmailDialogState extends State { + final TextEditingController _emailController = TextEditingController(); + final FocusNode _emailFocusNode = FocusNode(); + bool _isLoading = false; + String? _errorText; + + @override + void initState() { + super.initState(); + _emailController.text = widget.currentEmail; + } + + @override + void dispose() { + _emailController.dispose(); + _emailFocusNode.dispose(); + super.dispose(); + } + + bool _isValidEmail(String email) { + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); + } + + void _validateEmail() { + final email = _emailController.text.trim(); + if (email.isEmpty) { + setState(() { + _errorText = 'Введите email'; + }); + } else if (!_isValidEmail(email)) { + setState(() { + _errorText = 'Введите корректный email'; + }); + } else if (email == widget.currentEmail) { + setState(() { + _errorText = 'Новый email должен отличаться от текущего'; + }); + } else { + setState(() { + _errorText = null; + }); + } + } + + Future _changeEmail() async { + _validateEmail(); + if (_errorText != null) return; + + setState(() { + _isLoading = true; + }); + + try { + await widget.verifyEmailCubit.changeEmail(_emailController.text.trim()); + // Диалог закроется автоматически при успешной смене email через основной экран + } catch (e) { + if (mounted) { + showErrorToast('Ошибка при смене email. Попробуйте снова.'); + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.verifyEmailCubit, + child: BlocListener( + listener: (context, state) { + if (state is VerifyEmailStateChangeEmailSuccess) { + Navigator.of(context).pop(); // Закрываем диалог при успешной смене + } else if (state is VerifyEmailStateChangeEmailError) { + setState(() { + _isLoading = false; + }); + } + }, + child: Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Сменить email', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 16), + Text('Введите новый email для получения кода подтверждения', style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), + const SizedBox(height: 20), + TextField( + controller: _emailController, + focusNode: _emailFocusNode, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'Введите новый email', + hintStyle: TextStyles.textSMedium.copyWith(color: appTheme.gray), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.gray), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.blueA400), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.red), + ), + errorText: _errorText, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + onChanged: (_) => _validateEmail(), + onSubmitted: (_) => _changeEmail(), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: 'Отмена', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + ), + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomOutlinedButton( + text: _isLoading ? 'Отправка...' : 'Сменить', + height: 52, + isDisabled: _isLoading, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_isLoading ? appTheme.gray : appTheme.blueA400), + ), + onPressed: _isLoading ? null : _changeEmail, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} From 1680bd2556d9002d95909c9403788537574797f4 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 27 Aug 2025 12:04:20 +0300 Subject: [PATCH 16/34] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 2 ++ ios/Runner.xcodeproj/project.pbxproj | 8 -------- ios/fastlane/Fastfile | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index ac54d483..4353f3be 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -59,6 +59,8 @@ jobs: flutter pub get flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs + # Configure Flutter for release mode + flutter build ios --config-only --release cd ios pod update fastlane beta diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 080fb053..7d740632 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -213,14 +213,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -287,14 +283,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 9d9b239d..3a74adf3 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -44,6 +44,10 @@ platform :ios do UI.message "Using auth_key_path: #{auth_key_path}" + # Configure Flutter for release mode + UI.message "Configuring Flutter for release mode..." + sh("cd .. && flutter build ios --config-only --release") + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") From 12bf9f9e5f7177d40e357c33e0707faa5155079c Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 27 Aug 2025 14:39:22 +0300 Subject: [PATCH 17/34] feat(verify_email): add dialog --- .../pages/verify_email_screen.dart | 17 +++- .../widgets/code_not_received_dialog.dart | 82 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 lib/features/registration/presentation/widgets/code_not_received_dialog.dart diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index 8d1ef619..4ade2581 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart'; +import 'package:eatvio_app/features/registration/presentation/widgets/code_not_received_dialog.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -113,7 +114,21 @@ class _VerifyEmailScreenState extends State { Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), const SizedBox(height: 20), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), - const SizedBox(height: 40), + const SizedBox(height: 16), + GestureDetector( + onTap: () { + final verifyEmailCubit = context.read(); + showDialog(context: context, builder: (context) => CodeNotReceivedDialog( + currentEmail: email, + verifyEmailCubit: verifyEmailCubit, + )); + }, + child: Text( + 'Не приходит код?', + style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline), + ), + ), + const SizedBox(height: 24), CustomOutlinedButton( text: 'Запросить код повторно', height: 52, diff --git a/lib/features/registration/presentation/widgets/code_not_received_dialog.dart b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart new file mode 100644 index 00000000..c59d80d9 --- /dev/null +++ b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/gestures.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/registration/presentation/widgets/change_email_dialog.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; + +class CodeNotReceivedDialog extends StatelessWidget { + final String currentEmail; + final VerifyEmailCubit verifyEmailCubit; + + const CodeNotReceivedDialog({ + super.key, + required this.currentEmail, + required this.verifyEmailCubit + }); + + void _showChangeEmailDialog(BuildContext context) { + Navigator.of(context).pop(); // Закрываем текущий диалог + showDialog( + context: context, + builder: (context) => ChangeEmailDialog( + currentEmail: currentEmail, + verifyEmailCubit: verifyEmailCubit, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + border: Border.all(color: Colors.grey[800]!, width: 2.0), + ), + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Почему может не приходить код?', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 16), + RichText( + text: TextSpan( + style: TextStyles.textMMedium.copyWith(color: appTheme.gray600), + children: [ + const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. '), + TextSpan( + text: 'Ввести email повторно.', + style: TextStyles.textMMedium.copyWith( + color: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showChangeEmailDialog(context), + ), + const TextSpan(text: '\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.'), + ], + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: appTheme.blueA400, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text('Понятно', style: TextStyles.textMSemibold.copyWith(color: Colors.white)), + ), + ), + ], + ), + ), + ); + } +} From d13eb78f0e660b9019977ff463bd733240e38db6 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 14:25:13 +0300 Subject: [PATCH 18/34] feat(notifications): Issue-236 --- lib/core/app_router.dart | 177 +++++------- lib/core/locator_service.dart | 41 ++- .../models/notification_settings_model.dart | 39 +++ .../notification_settings_cubit.dart | 33 +++ .../notification_settings_state.dart | 31 +++ .../menu/presentation/pages/menu_screen.dart | 2 + .../pages/notifications_settings_screen.dart | 262 ++++++++++++++++++ .../notification_settings_repository.dart | 16 ++ .../notification_settings_service.dart | 16 ++ .../presentation/pages/settings_screen.dart | 5 +- 10 files changed, 483 insertions(+), 139 deletions(-) create mode 100644 lib/features/menu/models/notification_settings_model.dart create mode 100644 lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart create mode 100644 lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart create mode 100644 lib/features/menu/presentation/pages/notifications_settings_screen.dart create mode 100644 lib/features/menu/repositories/notification_settings_repository.dart create mode 100644 lib/features/menu/services/notification_settings_service.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index c691b0b2..cc028255 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -24,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; @@ -111,23 +112,13 @@ class AppRouter { name: 'authScreen', builder: (context, state) => const AuthScreen(), routes: [ - GoRoute( - path: 'registration-screen', - name: 'registrationScreen', - builder: (context, state) => const RegistrationScreen(), - ), + GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute( path: 'forget-password-screen', name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen( - isAuth: state.extra as bool, - ), - ), - GoRoute( - path: 'scan-qr-code-screen', - name: 'scanQRCodeScreen', - builder: (context, state) => const ScanQRCodeScreen(), + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), ), + GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), ], ), ShellRoute( @@ -165,19 +156,23 @@ class AppRouter { GoRoute( path: '/diary-screen', name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage( - child: DiaryScreen(), - ), + pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), ), GoRoute( - path: ConsultationsScreen.path, - name: ConsultationsScreen.name, - pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())), - GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())), + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())), + path: '/chat-select-screen', + name: 'chatSelectScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + ), + GoRoute( + path: '/notifications-screen', + name: 'notificationsScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), + ), GoRoute( path: '/menu-screen', name: 'menuScreen', @@ -194,11 +189,7 @@ class AppRouter { ), ], ), - GoRoute( - name: 'blockedUsersScreen', - path: '/blocked-users-screen', - builder: (context, state) => const BlockedUsersScreen(), - ), + GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), GoRoute( path: '/welcome-screen', name: 'welcomeScreen', @@ -241,26 +232,10 @@ class AppRouter { } }, ), - GoRoute( - path: ConsultationsArchiveScreen.path, - name: ConsultationsArchiveScreen.name, - builder: (context, state) => const ConsultationsArchiveScreen(), - ), - GoRoute( - path: ConsultantFinancesScreen.path, - name: ConsultantFinancesScreen.name, - builder: (context, state) => const ConsultantFinancesScreen(), - ), - GoRoute( - path: UserPurchasesScreen.path, - name: UserPurchasesScreen.name, - builder: (context, state) => const UserPurchasesScreen(), - ), - GoRoute( - path: '/user-finance-screen', - name: 'userFinanceScreen', - builder: (context, state) => const UserFinanceScreen(), - ), + GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), + GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), + GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), + GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), GoRoute( path: CardSettingsScreen.path, name: CardSettingsScreen.name, @@ -275,11 +250,7 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutPlanScreen( - slug: slug, - navigateToComments: false, - index: null, - ); + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); } final data = state.extra as Map; @@ -293,29 +264,23 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutRecipeScreen( - recipeSlug: slug, - navigateToComments: false, - index: null, - ); + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); } final data = state.extra as Map; - return AboutRecipeScreen( - recipeSlug: data['recipe'] as String, - index: data['index'] as int?, - navigateToComments: data['navigateToComments'] as bool, - ); + return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); }, ), GoRoute( path: RecipeCreateScreen.path, name: RecipeCreateScreen.name, - builder: (context, state) => RecipeCreateScreen( - recipe: state.extra != null ? state.extra as Recipe : null, - ), + builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), + ), + GoRoute( + path: '/profile-screen', + name: 'profileScreen', + builder: (context, state) => ProfileScreen(author: state.extra as Author), ), - GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)), GoRoute( path: '/subs-screen/:slug', name: 'subsScreen', @@ -327,11 +292,7 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutDietScreen( - slug: slug, - index: null, - navigateToComments: false, - ); + return AboutDietScreen(slug: slug, index: null, navigateToComments: false); } final data = state.extra as Map; return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); @@ -343,19 +304,18 @@ class AppRouter { builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), ), GoRoute( - path: '/blog-post-screen', - name: 'blogPostScreen', - builder: (context, state) { - final id = state.uri.queryParameters['id']; - final parsedId = int.tryParse(id ?? ''); - if (parsedId != null) { - return BlogPostScreen( - id: parsedId, - ); - } + path: '/blog-post-screen', + name: 'blogPostScreen', + builder: (context, state) { + final id = state.uri.queryParameters['id']; + final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { + return BlogPostScreen(id: parsedId); + } - return BlogPostScreen(id: state.extra as int); - }), + return BlogPostScreen(id: state.extra as int); + }, + ), GoRoute( path: '/chat-screen', name: 'chatScreen', @@ -363,10 +323,7 @@ class AppRouter { final data = state.extra as Map; final uuid = data['uuid'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen( - uuid: uuid, - sendInitialMessage: sendInitialMessage, - ); + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); }, ), GoRoute( @@ -399,7 +356,11 @@ class AppRouter { name: 'physicalFormScreen', builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), ), - GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)), + GoRoute( + path: 'pressure-screen', + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), GoRoute( path: 'change-password-screen', name: 'changePasswordScreen', @@ -416,9 +377,7 @@ class AppRouter { GoRoute( path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen( - slug: state.extra == null ? null : state.extra as String, - ), + builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), ), GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), @@ -446,7 +405,11 @@ class AppRouter { GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), - GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen(slug: state.extra as String), + ), GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), GoRoute( @@ -456,10 +419,7 @@ class AppRouter { final data = state.extra as Map; final planId = data['planId'] as int; final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen( - planId: planId, - successfulPayment: successfulPayment, - ); + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); }, ), GoRoute( @@ -469,10 +429,7 @@ class AppRouter { final data = state.extra as Map; final planId = data['planId'] as int; final url = data['url'] as String; - return PaymentScreen( - url: url, - planId: planId, - ); + return PaymentScreen(url: url, planId: planId); }, ), GoRoute( @@ -482,10 +439,7 @@ class AppRouter { final data = state.extra as Map; final diaryId = data['diaryId'] as int; final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen( - diaryId: diaryId, - currentPlanDay: currentPlanDay, - ); + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); }, ), GoRoute( @@ -493,16 +447,9 @@ class AppRouter { name: 'verifyScreen', builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), - GoRoute( - path: '/verify-email-screen', - name: 'verifyEmailScreen', - builder: (context, state) => const VerifyEmailScreen(), - ), - GoRoute( - path: MyRecipesScreen.path, - name: MyRecipesScreen.name, - builder: (context, state) => const MyRecipesScreen(), - ), + GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), + GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), ], ); } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9d0b5297..971ebb2c 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -43,8 +43,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_ import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; @@ -136,14 +139,16 @@ Future init() async { sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory(() => DiaryCubit( - chatRepository: sl(), - diaryRepository: sl(), - glossaryRepository: sl(), - profileRepository: sl(), - dietRepository: sl(), - settingsRepository: sl(), - )); + sl.registerFactory( + () => DiaryCubit( + chatRepository: sl(), + diaryRepository: sl(), + glossaryRepository: sl(), + profileRepository: sl(), + dietRepository: sl(), + settingsRepository: sl(), + ), + ); sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); @@ -172,6 +177,7 @@ Future init() async { sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => BlogPostCubit(sl())); sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); + sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); // Repository @@ -204,6 +210,7 @@ Future init() async { sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); sl.registerLazySingleton(() => CommentsRepository(service: sl())); + sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); // Services @@ -239,22 +246,12 @@ Future init() async { sl.registerLazySingleton(() => CommentsService(sl())); sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => ConsultantRatingService(sl())); + sl.registerLazySingleton(() => NotificationSettingsService(sl())); // External sl.registerLazySingleton( - () => Dio( - BaseOptions( - baseUrl: baseUrl, - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - )..interceptors.addAll( - [ - TokenInterceptor(), - PhoneVerifyErrorInterceptor(), - ], - ), + () => + Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) + ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), ); } diff --git a/lib/features/menu/models/notification_settings_model.dart b/lib/features/menu/models/notification_settings_model.dart new file mode 100644 index 00000000..2879a0ab --- /dev/null +++ b/lib/features/menu/models/notification_settings_model.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'notification_settings_model.g.dart'; +part 'notification_settings_model.freezed.dart'; + +@freezed +abstract class NotificationSettingsModel with _$NotificationSettingsModel { + const factory NotificationSettingsModel({ + @JsonKey(name: 'new_updates_notification') required bool newUpdatesNotification, + @JsonKey(name: 'weight_measurements_notification') required bool weightMeasurementsNotification, + @JsonKey(name: 'is_email_notifications_enabled') required bool isEmailNotificationsEnabled, + @JsonKey(name: 'is_push_notifications_enabled') required bool isPushNotificationsEnabled, + @JsonKey(name: 'diary_reminder_notifications') required DiaryReminderNotifications diaryReminderNotifications, + }) = _NotificationSettingsModel; + + factory NotificationSettingsModel.fromJson(Map json) => _$NotificationSettingsModelFromJson(json); +} + +@freezed +abstract class DiaryReminderNotifications with _$DiaryReminderNotifications { + const factory DiaryReminderNotifications({ + @JsonKey(name: 'first_reminder') required ReminderSettings firstReminder, + @JsonKey(name: 'second_reminder') required ReminderSettings secondReminder, + @JsonKey(name: 'third_reminder') required ReminderSettings thirdReminder, + }) = _DiaryReminderNotifications; + + factory DiaryReminderNotifications.fromJson(Map json) => _$DiaryReminderNotificationsFromJson(json); +} + +@freezed +abstract class ReminderSettings with _$ReminderSettings { + const factory ReminderSettings({ + required bool enabled, + @JsonKey(name: 'local_time_hours') required int localTimeHours, + @JsonKey(name: 'local_time_mins') required int localTimeMins, + }) = _ReminderSettings; + + factory ReminderSettings.fromJson(Map json) => _$ReminderSettingsFromJson(json); +} diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart new file mode 100644 index 00000000..6c5c567c --- /dev/null +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart @@ -0,0 +1,33 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; + +class NotificationSettingsCubit extends Cubit { + final NotificationSettingsRepository _notificationSettingsRepository; + + NotificationSettingsCubit({required NotificationSettingsRepository notificationSettingsRepository}) + : _notificationSettingsRepository = notificationSettingsRepository, + super(const NotificationSettingsStateLoading()); + + Future getNotificationSettings() async { + emit(const NotificationSettingsStateLoading()); + try { + final notificationSettings = await _notificationSettingsRepository.getNotificationSettings(); + emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); + } catch (e, stackTrace) { + log('Error getting notification settings: $e', stackTrace: stackTrace); + emit(NotificationSettingsStateError(errorMessage: e.toString())); + } + } + + Future updateNotificationSettings(Map data) async { + try { + final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data); + emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); + } catch (e, stackTrace) { + log('Error updating notification settings: $e', stackTrace: stackTrace); + emit(NotificationSettingsStateError(errorMessage: e.toString())); + } + } +} diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart new file mode 100644 index 00000000..ef461f7c --- /dev/null +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; + +abstract class NotificationSettingsState extends Equatable { + const NotificationSettingsState(); + + @override + List get props => []; +} + +class NotificationSettingsStateLoading extends NotificationSettingsState { + const NotificationSettingsStateLoading(); +} + +class NotificationSettingsStateLoaded extends NotificationSettingsState { + final NotificationSettingsModel notificationSettings; + + const NotificationSettingsStateLoaded({required this.notificationSettings}); + + @override + List get props => [notificationSettings]; +} + +class NotificationSettingsStateError extends NotificationSettingsState { + final String errorMessage; + + const NotificationSettingsStateError({required this.errorMessage}); + + @override + List get props => [errorMessage]; +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 2ebce8f4..5c350d3c 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -67,6 +67,8 @@ class MenuScreen extends StatelessWidget { _divider(context), _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), + _tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')), + _divider(context), _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), _divider(context), _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart new file mode 100644 index 00000000..2c7f4e04 --- /dev/null +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -0,0 +1,262 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class NotificationsSettingsScreen extends StatelessWidget { + const NotificationsSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..getNotificationSettings(), child: const NotificationsSettingsContent()); + } +} + +class NotificationsSettingsContent extends StatelessWidget { + const NotificationsSettingsContent({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Уведомления')), + body: BlocBuilder( + builder: (context, state) { + if (state is NotificationSettingsStateLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is NotificationSettingsStateError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Ошибка: ${state.errorMessage}'), + ElevatedButton( + onPressed: () { + context.read().getNotificationSettings(); + }, + child: const Text('Повторить'), + ), + ], + ), + ); + } else if (state is NotificationSettingsStateLoaded) { + return _buildSettingsContent(context, state.notificationSettings); + } + return const Center(child: Text('Нет данных')); + }, + ), + ); + } + + Widget _buildSettingsContent(BuildContext context, NotificationSettingsModel notificationSettings) { + final cubit = context.read(); + return Column( + children: [ + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Уведомление о новых обновлениях', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.newUpdatesNotification, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'new_updates_notification': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Уведомления о взвешивании', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.weightMeasurementsNotification, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'weight_measurements_notification': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Email уведомления', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.isEmailNotificationsEnabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'is_email_notifications_enabled': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Push уведомления', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.isPushNotificationsEnabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'is_push_notifications_enabled': newValue}); + }, + ), + ), + _divider(context), + const SizedBox(height: 16), + _buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit), + ], + ); + } + + Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Напоминания о дневнике', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: appTheme.blueA400), + ), + ), + _buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), + _buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), + _buildReminderTile(context, 'Третье напоминание', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), + ], + ); + } + + void _updateNotificationSetting(NotificationSettingsCubit cubit, Map data) { + cubit.updateNotificationSettings(data); + } + + void _updateReminderSetting(NotificationSettingsCubit cubit, String reminderType, Map data) { + final updateData = { + 'diary_reminder_notifications': {reminderType: data}, + }; + cubit.updateNotificationSettings(updateData); + } + + Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { + return _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: reminder.enabled + ? () { + _showTimePicker(context, reminder, reminderType, cubit); + } + : null, + title: title, + subtitle: reminder.enabled + ? Row( + children: [ + Icon(Icons.access_time, size: 16, color: appTheme.blueA400), + const SizedBox(width: 8), + Text( + '${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}', + style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500), + ), + const SizedBox(width: 4), + Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CupertinoSwitch( + value: reminder.enabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateReminderSetting(cubit, reminderType, { + 'enabled': newValue, + 'local_time_hours': reminder.localTimeHours, + 'local_time_mins': reminder.localTimeMins, + }); + }, + ), + ], + ), + ); + } + + void _showTimePicker(BuildContext context, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { + int selectedHour = reminder.localTimeHours; + int selectedMinute = reminder.localTimeMins; + + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return Container( + height: 300, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton(child: const Text('Отмена'), onPressed: () => Navigator.of(context).pop()), + CupertinoButton( + child: const Text('Готово'), + onPressed: () { + _updateReminderSetting(cubit, reminderType, { + 'enabled': reminder.enabled, + 'local_time_hours': selectedHour, + 'local_time_mins': selectedMinute, + }); + Navigator.of(context).pop(); + }, + ), + ], + ), + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + initialDateTime: DateTime(2024, 1, 1, selectedHour, selectedMinute), + onDateTimeChanged: (DateTime newDateTime) { + selectedHour = newDateTime.hour; + selectedMinute = newDateTime.minute; + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + + ListTile _tile({required String title, required Function()? onTap, Widget? trailing, EdgeInsets? padding, Widget? subtitle}) { + return ListTile( + contentPadding: padding, + onTap: onTap, + title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)), + subtitle: subtitle, + trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38), + ); + } + + Center _divider(BuildContext context) { + return Center( + child: Container( + height: 0.4, + width: MediaQuery.of(context).size.width - 32, + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), + ), + ); + } +} diff --git a/lib/features/menu/repositories/notification_settings_repository.dart b/lib/features/menu/repositories/notification_settings_repository.dart new file mode 100644 index 00000000..e0aeaa31 --- /dev/null +++ b/lib/features/menu/repositories/notification_settings_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; + +class NotificationSettingsRepository { + final NotificationSettingsService _notificationSettingsService; + + NotificationSettingsRepository(this._notificationSettingsService); + + Future getNotificationSettings() async { + return await _notificationSettingsService.getNotificationSettings(); + } + + Future updateNotificationSettings(Map data) async { + return await _notificationSettingsService.updateNotificationSettings(data: data); + } +} diff --git a/lib/features/menu/services/notification_settings_service.dart b/lib/features/menu/services/notification_settings_service.dart new file mode 100644 index 00000000..f489267f --- /dev/null +++ b/lib/features/menu/services/notification_settings_service.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'notification_settings_service.g.dart'; + +@RestApi() +abstract class NotificationSettingsService { + factory NotificationSettingsService(Dio dio, {String baseUrl}) = _NotificationSettingsService; + + @GET('/api/profile/notification_settings') + Future getNotificationSettings(); + + @POST('/api/profile/update_notification_settings') + Future updateNotificationSettings({@Body() required Map data}); +} diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index b7aa0d59..1c304f29 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -58,8 +58,8 @@ class _SettingsScreenState extends State { children: [ _additionalInfo(state), const SizedBox(height: 12), - _notifications(state), - const SizedBox(height: 12), + // _notifications(state), + // const SizedBox(height: 12), _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -147,6 +147,7 @@ class _SettingsScreenState extends State { ); } + // ignore: unused_element Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, From dcf2805df7fe0852882875ae5ce011d93345c7c8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 14:26:28 +0300 Subject: [PATCH 19/34] fix: analyzer --- .../menu/presentation/pages/notifications_settings_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart index 2c7f4e04..1c1065a3 100644 --- a/lib/features/menu/presentation/pages/notifications_settings_screen.dart +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -165,7 +165,7 @@ class NotificationsSettingsContent extends StatelessWidget { style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500), ), const SizedBox(width: 4), - Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), + const Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), ], ) : null, From 39fce4b0f9aa2558b89e40e2e8d01fa74418da33 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 15:16:40 +0300 Subject: [PATCH 20/34] fix: mv env --- .gitea/workflows/test-flight-cd.yaml | 7 +++---- .gitea/workflows/test.yml | 7 +++---- env/.env | 0 lib/main.dart | 2 +- pubspec.yaml | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) delete mode 100644 env/.env diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4353f3be..c9649f0e 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -21,20 +21,19 @@ jobs: - name: Copy .env file run: | - mkdir -p env - cp env_repo/.env env/.env + cp env_repo/.env .env - name: Load environment variables from .env run: | set -a - source env/.env + source .env set +a # Export all variables to GITHUB_ENV while IFS= read -r line; do if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then echo "$line" >> $GITHUB_ENV fi - done < env/.env + done < .env - name: Unlock login keychain run: | diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index d209778b..f1f63162 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -26,20 +26,19 @@ jobs: - name: Copy .env file run: | - mkdir -p env - cp env_repo/.env env/.env + cp env_repo/.env .env - name: Load environment variables from .env run: | set -a - source env/.env + source .env set +a # Export all variables to GITHUB_ENV while IFS= read -r line; do if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then echo "$line" >> $GITHUB_ENV fi - done < env/.env + done < .env - name: Create key.properties file run: | diff --git a/env/.env b/env/.env deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/main.dart b/lib/main.dart index 468de90f..60293ed8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await dotenv.load(fileName: "env/.env"); + await dotenv.load(fileName: ".env"); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); diff --git a/pubspec.yaml b/pubspec.yaml index 1505f232..ae850a35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,7 +90,7 @@ dev_dependencies: flutter: uses-material-design: true assets: - - env/.env + - .env - assets/ - assets/images/ - assets/audio/ From b9dc5c013685ec91fb0c6b97554e643bcb960562 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 12:16:55 +0000 Subject: [PATCH 21/34] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20env/.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env/.env | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 env/.env diff --git a/env/.env b/env/.env deleted file mode 100644 index e69de29b..00000000 From b665692779185307fae230704a1298c9f616b880 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 1 Sep 2025 13:03:43 +0300 Subject: [PATCH 22/34] fix: Issue-645 --- .../widgets/code_not_received_dialog.dart | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/features/registration/presentation/widgets/code_not_received_dialog.dart b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart index c59d80d9..3baa5742 100644 --- a/lib/features/registration/presentation/widgets/code_not_received_dialog.dart +++ b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart @@ -9,20 +9,13 @@ class CodeNotReceivedDialog extends StatelessWidget { final String currentEmail; final VerifyEmailCubit verifyEmailCubit; - const CodeNotReceivedDialog({ - super.key, - required this.currentEmail, - required this.verifyEmailCubit - }); + const CodeNotReceivedDialog({super.key, required this.currentEmail, required this.verifyEmailCubit}); void _showChangeEmailDialog(BuildContext context) { Navigator.of(context).pop(); // Закрываем текущий диалог showDialog( context: context, - builder: (context) => ChangeEmailDialog( - currentEmail: currentEmail, - verifyEmailCubit: verifyEmailCubit, - ), + builder: (context) => ChangeEmailDialog(currentEmail: currentEmail, verifyEmailCubit: verifyEmailCubit), ); } @@ -31,11 +24,7 @@ class CodeNotReceivedDialog extends StatelessWidget { return Dialog( backgroundColor: Colors.transparent, child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16.0), - border: Border.all(color: Colors.grey[800]!, width: 2.0), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16.0)), padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, @@ -46,17 +35,17 @@ class CodeNotReceivedDialog extends StatelessWidget { text: TextSpan( style: TextStyles.textMMedium.copyWith(color: appTheme.gray600), children: [ - const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. '), + const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. ', style: TextStyles.textFS13FW400), TextSpan( text: 'Ввести email повторно.', - style: TextStyles.textMMedium.copyWith( - color: appTheme.blueA400, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => _showChangeEmailDialog(context), + style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () => _showChangeEmailDialog(context), + ), + const TextSpan( + style: TextStyles.textFS13FW400, + text: + '\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.', ), - const TextSpan(text: '\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.'), ], ), ), From bf6e23e96a6a4002bb9b4b5a285ace32d11a05a0 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 3 Sep 2025 11:34:41 +0300 Subject: [PATCH 23/34] feat(metrica): Issue-685 --- .../profile_avatar_cubit.dart | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index cc7939dd..935eecf7 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -1,24 +1,55 @@ +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:permission_handler/permission_handler.dart'; class ProfileAvatarCubit extends Cubit { final ProfileRepository profileRepository; - ProfileAvatarCubit({ - required this.profileRepository, - }) : super(const ProfileAvatarState.loading()); + ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading()); + + int _calculateAge(String birthdate) { + try { + final dateParts = birthdate.split('-'); + if (dateParts.length == 3) { + final birthYear = int.parse(dateParts[0]); + final birthMonth = int.parse(dateParts[1]); + final birthDay = int.parse(dateParts[2]); + + final now = DateTime.now(); + int age = now.year - birthYear; + + if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) { + age--; + } + + return age; + } + } catch (e) { + return 0; + } + return 0; + } Future getMyProfile() async { emit(const ProfileAvatarState.loading()); + final profile = await profileRepository.getMyProfile(); + final profileDetails = await profileRepository.getMyProfileDetails(); try { - emit( - ProfileAvatarState.loaded( - profile: await profileRepository.getMyProfile(), - ), - ); + emit(ProfileAvatarState.loaded(profile: profile)); + final notificationStatus = await Permission.notification.status; + final isNotificationEnabled = notificationStatus.isGranted; + AppMetricaUserProfile userProfile = AppMetricaUserProfile([ + AppMetricaNameAttribute.withValue(profile.name), + AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female), + AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)), + AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled), + ]); + AppMetrica.setUserProfileID(profile.slug); + AppMetrica.reportUserProfile(userProfile); } catch (e) { AppRouter.navigatorKey.currentContext?.go('/auth-screen'); emit(ProfileAvatarState.error(errorMessage: e.toString())); From b6b1c8b3c13356da606f26c218e3475e3207a441 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 8 Sep 2025 14:14:41 +0300 Subject: [PATCH 24/34] =?UTF-8?q?feat(diary):=20=D0=BA=D0=B1=D0=B6=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diary/models/diary_day_model.dart | 38 +-- .../widgets/meal_button_widget.dart | 116 +++++---- .../widgets/meal_summary_widget.dart | 112 +++++++++ .../presentation/widgets/meal_widget.dart | 25 +- .../presentation/widgets/product_widget.dart | 157 +++++++----- .../widgets/products_list_widget.dart | 25 +- .../diary/repositories/diary_repository.dart | 225 +++++++----------- .../diary/services/diary_service.dart | 94 ++------ .../bloc/feed_cubit/feed_cubit.dart | 61 ++--- .../pages/registration_screen.dart | 6 + 10 files changed, 461 insertions(+), 398 deletions(-) create mode 100644 lib/features/diary/presentation/widgets/meal_summary_widget.dart diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart index 6e08f6d0..e1ad4572 100644 --- a/lib/features/diary/models/diary_day_model.dart +++ b/lib/features/diary/models/diary_day_model.dart @@ -22,12 +22,7 @@ enum PortionState { @freezed abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel { - const factory DiaryDayWrapperModel({ - @JsonKey( - name: "diary", - ) - required DiaryDayModel diary, - }) = _DiaryDayWrapperModel; + const factory DiaryDayWrapperModel({@JsonKey(name: "diary") required DiaryDayModel diary}) = _DiaryDayWrapperModel; factory DiaryDayWrapperModel.fromJson(Map json) => _$DiaryDayWrapperModelFromJson(json); } @@ -71,21 +66,13 @@ DiaryDietModel? _parseDiet(dynamic json) { @freezed abstract class DiaryDietModel with _$DiaryDietModel { - const factory DiaryDietModel({ - required int id, - required int duration, - required String name, - }) = _DiaryDietModel; + const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel; factory DiaryDietModel.fromJson(Map json) => _$DiaryDietModelFromJson(json); } TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) { - return TypeOfDiaryModel( - hasChosenDiet: json['has_chosen_diet'] as bool, - hasChosenPlan: json['has_chosen_plan'] as bool, - diaryState: DiaryStateEnum.init, - ); + return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init); } @freezed @@ -115,11 +102,30 @@ abstract class DiaryPhotoModel with _$DiaryPhotoModel { factory DiaryPhotoModel.fromJson(Map json) => _$DiaryPhotoModelFromJson(json); } +@freezed +abstract class ProductCaloriesInfoModel with _$ProductCaloriesInfoModel { + const factory ProductCaloriesInfoModel({ + @JsonKey(name: "id") required int id, + @JsonKey(name: "product_name") required String productName, + @JsonKey(name: "energy_on_100gr") required double calories, + @JsonKey(name: "proteins_on_100gr") required double proteins, + @JsonKey(name: "fats_on_100gr") required double fats, + @JsonKey(name: "carbohydrates_on_100gr") required double carbohydrates, + @JsonKey(name: "energy_on_portion") required double energyOnPortion, + @JsonKey(name: "proteins_on_portion") required double proteinsOnPortion, + @JsonKey(name: "fats_on_portion") required double fatsOnPortion, + @JsonKey(name: "carbohydrates_on_portion") required double carbohydratesOnPortion, + }) = _ProductCaloriesInfoModel; + + factory ProductCaloriesInfoModel.fromJson(Map json) => _$ProductCaloriesInfoModelFromJson(json); +} + @freezed abstract class PortionModel with _$PortionModel { const factory PortionModel({ @JsonKey(name: "id") required int id, @JsonKey(name: "product_nutritional_value_id") int? productNutritionalValueId, + ProductCaloriesInfoModel? productCaloriesInfo, @JsonKey(name: "product_name") required String productName, @JsonKey(name: "quantity") required int quantity, @JsonKey(name: "portion") required String portion, diff --git a/lib/features/diary/presentation/widgets/meal_button_widget.dart b/lib/features/diary/presentation/widgets/meal_button_widget.dart index d8937b14..9e17c217 100644 --- a/lib/features/diary/presentation/widgets/meal_button_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_button_widget.dart @@ -1,16 +1,15 @@ import 'package:collection/collection.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MealButtonWidget extends StatefulWidget { final int mealNumber; - const MealButtonWidget({ - super.key, - required this.mealNumber, - }); + const MealButtonWidget({super.key, required this.mealNumber}); @override State createState() => _MealButtonWidgetState(); @@ -22,9 +21,7 @@ class _MealButtonWidgetState extends State { return BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded) { - final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull( - (element) => element.typeNo == widget.mealNumber, - ); + final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull((element) => element.typeNo == widget.mealNumber); final Color bgColor; if (state.selectedMeal == widget.mealNumber) { @@ -34,55 +31,84 @@ class _MealButtonWidgetState extends State { } else { bgColor = const Color(0xFFF1F2F6); } - return InkWell( - onTap: () { - context.read().setSelectedMeal(widget.mealNumber); - }, - borderRadius: BorderRadius.circular(100), - child: Ink( - width: MediaQuery.of(context).size.width / 5 - (96 / 5), - height: 32, - decoration: BoxDecoration( + + final mealIndex = widget.mealNumber - 1; + final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : []; + final totals = MealSummaryWidget.calculateMealTotals(portions); + final calories = totals['calories']!.round(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + context.read().setSelectedMeal(widget.mealNumber); + }, borderRadius: BorderRadius.circular(100), - color: bgColor, - ), - child: Center( - child: Text( - '${widget.mealNumber}', - style: TextStyle( - color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), - fontSize: 16, - fontWeight: FontWeight.w600, + child: Ink( + width: MediaQuery.of(context).size.width / 5 - (96 / 5), + height: 32, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: bgColor), + child: Center( + child: Text( + '${widget.mealNumber}', + style: TextStyle( + color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ), - ), + const SizedBox(height: 4), + Text( + calories > 0 ? '$calories ккал' : '— ккал', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ); } if (state is DiaryStateLoadedTomorrowWithDiet) { - return InkWell( - onTap: () { - context.read().setSelectedMeal(widget.mealNumber); - }, - borderRadius: BorderRadius.circular(100), - child: Ink( - width: MediaQuery.of(context).size.width / 5 - (96 / 5), - height: 32, - decoration: BoxDecoration( + + final mealIndex = widget.mealNumber - 1; + final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : []; + final totals = MealSummaryWidget.calculateMealTotals(portions); + final calories = totals['calories']!.round(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + context.read().setSelectedMeal(widget.mealNumber); + }, borderRadius: BorderRadius.circular(100), - color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), - ), - child: Center( - child: Text( - '${widget.mealNumber}', - style: TextStyle( - color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), - fontSize: 16, - fontWeight: FontWeight.w600, + child: Ink( + width: MediaQuery.of(context).size.width / 5 - (96 / 5), + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), + ), + child: Center( + child: Text( + '${widget.mealNumber}', + style: TextStyle( + color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ), - ), + const SizedBox(height: 4), + Text( + calories > 0 ? '$calories ккал' : '— ккал', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ); } return const SizedBox.shrink(); diff --git a/lib/features/diary/presentation/widgets/meal_summary_widget.dart b/lib/features/diary/presentation/widgets/meal_summary_widget.dart new file mode 100644 index 00000000..dd7108e6 --- /dev/null +++ b/lib/features/diary/presentation/widgets/meal_summary_widget.dart @@ -0,0 +1,112 @@ +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:flutter/material.dart'; + +class MealSummaryWidget extends StatelessWidget { + final List portions; + final int mealNumber; + + const MealSummaryWidget({super.key, required this.portions, required this.mealNumber}); + + @override + Widget build(BuildContext context) { + final totals = _calculateTotals(); + + if (totals['calories'] == 0 && totals['proteins'] == 0 && totals['fats'] == 0 && totals['carbohydrates'] == 0) { + return const SizedBox.shrink(); + } + + return Container( + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFF2F80ED), width: 2), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Прием $mealNumber', + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600), + ), + Row( + children: [ + _buildNutritionChip('К', totals['calories']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('Б', totals['proteins']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('Ж', totals['fats']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('У', totals['carbohydrates']!.round()), + ], + ), + ], + ), + ); + } + + Widget _buildNutritionChip(String label, int value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$label ', + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w600), + ), + TextSpan( + text: '$value', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + + Map _calculateTotals() { + return calculateMealTotals(portions); + } + + static Map calculateMealTotals(List portions) { + double totalCalories = 0; + double totalProteins = 0; + double totalFats = 0; + double totalCarbohydrates = 0; + + for (final portion in portions) { + if (portion.portionState == PortionState.eatenDeleted) { + continue; + } + + final caloriesInfo = portion.productCaloriesInfo; + if (caloriesInfo == null) { + continue; + } + + double calories, proteins, fats, carbohydrates; + + if (portion.portion == 'gr') { + calories = (caloriesInfo.calories * portion.quantity) / 100; + proteins = (caloriesInfo.proteins * portion.quantity) / 100; + fats = (caloriesInfo.fats * portion.quantity) / 100; + carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100; + } else { + calories = caloriesInfo.energyOnPortion; + proteins = caloriesInfo.proteinsOnPortion; + fats = caloriesInfo.fatsOnPortion; + carbohydrates = caloriesInfo.carbohydratesOnPortion; + } + + totalCalories += calories; + totalProteins += proteins; + totalFats += fats; + totalCarbohydrates += carbohydrates; + } + + return {'calories': totalCalories, 'proteins': totalProteins, 'fats': totalFats, 'carbohydrates': totalCarbohydrates}; + } +} diff --git a/lib/features/diary/presentation/widgets/meal_widget.dart b/lib/features/diary/presentation/widgets/meal_widget.dart index ce71ef66..21b7fe20 100644 --- a/lib/features/diary/presentation/widgets/meal_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_widget.dart @@ -40,11 +40,7 @@ class _MealWidgetState extends State { children: [ const Text( 'Прием пищи', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 18, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), const Row( @@ -57,23 +53,17 @@ class _MealWidgetState extends State { MealButtonWidget(mealNumber: 5), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), const TimeOfMealPickerWidget(), const ChosenDietWidget(), const ChosenPlanWidget(), const ProductsListWidget(), - if (widget.showAddButton) ...[ - const SizedBox(height: 12), - const AddProductButtonWidget(), - ], + if (widget.showAddButton) ...[const SizedBox(height: 12), const AddProductButtonWidget()], const SizedBox(height: 12), const PhotoGalleryWidget(), const SizedBox(height: 12), const SatietyRatingWidget(), - if (widget.showMood) ...[ - const SizedBox(height: 12), - const MoodRatingWidget(), - ], + if (widget.showMood) ...[const SizedBox(height: 12), const MoodRatingWidget()], BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) { @@ -91,10 +81,7 @@ class _MealWidgetState extends State { if (consultantCommentOfMeal != null) { return Column( children: [ - Text( - '${s.mealComment} ${state.selectedMeal}', - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + Text('${s.mealComment} ${state.selectedMeal}', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), const SizedBox(height: 12), CommentContainer(comment: consultantCommentOfMeal.comment), ], @@ -103,7 +90,7 @@ class _MealWidgetState extends State { } return const SizedBox.shrink(); }, - ) + ), ], ), ), diff --git a/lib/features/diary/presentation/widgets/product_widget.dart b/lib/features/diary/presentation/widgets/product_widget.dart index a221ffc6..cdf34f28 100644 --- a/lib/features/diary/presentation/widgets/product_widget.dart +++ b/lib/features/diary/presentation/widgets/product_widget.dart @@ -1,3 +1,4 @@ + import 'package:collection/collection.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -15,10 +16,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class ProductWidget extends StatefulWidget { final PortionModel portion; - const ProductWidget({ - super.key, - required this.portion, - }); + const ProductWidget({super.key, required this.portion}); @override State createState() => _ProductWidgetState(); @@ -37,6 +35,60 @@ class _ProductWidgetState extends State { isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; } + Widget _buildNutritionalInfo() { + if (widget.portion.productNutritionalValueId == null) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), + child: const Text( + 'Продукт отправлен на модерацию', + style: TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + } + + final caloriesInfo = widget.portion.productCaloriesInfo; + if (caloriesInfo == null) { + return const SizedBox.shrink(); + } + double calories, proteins, fats, carbohydrates; + + if (widget.portion.portion == 'gr') { + calories = (caloriesInfo.calories * widget.portion.quantity) / 100; + proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100; + fats = (caloriesInfo.fats * widget.portion.quantity) / 100; + carbohydrates = (caloriesInfo.carbohydrates * widget.portion.quantity) / 100; + } else { + calories = caloriesInfo.energyOnPortion; + proteins = caloriesInfo.proteinsOnPortion; + fats = caloriesInfo.fatsOnPortion; + carbohydrates = caloriesInfo.carbohydratesOnPortion; + } + + return Row( + children: [ + _buildNutritionChip('К', calories.round()), + const SizedBox(width: 4), + _buildNutritionChip('Б', proteins.round()), + const SizedBox(width: 4), + _buildNutritionChip('Ж', fats.round()), + const SizedBox(width: 4), + _buildNutritionChip('У', carbohydrates.round()), + ], + ); + } + + Widget _buildNutritionChip(String label, int value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), + child: Text( + '$label $value', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + } + @override void didUpdateWidget(ProductWidget oldWidget) { super.didUpdateWidget(oldWidget); @@ -48,15 +100,9 @@ class _ProductWidgetState extends State { return showModalBottomSheet( useRootNavigator: true, context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))), isScrollControlled: true, - builder: (context) => AddProductModal( - state: state, - portion: portion, - diaryCubit: diaryCubit, - ), + builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit), ); } @@ -69,15 +115,21 @@ class _ProductWidgetState extends State { horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), tileColor: const Color(0xFFF6FAFF), ); } if (state is DiaryStateLoaded) { - final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || + final isDiaryCommented = + state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; final isDiaryFinished = @@ -99,21 +151,20 @@ class _ProductWidgetState extends State { color: Color(0xFF6D6F71), ), child: ListTile( - onTap: () => diaryCubit.restore( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ), - trailing: SvgPicture.asset( - 'assets/icons/revert_product.svg', - ), + onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id), + trailing: SvgPicture.asset('assets/icons/revert_product.svg'), horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), // ignore: use_full_hex_values_for_flutter_colors tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01), ), @@ -122,12 +173,13 @@ class _ProductWidgetState extends State { Widget? trailing; if (state.isConsultantView && !isDiaryCommented) { trailing = GestureDetector( - onTap: () { - setState(() { - showCommentWidget = !showCommentWidget; - }); - }, - child: SvgPicture.asset('assets/icons/edit.svg')); + onTap: () { + setState(() { + showCommentWidget = !showCommentWidget; + }); + }, + child: SvgPicture.asset('assets/icons/edit.svg'), + ); } else if (!isDiaryCommented && !widget.portion.fromDiet) { trailing = const Icon(Icons.more_vert_rounded); } @@ -147,47 +199,36 @@ class _ProductWidgetState extends State { isEaten = value!; }); if (isEaten) { - diaryCubit.markEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); + diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); } else { - diaryCubit.unmarkEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); + diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); } } : null, activeColor: appTheme.blueA400, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - side: const BorderSide( - color: Color(0xFF809CB9), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + side: const BorderSide(color: Color(0xFF809CB9)), ), ) : null, horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), tileColor: const Color(0xFFF6FAFF), ), Padding( padding: const EdgeInsets.only(top: 8.0), - child: PortionCommentWidget( - enabled: showCommentWidget, - portionId: widget.portion.id, - comment: portionComment?.comment, - ), - ) + child: PortionCommentWidget(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment), + ), ], ); } diff --git a/lib/features/diary/presentation/widgets/products_list_widget.dart b/lib/features/diary/presentation/widgets/products_list_widget.dart index 6882af08..d3d7ac10 100644 --- a/lib/features/diary/presentation/widgets/products_list_widget.dart +++ b/lib/features/diary/presentation/widgets/products_list_widget.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart'; import 'package:flutter/material.dart'; @@ -16,33 +17,33 @@ class _ProductsListWidgetState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (BuildContext context, DiaryState state) { + builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { int mealIndex = state.selectedMeal - 1; + final portions = state.diary.meals[mealIndex].portions; return Column( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[ - ProductWidget(portion: state.diary.meals[mealIndex].portions[i]), - i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), + for (int i = 0; i < portions.length; i++) ...[ + ProductWidget(portion: portions[i]), + i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], - if (state.isLoadingAddingProductInMeal) - const Padding( - padding: EdgeInsets.only(top: 8), - child: ProductLoaderWidget(), - ), + if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal), + if (state.isLoadingAddingProductInMeal) const Padding(padding: EdgeInsets.only(top: 8), child: ProductLoaderWidget()), ], ); } if (state is DiaryStateLoadedTomorrowWithDiet) { int mealIndex = state.selectedMeal - 1; + final portions = state.diary.meals[mealIndex].portions; return Column( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[ - ProductWidget(portion: state.diary.meals[mealIndex].portions[i]), - i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), + for (int i = 0; i < portions.length; i++) ...[ + ProductWidget(portion: portions[i]), + i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], + if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal), ], ); } diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 2ac30803..0165ad7f 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -15,13 +15,55 @@ class DiaryRepository { Stream get forceFetchStream => _diaryForceFetchController.stream; - DiaryRepository({ - required this.diaryService, - }); + DiaryRepository({required this.diaryService}); Future getDiaryByDate(String date) async { try { - return (await getDiaryIndexByDate(date)).last; + Set ids = {}; + final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last; + for (var meal in diaryDay.meals) { + for (var portion in meal.portions) { + if (portion.productNutritionalValueId != null) { + ids.add(portion.productNutritionalValueId!); + } + } + } + if (ids.isNotEmpty) { + final glossary = await getGlossariesIndex(ids: ids.toList()); + final List updatedMeals = List.from(diaryDay.meals); + for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) { + final meal = updatedMeals[mealIndex]; + final updatedPortions = meal.portions.map((portion) { + if (portion.productNutritionalValueId != null) { + try { + final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId); + return portion.copyWith( + productCaloriesInfo: ProductCaloriesInfoModel( + id: glossaryItem.id, + productName: glossaryItem.name, + calories: glossaryItem.energyOn100Gr, + proteins: glossaryItem.proteinsOn100Gr, + fats: glossaryItem.fatsOn100Gr, + carbohydrates: glossaryItem.carbohydratesOn100Gr, + energyOnPortion: glossaryItem.energyOnPortion, + proteinsOnPortion: glossaryItem.proteinsOnPortion, + fatsOnPortion: glossaryItem.fatsOnPortion, + carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion, + ), + ); + } catch (e) { + return portion; + } + } + return portion; + }).toList(); + + updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions); + } + final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals); + return updatedDiaryDay; + } + return diaryDay; } catch (e) { rethrow; } @@ -75,109 +117,47 @@ class DiaryRepository { int? productNutritionalValueId, required PortionState portionState, }) async { - Map data = ProductInMeal( - productName: productName, - portion: portion, - quantity: quantity, - ).toJson(); + Map data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson(); data['product_nutritional_value_id'] = productNutritionalValueId; data['state'] = portionState.name; - return await diaryService.updateProductInMeal( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - productInMeal: data, - ); + return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data); } - Future markEaten({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.markEaten( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future markEaten({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future unmarkEaten({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.unmarkEaten( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future restore({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.restore( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future restore({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future deleteProductFromMeal({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.deleteProductFromMeal( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future uploadPhotos({ - required int diaryId, - required int mealId, - required List files, - }) async { + Future uploadPhotos({required int diaryId, required int mealId, required List files}) async { try { for (int i = 0; i < files.length; i++) { - await diaryService.uploadPhotoOfMeal( - diary: diaryId, - typeNo: mealId, - photo: files[i], - ); + await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]); } } catch (e) { rethrow; } } - Future deletePhoto({ - required int diaryId, - required int mealId, - required DiaryPhotoModel photo, - }) async { + Future deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async { try { - await diaryService.deletePhotoOfMeal( - diaryId: diaryId, - typeNo: mealId, - userAttachment: photo.id, - ); + await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id); } catch (e) { rethrow; } } - Future finishDiaryDay({ - required int diaryId, - required String wakeUp, - required String retire, - String? comment, - }) async { + Future finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async { return await diaryService.finishDiaryDay( diary: diaryId, finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment), @@ -192,87 +172,45 @@ class DiaryRepository { required double carbohydrates, }) async { return await diaryService.sendProductToModeration( - data: ProductModerationModel( - name: name, - energy: energy, - proteins: proteins, - fats: fats, - carbohydrates: carbohydrates, - ).toJson(), + data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(), ); } - Future fillSatiety({ - required int diaryId, - required int mealId, - required int satiety, - }) async { + Future fillSatiety({required int diaryId, required int mealId, required int satiety}) async { try { - return await diaryService.fillSatiety( - diary: diaryId, - typeNo: mealId, - satiety: satiety, - ); + return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety); } catch (e) { rethrow; } } - Future fillMood({ - required int diaryId, - required int mealId, - required int mood, - }) async { + Future fillMood({required int diaryId, required int mealId, required int mood}) async { try { - return await diaryService.fillMood( - diary: diaryId, - typeNo: mealId, - mood: mood, - ); + return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood); } catch (e) { rethrow; } } - Future fillTime({ - required int diaryId, - required int mealId, - required String time, - }) async { + Future fillTime({required int diaryId, required int mealId, required String time}) async { try { - return await diaryService.fillTime( - diary: diaryId, - typeNo: mealId, - time: time, - ); + return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time); } catch (e) { rethrow; } } - Future fillWakeup({ - required int diaryId, - required String time, - }) async { + Future fillWakeup({required int diaryId, required String time}) async { try { - return await diaryService.fillWakeup( - diary: diaryId, - wakeUp: time, - ); + return await diaryService.fillWakeup(diary: diaryId, wakeUp: time); } catch (e) { rethrow; } } - Future fillRetire({ - required int diaryId, - required String time, - }) async { + Future fillRetire({required int diaryId, required String time}) async { try { - return await diaryService.fillRetire( - diary: diaryId, - retire: time, - ); + return await diaryService.fillRetire(diary: diaryId, retire: time); } catch (e) { rethrow; } @@ -287,10 +225,7 @@ class DiaryRepository { } } - Future getDiaryComments({ - required int planId, - required int diaryId, - }) async { + Future getDiaryComments({required int planId, required int diaryId}) async { try { return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); } catch (e) { @@ -305,4 +240,18 @@ class DiaryRepository { rethrow; } } + + Future getGlossariesIndex({required List ids}) async { + Map filters = {}; + if (ids.isNotEmpty) { + for (int i = 0; i < ids.length; i++) { + filters['filters[id][\$in][$i]'] = ids[i]; + } + } + try { + return await diaryService.getGlossariesIndex(filters: filters); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index d9967200..8cba6f33 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -15,32 +15,20 @@ abstract class DiaryService { factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService; @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDate({ - @Path('date') required String date, - }); + Future> getDiaryIndexByDate({@Path('date') required String date}); @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDateForDietOnly({ - @Path('date') required String date, - }); + Future> getDiaryIndexByDateForDietOnly({@Path('date') required String date}); @GET('/api/diary/{diary}/show') - Future getDiaryDay({ - @Path('diary') required int diary, - }); + Future getDiaryDay({@Path('diary') required int diary}); @GET('/api/diary/{diary}/dailyNutritionData') - Future getNutrition({ - @Path('diary') required int diary, - }); + Future getNutrition({@Path('diary') required int diary}); @POST('/api/diary/{diary}/meals/{type_no}/photo/upload') @MultiPart() - Future uploadPhotoOfMeal({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Part(name: 'photo') required File photo, - }); + Future uploadPhotoOfMeal({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Part(name: 'photo') required File photo}); @DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete') Future deletePhotoOfMeal({ @@ -68,25 +56,13 @@ abstract class DiaryService { }); @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten') - Future markEaten({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future markEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten') - Future unmarkEaten({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future unmarkEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore') - Future restore({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future restore({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete') Future deleteProductFromMeal({ @@ -96,63 +72,35 @@ abstract class DiaryService { }); @POST('/api/diary/{diary}/finishDay') - Future finishDiaryDay({ - @Path('diary') required int diary, - @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel, - }); + Future finishDiaryDay({@Path('diary') required int diary, @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel}); @POST('/api/diary/{diary}/meals/{type_no}/satiety/fill') - Future fillSatiety({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required int satiety, - }); + Future fillSatiety({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int satiety}); @POST('/api/diary/{diary}/meals/{type_no}/mood/fill') - Future fillMood({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required int mood, - }); + Future fillMood({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int mood}); @POST('/api/diary/{diary}/meals/{type_no}/time/fill') - Future fillTime({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required String time, - }); + Future fillTime({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required String time}); @POST('/api/diary/{diary}/wakeUp/fill') - Future fillWakeup({ - @Path('diary') required int diary, - @Field() required String wakeUp, - }); + Future fillWakeup({@Path('diary') required int diary, @Field() required String wakeUp}); @POST('/api/diary/{diary}/retire/fill') - Future fillRetire({ - @Path('diary') required int diary, - @Field() required String retire, - }); + Future fillRetire({@Path('diary') required int diary, @Field() required String retire}); @POST('/api/diary/product_nutritional_values/sendToModeration') - Future sendProductToModeration({ - @Body() required Map data, - }); + Future sendProductToModeration({@Body() required Map data}); @POST('/api/diary/{id}/consultant/comment') - Future commentUserDay({ - @Path('id') required int id, - @Body() required ConsultantCommentModel body, - }); + Future commentUserDay({@Path('id') required int id, @Body() required ConsultantCommentModel body}); @GET('/api/chosen_plans/{planId}/{diaryId}/comments/show') - Future getDiaryComments({ - @Path() required int planId, - @Path() required int diaryId, - }); + Future getDiaryComments({@Path() required int planId, @Path() required int diaryId}); @POST('/api/diary/{diaryId}/consultant/comment/view') - Future viewCommentOfDiaryDayByConsultant({ - @Path() required int diaryId, - }); + Future viewCommentOfDiaryDayByConsultant({@Path() required int diaryId}); + + @GET('/api/glossaries/index') + Future getGlossariesIndex({@Queries() Map? filters}); } diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index d124a9f9..d3ba615a 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -12,70 +12,57 @@ class FeedCubit extends Cubit { bool isLoading = false; String? _query; - FeedCubit({ - required this.feedRepository, - }) : super(const FeedState.loading()); + FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { - if (feedBlogPostModel == null) return; + if (feedBlogPostModel == null || isClosed) return; List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; final int index = feedBlogPostList.indexOf(feedBlogPostModel); feedBlogPostList[index] = feedBlogPostModel.copyWith( wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, ); - emit( - FeedState.loaded( - feedBlogPostList: feedBlogPostList, - isLoading: isLoading, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading)); + } } - Future loadFeedBlogPostList({ - String? query, - }) async { + Future loadFeedBlogPostList({String? query}) async { try { _query = query; final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query); lastPage = feedBlogPosts.meta.lastPage; - emit( - FeedState.loaded( - feedBlogPostList: feedBlogPosts.feedBlogPostList, - isLoading: false, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false)); + } } catch (e) { - emit(FeedState.error(errorMessage: e.toString())); + if (!isClosed) { + emit(FeedState.error(errorMessage: e.toString())); + } } } Future loadNextPage() async { - if (currentPage < lastPage && !isLoading) { + if (currentPage < lastPage && !isLoading && !isClosed) { isLoading = true; - emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); + if (!isClosed) { + emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); + } try { - if (currentPage < lastPage) { + if (currentPage < lastPage && !isClosed) { currentPage++; - final feedBlogPosts = await feedRepository.getFeedBlogPosts( - page: currentPage, - query: _query, - ); + final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage, query: _query); isLoading = false; - emit( - FeedState.loaded( - feedBlogPostList: [ - ...(state as FeedStateLoaded).feedBlogPostList, - ...feedBlogPosts.feedBlogPostList, - ], - isLoading: isLoading, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading)); + } } } catch (e) { - emit(FeedState.error(errorMessage: e.toString())); + if (!isClosed) { + emit(FeedState.error(errorMessage: e.toString())); + } } } } diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 9f30ca33..b00df3ea 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -40,6 +40,12 @@ class _RegistrationScreenState extends State { final pageController = PageController(); final RegistrationCubit registrationCubit = sl(); + @override + void initState() { + super.initState(); + RegistrationAnalyticsService.reportRegistrationStep(step); + } + @override Widget build(BuildContext context) { return BlocProvider( From 89f0a46192d0be084b473250722562a6d5109934 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 9 Sep 2025 18:58:39 +0300 Subject: [PATCH 25/34] fix(diet): Issue-180 --- .../presentation/diet_preview_screen.dart | 175 ++++-------------- .../presentation/widgets/add_meal_widget.dart | 136 ++++---------- 2 files changed, 69 insertions(+), 242 deletions(-) diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 6c337142..94b21709 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -26,10 +26,7 @@ class DietPreviewScreen extends StatefulWidget { late final List selectedIndexes; static const name = 'createDietScreenPreview'; static const path = '/create-diet-screen/preview'; - DietPreviewScreen({ - super.key, - required this.diet, - }) { + DietPreviewScreen({super.key, required this.diet}) { selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0); } @@ -65,11 +62,7 @@ class _DietPreviewScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), decoration: BoxDecoration( color: Colors.white, - border: Border( - top: BorderSide( - color: Colors.black.withValues(alpha: 0.1), - ), - ), + border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -79,34 +72,16 @@ class _DietPreviewScreenState extends State { Row( children: [ Expanded( - child: SizedBox( - child: FloatingActionButton.extended( - heroTag: 'nextButtonRegistration', - highlightElevation: 0, - elevation: 0, - focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - backgroundColor: appTheme.greenA700, - onPressed: () {}, - label: Text( - s.startDiet, - style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - ), - ), + child: BookmarkButton( + isFavorite: widget.diet.isFavorite, + onTap: () {}, + width: 52, + height: 52, + size: 24, + shape: BoxShape.circle, + color: appTheme.gray50, ), ), - const SizedBox(width: 8), - BookmarkButton( - isFavorite: widget.diet.isFavorite, - onTap: () {}, - width: 52, - height: 52, - size: 24, - shape: BoxShape.circle, - color: appTheme.gray50, - ), ], ), const SizedBox(height: 8), @@ -133,24 +108,15 @@ class _DietPreviewScreenState extends State { filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), ), ), @@ -160,17 +126,10 @@ class _DietPreviewScreenState extends State { GestureDetector( onTap: () {}, child: Container( - decoration: BoxDecoration( - color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle), height: 32, width: 32, - child: Icon( - Icons.send_rounded, - size: 18, - color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black, - ), + child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black), ), ), ], @@ -212,12 +171,7 @@ class _DietPreviewScreenState extends State { children: [ Stack( children: [ - Image.memory( - imageData, - height: 300, - width: MediaQuery.sizeOf(context).width, - fit: BoxFit.fill, - ), + Image.memory(imageData, height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill), // CachedNetworkImage( // imageUrl: baseUrl + '/storage/' + widget.diet.image, // ), @@ -233,10 +187,7 @@ class _DietPreviewScreenState extends State { children: [ SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12), const SizedBox(width: 8), - Text( - widget.diet.totalViews.toString(), - style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), - ), + Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), const SizedBox(width: 12), @@ -252,10 +203,7 @@ class _DietPreviewScreenState extends State { colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null, ), const SizedBox(width: 8), - Text( - widget.diet.totalLikes.toString(), - style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), - ), + Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), ), @@ -265,10 +213,7 @@ class _DietPreviewScreenState extends State { children: [ SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12), const SizedBox(width: 8), - Text( - widget.diet.totalComments.toString(), - style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), - ), + Text(widget.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), ], @@ -279,23 +224,15 @@ class _DietPreviewScreenState extends State { const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - s.about_diet, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), + child: Text(s.about_diet, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), ), const SizedBox(height: 12), - InfoRow(diet: widget.diet, type: ProductType.diet,), - ExpandableText( - text: widget.diet.details, - ), + InfoRow(diet: widget.diet, type: ProductType.diet), + ExpandableText(text: widget.diet.details), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: AuthorCard( - author: widget.diet.author, - title: s.diet_author, - ), + child: AuthorCard(author: widget.diet.author, title: s.diet_author), ), const SizedBox(height: 16), ], @@ -308,9 +245,7 @@ class _DietPreviewScreenState extends State { child: Center( child: Text( '${s.diet_plan}(${widget.diet.duration.toString()} ${getDayForm(widget.diet.duration, s)})', - style: TextStyles.headingXLSemibold.copyWith( - color: appTheme.black, - ), + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), ), ), @@ -320,7 +255,7 @@ class _DietPreviewScreenState extends State { physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { - return _dayOfPlan(widget.diet.dailySchedule[index] ?? {}, index, s); + return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s); }, itemCount: widget.diet.dailySchedule.length, ), @@ -331,12 +266,7 @@ class _DietPreviewScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft, - child: Text( - '0 ${getCommentForm(widget.diet.totalComments, s)}', - style: TextStyles.textMMedium.copyWith( - color: appTheme.gray500, - ), - ), + child: Text('0 ${getCommentForm(widget.diet.totalComments, s)}', style: TextStyles.textMMedium.copyWith(color: appTheme.gray500)), ), const SizedBox(height: 16), ], @@ -355,14 +285,10 @@ class _DietPreviewScreenState extends State { pressElevation: 0, showCheckmark: false, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), label: Text((index + 1).toString()), - labelStyle: TextStyles.textXSSemibold.copyWith( - color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600, - ), + labelStyle: TextStyles.textXSSemibold.copyWith(color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600), selected: widget.selectedIndexes[dayNumber] == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { @@ -376,9 +302,7 @@ class _DietPreviewScreenState extends State { return Padding( padding: const EdgeInsets.only(bottom: 8), child: MainBackgroundBouble( - padding: const EdgeInsets.only( - top: 16, - ), + padding: const EdgeInsets.only(top: 16), child: Column( children: [ Padding( @@ -386,31 +310,12 @@ class _DietPreviewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '${s.day} ${dayNumber + 1}', - style: TextStyles.headingXLSemibold.copyWith( - color: appTheme.black, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: CustomDivider(), - ), - Text( - s.meal, - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + Text('${s.day} ${dayNumber + 1}', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: CustomDivider()), + Text(s.meal, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - choiceChipWidget(0), - choiceChipWidget(1), - choiceChipWidget(2), - choiceChipWidget(3), - choiceChipWidget(4), - ], + children: [choiceChipWidget(0), choiceChipWidget(1), choiceChipWidget(2), choiceChipWidget(3), choiceChipWidget(4)], ), const SizedBox(height: 8), ListView.builder( @@ -443,20 +348,10 @@ class _DietPreviewScreenState extends State { children: [ SizedBox( width: MediaQuery.sizeOf(context).width * 0.6, - child: Text( - daily.name, - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + child: Text(daily.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), const Spacer(), - Text( - '${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', - style: TextStyles.textMSemibold.copyWith( - color: appTheme.black, - ), - ), + Text('${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), ], ), ); diff --git a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart index 8f125abd..ececed87 100644 --- a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart +++ b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart @@ -46,21 +46,14 @@ class _AddMealWidgetState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(16) + - EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), + padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Добавление блюда', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF323F4B), - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), Row( @@ -73,21 +66,17 @@ class _AddMealWidgetState extends State { autofocus: true, hintText: 'Введите название продукта/блюда', onChanged: (query) { - EasyDebounce.debounce( - 'glossary-search', - const Duration(milliseconds: 250), - () async { + EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; selectedGlossary = null; - if (query.isEmpty) { - glossariesList = []; - selectedGlossary = null; - setState(() {}); - return; - } - glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query); setState(() {}); - }, - ); + return; + } + glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query); + setState(() {}); + }); }, ), ), @@ -100,22 +89,20 @@ class _AddMealWidgetState extends State { hintText: selectedGlossary == null ? '100' : selectedGlossary!.grInPortion == 0 - ? '100' - : '1', + ? '100' + : '1', suffixText: selectedGlossary == null ? 'гр' : selectedGlossary?.grInPortion == 0 - ? 'гр' - : 'шт', + ? 'гр' + : 'шт', ), ), ], ), glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(), Container( - constraints: const BoxConstraints( - maxHeight: 256, - ), + constraints: const BoxConstraints(maxHeight: 256), child: ListView.separated( physics: const BouncingScrollPhysics(), shrinkWrap: true, @@ -130,28 +117,12 @@ class _AddMealWidgetState extends State { setState(() {}); FocusManager.instance.primaryFocus?.nextFocus(); }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: Text( - glossariesList[i].name, - style: const TextStyle( - height: 1, - ), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text(glossariesList[i].name, style: const TextStyle(height: 1)), subtitle: glossariesList[i].grInPortion == 0 ? null - : Text( - '${glossariesList[i].grInPortion} г', - style: TextStyle( - height: 1, - color: Colors.grey.shade600, - ), - ), - trailing: Icon( - Icons.add_rounded, - color: Colors.grey.shade400, - ), + : Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)), + trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400), contentPadding: const EdgeInsets.only(left: 12), ); }, @@ -166,25 +137,15 @@ class _AddMealWidgetState extends State { if (widget.portionIndex != null) InkWell( onTap: () { - widget.createDietCubit.removePortion( - day: widget.dayIndex, - meal: widget.mealIndex, - portionIndex: widget.portionIndex!, - ); + widget.createDietCubit.removePortion(day: widget.dayIndex, meal: widget.mealIndex, portionIndex: widget.portionIndex!); Navigator.of(context).pop(); }, borderRadius: BorderRadius.circular(20), child: Ink( width: 52, height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), - child: const Icon( - Icons.delete_outline_rounded, - color: Colors.red, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), + child: const Icon(Icons.delete_outline_rounded, color: Colors.red), ), ), if (widget.portionIndex != null) const SizedBox(width: 8), @@ -194,18 +155,11 @@ class _AddMealWidgetState extends State { child: Ink( width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92), height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: Center( child: Text( widget.portionIndex == null ? 'Добавить' : 'Сохранить', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), ), ), ), @@ -222,8 +176,8 @@ class _AddMealWidgetState extends State { var measurementUnits = selectedGlossary?.grInPortion == null ? Portion.pcs : selectedGlossary?.grInPortion == 0 - ? Portion.grams - : Portion.pcs; + ? Portion.grams + : Portion.pcs; final glossary = selectedGlossary; if (glossary != null) { final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits); @@ -235,19 +189,11 @@ class _AddMealWidgetState extends State { portion: measurementUnits, ); if (portionIndex == null) { - widget.createDietCubit.addPortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portion: portion, - ); + widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion); } else { - widget.createDietCubit.updatePortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portionIndex: portionIndex, - portion: portion, - ); + widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portionIndex: portionIndex, portion: portion); } + Navigator.of(context).pop(); } else { Navigator.of(context).pop(); final newGlossary = await showModalBottomSheet( @@ -255,13 +201,8 @@ class _AddMealWidgetState extends State { useRootNavigator: true, context: context, builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: GlossaryModerationRequestModalWidget( - productName: productNameController.text, - diaryCubit: sl(), - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: sl()), ), ); if (newGlossary != null) { @@ -275,18 +216,9 @@ class _AddMealWidgetState extends State { portion: measurementUnits, ); if (portionIndex == null) { - widget.createDietCubit.addPortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portion: portion, - ); + widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion); } else { - widget.createDietCubit.updatePortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portion: portion, - portionIndex: portionIndex, - ); + widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion, portionIndex: portionIndex); } } } From f1f5c796618f13433e6d13b7b4729c00e233667e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 10 Sep 2025 15:58:26 +0300 Subject: [PATCH 26/34] fix(notifications): Issue-236 --- lib/core/theme/theme_helper.dart | 2 + .../pages/notifications_settings_screen.dart | 125 +++++++++++------- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ru.dart | 1 + lib/generated/l10n.dart | 17 ++- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_ru.arb | 3 +- 7 files changed, 95 insertions(+), 57 deletions(-) diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 3b2a063d..c0c0af29 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -202,6 +202,8 @@ class PrimaryColors { Color get darkGray => const Color(0XFFBBBCC0); Color get commentBg => const Color(0XFFFFFDD8); + + Color get cardNotificationsBG => const Color(0xFFA4A4A4); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart index 1c1065a3..e2afa516 100644 --- a/lib/features/menu/presentation/pages/notifications_settings_screen.dart +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -3,9 +3,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class NotificationsSettingsScreen extends StatelessWidget { const NotificationsSettingsScreen({super.key}); @@ -21,8 +23,10 @@ class NotificationsSettingsContent extends StatelessWidget { @override Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( - appBar: AppBar(title: const Text('Уведомления')), + appBar: AppBar(title: Text(s.notifications)), body: BlocBuilder( builder: (context, state) { if (state is NotificationSettingsStateLoading) { @@ -58,7 +62,7 @@ class NotificationsSettingsContent extends StatelessWidget { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () {}, - title: 'Уведомление о новых обновлениях', + title: 'Новые обновления', subtitle: null, trailing: CupertinoSwitch( value: notificationSettings.newUpdatesNotification, @@ -72,7 +76,7 @@ class NotificationsSettingsContent extends StatelessWidget { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () {}, - title: 'Уведомления о взвешивании', + title: 'Напоминать о взвешивании', subtitle: null, trailing: CupertinoSwitch( value: notificationSettings.weightMeasurementsNotification, @@ -111,27 +115,29 @@ class NotificationsSettingsContent extends StatelessWidget { ), ), _divider(context), - const SizedBox(height: 16), _buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit), ], ); } Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'Напоминания о дневнике', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: appTheme.blueA400), + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Напоминание о заполнении дневника', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: appTheme.cardNotificationsBG), ), - ), - _buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), - _buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), - _buildReminderTile(context, 'Третье напоминание', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), - ], + const SizedBox(height: 8), + _buildReminderTile(context, '№1 Напоминание о приеме пищи', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), + const SizedBox(height: 8), + _buildReminderTile(context, '№2 Напоминание о приеме пищи', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), + const SizedBox(height: 8), + _buildReminderTile(context, '№3 Напоминание о приеме пищи', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), + ], + ), ); } @@ -147,41 +153,58 @@ class NotificationsSettingsContent extends StatelessWidget { } Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { - return _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: reminder.enabled - ? () { - _showTimePicker(context, reminder, reminderType, cubit); - } - : null, - title: title, - subtitle: reminder.enabled - ? Row( - children: [ - Icon(Icons.access_time, size: 16, color: appTheme.blueA400), - const SizedBox(width: 8), - Text( - '${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}', - style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500), - ), - const SizedBox(width: 4), - const Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), - ], - ) - : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.dividerColor), + child: Column( children: [ - CupertinoSwitch( - value: reminder.enabled, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - _updateReminderSetting(cubit, reminderType, { - 'enabled': newValue, - 'local_time_hours': reminder.localTimeHours, - 'local_time_mins': reminder.localTimeMins, - }); - }, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + reminderType == 'third_reminder' ? SvgPicture.asset('assets/icons/retire.svg') : SvgPicture.asset('assets/icons/wakeup.svg'), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: GestureDetector( + onTap: reminder.enabled + ? () { + _showTimePicker(context, reminder, reminderType, cubit); + } + : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration(color: reminder.enabled ? Colors.white : Colors.white.withAlpha((255 * 0.7).toInt()), borderRadius: BorderRadius.circular(48)), + child: Center( + child: Text( + '${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}', + style: TextStyle(fontSize: 17, color: appTheme.gray, fontWeight: FontWeight.w500), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + CupertinoSwitch( + value: reminder.enabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateReminderSetting(cubit, reminderType, { + 'enabled': newValue, + 'local_time_hours': reminder.localTimeHours, + 'local_time_mins': reminder.localTimeMins, + }); + }, + ), + ], ), ], ), diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 373b48eb..6403bc49 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -511,6 +511,7 @@ class MessageLookup extends MessageLookupByLibrary { "not_liked_groceries": MessageLookupByLibrary.simpleMessage( "Не любимые продукты", ), + "notifications": MessageLookupByLibrary.simpleMessage("Уведомления"), "ok": MessageLookupByLibrary.simpleMessage("Хорошо"), "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "option": MessageLookupByLibrary.simpleMessage("Опция"), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 82821c9a..b7db0a45 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -241,6 +241,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "notifications": MessageLookupByLibrary.simpleMessage("Уведомления"), "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "option": MessageLookupByLibrary.simpleMessage("Опция"), "option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 25d71d68..105b40c7 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,10 +28,9 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = - (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; @@ -3469,6 +3468,16 @@ class S { args: [], ); } + + /// `Уведомления` + String get notifications { + return Intl.message( + 'Уведомления', + name: 'notifications', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 7424fe87..c6898871 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -411,6 +411,7 @@ "itCantBeRecovered": "Его нельзя будет восстановить", "delete": "Удалить", "cannotDeleteAccount": "Не получилось удалить ваш аккаунт", - "resend": "Отправить повторно" + "resend": "Отправить повторно", + "notifications": "Уведомления" } diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 221d36ff..b52f418f 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -213,5 +213,6 @@ "my_plans": "Мои планы", "from": "от", "user_data": "Данные пользователя", - "infoAboutOrder": "Информация о заказе" + "infoAboutOrder": "Информация о заказе", + "notifications": "Уведомления" } From 0a5d78be1f1af1e646ed108c2791d582d9635564 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 10 Sep 2025 16:45:22 +0300 Subject: [PATCH 27/34] fix: ver upd --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7d740632..198e6f7b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5cba2fb5..b05e5fac 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 71 + 72 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index ae850a35..acf68969 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.11+71 +version: 1.2.12+72 publish_to: 'none' environment: From 5c911707a3e59fa31da645e49bbe6cb882483bd7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 10 Sep 2025 17:36:46 +0300 Subject: [PATCH 28/34] fix(comments): Issue-548 --- .../comments_actions_bottom_sheet.dart | 116 +++++++----------- .../presentation/widgets/comments_widget.dart | 52 ++------ lib/generated/intl/messages_en.dart | 3 + lib/generated/intl/messages_ru.dart | 3 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ru.arb | 1 + 7 files changed, 68 insertions(+), 118 deletions(-) diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index 3c1c3859..c4f3e540 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -18,25 +18,23 @@ void showCommentsActionsBottomSheet( required bool isMyComment, }) { showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => Container( - margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: appTheme.whiteA700, - ), - child: CommentsActionsBottomSheet( - isMyComment: isMyComment, - reply: reply, - copy: copy, - edit: edit, - delete: delete, - complain: complain, - commentText: commentText, - ), - )); + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700), + child: CommentsActionsBottomSheet( + isMyComment: isMyComment, + reply: reply, + copy: copy, + edit: edit, + delete: delete, + complain: complain, + commentText: commentText, + ), + ), + ); } class CommentsActionsBottomSheet extends StatefulWidget { @@ -82,44 +80,33 @@ class _CommentsActionsBottomSheetState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Row(children: [ - Expanded( - child: CustomTextFormField( - autofocus: true, - controller: controller, - onChanged: (value) => setState(() {}), + Row( + children: [ + Expanded( + child: CustomTextFormField(autofocus: true, controller: controller, onChanged: (value) => setState(() {})), ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - widget.edit(controller.text); - GoRouter.of(context).pop(); - }, - child: Container( - decoration: BoxDecoration( - color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, - shape: BoxShape.circle, - ), - height: 32, - width: 32, - child: Icon( - Icons.send_rounded, - size: 18, - color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black, + const SizedBox(width: 8), + GestureDetector( + onTap: () { + widget.edit(controller.text); + GoRouter.of(context).pop(); + }, + child: Container( + decoration: BoxDecoration(color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle), + height: 32, + width: 32, + child: Icon(Icons.send_rounded, size: 18, color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black), ), ), - ) - ]) + ], + ), ], ), ) : Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 7, - ), + const SizedBox(height: 7), const BottomSheetHandlerBar(), if (widget.isMyComment) BottomSheetActionWidget( @@ -131,43 +118,22 @@ class _CommentsActionsBottomSheetState extends State }), ) else if (widget.reply != null) - BottomSheetActionWidget( - text: s.reply, - iconPath: 'assets/icons/reply.svg', - onTap: widget.reply!, - ), + BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!), if (widget.reply != null) const CustomDivider(), - BottomSheetActionWidget( - text: s.copy, - iconPath: 'assets/icons/copy2.svg', - onTap: widget.copy, - ), + BottomSheetActionWidget(text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy), if (widget.isMyComment) ...[ if (widget.reply != null) ...[ const CustomDivider(), - BottomSheetActionWidget( - text: s.reply, - iconPath: 'assets/icons/reply.svg', - onTap: widget.reply!, - ), + BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!), ], const CustomDivider(), - BottomSheetActionWidget( - text: s.deleteComment, - iconPath: 'assets/icons/red_trash.svg', - onTap: widget.delete, - ), + BottomSheetActionWidget(text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete), ] else ...[ const CustomDivider(), - BottomSheetActionWidget( - text: s.complain, - iconPath: 'assets/icons/complain.svg', - onTap: widget.complain, - ), - ] + BottomSheetActionWidget(text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain), + ], ], ), ); } } - diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index e36bdf52..3617fe33 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -25,13 +25,7 @@ class CommentsWidget extends StatefulWidget { final CommentModel comment; final int? replyId; - const CommentsWidget({ - super.key, - required this.id, - required this.commentSourceType, - required this.comment, - this.replyId, - }); + const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId}); @override State createState() => _CommentsWidgetState(); @@ -47,10 +41,7 @@ class _CommentsWidgetState extends State { _comment(s, widget.comment.data[i]), if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8), for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++) - Padding( - padding: const EdgeInsets.only(left: 40), - child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true), - ), + Padding(padding: const EdgeInsets.only(left: 40), child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true)), const SizedBox(height: 8), ], const SizedBox(height: 48), @@ -72,7 +63,7 @@ class _CommentsWidgetState extends State { : null, copy: () { Clipboard.setData(ClipboardData(text: comment.body)); - showInfoToast(s.link_was_copied); + showInfoToast(s.text_was_copied); }, edit: (String text) { context.read().updateComment(id: comment.id, body: text); @@ -84,10 +75,7 @@ class _CommentsWidgetState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (context) => ComplainingBottomSheet( - complainedContentType: ComplainedContent.comment, - id: comment.id, - ), + builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.comment, id: comment.id), ); }, isMyComment: isMe, @@ -117,9 +105,7 @@ class _CommentsWidgetState extends State { ); }, child: CircleAvatar( - foregroundImage: NetworkImage( - comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, - ), + foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), @@ -131,24 +117,14 @@ class _CommentsWidgetState extends State { children: [ Text( comment.authorName, - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 4), SizedBox( width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8, child: Text( comment.body, - style: TextStyle( - height: 1, - color: appTheme.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: TextStyle(height: 1, color: appTheme.black, fontSize: 16, fontWeight: FontWeight.w500), ), ), const SizedBox(height: 4), @@ -156,12 +132,7 @@ class _CommentsWidgetState extends State { children: [ Text( comment.createdAt.format('dd MMMM', 'ru'), - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(width: 16), if (!isChild) @@ -171,12 +142,7 @@ class _CommentsWidgetState extends State { }, child: Text( s.answer, - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w600), ), ), ], diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 6403bc49..c054a4b5 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -694,6 +694,9 @@ class MessageLookup extends MessageLookupByLibrary { "tell_your_schedule": MessageLookupByLibrary.simpleMessage( "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:", ), + "text_was_copied": MessageLookupByLibrary.simpleMessage( + "Text copied successfully", + ), "there_is_a_video": MessageLookupByLibrary.simpleMessage("Есть видео"), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), "time": MessageLookupByLibrary.simpleMessage("Время прохождения"), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index b7db0a45..097e1180 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -326,6 +326,9 @@ class MessageLookup extends MessageLookupByLibrary { "tell_your_schedule": MessageLookupByLibrary.simpleMessage( "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:", ), + "text_was_copied": MessageLookupByLibrary.simpleMessage( + "Текст успешно скопирован", + ), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 105b40c7..85f74918 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1054,6 +1054,16 @@ class S { ); } + /// `Text copied successfully` + String get text_was_copied { + return Intl.message( + 'Text copied successfully', + name: 'text_was_copied', + desc: '', + args: [], + ); + } + /// `Почта успешно скопирована` String get mail_was_copied { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c6898871..57145fb9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -129,6 +129,7 @@ "carbohydrates": "Углеводы", "answer": "Ответить", "link_was_copied": "Ссылка успешно скопирована", + "text_was_copied": "Text copied successfully", "mail_was_copied": "Почта успешно скопирована", "choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "continue_": "Продолжить", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b52f418f..e5d6f7c9 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -129,6 +129,7 @@ "carbohydrates": "Углеводы", "answer": "Ответить", "link_was_copied": "Ссылка успешно скопирована", + "text_was_copied": "Текст успешно скопирован", "mail_was_copied": "Почта успешно скопирована", "choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "continue_": "Продолжить", From f3dbbf35377e236deeae6b3d7733321e68075cd8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 19:51:16 +0300 Subject: [PATCH 29/34] fix(settings): Issue-236 --- .../presentation/pages/settings_screen.dart | 55 +------------------ 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 1c304f29..41066d54 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -58,8 +58,8 @@ class _SettingsScreenState extends State { children: [ _additionalInfo(state), const SizedBox(height: 12), - // _notifications(state), - // const SizedBox(height: 12), + _notifications(state), + const SizedBox(height: 12), _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -147,63 +147,14 @@ class _SettingsScreenState extends State { ); } - // ignore: unused_element Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), + child: Text('При использовании дневника', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), ), - _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: () { - setState(() { - updatesNotificationsSwitchState = !updatesNotificationsSwitchState; - }); - settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState)); - }, - title: 'Уведомление о новых обновлениях', - trailing: CupertinoSwitch( - value: updatesNotificationsSwitchState, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - setState(() { - updatesNotificationsSwitchState = newValue; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState), - ); - }, - ), - ), - _divider(context), - _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: () { - setState(() { - fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), - ); - }, - title: 'Напоминание о заполнении дневника', - trailing: CupertinoSwitch( - value: fillDiaryNotificationsSwitchState, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - setState(() { - fillDiaryNotificationsSwitchState = newValue; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), - ); - }, - ), - ), - _divider(context), _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { From b840c936bf9dc52f11600d1db7d177c9dcfcc3a8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 20:02:48 +0300 Subject: [PATCH 30/34] fix(finances): Issue-332 --- .../presentation/user_purchases_screen.dart | 88 +++++-------------- 1 file changed, 24 insertions(+), 64 deletions(-) diff --git a/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart b/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart index 6f137211..67d4652f 100644 --- a/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart +++ b/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart @@ -33,21 +33,12 @@ class _UserPurchasesScreen extends State { return Scaffold( backgroundColor: const Color(0xFFF0F2F5), appBar: AppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.vertical( - bottom: Radius.circular(12), - ), - ), - title: Text( - 'Финансы', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))), + title: Text('Финансы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), centerTitle: true, ), body: state.isLoading - ? const Center( - child: Loader(), - ) + ? const Center(child: Loader()) : SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( @@ -67,10 +58,7 @@ class _UserPurchasesScreen extends State { .map( (value) => DropdownMenuItem( value: value, - child: Text( - value.name, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), + child: Text(value.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), ) .toList(), @@ -105,60 +93,41 @@ class _UserPurchasesScreen extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Builder(builder: (context) { - final idLen = state.purchasesModels[index].bankId.length; - return Text( - '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ); - }), + Builder( + builder: (context) { + final idLen = state.purchasesModels[index].bankId.length; + return Text( + '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ); + }, + ), Text( - state.purchasesModels[index].startDate.format('yyyy.MM.dd'), - style: const TextStyle( - color: Color(0xFF6D6F71), - height: 1, - fontWeight: FontWeight.w500, - ), + state.purchasesModels[index].startDate.format('dd.MM.yyyy'), + style: const TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 8), Text( state.purchasesModels[index].title, - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Продавец: ${state.purchasesModels[index].consultantName}', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Text( '${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Container( height: 1, width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFDEDEDF), - ), + decoration: const BoxDecoration(color: Color(0xFFDEDEDF)), ), const SizedBox(height: 8), Row( @@ -166,16 +135,9 @@ class _UserPurchasesScreen extends State { children: [ Text( '${state.purchasesModels[index].paymentPrice} ₽', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - StatusWidget( - status: purchaseStatus, + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), + StatusWidget(status: purchaseStatus), ], ), ], @@ -189,10 +151,7 @@ class _UserPurchasesScreen extends State { else const Padding( padding: EdgeInsets.symmetric(vertical: 40.0), - child: Text( - 'У вас нет таких консультаций', - style: TextStyles.textMMedium, - ), + child: Text('У вас нет таких консультаций', style: TextStyles.textMMedium), ), ], ), @@ -200,7 +159,8 @@ class _UserPurchasesScreen extends State { ), ), ], - )), + ), + ), ); }, ), From 74b5c6732d0cd85d0a4690fe466f8e93a0b1837c Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 20:11:37 +0300 Subject: [PATCH 31/34] fix(settings): Issue-256 --- lib/core/locator_service.dart | 2 +- .../bloc/settings_cubit/settings_cubit.dart | 47 +++++++++++++------ .../bloc/settings_cubit/settings_state.dart | 2 + .../presentation/pages/settings_screen.dart | 39 ++++++++++++++- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 971ebb2c..e4a13866 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -137,7 +137,7 @@ Future init() async { sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); - sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); + sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory( () => DiaryCubit( diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart index ae498824..2a64e2e9 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -1,5 +1,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; @@ -11,16 +13,20 @@ class SettingsCubit extends Cubit { final ProfileRepository profileRepository; final SettingsRepository settingsRepository; final ConsultantRepository consultantRepository; + final ConsultationsRepository consultationsRepository; SettingsCubit({ - required this.profileRepository, - required this.settingsRepository, + required this.profileRepository, + required this.settingsRepository, required this.consultantRepository, + required this.consultationsRepository, }) : super(const SettingsState.loading()); Future init() async { final profile = await profileRepository.getMyProfile(); late final ConsultationRequestModel? consultantInfo; + late final bool hasActiveConsultation; + try { if (profile.phoneConfirmationState.contains('confirmed')) { consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); @@ -30,6 +36,13 @@ class SettingsCubit extends Cubit { } catch (e) { consultantInfo = null; } + + try { + hasActiveConsultation = await _checkActiveConsultation(); + } catch (e) { + hasActiveConsultation = false; + } + try { emit( SettingsState.loaded( @@ -37,6 +50,7 @@ class SettingsCubit extends Cubit { profileDetails: await profileRepository.getMyProfileDetails(), profile: profile, consultationRequestModel: consultantInfo, + hasActiveConsultation: hasActiveConsultation, ), ); } catch (e) { @@ -44,13 +58,9 @@ class SettingsCubit extends Cubit { } } - Future updateProfileDetails({ - required ProfileDetailsModel profileDetailsModel, - }) async { + Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async { try { - await settingsRepository.updateProfileDetails( - profileDetailsModel: profileDetailsModel, - ); + await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel); } catch (e) { rethrow; } finally { @@ -58,13 +68,9 @@ class SettingsCubit extends Cubit { } } - Future updateProfileSettings({ - required ProfileSettingsModel profileSettingsModel, - }) async { + Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async { try { - await settingsRepository.updateProfileSettings( - profileSettingsModel: profileSettingsModel, - ); + await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel); } catch (e) { rethrow; } finally { @@ -80,4 +86,17 @@ class SettingsCubit extends Cubit { return false; } } + + Future _checkActiveConsultation() async { + try { + final userPurchases = await consultationsRepository.getUserPurchases(null, null); + final activeStates = ChosenPlanStateEnum.getActiveStates(); + + return userPurchases.any((purchase) => + activeStates.contains(purchase.chosenPlanState) + ); + } catch (e) { + return false; + } + } } diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart index f220fe5e..4b232166 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/models/profile_details_model.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -16,6 +17,7 @@ abstract class SettingsState with _$SettingsState { required ProfileDetailsModel profileDetails, required ProfileModel profile, ConsultationRequestModel? consultationRequestModel, + bool hasActiveConsultation, }) = SettingsStateLoaded; const factory SettingsState.error({ diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 41066d54..3e82fab3 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -32,6 +32,24 @@ class _SettingsScreenState extends State { late bool bedTimeSwitchState; late bool moodSwitchState; + void _showConsultationWarning() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Предупреждение'), + content: const Text('Во время консультации этот параметр нельзя отключить'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Понятно'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -158,16 +176,24 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { + if (state.hasActiveConsultation && !moodSwitchState) { + _showConsultationWarning(); + return; + } setState(() { moodSwitchState = !moodSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, - title: 'Указывать “настроение” в заполнение дневника', + title: 'Указывать "настроение" в заполнение дневника', trailing: CupertinoSwitch( value: moodSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { + if (state.hasActiveConsultation && !newValue) { + _showConsultationWarning(); + return; + } setState(() { moodSwitchState = newValue; }); @@ -179,16 +205,25 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { + + if (state.hasActiveConsultation && !bedTimeSwitchState) { + _showConsultationWarning(); + return; + } setState(() { bedTimeSwitchState = !bedTimeSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, - title: 'Указывать “отход ко сну” в заполнение дневника', + title: 'Указывать "отход ко сну" в заполнение дневника', trailing: CupertinoSwitch( value: bedTimeSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { + if (state.hasActiveConsultation && !newValue) { + _showConsultationWarning(); + return; + } setState(() { bedTimeSwitchState = newValue; }); From 8c348f61b1871038440ace13ff61c8614cf62e8a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 21:35:28 +0300 Subject: [PATCH 32/34] fix(settings): Issue-256 --- .../bloc/settings_cubit/settings_state.dart | 3 +-- .../presentation/pages/settings_screen.dart | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart index 4b232166..86471f31 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; -import 'package:eatvio_app/features/profile/models/profile_details_model.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -17,7 +16,7 @@ abstract class SettingsState with _$SettingsState { required ProfileDetailsModel profileDetails, required ProfileModel profile, ConsultationRequestModel? consultationRequestModel, - bool hasActiveConsultation, + required bool hasActiveConsultation, }) = SettingsStateLoaded; const factory SettingsState.error({ diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 3e82fab3..ad42a7b4 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -176,7 +176,7 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { - if (state.hasActiveConsultation && !moodSwitchState) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } @@ -187,10 +187,10 @@ class _SettingsScreenState extends State { }, title: 'Указывать "настроение" в заполнение дневника', trailing: CupertinoSwitch( - value: moodSwitchState, + value: state.hasActiveConsultation ? true : moodSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { - if (state.hasActiveConsultation && !newValue) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } @@ -205,8 +205,7 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { - - if (state.hasActiveConsultation && !bedTimeSwitchState) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } @@ -217,10 +216,10 @@ class _SettingsScreenState extends State { }, title: 'Указывать "отход ко сну" в заполнение дневника', trailing: CupertinoSwitch( - value: bedTimeSwitchState, + value: state.hasActiveConsultation ? true : bedTimeSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { - if (state.hasActiveConsultation && !newValue) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } From 4bc8cba9413a4397118316373147ca10a59fae97 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 12 Sep 2025 13:34:16 +0300 Subject: [PATCH 33/34] fix(profile): Issue-416 --- .../presentation/pages/profile_screen.dart | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 990beed1..cc8f7245 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; @@ -93,7 +94,7 @@ class _ProfileScreenState extends State { } isFirstFetch = false; } - + // Проверяем, заблокировал ли пользователь меня if (state.profile.isBlockedByUser) { return Scaffold( @@ -104,20 +105,12 @@ class _ProfileScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.block, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.block, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Пользователь ограничил доступ к своей странице', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]), ), ], ), @@ -125,7 +118,7 @@ class _ProfileScreenState extends State { ), ); } - + return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -211,6 +204,14 @@ class _ProfileScreenState extends State { if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state), + if (state.blogPosts.isNotEmpty) + const Padding( + padding: EdgeInsets.only(left: 8, top: 16, bottom: 8), + child: Text( + 'Блог', + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), + ), + ), _blogPosts(state), ], ), @@ -277,9 +278,9 @@ class _ProfileScreenState extends State { _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.roles.contains('consultant')) _socialLinks(state), + Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant')) _education(state), if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), - Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), _newBlogPostButton(), _blogPosts(state), @@ -512,6 +513,7 @@ class _ProfileScreenState extends State { children: [ ListView.separated( shrinkWrap: true, + padding: const EdgeInsets.all(0), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _educationBlock( state.consultantInfo!.educationModel.completedCourses![index].values.first.title, @@ -766,7 +768,7 @@ class _ProfileScreenState extends State { ListView _blogPosts(ProfileStateLoaded state) { return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.only(bottom: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), From 0fce3f9d30f1b4d8eeeee68fa214136463d461a1 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 12 Sep 2025 13:38:50 +0300 Subject: [PATCH 34/34] fix(diary): Issue-675 --- .../diary/presentation/widgets/meal_summary_widget.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/features/diary/presentation/widgets/meal_summary_widget.dart b/lib/features/diary/presentation/widgets/meal_summary_widget.dart index dd7108e6..f4537b79 100644 --- a/lib/features/diary/presentation/widgets/meal_summary_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_summary_widget.dart @@ -82,6 +82,11 @@ class MealSummaryWidget extends StatelessWidget { continue; } + // Для продуктов из диеты учитываем только съеденные (с галочкой) + if (portion.fromDiet && portion.portionState != PortionState.eatenFromDiet) { + continue; + } + final caloriesInfo = portion.productCaloriesInfo; if (caloriesInfo == null) { continue;