forked from eatvio/eatvio_app
feat(UGPC): issue-777
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart
|
||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/widgets/option_widget.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/widgets/options_count_button.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart';
|
||||
@@ -25,11 +26,23 @@ import 'package:go_router/go_router.dart';
|
||||
class CreatePlanScreen extends StatelessWidget {
|
||||
static const name = 'createPlanScreen';
|
||||
static const path = '/create-Plan-screen';
|
||||
const CreatePlanScreen({super.key});
|
||||
const CreatePlanScreen({super.key, this.plan});
|
||||
|
||||
final PlanModel? plan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(create: (context) => sl<CreatePlanCubit>(), child: const CreatePlanScreenContent());
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
final cubit = sl<CreatePlanCubit>();
|
||||
final existingPlan = plan;
|
||||
if (existingPlan != null) {
|
||||
cubit.loadExisting(existingPlan);
|
||||
}
|
||||
return cubit;
|
||||
},
|
||||
child: const CreatePlanScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,17 +60,19 @@ class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
final isEditing = context.read<CreatePlanCubit>().sourcePlan != null;
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
buildWhen: (previous, current) => previous.planCreated != current.planCreated || previous.isLoading != current.isLoading,
|
||||
builder: (context, state) {
|
||||
if (state.planCreated) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
final isEditing = context.read<CreatePlanCubit>().sourcePlan != null;
|
||||
context.pop(true);
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.planData.name ?? 'План', isPlan: true),
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.planData.name ?? 'План', isPlan: true, isUpdated: isEditing),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -68,7 +83,7 @@ class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.adding_plan,
|
||||
isEditing ? 'Редактирование плана' : s.adding_plan,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
@@ -102,7 +117,9 @@ class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
return Column(
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
key: const ValueKey('plan-name'),
|
||||
hintText: s.plan_label_desc,
|
||||
initialValue: state.planData.name,
|
||||
hasError: state.validationState.titleHasError,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setName(value);
|
||||
@@ -144,6 +161,7 @@ class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
children: [
|
||||
MarkdownEditor(
|
||||
key: _mdKey,
|
||||
initialText: state.planData.details,
|
||||
hintText: s.plan_desc_example,
|
||||
hasError: state.validationState.descriptionHasError,
|
||||
minLines: 6,
|
||||
@@ -230,7 +248,7 @@ class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
onPressed: () async {
|
||||
await context.read<CreatePlanCubit>().publishPlan();
|
||||
},
|
||||
text: s.publish,
|
||||
text: isEditing ? 'Обновить' : s.publish,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
|
||||
@@ -4,16 +4,57 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.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:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
|
||||
class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
final CreatePlanRepository createPlanRepository;
|
||||
PlanModel? sourcePlan;
|
||||
|
||||
CreatePlanCubit({required this.createPlanRepository}) : super(CreatePlanState.initial());
|
||||
|
||||
void loadExisting(PlanModel plan) {
|
||||
sourcePlan = plan;
|
||||
final durationIndex = durationValues.indexOf(plan.duration);
|
||||
emit(
|
||||
state.copyWith(
|
||||
durationIndex: durationIndex >= 0 ? durationIndex : null,
|
||||
selectedOptionIndex: plan.packetsCount == 3 ? 0 : null,
|
||||
planData: state.planData.copyWith(
|
||||
name: plan.name,
|
||||
details: plan.details,
|
||||
duration: plan.duration,
|
||||
state: plan.state,
|
||||
type: [
|
||||
plan.type.slim ?? false,
|
||||
plan.type.fit ?? false,
|
||||
plan.type.wellness ?? false,
|
||||
plan.type.behavior ?? false,
|
||||
],
|
||||
options: [
|
||||
_packetOrEmpty(plan.packet1Options),
|
||||
plan.packetsCount == 3 ? _packetOrEmpty(plan.packet2Options) : PacketOptions.empty(),
|
||||
plan.packetsCount == 3 ? _packetOrEmpty(plan.packet3Options) : PacketOptions.empty(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PacketOptions _packetOrEmpty(PacketOptions? option) {
|
||||
if (option == null) {
|
||||
return PacketOptions.empty();
|
||||
}
|
||||
final price = option.price;
|
||||
final normalizedPrice = price is num ? price.toDouble() : double.tryParse('$price') ?? 1200;
|
||||
return option.copyWith(price: normalizedPrice);
|
||||
}
|
||||
|
||||
void setDetails(String text) {
|
||||
emit(state.copyWith(planData: state.planData.copyWith(details: text)));
|
||||
validateDetails();
|
||||
@@ -147,7 +188,7 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
}
|
||||
|
||||
bool validateImage() {
|
||||
if (state.planData.image == null) {
|
||||
if (state.planData.image == null && sourcePlan == null) {
|
||||
emit(state.copyWith.validationState(imageHasError: true));
|
||||
return false;
|
||||
}
|
||||
@@ -413,31 +454,34 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
'type[fit]': state.planData.type[1] ? '1' : '0',
|
||||
'type[wellness]': state.planData.type[2] ? '1' : '0',
|
||||
'type[behavior]': state.planData.type[3] ? '1' : '0',
|
||||
'image': state.planData.image != null
|
||||
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(state.planData.image!.path))).path,
|
||||
filename: state.planData.image!.name)
|
||||
: null
|
||||
});
|
||||
if (state.planData.image != null) {
|
||||
final image = state.planData.image!;
|
||||
planMap['image'] = await MultipartFile.fromFile(
|
||||
(await ImageCompressUtility.compressFile(File(image.path))).path,
|
||||
filename: image.name,
|
||||
);
|
||||
}
|
||||
if (state.planData.options[1].short.isNotEmpty) {
|
||||
planMap.addAll({
|
||||
'packets_count': 3,
|
||||
'packet1_options': {
|
||||
'short': state.planData.options[0].short,
|
||||
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
||||
'price': _optionPrice(state.planData.options[0].price),
|
||||
'description_md': state.planData.options[0].description,
|
||||
'items': Map.fromIterable(state.planData.options[0].items),
|
||||
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
'packet2_options': {
|
||||
'short': state.planData.options[1].short,
|
||||
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
|
||||
'price': _optionPrice(state.planData.options[1].price),
|
||||
'description_md': state.planData.options[1].description,
|
||||
'items': Map.fromIterable(state.planData.options[1].items),
|
||||
'has_personal_diet': state.planData.options[1].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
'packet3_options': {
|
||||
'short': state.planData.options[2].short,
|
||||
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
|
||||
'price': _optionPrice(state.planData.options[2].price),
|
||||
'description_md': state.planData.options[2].description,
|
||||
'items': Map.fromIterable(state.planData.options[2].items),
|
||||
'has_personal_diet': state.planData.options[2].hasPersonalDiet ? '1' : '0',
|
||||
@@ -448,20 +492,34 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
'packets_count': 1,
|
||||
'packet1_options': {
|
||||
'short': state.planData.options[0].short,
|
||||
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
||||
'price': _optionPrice(state.planData.options[0].price),
|
||||
'description_md': state.planData.options[0].description,
|
||||
'items': Map.fromIterable(state.planData.options[0].items),
|
||||
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
});
|
||||
}
|
||||
await createPlanRepository.createPlan(model: planMap);
|
||||
final editingPlan = sourcePlan;
|
||||
if (editingPlan == null) {
|
||||
await createPlanRepository.createPlan(model: planMap);
|
||||
} else {
|
||||
await createPlanRepository.updatePublishedPlan(slug: editingPlan.slug, model: planMap);
|
||||
}
|
||||
emit(state.copyWith(isLoading: false, planCreated: true));
|
||||
}
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
log(e.toString());
|
||||
emit(state.copyWith(isLoading: false, planCreated: true));
|
||||
emit(state.copyWith(isLoading: false, planCreated: false));
|
||||
final isForbidden = e is DioException && e.response?.statusCode == 403;
|
||||
showErrorToast(isForbidden ? 'Редактирование недоступно: клиент уже начал проходить план' : 'Не удалось сохранить план');
|
||||
}
|
||||
}
|
||||
|
||||
int _optionPrice(dynamic price) {
|
||||
if (price is num) {
|
||||
return price.round();
|
||||
}
|
||||
return int.parse(price.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -91,6 +93,18 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
builder: (context, state) {
|
||||
final selectedImage = state.planData.image;
|
||||
final existingImage = context.read<CreatePlanCubit>().sourcePlan?.image;
|
||||
final hasCover = selectedImage != null || (existingImage != null && existingImage.isNotEmpty);
|
||||
final ImageProvider? coverImage;
|
||||
if (selectedImage != null) {
|
||||
coverImage = FileImage(File(selectedImage.path));
|
||||
} else if (existingImage != null && existingImage.isNotEmpty) {
|
||||
final url = existingImage.startsWith('http') ? existingImage : '${sl<AppConfig>().baseUrl}/storage/$existingImage';
|
||||
coverImage = NetworkImage(url);
|
||||
} else {
|
||||
coverImage = null;
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
@@ -101,12 +115,12 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null,
|
||||
image: coverImage != null ? DecorationImage(fit: BoxFit.cover, image: coverImage) : 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: hasCover ? null : const EdgeInsets.all(8),
|
||||
child: selectedImage != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 6, right: 6),
|
||||
child: GestureDetector(
|
||||
@@ -131,6 +145,8 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
|
||||
),
|
||||
),
|
||||
)
|
||||
: hasCover
|
||||
? const SizedBox.shrink()
|
||||
: Stack(
|
||||
children: [
|
||||
DottedBorder(
|
||||
|
||||
Reference in New Issue
Block a user