Merge pull request 'Issue-998: Исправление профиля консультанта, исправление экрана подписчиков' (#438) from mso7nueh/eatvio_app:Issue-998 into main

Reviewed-on: eatvio/eatvio_app#438
This commit is contained in:
mso7nueh
2025-05-16 11:08:14 +00:00
10 changed files with 162 additions and 40 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 = 9;
CURRENT_PROJECT_VERSION = 12;
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 = 9;
CURRENT_PROJECT_VERSION = 12;
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 = 9;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

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

View File

@@ -107,7 +107,7 @@ Future<void> init() async {
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));

View File

@@ -0,0 +1,26 @@
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'consultation_request_model.freezed.dart';
part 'consultation_request_model.g.dart';
@freezed
class ConsultationRequestModel with _$ConsultationRequestModel {
const factory ConsultationRequestModel({
@JsonKey(name: 'rating_place') required int ratingPlace,
@JsonKey(name: 'education_resource') required EducationModel educationModel,
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
}) = _ConsultationRequestModel;
factory ConsultationRequestModel.fromJson(Map<String, dynamic> json) => _$ConsultationRequestModelFromJson(json);
}
@freezed
class CertPhotosModel with _$CertPhotosModel {
const factory CertPhotosModel({
required String url,
}) = _CertPhotosModel;
factory CertPhotosModel.fromJson(Map<String, dynamic> json) => _$CertPhotosModelFromJson(json);
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
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:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:http_parser/http_parser.dart';
import 'package:dio/dio.dart';
@@ -33,7 +34,9 @@ class ConsultantRepository {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(
fileBytes,
filename: file.path.split('/').last,
filename: file.path
.split('/')
.last,
contentType: MediaType('application', 'octet-stream'),
);
multipartFiles.add(multipartFile);
@@ -46,4 +49,12 @@ class ConsultantRepository {
rethrow;
}
}
Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async {
try {
return await consultantService.getConsultantRequestStatus(slug: slug);
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
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:retrofit/retrofit.dart';
part 'consultant_service.g.dart';
@@ -18,4 +19,9 @@ abstract class ConsultantService {
Future<List<String>> uploadPhotos({
@Part(name: 'photos[]', contentType: 'multipart/form-data') List<MultipartFile>? photos,
});
@GET('/api/consultant_request/{slug}/status')
Future<ConsultationRequestModel> getConsultantRequestStatus({
@Path() required String slug,
});
}

View File

@@ -26,7 +26,6 @@ class SubModel with _$SubModel {
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: 'profile_photo_url') required String profilePhotoUrl,
required List<String> roles,
@JsonKey(name: 'is_public_profile') required bool isPublicProfile,
}) = _SubModel;
factory SubModel.fromJson(Map<String, Object?> json) => _$SubModelFromJson(json);

View File

@@ -2,6 +2,7 @@ import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
@@ -10,20 +11,24 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class ProfileCubit extends Cubit<ProfileState> {
final ProfileRepository profileRepository;
final ChatRepository chatRepository;
final ConsultantRepository consultantRepository;
String _currentPeriod = '1';
ProfileCubit({
required this.profileRepository,
required this.chatRepository,
required this.consultantRepository,
}) : super(const ProfileState.loading());
Future<void> getProfileBySlug(String slug) async {
try {
final profile = await profileRepository.getProfileBySlug(slug);
emit(
ProfileState.loaded(
profile: await profileRepository.getProfileBySlug(slug),
profile: profile,
profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null,
),
);
} catch (e) {
@@ -44,6 +49,7 @@ class ProfileCubit extends Cubit<ProfileState> {
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug),
weightPoints: await profileRepository.getWeight(period: period),
circumferencePoints: await profileRepository.getCircumferences(period: period),
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null,
),
);
} catch (e, stackTrace) {

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -18,6 +19,7 @@ abstract class ProfileState with _$ProfileState {
required List<FeedBlogPostModel> blogPosts,
List<WeightModel>? weightPoints,
List<CircumferenceModel>? circumferencePoints,
ConsultationRequestModel? consultantInfo,
}) = ProfileStateLoaded;
const factory ProfileState.error({

View File

@@ -93,7 +93,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
} else if (state is ProfileStateError) {
return DataErrorWidget(
onReload: () async {
widget.author == null || widget.author!.authorIsMe ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.author!.authorSlug);
widget.author == null || widget.author!.authorIsMe
? await profileCubit.getMyProfile()
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
},
);
} else {
@@ -144,25 +146,24 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
const SizedBox(width: 12),
if (state.profile.ratingPlace != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.profile.ratingPlace!}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.consultantInfo!.ratingPlace}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
),
),
],
),
if (state.profile.educationResource?.hasMedicalEducation == 1)
if (state.consultantInfo!.educationModel.hasMedicalEducation == 1)
const Row(
children: [
SizedBox(width: 16),
@@ -221,6 +222,61 @@ class _ProfileScreenState extends State<ProfileScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_header(state),
if (state.profile.roles.contains('consultant'))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const SizedBox(width: 16),
const Text(
'Консультант',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(
0xFF323F4B,
),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.consultantInfo!.ratingPlace}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
),
),
],
),
if (state.consultantInfo!.educationModel.hasMedicalEducation == 1)
const Row(
children: [
SizedBox(width: 16),
Icon(
Icons.done_rounded,
color: Colors.green,
),
Text(
'Медицинское образование',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state),
],
),
@@ -371,7 +427,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
_diplomasAndCertificates(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant') || state.profile.diplomasAndCertificates == null || state.profile.diplomasAndCertificates!.isEmpty) {
if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
@@ -416,8 +472,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
if (state.profile.diplomasAndCertificates != null)
...state.profile.diplomasAndCertificates!.map((d) => _diplomaImage('$baseUrl/storage/${d.path}')),
if (state.consultantInfo!.photos.isNotEmpty)
...List.generate(
state.consultantInfo!.photos.length * 2 - 1,
(index) {
if (index.isEven) {
final photo = state.consultantInfo!.photos[index ~/ 2];
return _diplomaImage(photo.url);
} else {
return const SizedBox(width: 8);
}
},
),
],
),
),
@@ -456,8 +522,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
_education(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant') ||
state.profile.educationResource?.completedCourses == null ||
(state.profile.educationResource?.completedCourses?.isEmpty ?? false)) {
state.consultantInfo!.educationModel.completedCourses == null ||
(state.consultantInfo!.educationModel.completedCourses?.isEmpty ?? false)) {
return const SizedBox.shrink();
}
return Padding(
@@ -495,11 +561,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
child: Column(
children: [
if (state.profile.educationResource?.completedCourses != null)
...state.profile.educationResource!.completedCourses!.map((c) => _educationBlock(c.values.first.title, c.values.first.date)),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _educationBlock(state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
state.consultantInfo!.educationModel.completedCourses![index].values.first.date),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.consultantInfo!.educationModel.completedCourses!.length,
),
],
),
),
@@ -564,39 +636,39 @@ class _ProfileScreenState extends State<ProfileScreen> {
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.vkLink ?? '',
state.consultantInfo!.educationModel.vkLink ?? '',
),
mode: LaunchMode.externalApplication,
);
},
'assets/icons/vk.svg',
state.profile.educationResource?.vkLink == null ? true : false,
state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false,
),
const SizedBox(width: 12),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.vkLink ?? '',
state.consultantInfo!.educationModel.tgLink ?? '',
),
mode: LaunchMode.externalApplication,
);
},
'assets/icons/telegram.svg',
state.profile.educationResource?.vkLink == null ? true : false,
state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false,
),
const SizedBox(width: 12),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.personalSiteLink ?? '',
state.consultantInfo!.educationModel.personalSiteLink ?? '',
),
mode: LaunchMode.externalApplication,
);
},
'assets/icons/web.svg',
state.profile.educationResource?.personalSiteLink == null ? true : false,
state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false,
),
],
),
@@ -620,7 +692,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: SvgPicture.asset(
icon,
width: 24,
colorFilter: const ColorFilter.mode(Colors.black12, BlendMode.srcIn),
colorFilter: disabled ? const ColorFilter.mode(Colors.black12, BlendMode.srcIn) : const ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
),
),
@@ -659,7 +731,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
for (int i = 0; i < state.profile.badges.length; i++) ...[
state.profile.badges[i].image.contains('svg')
? Tooltip(
message: state.profile.badges[i].description,
message: state.profile.badges[i].description,
triggerMode: TooltipTriggerMode.tap,
textStyle: const TextStyle(
color: Colors.black,