feat(plans): plan creating has been completed

This commit is contained in:
Username1548
2025-04-29 14:44:33 +07:00
parent c8b828bfe1
commit 0fcb2c1a0d
43 changed files with 1602 additions and 635 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
assets/icons/star.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="32" height="29" viewBox="0 0 32 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.8652 10.0557C19.133 10.8796 19.9012 11.4374 20.7676 11.4375H30.041L22.5391 16.8887C21.8818 17.3662 21.5825 18.1896 21.7686 18.9697L21.8125 19.125L24.6777 27.9443L17.1758 22.4932C16.5186 22.0157 15.6434 21.9866 14.959 22.4043L14.8242 22.4932L7.32227 27.9443L10.1875 19.125C10.4385 18.3524 10.1967 17.5101 9.58789 16.9883L9.46094 16.8887L1.95898 11.4375H11.2324C12.0446 11.4374 12.77 10.9472 13.0781 10.207L13.1348 10.0557L16 1.23633L18.8652 10.0557Z" stroke="#2F80ED" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -136,7 +136,10 @@ class AppRouter {
GoRoute( GoRoute(
path: CardSettingsScreen.path, path: CardSettingsScreen.path,
name: CardSettingsScreen.name, name: CardSettingsScreen.name,
builder: (context, state) => const CardSettingsScreen(), builder: (context, state) {
final chosenPlanId = state.extra as int?;
return CardSettingsScreen(chosenPlanId: chosenPlanId);
},
), ),
GoRoute( GoRoute(
path: '/about-plan-screen', path: '/about-plan-screen',
@@ -182,7 +185,8 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final chatId = data['chatId'] as String; final chatId = data['chatId'] as String;
return ChatScreen(chatId: chatId); final sendInitialMessage = data['sendInitialMessage'] as bool;
return ChatScreen(chatId: chatId, sendInitialMessage: sendInitialMessage,);
}, },
), ),
GoRoute( GoRoute(

View File

@@ -19,6 +19,8 @@ import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_c
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart';
@@ -117,7 +119,7 @@ Future<void> init() async {
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl())); sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()));
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit()); sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl())); sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl())); sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl())); sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl()));
@@ -158,6 +160,7 @@ Future<void> init() async {
sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl())); sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl()));
sl.registerLazySingleton<ConsultationsRepository>(() => ConsultationsRepository(consultationService: sl())); sl.registerLazySingleton<ConsultationsRepository>(() => ConsultationsRepository(consultationService: sl()));
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl())); sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
// Services // Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl())); sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -184,6 +187,7 @@ Future<void> init() async {
sl.registerLazySingleton<CardsService>(() => CardsService(sl())); sl.registerLazySingleton<CardsService>(() => CardsService(sl()));
sl.registerLazySingleton<ConsultationService>(() => ConsultationService(sl())); sl.registerLazySingleton<ConsultationService>(() => ConsultationService(sl()));
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl())); sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
// External // External
sl.registerLazySingleton( sl.registerLazySingleton(

View File

@@ -147,6 +147,8 @@ class PrimaryColors {
Color get greenA70001 => const Color(0XFF00BE13); Color get greenA70001 => const Color(0XFF00BE13);
Color get green => const Color(0XFF0BB450);
// LightGreen // LightGreen
Color get lightGreen300 => const Color(0XFFB2DF8A); Color get lightGreen300 => const Color(0XFFB2DF8A);

View File

@@ -99,7 +99,7 @@ class CustomTextFormField extends StatelessWidget {
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
autofocus: autofocus, autofocus: autofocus,
style: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w500), style: textStyle ?? const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w500),
obscureText: obscureText!, obscureText: obscureText!,
textInputAction: textInputAction, textInputAction: textInputAction,
maxLines: maxLines, maxLines: maxLines,
@@ -115,20 +115,17 @@ class CustomTextFormField extends StatelessWidget {
prefixIconConstraints: prefixConstraints, prefixIconConstraints: prefixConstraints,
suffixIcon: suffix, suffixIcon: suffix,
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
suffixStyle: textStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), suffixStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)),
suffixIconConstraints: suffixConstraints, suffixIconConstraints: suffixConstraints,
isDense: false, isDense: false,
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
fillColor: const Color(0xFFF6F6F6), fillColor: const Color(0xFFF6F6F6),
filled: true, filled: true,
border: border: borderDecoration ??
borderDecoration ??
OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none), OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none),
enabledBorder: enabledBorder: borderDecoration ??
borderDecoration ??
OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none), OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none),
focusedBorder: focusedBorder: borderDecoration ??
borderDecoration ??
OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none), OutlineInputBorder(borderRadius: BorderRadius.circular(20), borderSide: hasError ? BorderSide(color: appTheme.red, width: 1) : BorderSide.none),
), ),
keyboardType: keyboardType, keyboardType: keyboardType,

View File

@@ -7,9 +7,10 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class SuccessPublishedDietBottomSheet extends StatelessWidget { class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
const SuccessPublishedDietBottomSheet({super.key, required this.title}); const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false});
final String title; final String title;
final bool isPlan;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
@@ -27,7 +28,7 @@ class SuccessPublishedDietBottomSheet extends StatelessWidget {
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
), ),
Text( Text(
'опубликованна', isPlan ? 'опубликован' : 'опубликованна',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
), ),
const SizedBox( const SizedBox(

View File

@@ -72,7 +72,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
} }
initChat(String chatId) async { initChat(String chatId, {bool sendInitialMessage = false}) async {
try { try {
final chatRoom = await chatRepository.getChatRoom(chatId); final chatRoom = await chatRepository.getChatRoom(chatId);
ChosenPlanModel? chosenPlan; ChosenPlanModel? chosenPlan;
@@ -92,10 +92,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
hasCard: false, hasCard: false,
), ),
); );
if (chatRoom.chosenPlan?.consultantId == chatRoom.users.receiver.id) { if (chatRoom.chosenPlan?.consultantId == chatRoom.users.receiver.id) {
await checkCards(); await checkCards();
} }
if (sendInitialMessage) {
await chatRepository.sendMessage(chatId: chatId, data: 'Здравствуйте, хочу заказать у вас: ${chosenPlan?.plan.name ?? 'план'}');
}
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
typingSubscription?.cancel(); typingSubscription?.cancel();

View File

@@ -15,6 +15,7 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
@@ -28,7 +29,8 @@ import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
final String chatId; final String chatId;
const ChatScreen({required this.chatId, super.key}); final bool sendInitialMessage;
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
@override @override
State<ChatScreen> createState() => _ChatScreenState(); State<ChatScreen> createState() => _ChatScreenState();
@@ -46,7 +48,7 @@ class _ChatScreenState extends State<ChatScreen> {
void initState() { void initState() {
super.initState(); super.initState();
scrollController.addListener(_onScroll); scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.chatId); chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage);
} }
_onScroll() { _onScroll() {
@@ -96,13 +98,24 @@ class _ChatScreenState extends State<ChatScreen> {
BannerType? bannerType; BannerType? bannerType;
if (chosenPlan == null) { if (chosenPlan == null) {
bannerType = null; bannerType = null;
} else if (amIConsultant && state.hasCard) { } else if (!amIConsultant && !state.hasCard) {
bannerType = BannerType.card; bannerType = BannerType.card;
} else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) { } else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
bannerType = BannerType.date; bannerType = BannerType.date;
} else if (!amIConsultant && } else if (!amIConsultant &&
(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) { (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) {
bannerType = BannerType.payment; bannerType = BannerType.payment;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone) {
bannerType = BannerType.collectCharge;
}
PacketOptions? chosenOption;
if (chosenPlan?.optionType == 'options1') {
chosenOption = chosenPlan?.plan.packet1Options;
} else if (chosenPlan?.optionType == 'options2') {
chosenOption = chosenPlan?.plan.packet2Options;
} else if (chosenPlan?.optionType == 'options3') {
chosenOption = chosenPlan?.plan.packet3Options;
} }
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
@@ -262,7 +275,7 @@ class _ChatScreenState extends State<ChatScreen> {
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0), itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (showUserDetailsButton) ...[ if (!showUserDetailsButton) ...[
// OrderCard( // OrderCard(
// plan: chosenPlan.plan, // plan: chosenPlan.plan,
// isCustomerView: !amIConsultant, // isCustomerView: !amIConsultant,
@@ -288,7 +301,15 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
), ),
if (bannerType != null) if (bannerType != null)
Padding(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: NotificationBanner(bannerType: bannerType)), Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: NotificationBanner(
bannerType: bannerType,
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
),
),
], ],
], ],
), ),

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
@@ -13,8 +14,11 @@ import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class NotificationBanner extends StatefulWidget { class NotificationBanner extends StatefulWidget {
const NotificationBanner({super.key, required this.bannerType}); const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId});
final BannerType bannerType; final BannerType bannerType;
final double? planPrice;
final String? planName;
final int? chosenPlanId;
@override @override
State<NotificationBanner> createState() => _NotificationBannerState(); State<NotificationBanner> createState() => _NotificationBannerState();
@@ -31,96 +35,145 @@ class _NotificationBannerState extends State<NotificationBanner> {
color: appTheme.gray50, color: appTheme.gray50,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only(top: 12, right: 8, left: 12), crossAxisAlignment: CrossAxisAlignment.start,
child: SvgPicture.asset('assets/icons/reversed_exclamation_mark.svg', height: 68, width: 68), children: [
if (widget.bannerType != BannerType.collectCharge)
Padding(
padding: const EdgeInsets.only(top: 12, right: 8, left: 12),
child: SvgPicture.asset('assets/icons/reversed_exclamation_mark.svg', height: 68, width: 68),
)
else
const Padding(
padding: EdgeInsets.only(top: 12, right: 8, left: 12),
child: _StarIcon(),
),
if (isLoading)
const Padding(
padding: EdgeInsets.only(left: 40, top: 24),
child: Loader(),
)
else
Padding(
padding: const EdgeInsets.only(top: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: switch (widget.bannerType) {
BannerType.date => [
SizedBox(
width: size.width * 0.6,
child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
final cubit = context.read<ChatRoomCubit>();
showModalBottomSheet(
context: context,
builder: (context) => ChosePlanDateWidget(
chatCubit: cubit,
),
);
},
child: Text(s.choose_date, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
BannerType.card => [
SizedBox(
width: size.width * 0.6,
child: Text(s.can_not_choose_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
GoRouter.of(context).pushReplacement(
CardSettingsScreen.path,
);
},
child: Text(s.add_card, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
BannerType.payment => [
SizedBox(
width: size.width * 0.6,
child: Text(s.tap_on_link_to_pay, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () async {
final planId = (context.read<ChatRoomCubit>().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
setState(() {
isLoading = true;
});
final paymentLink = await context.read<ChatRoomCubit>().getPaymentLink();
setState(() {
isLoading = false;
});
if (paymentLink != null && planId != null) {
if (!context.mounted) return;
GoRouter.of(context).pushReplacement(
PaymentScreen.path,
extra: {'url': paymentLink, 'planId': planId},
);
}
},
child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
BannerType.collectCharge => [
SizedBox(
width: size.width * 0.6,
child:
Text('${s.congratsYouHaveFinishedPlan} «${widget.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 8),
SizedBox(
width: size.width * 0.6,
child: Text('${s.userConfirmedPlanCompletion} ${widget.planPrice}₽', style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 8),
],
},
),
),
],
), ),
if (isLoading) if (widget.bannerType == BannerType.collectCharge)
const Padding(
padding: EdgeInsets.only(left: 40, top: 24),
child: Loader(),
)
else
Padding( Padding(
padding: const EdgeInsets.only(top: 16), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column( child: CustomOutlinedButton(
mainAxisSize: MainAxisSize.min, text: s.collectCharge,
crossAxisAlignment: CrossAxisAlignment.start, height: 52,
children: switch (widget.bannerType) { buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
BannerType.date => [ buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
SizedBox( onPressed: () async {
width: size.width * 0.6, GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.chosenPlanId);
child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
final cubit = context.read<ChatRoomCubit>();
showModalBottomSheet(
context: context,
builder: (context) => ChosePlanDateWidget(
chatCubit: cubit,
),
);
},
child: Text(s.choose_date, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
BannerType.card => [
SizedBox(
width: size.width * 0.6,
child: Text(s.can_not_choose_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
GoRouter.of(context).pushReplacement(
CardSettingsScreen.path,
);
},
child: Text(s.add_card, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
BannerType.payment => [
SizedBox(
width: size.width * 0.6,
child: Text(s.tap_on_link_to_pay, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () async {
final planId = (context.read<ChatRoomCubit>().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
setState(() {
isLoading = true;
});
final paymentLink = await context.read<ChatRoomCubit>().getPaymentLink();
setState(() {
isLoading = false;
});
if (paymentLink != null && planId != null) {
if (!context.mounted) return;
GoRouter.of(context).pushReplacement(
PaymentScreen.path,
extra: {'url': paymentLink, 'planId': planId},
);
}
},
child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
}, },
), ),
), )
], ],
), ),
); );
} }
} }
enum BannerType { date, card, payment } class _StarIcon extends StatelessWidget {
const _StarIcon();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: appTheme.whiteA700, shape: BoxShape.circle),
child: SvgPicture.asset('assets/icons/star.svg'),
);
}
}
enum BannerType { date, card, payment, collectCharge }

View File

@@ -72,7 +72,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
showBottomSheetWithBlur( showBottomSheetWithBlur(
userRootNavigator: true, userRootNavigator: true,
context: context, context: context,
child: SuccessPublishedDietBottomSheet( child: SuccessPublishedDietOrPlanBottomSheet(
title: state.dietData.name ?? 'Диета', title: state.dietData.name ?? 'Диета',
), ),
); );
@@ -83,7 +83,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
context.pop(); context.pop();
showBottomSheetWithBlur( showBottomSheetWithBlur(
context: context, context: context,
child: SuccessPublishedDietBottomSheet( child: SuccessPublishedDietOrPlanBottomSheet(
title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
), ),
); );
@@ -193,8 +193,8 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
context.read<CreateDietCubit>().setDietState(DietState.draft); context.read<CreateDietCubit>().setDietState(DietState.draft);
if (widget.diet != null) { if (widget.diet != null) {
final result = await context.read<CreateDietCubit>().updateDiet( final result = await context.read<CreateDietCubit>().updateDiet(
widget.diet!.id, widget.diet!.id,
); );
if (result == 'error') { if (result == 'error') {
if (!context.mounted) return; if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}'); showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');

View File

@@ -15,8 +15,8 @@ class CreatePlanModel with _$CreatePlanModel {
const factory CreatePlanModel({ const factory CreatePlanModel({
String? name, String? name,
String? details, String? details,
String? state, @Default('active') String? state,
int? duration, int? duration,
required List<bool> type, required List<bool> type,
XFile? image, XFile? image,
required List<PacketOptions> options, required List<PacketOptions> options,
@@ -25,9 +25,12 @@ class CreatePlanModel with _$CreatePlanModel {
Future<Map<String, dynamic>> toCreationJson() async => { Future<Map<String, dynamic>> toCreationJson() async => {
'name': name, 'name': name,
'duration': duration, 'duration': duration,
'state': state, 'state': 'active',
'type': type,
'description_md': details, 'description_md': details,
'type[slim]': type[0] ? '1' : '0',
'type[fit]': type[1] ? '1' : '0',
'type[wellness]': type[2] ? '1' : '0',
'type[behavior]': type[3] ? '1' : '0',
'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image!.path))).path, filename: image!.name) : null, 'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image!.path))).path, filename: image!.name) : null,
}; };

View File

@@ -0,0 +1,11 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'plan_slug_model.freezed.dart';
part 'plan_slug_model.g.dart';
@freezed
class PlanSlugModel with _$PlanSlugModel {
factory PlanSlugModel({@JsonKey(name: 'slug') required String slug}) = _PlanSlugModel;
factory PlanSlugModel.fromJson(Map<String, dynamic> json) => _$PlanSlugModelFromJson(json);
}

View File

@@ -1,9 +1,13 @@
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/presentation/widgets/option_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/widgets/option_widget.dart';
@@ -15,6 +19,7 @@ import 'package:eatvio_app/features/create_plan/presentation/widgets/select_exac
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class CreatePlanScreen extends StatelessWidget { class CreatePlanScreen extends StatelessWidget {
static const name = 'createPlanScreen'; static const name = 'createPlanScreen';
@@ -36,177 +41,229 @@ class CreatePlanScreenContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return Scaffold( return BlocBuilder<CreatePlanCubit, CreatePlanState>(
backgroundColor: appTheme.gray100, buildWhen: (previous, current) => previous.planCreated != current.planCreated || previous.isLoading != current.isLoading,
appBar: AppBar( builder: (context, state) {
centerTitle: true, if (state.planCreated) {
title: Text( WidgetsBinding.instance.addPostFrameCallback((_) {
s.adding_plan, if (!context.mounted) return;
textAlign: TextAlign.center, context.pop();
overflow: TextOverflow.ellipsis, showBottomSheetWithBlur(
softWrap: true, userRootNavigator: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), context: context,
), child: SuccessPublishedDietOrPlanBottomSheet(
), title: state.planData.name ?? 'План',
body: Stack( isPlan: true,
children: [ ),
SingleChildScrollView( );
child: Column( });
crossAxisAlignment: CrossAxisAlignment.start, }
children: [ return Scaffold(
const SizedBox(height: 8), backgroundColor: appTheme.gray100,
MainBackgroundBouble( appBar: AppBar(
padding: const EdgeInsets.symmetric(horizontal: 16), centerTitle: true,
child: Column( title: Text(
crossAxisAlignment: CrossAxisAlignment.start, s.adding_plan,
children: [ textAlign: TextAlign.center,
const SizedBox(height: 16), overflow: TextOverflow.ellipsis,
Text( softWrap: true,
s.label, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.plan_label_desc,
onChanged: (value) {
context.read<CreatePlanCubit>().setName(value);
},
maxLength: 150,
),
const SizedBox(height: 12),
Text(
s.plan_duration,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
const PlanDurationDropDownWidget(),
const SizedBox(height: 20),
const PlanImagePickerWidget(),
const SizedBox(height: 16),
const PlanTypeChoiceWidget(),
const SizedBox(height: 16),
Text(
s.plan_desc,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
SizedBox(
height: 132,
child: CustomTextFormField(
textAlignVertical: TextAlignVertical.top,
maxLength: 1000,
maxLines: null,
minLines: null,
expands: true,
hintText: s.plan_desc_example,
onChanged: (value) {
context.read<CreatePlanCubit>().setDetails(value);
},
),
),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
s.services,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.blueGray400),
),
),
const SizedBox(height: 8),
const OptionsCountButton(),
const SizedBox(height: 20),
BlocBuilder<CreatePlanCubit, CreatePlanState>(
buildWhen: (previous, current) => previous.selectedOptionIndex != current.selectedOptionIndex,
builder: (context, state) {
if (state.selectedOptionIndex == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
'${s.option} ${(state.selectedOptionIndex ?? 0) + 1}',
style: TextStyles.textLSemibold.copyWith(color: appTheme.blueGray400),
),
);
},
),
const SizedBox(height: 8),
const OptionWidget(),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
CustomOutlinedButton(
onPressed: () {},
text: s.to_draft,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
),
),
const SizedBox(height: 12),
CustomOutlinedButton(
onPressed: () async {},
text: s.preview,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
),
),
const SizedBox(height: 12),
CustomOutlinedButton(
onPressed: () {},
text: s.publish,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
],
),
),
],
), ),
), ),
BlocBuilder<CreatePlanCubit, CreatePlanState>( body: Stack(
builder: (context, state) { children: [
if (state.selectedOptionIndex == null) { SingleChildScrollView(
return const SizedBox.shrink(); child: Column(
} crossAxisAlignment: CrossAxisAlignment.start,
return const Padding( children: [
padding: EdgeInsets.only(bottom: 24.0), const SizedBox(height: 8),
child: Align( MainBackgroundBouble(
alignment: Alignment.bottomCenter, padding: const EdgeInsets.symmetric(horizontal: 16),
child: SelectExactOptionWidget(), child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
s.label,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
BlocBuilder<CreatePlanCubit, CreatePlanState>(
buildWhen: (previous, current) => previous.validationState != current.validationState,
builder: (context, state) {
return Column(
children: [
CustomTextFormField(
hintText: s.plan_label_desc,
hasError: state.validationState.titleHasError,
onChanged: (value) {
context.read<CreatePlanCubit>().setName(value);
},
maxLength: 150,
),
if (state.validationState.titleHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
],
);
},
),
const SizedBox(height: 12),
Text(
s.plan_duration,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
const PlanDurationDropDownWidget(),
const SizedBox(height: 20),
const PlanImagePickerWidget(),
const SizedBox(height: 16),
const PlanTypeChoiceWidget(),
const SizedBox(height: 16),
Text(
s.plan_desc,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
BlocBuilder<CreatePlanCubit, CreatePlanState>(
buildWhen: (previous, current) => previous.validationState != current.validationState,
builder: (context, state) {
return Column(
children: [
SizedBox(
height: 132,
child: CustomTextFormField(
hasError: state.validationState.descriptionHasError,
textAlignVertical: TextAlignVertical.top,
maxLength: 1000,
maxLines: null,
minLines: null,
expands: true,
hintText: s.plan_desc_example,
onChanged: (value) {
context.read<CreatePlanCubit>().setDetails(value);
},
),
),
if (state.validationState.descriptionHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
],
);
},
),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
s.services,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.blueGray400),
),
),
const SizedBox(height: 8),
const OptionsCountButton(),
const SizedBox(height: 20),
BlocBuilder<CreatePlanCubit, CreatePlanState>(
buildWhen: (previous, current) => previous.selectedOptionIndex != current.selectedOptionIndex,
builder: (context, state) {
if (state.selectedOptionIndex == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
'${s.option} ${(state.selectedOptionIndex ?? 0) + 1}',
style: TextStyles.textLSemibold.copyWith(color: appTheme.blueGray400),
),
);
},
),
const SizedBox(height: 8),
const OptionWidget(),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// CustomOutlinedButton(
// onPressed: () {},
// text: s.to_draft,
// height: 52,
// buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
// buttonStyle: ButtonStyle(
// side: const WidgetStatePropertyAll(BorderSide.none),
// backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
// ),
// ),
// const SizedBox(height: 12),
// CustomOutlinedButton(
// onPressed: () async {},
// text: s.preview,
// height: 52,
// buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
// buttonStyle: ButtonStyle(
// side: const WidgetStatePropertyAll(BorderSide.none),
// backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
// ),
// ),
// const SizedBox(height: 12),
state.isLoading
? const Loader()
: CustomOutlinedButton(
onPressed: () async {
await context.read<CreatePlanCubit>().publishPlan();
},
text: s.publish,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
],
),
),
const SizedBox(height: 80),
],
), ),
); ),
}, BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) {
if (state.selectedOptionIndex == null) {
return const SizedBox.shrink();
}
return const Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Align(
alignment: Alignment.bottomCenter,
child: SelectExactOptionWidget(),
),
);
},
),
],
), ),
], );
), },
); );
} }
} }

View File

@@ -1,56 +1,65 @@
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
class CreatePlanCubit extends Cubit<CreatePlanState> { class CreatePlanCubit extends Cubit<CreatePlanState> {
CreatePlanCubit() : super(CreatePlanState.initial()); final CreatePlanRepository createPlanRepository;
CreatePlanCubit({required this.createPlanRepository}) : super(CreatePlanState.initial());
void setDetails(String text) { void setDetails(String text) {
emit(state.copyWith(planData: state.planData.copyWith(details: text))); emit(state.copyWith(planData: state.planData.copyWith(details: text)));
validateDetails();
} }
void setName(String name) { void setName(String name) {
emit(state.copyWith(planData: state.planData.copyWith(name: name))); emit(state.copyWith(planData: state.planData.copyWith(name: name)));
validateName();
} }
void setOptionTitle(String title) { void setOptionTitle(String title) {
final options = [...state.planData.options]; final options = [...state.planData.options];
options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(short: title); options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(short: title);
emit(state.copyWith(planData: state.planData.copyWith(options: options))); emit(state.copyWith(planData: state.planData.copyWith(options: options)));
validateSelectedOptionTitle();
} }
void setPrice(double price){ void setPrice(double price) {
final selectedOptionIndex = state.selectedOptionIndex ?? 0; final selectedOptionIndex = state.selectedOptionIndex ?? 0;
final options = [...state.planData.options]; final options = [...state.planData.options];
options[selectedOptionIndex] = options[selectedOptionIndex].copyWith(price: price); options[selectedOptionIndex] = options[selectedOptionIndex].copyWith(price: price);
emit(state.copyWith(planData: state.planData.copyWith(options: options))); emit(state.copyWith(planData: state.planData.copyWith(options: options)));
validateSelectedOptionPrice();
} }
void setOptionDescription(String description) { void setOptionDescription(String description) {
final options = [...state.planData.options]; final options = [...state.planData.options];
options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(description: description); options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(description: description);
emit(state.copyWith(planData: state.planData.copyWith(options: options))); emit(state.copyWith(planData: state.planData.copyWith(options: options)));
validateSelectedOptionDescription();
} }
void setOptionContent(String content, index) { void setOptionContent(String content, index) {
final options = [...state.planData.options]; final options = [...state.planData.options];
final items = [...options[state.selectedOptionIndex ?? 0].items]; final items = [...options[state.selectedOptionIndex ?? 0].items];
if (items.length <= index) { if (items.length <= index) {
items.add(content); items.add(content);
}else { } else {
items[index] = content; items[index] = content;
} }
options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: items); options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: items);
emit(state.copyWith(planData: state.planData.copyWith(options: options))); emit(state.copyWith(planData: state.planData.copyWith(options: options)));
validateSelectedOptionItems();
} }
void removeOptionContentItem(int index) { void removeOptionContentItem(int index) {
final options = [...state.planData.options]; final options = [...state.planData.options];
final updatedItems = [...options[state.selectedOptionIndex ?? 0].items]; final updatedItems = [...options[state.selectedOptionIndex ?? 0].items];
updatedItems.removeAt(index); updatedItems.removeAt(index);
options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: updatedItems); options[state.selectedOptionIndex ?? 0] = options[state.selectedOptionIndex ?? 0].copyWith(items: updatedItems);
emit(state.copyWith(planData: state.planData.copyWith(options: options))); emit(state.copyWith(planData: state.planData.copyWith(options: options)));
validateSelectedOptionItems();
} }
void setDuration(int index) { void setDuration(int index) {
@@ -62,10 +71,12 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
), ),
), ),
); );
validateDuration();
} }
void setImage(XFile? image) { void setImage(XFile? image) {
emit(state.copyWith(planData: state.planData.copyWith(image: image))); emit(state.copyWith(planData: state.planData.copyWith(image: image)));
validateImage();
} }
void setPlanState(String planState) { void setPlanState(String planState) {
@@ -74,9 +85,319 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
void setPlanType(List<bool> type) { void setPlanType(List<bool> type) {
emit(state.copyWith(planData: state.planData.copyWith(type: type))); emit(state.copyWith(planData: state.planData.copyWith(type: type)));
if (type.any((elem) => elem)) {
validateType();
}
} }
void setSelectedOptionIndex(int? index) { void setSelectedOptionIndex(int? index) {
emit(state.copyWith(selectedOptionIndex: index)); emit(state.copyWith(selectedOptionIndex: index));
} }
bool validateName() {
final name = state.planData.name;
if (name == null || name.length < 3) {
emit(state.copyWith.validationState(titleHasError: true));
return false;
}
emit(state.copyWith.validationState(titleHasError: false));
return true;
}
bool validateDetails() {
final details = state.planData.details;
if (details == null || details.length < 3) {
emit(state.copyWith.validationState(descriptionHasError: true));
return false;
}
emit(state.copyWith.validationState(descriptionHasError: false));
return true;
}
bool validateDuration() {
if (state.planData.duration == null) {
emit(state.copyWith.validationState(durationHasError: true));
return false;
}
emit(state.copyWith.validationState(durationHasError: false));
return true;
}
bool validateType() {
final type = state.planData.type;
if (type.isEmpty || type.every((elem) => elem)) {
emit(state.copyWith.validationState(typeHasError: true));
return false;
}
emit(state.copyWith.validationState(typeHasError: false));
return true;
}
bool validateImage() {
if (state.planData.image == null) {
emit(state.copyWith.validationState(imageHasError: true));
return false;
}
emit(state.copyWith.validationState(imageHasError: false));
return true;
}
bool validateSelectedOptionPrice() {
final option = state.planData.options[state.selectedOptionIndex ?? 0];
final optionsValidationsStates = [...state.validationState.optionsValidationState];
if (option.price < 1200) {
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(priceHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return false;
}
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(priceHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return true;
}
bool validatePriceOfAllFilledOptions() {
final options = state.planData.options;
final optionsValidationsStates = [...state.validationState.optionsValidationState];
bool result = true;
if (options[0].price < 1200) {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(priceHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(priceHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
if (options[1].price < 1200) {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(priceHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(priceHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
if (options[2].price < 1200) {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(priceHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(priceHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
return result;
}
bool validateSelectedOptionItems() {
final option = state.planData.options[state.selectedOptionIndex ?? 0];
final optionsValidationsStates = [...state.validationState.optionsValidationState];
bool result = true;
if (option.items.isEmpty) {
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(isItemsEmpty: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return false;
}
final itemsErrors = Map<int, bool>.from(optionsValidationsStates[state.selectedOptionIndex ?? 0].itemsErrors);
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(isItemsEmpty: false);
for (final (index, item) in option.items.indexed) {
if (item.length < 3) {
itemsErrors[index] = true;
result = false;
} else {
itemsErrors[index] = false;
}
}
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(itemsErrors: itemsErrors);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return result;
}
bool validateItemsOfAllFilledOptions() {
final options = state.planData.options;
final optionsValidationsStates = [...state.validationState.optionsValidationState];
bool result = true;
if (options[0].items.isEmpty) {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(isItemsEmpty: true);
result = false;
} else {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(isItemsEmpty: false);
}
final option1ItemsErrors = Map<int, bool>.from(optionsValidationsStates[0].itemsErrors);
for (final (index, item) in options[0].items.indexed) {
if (item.length < 3) {
option1ItemsErrors[index] = true;
result = false;
} else {
option1ItemsErrors[index] = false;
}
}
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(itemsErrors: option1ItemsErrors);
if (state.selectedOptionIndex == null) {
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return result;
}
if (options[1].items.isEmpty) {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(isItemsEmpty: true);
result = false;
} else {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(isItemsEmpty: false);
}
final option2ItemsErrors = Map<int, bool>.from(optionsValidationsStates[1].itemsErrors);
for (final (index, item) in options[1].items.indexed) {
if (item.length < 3) {
option2ItemsErrors[index] = true;
result = false;
} else {
option2ItemsErrors[index] = false;
}
}
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(itemsErrors: option2ItemsErrors);
if (options[2].items.isEmpty) {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(isItemsEmpty: true);
result = false;
} else {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(isItemsEmpty: false);
}
final option3ItemsErrors = Map<int, bool>.from(optionsValidationsStates[2].itemsErrors);
for (final (index, item) in options[2].items.indexed) {
if (item.length < 3) {
option3ItemsErrors[index] = true;
result = false;
} else {
option3ItemsErrors[index] = false;
}
}
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(itemsErrors: option3ItemsErrors);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return result;
}
bool validateSelectedOptionTitle() {
final option = state.planData.options[state.selectedOptionIndex ?? 0];
final optionsValidationsStates = [...state.validationState.optionsValidationState];
if (option.short.length < 3) {
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(titleHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return false;
}
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(titleHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return true;
}
bool validateTitlesOfAllFilledOptions() {
final options = state.planData.options;
final optionsValidationsStates = [...state.validationState.optionsValidationState];
bool result = true;
if (options[0].short.length < 3) {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(titleHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(titleHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
if (state.selectedOptionIndex == null) return result;
if (options[1].short.length < 3) {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(titleHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(titleHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
if (options[2].short.length < 3) {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(titleHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(titleHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
return result;
}
bool validateSelectedOptionDescription() {
final option = state.planData.options[state.selectedOptionIndex ?? 0];
final optionsValidationsStates = [...state.validationState.optionsValidationState];
if (option.description.length < 3) {
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(descriptionHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return false;
}
optionsValidationsStates[state.selectedOptionIndex ?? 0] = optionsValidationsStates[state.selectedOptionIndex ?? 0].copyWith(descriptionHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
return true;
}
bool validateDescriptionsOfAllOptions() {
final options = state.planData.options;
final optionsValidationsStates = [...state.validationState.optionsValidationState];
bool result = true;
if (options[0].description.length < 3) {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(descriptionHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[0] = optionsValidationsStates[0].copyWith(descriptionHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
if (state.selectedOptionIndex == null) return result;
if (options[1].description.length < 3) {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(descriptionHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[1] = optionsValidationsStates[1].copyWith(descriptionHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
if (options[2].description.length < 3) {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(descriptionHasError: true);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
result = false;
} else {
optionsValidationsStates[2] = optionsValidationsStates[2].copyWith(descriptionHasError: false);
emit(state.copyWith.validationState(optionsValidationState: optionsValidationsStates));
}
return result;
}
bool validateAllFields() {
final isNameValid = validateName();
final isDetailsValid = validateDetails();
final isImageValid = validateImage();
final isDurationValid = validateDuration();
final areTitlesValid = validateTitlesOfAllFilledOptions();
final arePricesValid = validatePriceOfAllFilledOptions();
final areDescriptionsValid = validateDescriptionsOfAllOptions();
final areItemsValid = validateItemsOfAllFilledOptions();
return isNameValid && isDetailsValid && isImageValid && isDurationValid && areTitlesValid && arePricesValid && areDescriptionsValid && areItemsValid;
}
Future<void> publishPlan() async {
if (validateAllFields()) {
emit(state.copyWith(isLoading: true));
final planModel = await state.planData.toCreationJson();
final plan = await createPlanRepository.createPlan(model: planModel);
if (state.planData.options[1].short.isNotEmpty) {
final options = state.planData.toOptionsCreationJson();
await createPlanRepository.createPacketOptions(slug: plan.slug, model: options);
} else {
final option = state.planData.toOptionCreationJson();
await createPlanRepository.createPacketOption(slug: plan.slug, model: option);
}
emit(state.copyWith(isLoading: false, planCreated: true));
}
}
} }

View File

@@ -9,7 +9,9 @@ class CreatePlanState with _$CreatePlanState {
const factory CreatePlanState({ const factory CreatePlanState({
required bool isLoading, required bool isLoading,
required String? errorMessage, required String? errorMessage,
required CreatePlanModel planData, required CreatePlanModel planData,
required PlanValidationState validationState,
required bool planCreated,
int? durationIndex, int? durationIndex,
int? selectedOptionIndex, int? selectedOptionIndex,
}) = _CreatePlanState; }) = _CreatePlanState;
@@ -17,9 +19,55 @@ class CreatePlanState with _$CreatePlanState {
factory CreatePlanState.initial() => CreatePlanState( factory CreatePlanState.initial() => CreatePlanState(
isLoading: false, isLoading: false,
errorMessage: null, errorMessage: null,
planCreated: false,
validationState: PlanValidationState.empty(),
planData: CreatePlanModel( planData: CreatePlanModel(
type: [false, false, false, false], type: [false, false, false, false],
options: [PacketOptions.empty(), PacketOptions.empty(), PacketOptions.empty()], options: [PacketOptions.empty(), PacketOptions.empty(), PacketOptions.empty()],
), ),
);
}
@freezed
class PlanValidationState with _$PlanValidationState {
factory PlanValidationState({
required bool titleHasError,
required bool durationHasError,
required bool typeHasError,
required bool descriptionHasError,
required bool imageHasError,
required List<OptionValidationState> optionsValidationState,
}) = _PlanValidationState;
factory PlanValidationState.empty() => PlanValidationState(
titleHasError: false,
durationHasError: false,
typeHasError: false,
descriptionHasError: false,
imageHasError: false,
optionsValidationState: [
OptionValidationState.empty(),
OptionValidationState.empty(),
OptionValidationState.empty(),
],
);
}
@freezed
class OptionValidationState with _$OptionValidationState {
factory OptionValidationState({
required bool titleHasError,
required bool priceHasError,
required bool descriptionHasError,
required Map<int, bool> itemsErrors,
required bool isItemsEmpty,
}) = _OptionValidationState;
factory OptionValidationState.empty() => OptionValidationState(
descriptionHasError: false,
titleHasError: false,
priceHasError: false,
isItemsEmpty: false,
itemsErrors: {},
); );
} }

View File

@@ -1,6 +1,8 @@
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -39,34 +41,53 @@ class _AddItemWidgetState extends State<AddItemWidget> {
if (widget.items.length > widget.index && controller.text != widget.items[widget.index]) { if (widget.items.length > widget.index && controller.text != widget.items[widget.index]) {
controller.text = widget.items[widget.index]; controller.text = widget.items[widget.index];
} }
return Row( return BlocBuilder<CreatePlanCubit, CreatePlanState>(
children: [ buildWhen: (previous, current) => previous.validationState != current.validationState,
Expanded( builder: (context, state) {
child: CustomTextFormField( final hasError = state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty ||
controller: controller, (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false);
hintText: widget.enableHint ? s.option_content_hint_text : null, return Column(
onChanged: (value) { children: [
context.read<CreatePlanCubit>().setOptionContent(value, widget.index); Row(
}, children: [
), Expanded(
), child: CustomTextFormField(
if (widget.canBeDeleted) ...[ controller: controller,
const SizedBox(width: 8), hasError: hasError,
GestureDetector( hintText: widget.enableHint ? s.option_content_hint_text : null,
onTap: () { onChanged: (value) {
context.read<CreatePlanCubit>().removeOptionContentItem(widget.index); context.read<CreatePlanCubit>().setOptionContent(value, widget.index);
}, },
child: Container( ),
padding: const EdgeInsets.all(14), ),
decoration: BoxDecoration( if (widget.canBeDeleted) ...[
color: appTheme.lightGray0, const SizedBox(width: 8),
borderRadius: BorderRadius.circular(20), GestureDetector(
), onTap: () {
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24), context.read<CreatePlanCubit>().removeOptionContentItem(widget.index);
},
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: appTheme.lightGray0,
borderRadius: BorderRadius.circular(20),
),
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24),
),
),
],
],
), ),
), if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty) ...[
], const SizedBox(height: 4),
], ValidationErrorWidget(error: s.addAtLeastOneItem)
] else if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols)
]
],
);
},
); );
} }
} }

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/presentation/widgets/add_item_widget.dart'; import 'package:eatvio_app/features/create_plan/presentation/widgets/add_item_widget.dart';
@@ -32,35 +33,9 @@ class _OptionWidgetState extends State<OptionWidget> {
super.initState(); super.initState();
customerPriceController.addListener(() { customerPriceController.addListener(() {
if (!customerPriceController.text.endsWith('₽')) { if (!customerPriceController.text.endsWith('₽')) {
final textWithoutCurrency = customerPriceController.text.replaceAll('₽', ''); final text = customerPriceController.text.replaceAll('₽', '');
customerPriceController.value = TextEditingValue( customerPriceController.text = '$text₽';
text: "$textWithoutCurrency₽", customerPriceController.selection = TextSelection.collapsed(offset: text.length);
selection: TextSelection.collapsed(offset: textWithoutCurrency.length),
);
}
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
if (price != null && price < 1200) {
customerPriceController.value = const TextEditingValue(
text: "1200₽",
selection: TextSelection.collapsed(offset: 4),
);
}
});
creatorPriceController.addListener(() {
if (!creatorPriceController.text.endsWith('₽')) {
final textWithoutCurrency = creatorPriceController.text.replaceAll('₽', '');
creatorPriceController.value = TextEditingValue(
text: "$textWithoutCurrency₽",
selection: TextSelection.collapsed(offset: textWithoutCurrency.length),
);
}
final price = double.tryParse(creatorPriceController.text.replaceAll('₽', ''));
if (price != null && price < 1000) {
creatorPriceController.value = const TextEditingValue(
text: "1000₽",
selection: TextSelection.collapsed(offset: 4),
);
} }
}); });
} }
@@ -94,12 +69,17 @@ class _OptionWidgetState extends State<OptionWidget> {
const SizedBox(height: 4), const SizedBox(height: 4),
CustomTextFormField( CustomTextFormField(
hintText: optionNameHintText, hintText: optionNameHintText,
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError,
controller: optionTitleController, controller: optionTitleController,
onChanged: (value) { onChanged: (value) {
context.read<CreatePlanCubit>().setOptionTitle(value); context.read<CreatePlanCubit>().setOptionTitle(value);
}, },
maxLength: 150, maxLength: 150,
), ),
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
s.option_desc, s.option_desc,
@@ -114,6 +94,7 @@ class _OptionWidgetState extends State<OptionWidget> {
child: CustomTextFormField( child: CustomTextFormField(
controller: optionDescriptionController, controller: optionDescriptionController,
textAlignVertical: TextAlignVertical.top, textAlignVertical: TextAlignVertical.top,
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError,
maxLength: 1000, maxLength: 1000,
minLines: null, minLines: null,
maxLines: null, maxLines: null,
@@ -124,6 +105,10 @@ class _OptionWidgetState extends State<OptionWidget> {
}, },
), ),
), ),
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
@@ -141,14 +126,8 @@ class _OptionWidgetState extends State<OptionWidget> {
const SizedBox(height: 4), const SizedBox(height: 4),
CustomTextFormField( CustomTextFormField(
controller: creatorPriceController, controller: creatorPriceController,
onChanged: (value) { readOnly: true,
double? price = double.tryParse(value.replaceAll('₽', '')); textStyle: TextStyle(color: appTheme.gray),
if (price != null) {
price = price * 1.2;
customerPriceController.text = price.toStringAsFixed(2);
context.read<CreatePlanCubit>().setPrice(price);
}
},
maxLength: 20, maxLength: 20,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -169,10 +148,11 @@ class _OptionWidgetState extends State<OptionWidget> {
CustomTextFormField( CustomTextFormField(
controller: customerPriceController, controller: customerPriceController,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: const TextInputType.numberWithOptions(decimal: true),
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError,
onChanged: (value) { onChanged: (value) {
final price = double.tryParse(value.replaceAll('₽', '')); final price = double.tryParse(value.replaceAll('₽', ''));
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
if (price != null) { if (price != null) {
creatorPriceController.text = (price * 0.8).toStringAsFixed(2);
context.read<CreatePlanCubit>().setPrice(price); context.read<CreatePlanCubit>().setPrice(price);
} }
}, },
@@ -235,33 +215,33 @@ class _OptionWidgetState extends State<OptionWidget> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const CustomDivider(), const CustomDivider(),
const SizedBox(height: 16), // const SizedBox(height: 16),
Row( // Row(
children: [ // children: [
Checkbox( // Checkbox(
value: isChecked, // value: isChecked,
activeColor: appTheme.gray, // activeColor: appTheme.gray,
checkColor: appTheme.gray, // checkColor: appTheme.gray,
fillColor: const WidgetStatePropertyAll(Colors.transparent), // fillColor: const WidgetStatePropertyAll(Colors.transparent),
side: WidgetStateBorderSide.resolveWith( // side: WidgetStateBorderSide.resolveWith(
(Set<WidgetState> states) { // (Set<WidgetState> states) {
return BorderSide(width: 1, color: appTheme.gray); // return BorderSide(width: 1, color: appTheme.gray);
}, // },
), // ),
onChanged: (bool? value) { // onChanged: (bool? value) {
setState(() { // setState(() {
isChecked = value ?? false; // isChecked = value ?? false;
}); // });
}, // },
), // ),
Expanded( // Expanded(
child: Text( // child: Text(
s.option_personal_diet_desc, // s.option_personal_diet_desc,
style: TextStyles.textMMedium.copyWith(color: appTheme.black), // style: TextStyles.textMMedium.copyWith(color: appTheme.black),
), // ),
), // ),
], // ],
), // ),
], ],
), ),
); );

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
@@ -36,28 +37,37 @@ class _PlanDurationDropDownWidgetState extends State<PlanDurationDropDownWidget>
final s = S.of(context); final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>( return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) { builder: (context, state) {
return CustomDropDownButton( return Column(
hint: Text( children: [
s.choose_plan_duration, CustomDropDownButton(
style: TextStyles.textMMedium.copyWith(color: appTheme.black), hasError: state.validationState.durationHasError,
), hint: Text(
onChanged: (value) { s.choose_plan_duration,
if (value != null) { style: TextStyles.textMMedium.copyWith(color: appTheme.black),
context.read<CreatePlanCubit>().setDuration(durationDropDownTitles.indexOf(value)); ),
} onChanged: (value) {
}, if (value != null) {
items: durationDropDownTitles context.read<CreatePlanCubit>().setDuration(durationDropDownTitles.indexOf(value));
.mapIndexed( }
(index, value) => DropdownMenuItem( },
value: durationDropDownTitles[index], items: durationDropDownTitles
child: Text( .mapIndexed(
durationDropDownTitles[index], (index, value) => DropdownMenuItem(
style: TextStyles.textMMedium.copyWith(color: appTheme.black), value: durationDropDownTitles[index],
), child: Text(
), durationDropDownTitles[index],
) style: TextStyles.textMMedium.copyWith(color: appTheme.black),
.toList(), ),
value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], ),
)
.toList(),
value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0],
),
if (state.validationState.durationHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.choose_plan_duration),
],
],
); );
}, },
); );

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dotted_border/dotted_border.dart'; import 'package:dotted_border/dotted_border.dart';
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
@@ -72,74 +73,79 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
final s = S.of(context); final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>( return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) { builder: (context, state) {
return GestureDetector( return Column(
onTap: () { children: [
_showImagePickerOptions(s, context.read<CreatePlanCubit>()); GestureDetector(
}, onTap: () {
child: Container( _showImagePickerOptions(s, context.read<CreatePlanCubit>());
height: 240, },
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(8), height: 240,
image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null, decoration: BoxDecoration(
color: appTheme.gray50, borderRadius: BorderRadius.circular(8),
image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null,
color: appTheme.gray50,
),
width: MediaQuery.sizeOf(context).width,
padding: state.planData.image != null ? null : const EdgeInsets.all(8),
child: state.planData.image != null
? Padding(
padding: const EdgeInsets.only(top: 6, right: 6),
child: GestureDetector(
onTap: () {
context.read<CreatePlanCubit>().setImage(null);
},
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appTheme.dividerColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close_rounded,
color: Colors.grey,
size: 16,
),
),
),
),
)
: Stack(
children: [
DottedBorder(
stackFit: StackFit.expand,
dashPattern: const [14, 14],
color: state.validationState.imageHasError ? appTheme.red : appTheme.dottedBorder,
borderType: BorderType.RRect,
radius: const Radius.circular(12),
child: const SizedBox.expand(),
),
Center(
child: Column(
children: [
const SizedBox(
height: 48,
),
SvgPicture.asset('assets/icons/eatvio_logo.svg'),
const SizedBox(
height: 24,
),
Text(
s.add_cover,
style: TextStyles.textMMedium.copyWith(color: appTheme.gray),
),
],
),
),
],
),
),
), ),
width: MediaQuery.sizeOf(context).width, if (state.validationState.imageHasError) ...[const SizedBox(height: 8), ValidationErrorWidget(error: s.add_cover)],
padding: state.planData.image != null ? null : const EdgeInsets.all(8), ],
child: state.planData.image != null
? Padding(
padding: const EdgeInsets.only(top: 6, right: 6),
child: GestureDetector(
onTap: () {
context.read<CreatePlanCubit>().setImage(null);
},
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appTheme.dividerColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close_rounded,
color: Colors.grey,
size: 16,
),
),
),
),
)
: Stack(
children: [
DottedBorder(
stackFit: StackFit.expand,
dashPattern: const [14, 14],
color: appTheme.dottedBorder,
borderType: BorderType.RRect,
radius: const Radius.circular(12),
child: const SizedBox.expand(),
),
Center(
child: Column(
children: [
const SizedBox(
height: 48,
),
SvgPicture.asset('assets/icons/eatvio_logo.svg'),
const SizedBox(
height: 24,
),
Text(
s.add_cover,
style: TextStyles.textMMedium.copyWith(color: appTheme.gray),
),
],
),
),
],
),
),
); );
}, },
); );

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
@@ -50,49 +51,55 @@ class _PlanTypeChoiceWidgetState extends State<PlanTypeChoiceWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>( return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) { builder: (context, state) {
return Row( return Column(
children: [ children: [
...List.generate(4, (index) { Row(
return Expanded( children: [
child: GestureDetector( ...List.generate(4, (index) {
onTap: () { return Expanded(
final planTypes = [...state.planData.type]; child: GestureDetector(
planTypes[index] = !planTypes[index]; onTap: () {
context.read<CreatePlanCubit>().setPlanType(planTypes); final planTypes = [...state.planData.type];
}, planTypes[index] = !planTypes[index];
child: Container( context.read<CreatePlanCubit>().setPlanType(planTypes);
margin: EdgeInsets.only(right: index == 3 ? 0 : 4), },
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(5), margin: EdgeInsets.only(right: index == 3 ? 0 : 4),
color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700, decoration: BoxDecoration(
border: Border.all( borderRadius: BorderRadius.circular(5),
width: 1, color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700,
color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00, border: Border.all(
width: 1,
color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00,
),
),
height: 104,
child: Column(
children: [
const SizedBox(height: 16),
Image.asset(
choiceContent[index].$1,
height: 30,
),
const SizedBox(height: 8),
Text(
choiceContent[index].$2,
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
style: TextStyles.textFS10FW500.copyWith(color: appTheme.black),
),
],
),
), ),
), ),
height: 104, );
child: Column( }),
children: [ ],
const SizedBox(height: 16), ),
Image.asset( if (state.validationState.typeHasError) ...[const SizedBox(height: 8), ValidationErrorWidget(error: s.chosePlanType)]
choiceContent[index].$1,
height: 30,
),
const SizedBox(height: 8),
Text(
choiceContent[index].$2,
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
style: TextStyles.textFS10FW500.copyWith(color: appTheme.black),
),
],
),
),
),
);
}),
], ],
); );
}, },

View File

@@ -1,3 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/models/plan_slug_model.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
class CreatePlanRepository { class CreatePlanRepository {
@@ -5,10 +7,11 @@ class CreatePlanRepository {
CreatePlanRepository({required this.createPlanService}); CreatePlanRepository({required this.createPlanService});
Future<void> createPlan({ Future<PlanSlugModel> createPlan({
required Map<String, dynamic> model, required Map<String, dynamic> model,
}) async { }) async {
createPlanService.createPlan(model: model); final formData = FormData.fromMap(model);
return await createPlanService.createPlan(formData: formData);
} }
Future<void> createPacketOption({ Future<void> createPacketOption({

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/models/plan_slug_model.dart';
import 'package:retrofit/retrofit.dart'; import 'package:retrofit/retrofit.dart';
part 'create_plan_service.g.dart'; part 'create_plan_service.g.dart';
@@ -9,8 +10,8 @@ abstract class CreatePlanService {
@POST('/api/plans/create') @POST('/api/plans/create')
@MultiPart() @MultiPart()
Future<void> createPlan({ Future<PlanSlugModel> createPlan({
@Body() required Map<String, dynamic> model, @Body() required FormData formData,
}); });
@POST('/api/plans/create/{slug}/packet_option') @POST('/api/plans/create/{slug}/packet_option')
@@ -19,7 +20,7 @@ abstract class CreatePlanService {
@Body() required Map<String, dynamic> model, @Body() required Map<String, dynamic> model,
}); });
@POST('/api/plans/create/{slug}/packet_options') @POST('/api/plans/create/{slug}/packet_options')
Future<void> createPacketOptions({ Future<void> createPacketOptions({
@Path() required String slug, @Path() required String slug,
@Body() required Map<String, dynamic> model, @Body() required Map<String, dynamic> model,

View File

@@ -36,6 +36,7 @@ class MealComment with _$MealComment {
factory MealComment({ factory MealComment({
required String comment, required String comment,
required int typeNo,
}) = _MealComment; }) = _MealComment;
factory MealComment.fromJson(Map<String, dynamic> json) => _$MealCommentFromJson(json); factory MealComment.fromJson(Map<String, dynamic> json) => _$MealCommentFromJson(json);
@@ -45,7 +46,7 @@ class MealComment with _$MealComment {
class PortionComment with _$PortionComment { class PortionComment with _$PortionComment {
factory PortionComment({ factory PortionComment({
@JsonKey(name: 'commentable_id') required int portionId, @JsonKey(name: 'id') required int portionId,
required String comment, required String comment,
}) = _PortionComment; }) = _PortionComment;

View File

@@ -479,6 +479,7 @@ MealComment _$MealCommentFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$MealComment { mixin _$MealComment {
String get comment => throw _privateConstructorUsedError; String get comment => throw _privateConstructorUsedError;
int get typeNo => throw _privateConstructorUsedError;
/// Serializes this MealComment to a JSON map. /// Serializes this MealComment to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -496,7 +497,7 @@ abstract class $MealCommentCopyWith<$Res> {
MealComment value, $Res Function(MealComment) then) = MealComment value, $Res Function(MealComment) then) =
_$MealCommentCopyWithImpl<$Res, MealComment>; _$MealCommentCopyWithImpl<$Res, MealComment>;
@useResult @useResult
$Res call({String comment}); $Res call({String comment, int typeNo});
} }
/// @nodoc /// @nodoc
@@ -515,12 +516,17 @@ class _$MealCommentCopyWithImpl<$Res, $Val extends MealComment>
@override @override
$Res call({ $Res call({
Object? comment = null, Object? comment = null,
Object? typeNo = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
comment: null == comment comment: null == comment
? _value.comment ? _value.comment
: comment // ignore: cast_nullable_to_non_nullable : comment // ignore: cast_nullable_to_non_nullable
as String, as String,
typeNo: null == typeNo
? _value.typeNo
: typeNo // ignore: cast_nullable_to_non_nullable
as int,
) as $Val); ) as $Val);
} }
} }
@@ -533,7 +539,7 @@ abstract class _$$MealCommentImplCopyWith<$Res>
__$$MealCommentImplCopyWithImpl<$Res>; __$$MealCommentImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({String comment}); $Res call({String comment, int typeNo});
} }
/// @nodoc /// @nodoc
@@ -550,12 +556,17 @@ class __$$MealCommentImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? comment = null, Object? comment = null,
Object? typeNo = null,
}) { }) {
return _then(_$MealCommentImpl( return _then(_$MealCommentImpl(
comment: null == comment comment: null == comment
? _value.comment ? _value.comment
: comment // ignore: cast_nullable_to_non_nullable : comment // ignore: cast_nullable_to_non_nullable
as String, as String,
typeNo: null == typeNo
? _value.typeNo
: typeNo // ignore: cast_nullable_to_non_nullable
as int,
)); ));
} }
} }
@@ -563,17 +574,19 @@ class __$$MealCommentImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$MealCommentImpl implements _MealComment { class _$MealCommentImpl implements _MealComment {
_$MealCommentImpl({required this.comment}); _$MealCommentImpl({required this.comment, required this.typeNo});
factory _$MealCommentImpl.fromJson(Map<String, dynamic> json) => factory _$MealCommentImpl.fromJson(Map<String, dynamic> json) =>
_$$MealCommentImplFromJson(json); _$$MealCommentImplFromJson(json);
@override @override
final String comment; final String comment;
@override
final int typeNo;
@override @override
String toString() { String toString() {
return 'MealComment(comment: $comment)'; return 'MealComment(comment: $comment, typeNo: $typeNo)';
} }
@override @override
@@ -581,12 +594,13 @@ class _$MealCommentImpl implements _MealComment {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$MealCommentImpl && other is _$MealCommentImpl &&
(identical(other.comment, comment) || other.comment == comment)); (identical(other.comment, comment) || other.comment == comment) &&
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, comment); int get hashCode => Object.hash(runtimeType, comment, typeNo);
/// Create a copy of MealComment /// Create a copy of MealComment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -605,13 +619,17 @@ class _$MealCommentImpl implements _MealComment {
} }
abstract class _MealComment implements MealComment { abstract class _MealComment implements MealComment {
factory _MealComment({required final String comment}) = _$MealCommentImpl; factory _MealComment(
{required final String comment,
required final int typeNo}) = _$MealCommentImpl;
factory _MealComment.fromJson(Map<String, dynamic> json) = factory _MealComment.fromJson(Map<String, dynamic> json) =
_$MealCommentImpl.fromJson; _$MealCommentImpl.fromJson;
@override @override
String get comment; String get comment;
@override
int get typeNo;
/// Create a copy of MealComment /// Create a copy of MealComment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -627,7 +645,7 @@ PortionComment _$PortionCommentFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$PortionComment { mixin _$PortionComment {
@JsonKey(name: 'commentable_id') @JsonKey(name: 'id')
int get portionId => throw _privateConstructorUsedError; int get portionId => throw _privateConstructorUsedError;
String get comment => throw _privateConstructorUsedError; String get comment => throw _privateConstructorUsedError;
@@ -647,7 +665,7 @@ abstract class $PortionCommentCopyWith<$Res> {
PortionComment value, $Res Function(PortionComment) then) = PortionComment value, $Res Function(PortionComment) then) =
_$PortionCommentCopyWithImpl<$Res, PortionComment>; _$PortionCommentCopyWithImpl<$Res, PortionComment>;
@useResult @useResult
$Res call({@JsonKey(name: 'commentable_id') int portionId, String comment}); $Res call({@JsonKey(name: 'id') int portionId, String comment});
} }
/// @nodoc /// @nodoc
@@ -689,7 +707,7 @@ abstract class _$$PortionCommentImplCopyWith<$Res>
__$$PortionCommentImplCopyWithImpl<$Res>; __$$PortionCommentImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({@JsonKey(name: 'commentable_id') int portionId, String comment}); $Res call({@JsonKey(name: 'id') int portionId, String comment});
} }
/// @nodoc /// @nodoc
@@ -725,14 +743,13 @@ class __$$PortionCommentImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$PortionCommentImpl implements _PortionComment { class _$PortionCommentImpl implements _PortionComment {
_$PortionCommentImpl( _$PortionCommentImpl(
{@JsonKey(name: 'commentable_id') required this.portionId, {@JsonKey(name: 'id') required this.portionId, required this.comment});
required this.comment});
factory _$PortionCommentImpl.fromJson(Map<String, dynamic> json) => factory _$PortionCommentImpl.fromJson(Map<String, dynamic> json) =>
_$$PortionCommentImplFromJson(json); _$$PortionCommentImplFromJson(json);
@override @override
@JsonKey(name: 'commentable_id') @JsonKey(name: 'id')
final int portionId; final int portionId;
@override @override
final String comment; final String comment;
@@ -775,14 +792,14 @@ class _$PortionCommentImpl implements _PortionComment {
abstract class _PortionComment implements PortionComment { abstract class _PortionComment implements PortionComment {
factory _PortionComment( factory _PortionComment(
{@JsonKey(name: 'commentable_id') required final int portionId, {@JsonKey(name: 'id') required final int portionId,
required final String comment}) = _$PortionCommentImpl; required final String comment}) = _$PortionCommentImpl;
factory _PortionComment.fromJson(Map<String, dynamic> json) = factory _PortionComment.fromJson(Map<String, dynamic> json) =
_$PortionCommentImpl.fromJson; _$PortionCommentImpl.fromJson;
@override @override
@JsonKey(name: 'commentable_id') @JsonKey(name: 'id')
int get portionId; int get portionId;
@override @override
String get comment; String get comment;

View File

@@ -48,22 +48,24 @@ Map<String, dynamic> _$$ConsultantCommentsImplToJson(
_$MealCommentImpl _$$MealCommentImplFromJson(Map<String, dynamic> json) => _$MealCommentImpl _$$MealCommentImplFromJson(Map<String, dynamic> json) =>
_$MealCommentImpl( _$MealCommentImpl(
comment: json['comment'] as String, comment: json['comment'] as String,
typeNo: (json['typeNo'] as num).toInt(),
); );
Map<String, dynamic> _$$MealCommentImplToJson(_$MealCommentImpl instance) => Map<String, dynamic> _$$MealCommentImplToJson(_$MealCommentImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'comment': instance.comment, 'comment': instance.comment,
'typeNo': instance.typeNo,
}; };
_$PortionCommentImpl _$$PortionCommentImplFromJson(Map<String, dynamic> json) => _$PortionCommentImpl _$$PortionCommentImplFromJson(Map<String, dynamic> json) =>
_$PortionCommentImpl( _$PortionCommentImpl(
portionId: (json['commentable_id'] as num).toInt(), portionId: (json['id'] as num).toInt(),
comment: json['comment'] as String, comment: json['comment'] as String,
); );
Map<String, dynamic> _$$PortionCommentImplToJson( Map<String, dynamic> _$$PortionCommentImplToJson(
_$PortionCommentImpl instance) => _$PortionCommentImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'commentable_id': instance.portionId, 'id': instance.portionId,
'comment': instance.comment, 'comment': instance.comment,
}; };

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
@@ -6,7 +7,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class MealButtonWidget extends StatefulWidget { class MealButtonWidget extends StatefulWidget {
final int mealNumber; final int mealNumber;
const MealButtonWidget({ const MealButtonWidget({
super.key, super.key,
required this.mealNumber, required this.mealNumber,
@@ -22,6 +22,18 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
return BlocBuilder<DiaryCubit, DiaryState>( return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) { builder: (context, state) {
if (state is DiaryStateLoaded) { if (state is DiaryStateLoaded) {
final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull(
(element) => element.typeNo == widget.mealNumber,
);
final Color bgColor;
if (state.selectedMeal == widget.mealNumber) {
bgColor = appTheme.blueA400;
} else if (consultantComment != null) {
bgColor = appTheme.commentBg;
} else {
bgColor = const Color(0xFFF1F2F6);
}
return InkWell( return InkWell(
onTap: () { onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber); context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
@@ -32,7 +44,7 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
height: 32, height: 32,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), color: bgColor,
), ),
child: Center( child: Center(
child: Text( child: Text(

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
@@ -81,8 +82,10 @@ class _MealWidgetState extends State<MealWidget> {
BlocBuilder<DiaryCubit, DiaryState>( BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) { builder: (context, state) {
if (state is DiaryStateLoaded) { if (state is DiaryStateLoaded) {
final mealsComments = state.consultantReview?.consultantComments.mealsComments; final consultantCommentOfMeal = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull(
if (mealsComments != null && (state.selectedMeal - 1 < mealsComments.length)) { (element) => element.typeNo == state.selectedMeal,
);
if (consultantCommentOfMeal != null) {
return Column( return Column(
children: [ children: [
Text( Text(
@@ -90,7 +93,7 @@ class _MealWidgetState extends State<MealWidget> {
style: TextStyles.textLSemibold.copyWith(color: appTheme.black), style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
CommentContainer(comment: mealsComments[state.selectedMeal - 1].comment), CommentContainer(comment: consultantCommentOfMeal.comment),
], ],
); );
} }

View File

@@ -11,7 +11,7 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
class DiaryRepository { class DiaryRepository {
final DiaryService diaryService; final DiaryService diaryService;
final StreamController<String> _diaryForceFetchController = StreamController<String>(); final StreamController<String> _diaryForceFetchController = StreamController<String>.broadcast();
Stream<String> get forceFetchStream => _diaryForceFetchController.stream; Stream<String> get forceFetchStream => _diaryForceFetchController.stream;

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/models/pagination_model.dart'; import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'plan_pagination_model.freezed.dart'; part 'plan_pagination_model.freezed.dart';
@@ -70,10 +71,10 @@ class PacketOptions with _$PacketOptions {
class PlanType with _$PlanType { class PlanType with _$PlanType {
const PlanType._(); const PlanType._();
const factory PlanType({ const factory PlanType({
@JsonKey(name: "fit") required bool? fit, @JsonKey(name: "fit", fromJson: parseBool) required bool? fit,
@JsonKey(name: "slim") required bool? slim, @JsonKey(name: "slim", fromJson: parseBool) required bool? slim,
@JsonKey(name: "behavior") required bool? behavior, @JsonKey(name: "behavior", fromJson: parseBool) required bool? behavior,
@JsonKey(name: "wellness") required bool? wellness, @JsonKey(name: "wellness", fromJson: parseBool) required bool? wellness,
}) = _PlanType; }) = _PlanType;
factory PlanType.fromJson(Map<String, dynamic> json) => _$PlanTypeFromJson(json); factory PlanType.fromJson(Map<String, dynamic> json) => _$PlanTypeFromJson(json);

View File

@@ -118,7 +118,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
if (!context.mounted) return; if (!context.mounted) return;
showBottomSheetWithBlur( showBottomSheetWithBlur(
context: context, context: context,
child: SuccessPublishedDietBottomSheet(title: widget.diet.name), child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name),
); );
await context.read<MyDietsCubit>().loadAllDiets(); await context.read<MyDietsCubit>().loadAllDiets();
} else { } else {

View File

@@ -5,7 +5,6 @@ part 'card_model.g.dart';
@freezed @freezed
class CardModel with _$CardModel { class CardModel with _$CardModel {
factory CardModel({ factory CardModel({
required int id, required int id,
@JsonKey(name: 'bank_pan') required String cardNumber, @JsonKey(name: 'bank_pan') required String cardNumber,
@@ -16,7 +15,7 @@ class CardModel with _$CardModel {
} }
bool parseBool(json) { bool parseBool(json) {
if (json == 1 || json == true) { if (json == 1 || json == true || json == '1') {
return true; return true;
} }
return false; return false;

View File

@@ -28,20 +28,19 @@ class UserCardsCubit extends Cubit<UserCardsState> {
emit(state.copyWith(cards: cards, state: BaseState.success)); emit(state.copyWith(cards: cards, state: BaseState.success));
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit(state.copyWith(state: BaseState.error, error: e.toString())); emit(state.copyWith(state: BaseState.error, error: e.toString()));
} }
} }
Future<void> markAsDefault({required int id}) async { Future<void> markAsDefault({required int id}) async {
final oldCards = [...state.cards]; final oldCards = [...state.cards];
if (isClosed) return; if (isClosed) return;
final cards = final cards = oldCards.map((card) {
oldCards.map((card) { if (card.id == id) {
if (card.id == id) { return card.copyWith(isDefault: true);
return card.copyWith(isDefault: true); }
} return card.copyWith(isDefault: false);
return card.copyWith(isDefault: false); }).toList();
}).toList();
emit(state.copyWith(cards: cards)); emit(state.copyWith(cards: cards));
EasyDebounce.debounce('markAsDefault', Duration(milliseconds: id), () async { EasyDebounce.debounce('markAsDefault', Duration(milliseconds: id), () async {
@@ -66,4 +65,14 @@ class UserCardsCubit extends Cubit<UserCardsState> {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
} }
Future<bool> collectCharge({required int chosenPlaId}) async {
try {
await cardsRepository.collectCharge(chosenPlaId: chosenPlaId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
return false;
}
} }

View File

@@ -1,7 +1,11 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
@@ -11,104 +15,221 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
class CardSettingsScreen extends StatelessWidget { class CardSettingsScreen extends StatelessWidget {
static const path = '/card-settings-screen'; static const path = '/card-settings-screen';
static const name = 'cardSettingsScreen'; static const name = 'cardSettingsScreen';
const CardSettingsScreen({super.key}); const CardSettingsScreen({super.key, this.chosenPlanId});
final int? chosenPlanId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<UserCardsCubit>()..getCards(), child: const CardsSettingsContent()); return BlocProvider(
create: (context) => sl<UserCardsCubit>()..getCards(),
child: CardsSettingsContent(
chosenPlanId: chosenPlanId,
));
} }
} }
class CardsSettingsContent extends StatelessWidget { class CardsSettingsContent extends StatelessWidget {
const CardsSettingsContent({super.key}); const CardsSettingsContent({super.key, this.chosenPlanId});
final int? chosenPlanId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return Scaffold( return BlocBuilder<UserCardsCubit, UserCardsState>(
backgroundColor: const Color(0xFFF0F2F5), builder: (context, state) {
appBar: AppBar( return Scaffold(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), backgroundColor: const Color(0xFFF0F2F5),
title: Text(s.my_cards), appBar: AppBar(
centerTitle: true, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
), title: Text(s.my_cards),
body: Column( centerTitle: true,
children: [ ),
const SizedBox(height: 16), body: Column(
Container( children: [
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), const SizedBox(height: 16),
width: double.infinity, Container(
child: Column( decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
crossAxisAlignment: CrossAxisAlignment.start, width: double.infinity,
children: [ child: Column(
const SizedBox(height: 16), crossAxisAlignment: CrossAxisAlignment.start,
const Padding( children: [
padding: EdgeInsets.symmetric(horizontal: 16), const SizedBox(height: 16),
child: Text('Привязанные карты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w600, height: 1)), const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('Привязанные карты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w600, height: 1)),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Builder(
builder: (context) {
final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber;
if (defaultCardNumber != null) {
return Text(
'Выплату будут производиться\nна карту ${defaultCardNumber.substring(defaultCardNumber.length - 7)}',
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, height: 1),
);
}
return Text(s.no_default_cards, style: TextStyles.textMMedium.copyWith(color: appTheme.black));
},
),
),
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Builder(
builder: (context) {
switch (state.state) {
case BaseState.error:
return SizedBox(
width: MediaQuery.sizeOf(context).width,
child: Center(
child: DataErrorWidget(
onReload: () async {
context.read<UserCardsCubit>().getCards();
},
),
),
);
case BaseState.success:
return Padding(
padding: const EdgeInsets.only(left: 12),
child: Row(
spacing: 12,
children: List.generate(state.cards.length + 1, (index) {
if (index < state.cards.length) {
final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7);
return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber);
}
return const CardWidget(enabledAddCard: true);
}),
),
);
default:
return SizedBox(width: MediaQuery.sizeOf(context).width, height: 200, child: const Center(child: Loader()));
}
},
),
),
const SizedBox(height: 16),
],
), ),
const SizedBox(height: 8), ),
if (chosenPlanId != null && state.cards.isNotEmpty) ...[
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: BlocBuilder<UserCardsCubit, UserCardsState>( child: CustomOutlinedButton(
builder: (context, state) { text: s.collectCharge,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () async {
final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber; final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber;
if (defaultCardNumber != null) { final result = await context.read<UserCardsCubit>().collectCharge(chosenPlaId: chosenPlanId!);
return Text( if (context.mounted) {
'Выплату будут производиться\nна карту ${defaultCardNumber.substring(defaultCardNumber.length - 7)}', if (!result) {
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, height: 1), showBottomSheetWithBlur(
context: context,
child: Column(
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text(
s.chargeCollectionFailed,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(
height: 8,
),
Text(
s.contactSupport,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
AppRouter.navigatorKey.currentContext?.pop();
},
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
],
),
);
return;
}
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text(
s.chargeApproved,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(
height: 8,
),
Text(
'${s.moneySentToCard} ${defaultCardNumber?.substring(defaultCardNumber.length - 7)}',
textAlign: TextAlign.center,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
const SizedBox(
height: 8,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
AppRouter.navigatorKey.currentContext?.pop();
},
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
],
),
); );
} }
return Text(s.no_default_cards, style: TextStyles.textMMedium.copyWith(color: appTheme.black));
}, },
), ),
), )
const SizedBox(height: 16), ]
SingleChildScrollView( ],
scrollDirection: Axis.horizontal,
child: BlocBuilder<UserCardsCubit, UserCardsState>(
builder: (context, state) {
switch (state.state) {
case BaseState.error:
return SizedBox(
width: MediaQuery.sizeOf(context).width,
child: Center(
child: DataErrorWidget(
onReload: () async {
context.read<UserCardsCubit>().getCards();
},
),
),
);
case BaseState.success:
return Padding(
padding: const EdgeInsets.only(left: 12),
child: Row(
spacing: 12,
children: List.generate(state.cards.length + 1, (index) {
if (index < state.cards.length) {
final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7);
return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber);
}
return const CardWidget(enabledAddCard: true);
}),
),
);
default:
return SizedBox(width: MediaQuery.sizeOf(context).width, height: 200, child: const Center(child: Loader()));
}
},
),
),
const SizedBox(height: 16),
],
),
), ),
], );
), },
); );
} }
} }

View File

@@ -36,4 +36,12 @@ class CardsRepository {
rethrow; rethrow;
} }
} }
Future<void> collectCharge({required int chosenPlaId}) async {
try {
await cardsService.collectCharge(chosenPlanId: chosenPlaId);
} catch (e) {
rethrow;
}
}
} }

View File

@@ -24,5 +24,10 @@ abstract class CardsService {
Future<void> deleteCard({ Future<void> deleteCard({
@Path('id') required int id, @Path('id') required int id,
}); });
@POST('/api/chosen_plans/{id}/take_payin')
Future<void> collectCharge({
@Path('id') required int chosenPlanId,
});
} }

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart'; import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart';
import 'package:eatvio_app/features/order_info/presentation/widgets/collect_charge_banner.dart';
import 'package:eatvio_app/features/order_info/presentation/widgets/payment_banner.dart'; import 'package:eatvio_app/features/order_info/presentation/widgets/payment_banner.dart';
import 'package:eatvio_app/features/order_info/presentation/widgets/plan_info.dart'; import 'package:eatvio_app/features/order_info/presentation/widgets/plan_info.dart';
import 'package:eatvio_app/features/order_info/presentation/widgets/plan_props.dart'; import 'package:eatvio_app/features/order_info/presentation/widgets/plan_props.dart';
@@ -61,7 +62,7 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
builder: (context, state) { builder: (context, state) {
final plan = state.chosenPlan; final plan = state.chosenPlan;
if (state.isLoading) { if (state.isLoading) {
Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
s.infoAboutOrder, s.infoAboutOrder,
@@ -113,7 +114,7 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
planStatus = PlanStatusValues.paid; planStatus = PlanStatusValues.paid;
} else if (widget.successfulPayment == false) { } else if (widget.successfulPayment == false) {
planStatus = PlanStatusValues.paymentFailed; planStatus = PlanStatusValues.paymentFailed;
} else if (plan.chosenPlanState == ChosenPlanStateEnum.statePaid) { } else if (plan.chosenPlanState.isStateAfterPaidState()) {
planStatus = PlanStatusValues.paid; planStatus = PlanStatusValues.paid;
} else { } else {
planStatus = PlanStatusValues.pendingPayment; planStatus = PlanStatusValues.pendingPayment;
@@ -134,6 +135,12 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
CollectChargeBanner(
chosenPlan: plan,
price: chosenOption?.price ?? 0,
)
],
if (widget.successfulPayment != null) ...[ if (widget.successfulPayment != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
PaymentBanner( PaymentBanner(
@@ -148,7 +155,10 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
backgroundColor: appTheme.whiteA700, backgroundColor: appTheme.whiteA700,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
PlanProps(planDuration: plan.plan.duration, planPrice: chosenOption?.price.toDouble() ?? 0, planStatus: planStatus), PlanProps(
planDuration: plan.plan.duration,
planPrice: chosenOption?.price ?? 0,
planStatus: planStatus),
const SizedBox(height: 8), const SizedBox(height: 8),
PlanInfo( PlanInfo(
title: plan.plan.name, title: plan.plan.name,

View File

@@ -0,0 +1,72 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class CollectChargeBanner extends StatelessWidget {
const CollectChargeBanner({super.key, required this.chosenPlan, required this.price});
final ChosenPlanModel chosenPlan;
final double price;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Stack(
children: [
Positioned(
left: 0,
top: 0,
bottom: 0,
child: Container(
width: 4,
decoration: BoxDecoration(
color: appTheme.blueA400, borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20))),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 80),
child: Text(
'${s.congratsYouHaveFinishedPlan} «${chosenPlan.plan.name}»',
style: TextStyles.textMSemibold.copyWith(
color: appTheme.black,
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(right: 80),
child: Text(
'${s.userConfirmedPlanCompletion} $price₽',
style: TextStyles.textSMedium.copyWith(
color: appTheme.black,
),
),
),
const SizedBox(height: 12),
CustomOutlinedButton(
text: s.collectCharge,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () async {
GoRouter.of(context).push(CardSettingsScreen.path, extra: chosenPlan.id);
},
)
],
),
)
],
),
);
}
}

View File

@@ -14,6 +14,7 @@ class PaymentBanner extends StatefulWidget {
const PaymentBanner({ const PaymentBanner({
super.key, super.key,
required this.successfulPayment, required this.successfulPayment,
}); });
final bool successfulPayment; final bool successfulPayment;

View File

@@ -237,6 +237,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
'chatScreen', 'chatScreen',
extra: { extra: {
'chatId': chosenPlan.chatId, 'chatId': chosenPlan.chatId,
'sendInitialMessage': true,
}, },
); );
}, },

View File

@@ -33,6 +33,9 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"),
"add": MessageLookupByLibrary.simpleMessage("Добавить"), "add": MessageLookupByLibrary.simpleMessage("Добавить"),
"addAtLeastOneItem": MessageLookupByLibrary.simpleMessage(
"Добавьте хотя бы одину опцию",
),
"add_card": MessageLookupByLibrary.simpleMessage("Привязать карту"), "add_card": MessageLookupByLibrary.simpleMessage("Привязать карту"),
"add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
"add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"),
@@ -55,6 +58,9 @@ class MessageLookup extends MessageLookupByLibrary {
"another_form_of_stage": MessageLookupByLibrary.simpleMessage("этапа"), "another_form_of_stage": MessageLookupByLibrary.simpleMessage("этапа"),
"answer": MessageLookupByLibrary.simpleMessage("Ответить"), "answer": MessageLookupByLibrary.simpleMessage("Ответить"),
"archived": MessageLookupByLibrary.simpleMessage("В архиве"), "archived": MessageLookupByLibrary.simpleMessage("В архиве"),
"atLeast3Symbols": MessageLookupByLibrary.simpleMessage(
"Минимум 3 символа",
),
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"), "authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
"average_pressure": MessageLookupByLibrary.simpleMessage( "average_pressure": MessageLookupByLibrary.simpleMessage(
"Средние показатели давления:", "Средние показатели давления:",
@@ -86,6 +92,10 @@ class MessageLookup extends MessageLookupByLibrary {
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
), ),
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
"chargeApproved": MessageLookupByLibrary.simpleMessage("Выплата одобрена"),
"chargeCollectionFailed": MessageLookupByLibrary.simpleMessage(
"Не удалось оформить выплату",
),
"chat": MessageLookupByLibrary.simpleMessage("Чат"), "chat": MessageLookupByLibrary.simpleMessage("Чат"),
"check_out_video_recipe": MessageLookupByLibrary.simpleMessage( "check_out_video_recipe": MessageLookupByLibrary.simpleMessage(
"Посмотреть видео рецепт", "Посмотреть видео рецепт",
@@ -105,6 +115,7 @@ class MessageLookup extends MessageLookupByLibrary {
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage( "choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
), ),
"chosePlanType": MessageLookupByLibrary.simpleMessage("Выберите тип плана"),
"chose_plan_start_date_2": MessageLookupByLibrary.simpleMessage( "chose_plan_start_date_2": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты плана. Она отобразиться в дневнике пользователя", "Выберите дату старты плана. Она отобразиться в дневнике пользователя",
), ),
@@ -119,6 +130,7 @@ class MessageLookup extends MessageLookupByLibrary {
), ),
"city": MessageLookupByLibrary.simpleMessage("Город"), "city": MessageLookupByLibrary.simpleMessage("Город"),
"city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"), "city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"),
"collectCharge": MessageLookupByLibrary.simpleMessage("Получить выплату"),
"comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), "comfort": MessageLookupByLibrary.simpleMessage("Комфорт"),
"comment": MessageLookupByLibrary.simpleMessage("комментарий"), "comment": MessageLookupByLibrary.simpleMessage("комментарий"),
"commentForMeal": MessageLookupByLibrary.simpleMessage( "commentForMeal": MessageLookupByLibrary.simpleMessage(
@@ -138,9 +150,15 @@ class MessageLookup extends MessageLookupByLibrary {
"Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса.", "Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса.",
), ),
"confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"), "confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"),
"congratsYouHaveFinishedPlan": MessageLookupByLibrary.simpleMessage(
"Поздравляем, Вы закончили план",
),
"consultations_count": MessageLookupByLibrary.simpleMessage( "consultations_count": MessageLookupByLibrary.simpleMessage(
"Кол-во проведенных консулдьтаций", "Кол-во проведенных консулдьтаций",
), ),
"contactSupport": MessageLookupByLibrary.simpleMessage(
"Обратитесь в поддержку",
),
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"),
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"),
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"), "copied": MessageLookupByLibrary.simpleMessage("cкопирован"),
@@ -382,6 +400,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.", "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",
), ),
"milliliter": MessageLookupByLibrary.simpleMessage("мл"), "milliliter": MessageLookupByLibrary.simpleMessage("мл"),
"minPlanPrice": MessageLookupByLibrary.simpleMessage(
"Минимальная сумма 1200₽",
),
"minimal_customer_price": MessageLookupByLibrary.simpleMessage( "minimal_customer_price": MessageLookupByLibrary.simpleMessage(
"Минимальная цена для покупателя: 1200₽", "Минимальная цена для покупателя: 1200₽",
), ),
@@ -393,6 +414,9 @@ class MessageLookup extends MessageLookupByLibrary {
"misrepresentation_description": MessageLookupByLibrary.simpleMessage( "misrepresentation_description": MessageLookupByLibrary.simpleMessage(
"Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение.", "Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение.",
), ),
"moneySentToCard": MessageLookupByLibrary.simpleMessage(
"Средства отправлены на карту",
),
"more_about_order": MessageLookupByLibrary.simpleMessage( "more_about_order": MessageLookupByLibrary.simpleMessage(
"Подробнее о заказе", "Подробнее о заказе",
), ),
@@ -628,6 +652,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Верхние показатели давления:", "Верхние показатели давления:",
), ),
"upper": MessageLookupByLibrary.simpleMessage("Верхнее"), "upper": MessageLookupByLibrary.simpleMessage("Верхнее"),
"userConfirmedPlanCompletion": MessageLookupByLibrary.simpleMessage(
"Пользователь подтвердил прохождения плана. В ближайшее время Вы получите",
),
"user_authorization": MessageLookupByLibrary.simpleMessage( "user_authorization": MessageLookupByLibrary.simpleMessage(
"Аутентификация", "Аутентификация",
), ),

View File

@@ -3064,6 +3064,116 @@ class S {
args: [], args: [],
); );
} }
/// `Поздравляем, Вы закончили план`
String get congratsYouHaveFinishedPlan {
return Intl.message(
'Поздравляем, Вы закончили план',
name: 'congratsYouHaveFinishedPlan',
desc: '',
args: [],
);
}
/// `Пользователь подтвердил прохождения плана. В ближайшее время Вы получите`
String get userConfirmedPlanCompletion {
return Intl.message(
'Пользователь подтвердил прохождения плана. В ближайшее время Вы получите',
name: 'userConfirmedPlanCompletion',
desc: '',
args: [],
);
}
/// `Получить выплату`
String get collectCharge {
return Intl.message(
'Получить выплату',
name: 'collectCharge',
desc: '',
args: [],
);
}
/// `Выплата одобрена`
String get chargeApproved {
return Intl.message(
'Выплата одобрена',
name: 'chargeApproved',
desc: '',
args: [],
);
}
/// `Средства отправлены на карту`
String get moneySentToCard {
return Intl.message(
'Средства отправлены на карту',
name: 'moneySentToCard',
desc: '',
args: [],
);
}
/// `Не удалось оформить выплату`
String get chargeCollectionFailed {
return Intl.message(
'Не удалось оформить выплату',
name: 'chargeCollectionFailed',
desc: '',
args: [],
);
}
/// `Обратитесь в поддержку`
String get contactSupport {
return Intl.message(
'Обратитесь в поддержку',
name: 'contactSupport',
desc: '',
args: [],
);
}
/// `Минимум 3 символа`
String get atLeast3Symbols {
return Intl.message(
'Минимум 3 символа',
name: 'atLeast3Symbols',
desc: '',
args: [],
);
}
/// `Выберите тип плана`
String get chosePlanType {
return Intl.message(
'Выберите тип плана',
name: 'chosePlanType',
desc: '',
args: [],
);
}
/// `Минимальная сумма 1200₽`
String get minPlanPrice {
return Intl.message(
'Минимальная сумма 1200₽',
name: 'minPlanPrice',
desc: '',
args: [],
);
}
/// `Добавьте хотя бы одину опцию`
String get addAtLeastOneItem {
return Intl.message(
'Добавьте хотя бы одину опцию',
name: 'addAtLeastOneItem',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<S> { class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -369,6 +369,17 @@
"orderCanceled": "Заказ отменен", "orderCanceled": "Заказ отменен",
"couldNotCancelOrder": "Не удалось отменить заказ", "couldNotCancelOrder": "Не удалось отменить заказ",
"dayComment": "Комментарий ко всему дню", "dayComment": "Комментарий ко всему дню",
"mealComment": "Комментарий к приему пищи" "mealComment": "Комментарий к приему пищи",
"congratsYouHaveFinishedPlan": "Поздравляем, Вы закончили план",
"userConfirmedPlanCompletion": "Пользователь подтвердил прохождения плана. В ближайшее время Вы получите",
"collectCharge": "Получить выплату",
"chargeApproved": "Выплата одобрена",
"moneySentToCard": "Средства отправлены на карту",
"chargeCollectionFailed": "Не удалось оформить выплату",
"contactSupport": "Обратитесь в поддержку",
"atLeast3Symbols": "Минимум 3 символа",
"chosePlanType": "Выберите тип плана",
"minPlanPrice": "Минимальная сумма 1200₽",
"addAtLeastOneItem": "Добавьте хотя бы одину опцию"
} }