Merge pull request 'build 23' (#487) from main into test-flight

Reviewed-on: eatvio/eatvio_app#487
This commit is contained in:
mso7nueh
2025-06-04 08:29:30 +00:00
36 changed files with 722 additions and 382 deletions

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 21;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 21;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 21;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>21</string>
<string>22</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -101,7 +101,6 @@ class AppRouter {
path: '/my-profile-screen',
name: 'myProfileScreen',
redirect: (context, state) async {
await sl<WsManager>().initialize();
return null;
},
pageBuilder: (context, state) {
@@ -113,6 +112,10 @@ class AppRouter {
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
redirect: (context, state) async {
final data = state.extra as Map<String, dynamic>?;
if (data?['needToInitializeWebSocket'] == false) {
return null;
}
await sl<WsManager>().initialize();
return null;
},
@@ -293,7 +296,12 @@ class AppRouter {
),
GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
GoRoute(
path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', builder: (context, state) => const StepperBecomeConsultantScreen()),
path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
GoRoute(

View File

@@ -1,5 +1,5 @@
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
// String baseUrl = 'https://eatvio.ru';
String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];

View File

@@ -120,7 +120,7 @@ Future<void> init() async {
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(
() => DiaryCubit(chatRepository: sl(), diaryRepository: sl(), glossaryRepository: sl(), profileRepository: sl(), dietRepository: sl()));

View File

@@ -18,7 +18,7 @@ class WsManager {
PrivateChannel? _privateDirectChatChannel;
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
Stream<ChannelReadEvent>? get directChatStream => _privateDirectChatChannel?.bind('App\\Events\\ChatMessageUpdated');
@@ -135,4 +135,16 @@ class WsManager {
void unsubscribeDirectChatChannel() {
_privateDirectChatChannel?.unsubscribe();
}
Future<void> clear() async{
await client?.disconnect();
client = null;
userId = null;
_privateDirectChatChannel?.unsubscribe();
_privateMessagesChannel?.unsubscribe();
_privateNotificationChannel?.unsubscribe();
_privateDirectChatChannel = null;
_privateMessagesChannel = null;
_privateNotificationChannel = null;
}
}

View File

@@ -6,7 +6,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
@@ -98,12 +97,10 @@ class CustomPopup extends StatelessWidget {
),
);
});
if (complainedContentType == ComplainedContent.posts || complainedContentType == ComplainedContent.recipe) {
if (complainedContentType == ComplainedContent.posts) {
addItem('assets/icons/edit_paint.svg', 'Редактировать', () async {
if (complainedContentType == ComplainedContent.posts) {
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel);
} else if (complainedContentType == ComplainedContent.recipe) {
GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe);
}
});
}

View File

@@ -1,12 +1,11 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository authRepository;
@@ -28,9 +27,7 @@ class AuthCubit extends Cubit<AuthState> {
token: token,
),
);
showSuccessToast('Успешный вход');
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.go('/feed-screen');
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
log(e.toString());

View File

@@ -5,10 +5,13 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
@@ -49,6 +52,20 @@ class _AuthScreenState extends State<AuthScreen> {
});
}
Future<void> _reconnectWebSocket(BuildContext context) async {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -179,8 +196,7 @@ class _AuthScreenState extends State<AuthScreen> {
);
if (isAuthenticated) {
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
_reconnectWebSocket(context);
}
}
},

View File

@@ -14,18 +14,14 @@ class ChatsCubit extends Cubit<ChatsState> {
final ChatRepository chatRepository;
final ProfileRepository profileRepository;
final WsManager wsManager;
late StreamSubscription? newIncomingMessagesSubscription;
late StreamSubscription? newLastMessageSubscription;
StreamSubscription? newIncomingMessagesSubscription;
StreamSubscription? newLastMessageSubscription;
ChatsCubit({
required this.chatRepository,
required this.wsManager,
required this.profileRepository,
}) : super(ChatsState()) {
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
_handleNewMessageViaSocket(event.data);
});
}) : super(ChatsState()) {
newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) {
_setNewLastMessage(message);
});
@@ -33,6 +29,13 @@ class ChatsCubit extends Cubit<ChatsState> {
String? _query;
int page = 1;
late int lastPage;
Future<void> connectToWebSocket() async {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
_handleNewMessageViaSocket(event.data);
});
}
Future<void> getChatRoomsWithLoading({String? query}) async {
page = 1;
_query = query;

View File

@@ -8,9 +8,10 @@ part 'consultation_request_model.g.dart';
@freezed
abstract class ConsultationRequestModel with _$ConsultationRequestModel {
const factory ConsultationRequestModel({
@JsonKey(name: 'rating_place') required int ratingPlace,
@JsonKey(name: 'rating_place') int? ratingPlace,
@JsonKey(name: 'education_resource') required EducationModel educationModel,
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
@JsonKey(name: 'canceled_moderation') required CanceledModerationModel canceledModeration,
}) = _ConsultationRequestModel;
factory ConsultationRequestModel.fromJson(Map<String, dynamic> json) => _$ConsultationRequestModelFromJson(json);
@@ -24,3 +25,20 @@ abstract class CertPhotosModel with _$CertPhotosModel {
factory CertPhotosModel.fromJson(Map<String, dynamic> json) => _$CertPhotosModelFromJson(json);
}
enum ModerationStatus {
approved,
created,
canceled
}
@freezed
abstract class CanceledModerationModel with _$CanceledModerationModel {
const factory CanceledModerationModel({
required String title,
required ModerationStatus status,
required List<String>? additional,
}) = _CanceledModerationModel;
factory CanceledModerationModel.fromJson(Map<String, dynamic> json) => _$CanceledModerationModelFromJson(json);
}

View File

@@ -1,13 +1,15 @@
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
final ConsultantRepository consultantRepository;
bool _update = false;
RegisterConsultantCubit({
required this.consultantRepository,
@@ -17,12 +19,74 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
socialLinks: SocialLinksModel(),
hasMedicalEducation: true,
),
loading: true,
acceptOffer: false,
),
);
Future<File> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes);
} else {
throw Exception('Не удалось загрузить изображение');
}
}
getMyRequestStatus(String slug) async {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final List<String> photos = await Future.wait(
myRequestStatus.photos.map(
(e) async => (await downloadImageAsFile(e.url)).path,
),
);
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
return CertificatesModel(
title: course.title,
date: course.date,
);
}).toList();
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
photos: photos,
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
),
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
),
);
}
disableLoading() {
emit(state.copyWith(loading: false));
}
updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit(RegisterConsultantState(consultant: consultant, acceptOffer: acceptOffer));
emit(RegisterConsultantState(consultant: consultant, acceptOffer: acceptOffer, loading: false));
}
registerConsultant() async {
@@ -35,7 +99,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
),
);
}
await consultantRepository.registerConsultant(consultant: state.consultant);
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
}
Future<List<String>> uploadPhotos(List<File> files) async {

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'register_consultant_state.freezed.dart';
@@ -8,5 +9,7 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
const factory RegisterConsultantState({
required ConsultantProfileModel consultant,
required bool acceptOffer,
required bool loading,
ConsultationRequestModel? requestModel,
}) = _RegisterConsultantState;
}

View File

@@ -30,19 +30,20 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
void initState() {
super.initState();
if (context.read<RegisterConsultantCubit>().state.consultant.certificates != null) {
diplomasList = context
.read<RegisterConsultantCubit>()
.state
.consultant
.certificates!
.map(
(e) => DiplomaEntity(
date: DateTime.tryParse(e.date),
textEditingController: TextEditingController(text: e.title),
enableDeleteButton: true,
diplomasList = [
...context.read<RegisterConsultantCubit>().state.consultant.certificates!.map(
(e) => DiplomaEntity(
date: DateTime.tryParse(e.date),
textEditingController: TextEditingController(text: e.title),
enableDeleteButton: true,
),
),
)
.toList();
DiplomaEntity(
textEditingController: TextEditingController(),
enableDeleteButton: false,
date: null,
),
];
diplomasList.last.enableDeleteButton = false;
}
}

View File

@@ -1,5 +1,8 @@
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/additional_settings_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/certificates_and_diplomas_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/last_step_screen.dart';
@@ -15,7 +18,9 @@ import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
class StepperBecomeConsultantScreen extends StatefulWidget {
const StepperBecomeConsultantScreen({super.key});
final String? slug;
const StepperBecomeConsultantScreen({super.key, this.slug});
@override
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
@@ -25,249 +30,328 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
double progressValue = 1 / 6;
int step = 1;
final pageController = PageController();
final registrationConsultantCubit = sl<RegisterConsultantCubit>();
late final RegisterConsultantCubit registrationConsultantCubit;
bool isLoading = false;
@override
void initState() {
super.initState();
if (widget.slug == null) {
registrationConsultantCubit = sl<RegisterConsultantCubit>()..disableLoading();
} else {
registrationConsultantCubit = sl<RegisterConsultantCubit>()..getMyRequestStatus(widget.slug!);
}
}
@override
Widget build(BuildContext context) {
return BlocProvider<RegisterConsultantCubit>(
create: (context) => registrationConsultantCubit,
child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: isLoading ? Colors.grey : appTheme.blueA400,
onPressed: isLoading
? null
: () async {
if (step == 6) {
if (registrationConsultantCubit.state.acceptOffer) {
setState(() {
isLoading = true;
});
try {
await registrationConsultantCubit.registerConsultant();
if (!context.mounted) return;
await showModalBottomSheet(
useRootNavigator: true,
showDragHandle: true,
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Спасибо!\nВаши данные отправлены на модерацию',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Lottie.asset(
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
const SizedBox(height: 16),
const Text(
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
InkWell(
onTap: () {
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Закрыть',
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) {
if (state.loading == false) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
),
),
if (state.requestModel?.canceledModeration.status == ModerationStatus.canceled)
InkWell(
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(state.requestModel!.canceledModeration.title),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < state.requestModel!.canceledModeration.additional!.length; i++) ...[
Text('• ${state.requestModel!.canceledModeration.additional![i]}'),
]
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 48,
width: MediaQuery.of(context).size.width * 0.6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
child: const Center(
child: Text(
'Комментарий',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
height: 1,
),
),
),
),
),
Container(
height: 48,
width: state.requestModel?.canceledModeration.status == ModerationStatus.canceled
? MediaQuery.of(context).size.width * 0.2
: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: isLoading
? Colors.grey
: state.requestModel?.canceledModeration.status == ModerationStatus.canceled
? const Color(0xFFEAF2FD)
: appTheme.blueA400,
onPressed: isLoading
? null
: () async {
if (step == 6) {
if (registrationConsultantCubit.state.acceptOffer) {
setState(() {
isLoading = true;
});
try {
await registrationConsultantCubit.registerConsultant();
if (!context.mounted) return;
await showModalBottomSheet(
useRootNavigator: true,
showDragHandle: true,
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Спасибо!\nВаши данные отправлены на модерацию',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 16),
Lottie.asset(
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
const SizedBox(height: 16),
const Text(
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
InkWell(
onTap: () {
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Закрыть',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
),
],
),
),
);
} catch (e) {
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
content: Text(e.toString()),
actions: [
TextButton(
child: const Text('Ок'),
onPressed: () {
Navigator.of(context).pop();
},
);
} catch (e) {
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
content: Text(e.toString()),
actions: [
TextButton(
child: const Text('Ок'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
} finally {
setState(() {
isLoading = false;
});
}
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
content: const Text('Примите договор публичной офёрты'),
actions: [
TextButton(
child: const Text('Ок'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
],
);
}
} else {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: isLoading
? const CircularProgressIndicator()
: state.requestModel?.canceledModeration.status == ModerationStatus.canceled
? Icon(
Icons.arrow_forward_ios_outlined,
color: appTheme.blueA400,
)
: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
);
} finally {
setState(() {
isLoading = false;
});
}
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
content: const Text('Примите договор публичной офёрты'),
actions: [
TextButton(
child: const Text('Ок'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
}
} else {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: isLoading ? const CircularProgressIndicator() : Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 6'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 6,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 6'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 6,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 6;
});
},
children: const [
CertificatesAndDiplomasScreen(),
PhotoOrScansScreen(),
AdditionalSettingsScreen(),
SocialNetworksScreen(),
RegistrationAsScreen(),
LastStepScreen(),
],
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 6;
});
},
children: const [
CertificatesAndDiplomasScreen(),
PhotoOrScansScreen(),
AdditionalSettingsScreen(),
SocialNetworksScreen(),
RegistrationAsScreen(),
LastStepScreen(),
],
),
);
} else {
return const Scaffold(
body: Loader(),
);
}
},
),
);
}

View File

@@ -14,11 +14,18 @@ class ConsultantRepository {
Future<void> registerConsultant({
required ConsultantProfileModel consultant,
bool update = false,
}) async {
try {
return await consultantService.consultantRegistration(
consultantModel: consultant,
);
if (update) {
return await consultantService.consultantUpdate(
consultantModel: consultant,
);
} else {
return await consultantService.consultantRegistration(
consultantModel: consultant,
);
}
} catch (e) {
rethrow;
}
@@ -57,4 +64,12 @@ class ConsultantRepository {
rethrow;
}
}
Future<ConsultationRequestModel> getMyRequestStatus(String slug) async {
try {
return await consultantService.getMyRequestStatus(slug: slug);
} catch (e) {
rethrow;
}
}
}

View File

@@ -24,4 +24,14 @@ abstract class ConsultantService {
Future<ConsultationRequestModel> getConsultantRequestStatus({
@Path() required String slug,
});
@GET('/api/consultant_request/{slug}/status')
Future<ConsultationRequestModel> getMyRequestStatus({
@Path() required String slug,
});
@PUT('/api/consultant_request/update')
Future<void> consultantUpdate({
@Body() required ConsultantProfileModel consultantModel,
});
}

View File

@@ -293,6 +293,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
final diet = Diet(
id: 0,
name: state.dietData.name ?? '',
slug: '',
image: imageInBase64,
duration: state.dietData.duration ?? 0,
state: state.dietData.state ?? DietState.active,

View File

@@ -192,7 +192,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.id}',
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
),
const SizedBox(width: 16),

View File

@@ -21,21 +21,45 @@ class OptionWidget extends StatefulWidget {
}
class _OptionWidgetState extends State<OptionWidget> {
final customerPriceController = TextEditingController()..text = '1200₽';
final firstOptionCustomerPriceController = TextEditingController()..text = '1200₽';
final secondOptionCustomerPriceController = TextEditingController()..text = '1200₽';
final thirdOptionCustomerPriceController = TextEditingController()..text = '1200₽';
final creatorPriceController = TextEditingController()..text = '1000₽';
final optionTitleController = TextEditingController();
final optionDescriptionController = TextEditingController();
final firstOptionTitleController = TextEditingController();
final secondOptionTitleController = TextEditingController();
final thirdOptionTitleController = TextEditingController();
final firstOptionDescriptionController = TextEditingController();
final secondOptionDescriptionController = TextEditingController();
final thirdOptionDescriptionController = TextEditingController();
bool firstExplanationVisible = false;
bool isChecked = false;
bool secondExplanationVisible = false;
@override
void initState() {
super.initState();
customerPriceController.addListener(() {
if (!customerPriceController.text.endsWith('₽')) {
final text = customerPriceController.text.replaceAll('₽', '');
customerPriceController.text = '$text₽';
customerPriceController.selection = TextSelection.collapsed(offset: text.length);
firstOptionCustomerPriceController.addListener(() {
if (!firstOptionCustomerPriceController.text.endsWith('₽')) {
final text = firstOptionCustomerPriceController.text.replaceAll('₽', '');
firstOptionCustomerPriceController.text = '$text₽';
firstOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
}
});
secondOptionCustomerPriceController.addListener(() {
if (!secondOptionCustomerPriceController.text.endsWith('₽')) {
final text = secondOptionCustomerPriceController.text.replaceAll('₽', '');
secondOptionCustomerPriceController.text = '$text₽';
secondOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
}
});
thirdOptionCustomerPriceController.addListener(() {
if (!thirdOptionCustomerPriceController.text.endsWith('₽')) {
final text = thirdOptionCustomerPriceController.text.replaceAll('₽', '');
thirdOptionCustomerPriceController.text = '$text₽';
thirdOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
}
});
}
@@ -45,15 +69,32 @@ class _OptionWidgetState extends State<OptionWidget> {
final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) {
final optionNameHintText = switch (state.selectedOptionIndex ?? 1) {
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
final optionNameHintText = switch (selectedOptionIndex) {
0 => s.economy,
2 => s.comfort,
_ => s.standard,
};
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
optionTitleController.text = state.planData.options[selectedOptionIndex].short;
optionDescriptionController.text = state.planData.options[selectedOptionIndex].description;
final optionTitleController = switch (selectedOptionIndex) {
0 => firstOptionTitleController,
2 => thirdOptionTitleController,
_ => secondOptionTitleController,
};
final optionDescriptionController = switch (selectedOptionIndex) {
0 => firstOptionDescriptionController,
2 => thirdOptionDescriptionController,
_ => secondOptionDescriptionController,
};
final customerPriceController = switch (selectedOptionIndex) {
0 => firstOptionCustomerPriceController,
2 => thirdOptionCustomerPriceController,
_ => secondOptionCustomerPriceController,
};
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
firstOptionDescriptionController.text = state.planData.options[selectedOptionIndex].description;
return MainBackgroundBouble(
padding: const EdgeInsets.all(16),
child: Column(
@@ -151,7 +192,6 @@ class _OptionWidgetState extends State<OptionWidget> {
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError,
onChanged: (value) {
final price = double.tryParse(value.replaceAll('₽', ''));
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
if (price != null) {
context.read<CreatePlanCubit>().setPrice(price);
}

View File

@@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart';
class CreateRecipeCubit extends Cubit<CreateRecipeState> {
final RecipeRepository recipeRepository;
final GlossaryRepository glossaryRepository;
CreateRecipeCubit({
required this.recipeRepository,
required this.glossaryRepository,
@@ -192,7 +193,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: await downloadImageAsFile('$baseUrl/storage/${e.image}'),
image: e.image.isEmpty ? null : await downloadImageAsFile('$baseUrl/storage/${e.image}'),
),
),
);

View File

@@ -318,7 +318,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.id}',
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
@@ -499,7 +499,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.id}',
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
@@ -526,7 +526,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.id}',
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,

View File

@@ -20,6 +20,7 @@ abstract class DietsPagination with _$DietsPagination {
abstract class Diet with _$Diet {
const factory Diet({
@JsonKey(name: "id") required int id,
@JsonKey(name: "slug") required String slug,
@JsonKey(name: "name") required String name,
@JsonKey(name: "image") required String image,
@JsonKey(name: "duration") required int duration,

View File

@@ -90,7 +90,7 @@ class _DietsWidgetState extends State<DietsWidget> {
borderRadius: BorderRadius.circular(4),
child: CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.id}',
path: 'diet/${widget.diet.slug}',
id: widget.diet.id,
complainedContentType: ComplainedContent.diet,
diet: widget.diet,

View File

@@ -62,8 +62,9 @@ class _PlanWidgetState extends State<PlanWidget> {
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.plan.image}',
fit: BoxFit.fitWidth,
fit: BoxFit.cover,
width: MediaQuery.of(context).size.width,
height: 426,
),
),
Positioned(
@@ -95,7 +96,7 @@ class _PlanWidgetState extends State<PlanWidget> {
borderRadius: BorderRadius.circular(4),
child: CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
id: widget.plan.id,
slug: widget.plan.slug,
complainedContentType: ComplainedContent.plan,

View File

@@ -4,7 +4,6 @@ import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -48,14 +47,14 @@ class MenuScreen extends StatelessWidget {
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context),
_tile(
leading: ImageConstant.icnDevTools,
title: 'Для разработчиков',
onTap: () {
GoRouter.of(context).pushNamed(DevToolsScreen.name);
},
),
// _divider(context),
// _tile(
// leading: ImageConstant.icnDevTools,
// title: 'Для разработчиков',
// onTap: () {
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
// },
// ),
_divider(context),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
],

View File

@@ -1,3 +1,5 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
sealed class BaseNotificationModel {
abstract final NotificationType type;
abstract final bool isRead;
@@ -127,7 +129,7 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel {
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ConsultantLeftCommentNotification(
type: type,
date: json['diary_date'],
date: parseDate(json['diary_date']),
isRead: isRead ?? false,
);
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
@@ -11,14 +12,19 @@ class NotificationsCubit extends Cubit<NotificationsState> {
final List<BaseNotificationModel> _notifications = [];
final WsManager wsManager;
final NotificationsRepository notificationsRepository;
StreamSubscription? _newNotificationsSubscription;
NotificationsCubit({
required this.wsManager,
required this.notificationsRepository,
}) : super(const NotificationsState.empty()) {
wsManager.newNotificationsStream?.listen((event) {
}) : super(const NotificationsState.empty());
Future<void> connectToWebSocket() async {
await _newNotificationsSubscription?.cancel();
_newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) {
_handleNotification(event.data);
});
}
BaseNotificationModel _parseNotification(Map<String, dynamic> notificationJson, NotificationType type) {
try {
return switch (type) {

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
@@ -135,7 +136,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.consultantLeftComment(notification.date),
notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')),
);
}
}

View File

@@ -113,7 +113,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
complainedContentType: ComplainedContent.plan,
slug: widget.plan.slug,
),
@@ -143,7 +143,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
complainedContentType: ComplainedContent.plan,
slug: widget.plan.slug,
),
@@ -362,7 +362,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
complainedContentType: ComplainedContent.plan,
slug: widget.plan.slug,
),

View File

@@ -33,6 +33,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -329,7 +330,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(height: 8),
SizedBox(
height: 378,
height: 781,
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
return PlanWidget(
@@ -347,6 +348,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(height: 8),
_bullets(_currentBullet, state.plansList.length),
const SizedBox(height: 8),
],
);
}
@@ -449,12 +451,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
},
child: Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
@@ -544,12 +551,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
},
child: Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
@@ -584,6 +596,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
_educationBlock(String title, String date) {
final parsedDate = DateTime.tryParse(date);
final formattedDate = parsedDate != null
? DateFormat('d MMMM y', 'ru_RU').format(parsedDate)
: date;
return SizedBox(
width: double.infinity,
child: Material(
@@ -605,7 +622,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(height: 4),
Text(
date,
formattedDate,
style: const TextStyle(
height: 1,
color: Color(0xFF6D6F71),
@@ -620,6 +637,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
_socialLinks(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant')) {
return const SizedBox.shrink();

View File

@@ -6,7 +6,6 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
@@ -131,29 +130,6 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
GoRouter.of(context).pushNamed(
RecipeCreateScreen.name,
extra: await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
);
},
text: s.edit,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(

View File

@@ -1,3 +1,5 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
@@ -8,19 +10,31 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsCubit extends Cubit<SettingsState> {
final ProfileRepository profileRepository;
final SettingsRepository settingsRepository;
final ConsultantRepository consultantRepository;
SettingsCubit({
required this.profileRepository,
required this.settingsRepository,
required this.consultantRepository,
}) : super(const SettingsState.loading());
Future<void> init() async {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
try {
emit(SettingsState.loaded(
profileSettings: await settingsRepository.getProfileSettings(),
profileDetails: await profileRepository.getMyProfileDetails(),
profile: await profileRepository.getMyProfile(),
),);
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
} catch (e) {
consultantInfo = null;
}
try {
emit(
SettingsState.loaded(
profileSettings: await settingsRepository.getProfileSettings(),
profileDetails: await profileRepository.getMyProfileDetails(),
profile: profile,
consultationRequestModel: consultantInfo,
),
);
} catch (e) {
emit(SettingsState.error(errorMessage: e.toString()));
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -14,6 +15,7 @@ abstract class SettingsState with _$SettingsState {
required ProfileSettingsModel profileSettings,
required ProfileDetailsModel profileDetails,
required ProfileModel profile,
ConsultationRequestModel? consultationRequestModel,
}) = SettingsStateLoaded;
const factory SettingsState.error({

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
@@ -58,11 +59,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 12),
_aboutEatvio(state),
const SizedBox(height: 12),
if (!state.profile.roles.contains('consultant'))
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _becomeConsultantButton(state),
),
_becomeConsultantButton(state),
_deleteAccountButton(state),
],
),
@@ -355,31 +352,84 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
_becomeConsultantButton(SettingsStateLoaded state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: InkWell(
onTap: () => GoRouter.of(context).pushNamed('becomeConsultantScreen'),
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Стать консультантом',
style: TextStyle(
color: Colors.white,
height: 1,
fontSize: 18,
if (state.consultationRequestModel == null) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () => GoRouter.of(context).pushNamed('becomeConsultantScreen'),
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Стать консультантом',
style: TextStyle(
color: Colors.white,
height: 1,
fontSize: 18,
),
),
),
),
),
),
);
);
} else {
if (state.consultationRequestModel!.canceledModeration.status == ModerationStatus.created) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFEAEAEA),
),
child: const Center(
child: Text(
'Данные на проверке...',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
),
),
);
} else if (state.consultationRequestModel!.canceledModeration.status == ModerationStatus.canceled) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug),
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
child: const Center(
child: Text(
'Заявка обработана',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
),
),
),
);
}
}
return const SizedBox.shrink();
}
_deleteAccountButton(SettingsStateLoaded state) {

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.1.1+1
version: 1.1.1+30
publish_to: 'none'
environment: