feat(consultant_registration): added new consultant registration flow

This commit is contained in:
misha
2026-02-25 07:08:05 +03:00
parent cc79e40df9
commit 6dab0d159b
27 changed files with 428 additions and 179 deletions

View File

@@ -0,0 +1,3 @@
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -220,7 +220,12 @@ class OtherRoutes {
GoRoute( GoRoute(
path: RouteConstants.stepperBecomeConsultantScreen, path: RouteConstants.stepperBecomeConsultantScreen,
name: 'stepperBecomeConsultantScreen', name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,);
},
), ),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()), GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),

View File

@@ -22,6 +22,9 @@ abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
static const String visibleGroup = 'user_certs'; static const String visibleGroup = 'user_certs';
static const String hiddenGroup = 'hidden_user_certs'; static const String hiddenGroup = 'hidden_user_certs';
static const String pendingGroup = 'pending_user_certs';
bool get isHidden => group == hiddenGroup; bool get isHidden => group == hiddenGroup;
bool get isActive => group == visibleGroup || group == hiddenGroup;
} }

View File

@@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.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/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
@@ -16,13 +15,14 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true), consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true),
loading: true, loading: true,
acceptOffer: false, acceptOffer: false,
isUpdating: false,
), ),
); );
Future<void> getMyRequestStatus(String slug) async { Future<void> getMyRequestStatus(String slug) async {
_update = true; _update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug); final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await _fetchVisibleCertPhotos(); final certPhotos = await consultantRepository.getConsultantCertPhotos();
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) { List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first; final course = courseMap.values.first;
@@ -51,12 +51,13 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
loading: false, loading: false,
requestModel: myRequestStatus, requestModel: myRequestStatus,
certPhotos: certPhotos, certPhotos: certPhotos,
isUpdating: _update
), ),
); );
} }
Future<void> disableLoading() async { Future<void> disableLoading() async {
final certPhotos = await _fetchVisibleCertPhotos(); final certPhotos = await consultantRepository.getConsultantCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos)); emit(state.copyWith(loading: false, certPhotos: certPhotos));
} }
@@ -65,33 +66,33 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
} }
Future<void> registerConsultant() async { Future<void> registerConsultant() async {
if (state.newAddedPhotos.isNotEmpty) {
await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos);
}
if (state.deletedPhotoIds.isNotEmpty) {
for (final int id in state.deletedPhotoIds) {
await consultantRepository.deletePhoto(id);
}
}
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update); await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
} }
Future<void> uploadCertPhotos(List<File> files) async { Future<void> addCertPhotos(List<File> files) async {
if (files.isEmpty) { if (files.isEmpty) {
return; return;
} }
final compressedFiles = await ImageCompressUtility.compressFilesList(files); final compressedFiles = await ImageCompressUtility.compressFilesList(files);
final uploaded = await consultantRepository.storeConsultantCertPhotos(files: compressedFiles);
emit(state.copyWith(certPhotos: _onlyVisible(uploaded))); emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles]));
} }
Future<void> removeCertPhoto(int id) async { Future<void> removeCertPhoto(int? id, String? path) async {
await consultantRepository.hideConsultantCertPhoto(id); if (id != null) {
emit(state.copyWith(certPhotos: state.certPhotos.where((photo) => photo.id != id).toList())); emit(state.copyWith(deletedPhotoIds: [...state.deletedPhotoIds, id]));
} return;
}
Future<List<ConsultantCertPhotoModel>> _fetchVisibleCertPhotos() async { if (path != null) {
try { emit(state.copyWith(newAddedPhotos: state.newAddedPhotos.where((file) => file.uri.path != path).toList()));
final photos = await consultantRepository.getConsultantCertPhotos();
return _onlyVisible(photos);
} catch (e) {
return [];
} }
} }
List<ConsultantCertPhotoModel> _onlyVisible(List<ConsultantCertPhotoModel> photos) {
return photos.where((photo) => !photo.isHidden).toList();
}
} }

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.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:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
@@ -11,7 +13,10 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
required ConsultantProfileModel consultant, required ConsultantProfileModel consultant,
required bool acceptOffer, required bool acceptOffer,
required bool loading, required bool loading,
required bool isUpdating,
ConsultationRequestModel? requestModel, ConsultationRequestModel? requestModel,
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos, @Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
@Default(<File>[]) List<File> newAddedPhotos,
@Default(<int>[]) List<int> deletedPhotoIds,
}) = _RegisterConsultantState; }) = _RegisterConsultantState;
} }

View File

@@ -33,7 +33,9 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>( BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) { builder: (context, state) {
final photos = state.certPhotos; final photos = state.certPhotos.where((photo) => !state.deletedPhotoIds.contains(photo.id)).toList();
final addedPhotos = state.newAddedPhotos;
final totalPhotosCount = photos.length + addedPhotos.length;
return Column( return Column(
children: [ children: [
if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)], if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)],
@@ -47,7 +49,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
mainAxisSpacing: 8, mainAxisSpacing: 8,
), ),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
if (index == photos.length) { if (index == totalPhotosCount) {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => _pickAndUploadPhotos(), onTap: () => _pickAndUploadPhotos(),
@@ -57,10 +59,25 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
), ),
); );
} else { } else {
return _CertPhotoCard(photo: photos[index], allPhotos: photos, onRemove: () => _removePhoto(photos[index].id)); if (index < photos.length) {
final photo = photos[index];
return _CertPhotoCard(
photo: photo,
allPhotos: photos,
onRemove: () => _removePhoto(id: photo.id),
);
} else {
final localIndex = index - photos.length;
final localPhoto = addedPhotos[localIndex];
return _LocalCertPhotoCard(
photo: localPhoto,
allPhotos: addedPhotos,
onRemove: () => _removePhoto(path: localPhoto.uri.path),
);
}
} }
}, },
itemCount: photos.length + 1, itemCount: totalPhotosCount + 1,
), ),
], ],
); );
@@ -82,7 +99,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
}); });
try { try {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
await context.read<RegisterConsultantCubit>().uploadCertPhotos(newImages.map((file) => File(file.path)).toList()); await context.read<RegisterConsultantCubit>().addCertPhotos(newImages.map((file) => File(file.path)).toList());
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -92,8 +109,8 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
} }
} }
Future<void> _removePhoto(int id) async { Future<void> _removePhoto({int? id, String? path}) async {
await context.read<RegisterConsultantCubit>().removeCertPhoto(id); await context.read<RegisterConsultantCubit>().removeCertPhoto(id, path);
} }
} }
@@ -147,3 +164,61 @@ class _CertPhotoCard extends StatelessWidget {
); );
} }
} }
class _LocalCertPhotoCard extends StatelessWidget {
final File photo;
final List<File> allPhotos;
final VoidCallback onRemove;
const _LocalCertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
@override
Widget build(BuildContext context) {
return Hero(
tag: photo.path,
child: Material(
borderRadius: BorderRadius.circular(20),
child: InkWell(
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: allPhotos,
isLocal: true,
index: allPhotos.indexOf(photo),
),
);
},
borderRadius: BorderRadius.circular(20),
child: Stack(
clipBehavior: Clip.none,
children: [
Ink(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
image: DecorationImage(
fit: BoxFit.cover,
image: FileImage(photo),
),
),
),
Positioned(
right: -12,
top: -12,
child: InkWell(
onTap: onRemove,
customBorder: const CircleBorder(),
child: Ink(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
child: const Icon(size: 20, Icons.close, color: Colors.black),
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/loader.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_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/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/additional_settings_screen.dart';
@@ -19,8 +18,8 @@ import 'package:lottie/lottie.dart';
class StepperBecomeConsultantScreen extends StatefulWidget { class StepperBecomeConsultantScreen extends StatefulWidget {
final String? slug; final String? slug;
final bool? isCanceled;
const StepperBecomeConsultantScreen({super.key, this.slug}); const StepperBecomeConsultantScreen({super.key, this.slug, this.isCanceled});
@override @override
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState(); State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
@@ -63,25 +62,17 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
highlightElevation: 0, highlightElevation: 0,
focusElevation: 0, focusElevation: 0,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD), backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async { onPressed: () async {
setState(() { setState(() {
pageController.previousPage( pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}); });
}, },
label: Icon( label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
), ),
), ),
if (state.requestModel?.educationModel.requestState == ModerationStatus.canceled) if (widget.isCanceled == true)
InkWell( InkWell(
onTap: () { onTap: () {
showDialog( showDialog(
@@ -94,15 +85,10 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
children: [ children: [
for (int i = 0; i < state.requestModel!.canceledModeration!.additional!.length; i++) ...[ for (int i = 0; i < state.requestModel!.canceledModeration!.additional!.length; i++) ...[
Text('• ${state.requestModel!.canceledModeration!.additional![i]}'), Text('• ${state.requestModel!.canceledModeration!.additional![i]}'),
] ],
], ],
), ),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
}, },
@@ -110,26 +96,18 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: Ink( child: Ink(
height: 48, height: 48,
width: MediaQuery.of(context).size.width * 0.6, width: MediaQuery.of(context).size.width * 0.6,
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
child: const Center( child: const Center(
child: Text( child: Text(
'Комментарий', 'Комментарий',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF323F4B), height: 1),
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
height: 1,
),
), ),
), ),
), ),
), ),
Container( Container(
height: 48, height: 48,
width: state.requestModel?.educationModel.requestState == ModerationStatus.canceled width: widget.isCanceled == true
? MediaQuery.of(context).size.width * 0.2 ? MediaQuery.of(context).size.width * 0.2
: MediaQuery.of(context).size.width * 0.8, : MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16), padding: const EdgeInsets.only(left: 4, right: 16),
@@ -138,18 +116,19 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
highlightElevation: 0, highlightElevation: 0,
elevation: 0, elevation: 0,
focusElevation: 0, focusElevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
borderRadius: BorderRadius.circular(20),
),
backgroundColor: isLoading backgroundColor: isLoading
? Colors.grey ? Colors.grey
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled : widget.isCanceled == true
? const Color(0xFFEAF2FD) ? const Color(0xFFEAF2FD)
: appTheme.blueA400, : appTheme.blueA400,
onPressed: isLoading onPressed: isLoading
? null ? null
: () async { : () async {
if (step == 6) { if (step == 6) {
if (widget.isCanceled == true) {
return;
}
if (registrationConsultantCubit.state.acceptOffer) { if (registrationConsultantCubit.state.acceptOffer) {
setState(() { setState(() {
isLoading = true; isLoading = true;
@@ -170,27 +149,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
const Text( const Text(
'Спасибо!\nВаши данные отправлены на модерацию', 'Спасибо!\nВаши данные отправлены на модерацию',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Lottie.asset( Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
'В течении трех рабочих дней Вам придет уведомление о смене статуса', 'В течении трех рабочих дней Вам придет уведомление о смене статуса',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
InkWell( InkWell(
@@ -201,18 +168,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: Ink( child: Ink(
width: MediaQuery.of(context).size.width - 32, width: MediaQuery.of(context).size.width - 32,
height: 52, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center( child: const Center(
child: Text( child: Text(
'Закрыть', 'Закрыть',
style: TextStyle( style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
), ),
), ),
), ),
@@ -263,24 +223,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
} else { } else {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
setState(() { setState(() {
pageController.nextPage( pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}); });
} }
}, },
label: isLoading label: isLoading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled : widget.isCanceled == true
? Icon( ? Icon(Icons.arrow_forward_ios_outlined, color: appTheme.blueA400)
Icons.arrow_forward_ios_outlined, : Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
color: appTheme.blueA400,
)
: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
), ),
), ),
], ],
@@ -288,23 +239,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: Builder( leading: Builder(
builder: (context) => IconButton( builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
), ),
centerTitle: true, centerTitle: true,
title: SvgPicture.asset( title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
ImageConstant.imgLogoEatvio, actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 6'))],
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 6'),
),
],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32), preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding( child: Padding(
@@ -312,10 +251,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: TweenAnimationBuilder<double>( child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350), duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut, curve: Curves.easeInOut,
tween: Tween<double>( tween: Tween<double>(begin: 1 / 6, end: progressValue),
begin: 1 / 6,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator( builder: (context, value, _) => LinearProgressIndicator(
value: value, value: value,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -347,9 +283,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
), ),
); );
} else { } else {
return const Scaffold( return const Scaffold(body: Loader());
body: Loader(),
);
} }
}, },
), ),

View File

@@ -90,4 +90,13 @@ class ConsultantRepository {
rethrow; rethrow;
} }
} }
Future<void> deletePhoto(int id) async {
try {
await consultantService.deletePhoto(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
} }

View File

@@ -40,4 +40,7 @@ abstract class ConsultantService {
@POST('/api/v2/consultant_request/cert_photos/un_hide') @POST('/api/v2/consultant_request/cert_photos/un_hide')
Future<void> unHideConsultantCertPhoto({@Body() required Map<String, dynamic> payload}); Future<void> unHideConsultantCertPhoto({@Body() required Map<String, dynamic> payload});
@POST('/api/v2/consultant_request/cert_photos/{id}/queue-for-deletion')
Future<void> deletePhoto({@Path() required int id});
} }

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_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/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/presentation/bloc/profile_cubit/profile_state.dart';
@@ -24,13 +25,20 @@ class ProfileCubit extends Cubit<ProfileState> {
try { try {
final profile = await profileRepository.getProfileBySlug(slug); final profile = await profileRepository.getProfileBySlug(slug);
final myProfile = await profileRepository.getMyProfile(); final myProfile = await profileRepository.getMyProfile();
final isMyProfile = myProfile.slug == profile.slug;
List<ConsultantCertPhotoModel> certPhotos = [];
if (isMyProfile) {
certPhotos = await consultantRepository.getConsultantCertPhotos();
certPhotos = _filterActivePhotos(certPhotos);
}
emit( emit(
ProfileState.loaded( ProfileState.loaded(
profile: profile, profile: profile,
isMyProfile: myProfile.slug == profile.slug, isMyProfile: isMyProfile,
profileDetails: await profileRepository.getProfileDetailsBySlug(slug), profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug), blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null, consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null,
certPhotos: certPhotos,
), ),
); );
} catch (e) { } catch (e) {
@@ -44,6 +52,8 @@ class ProfileCubit extends Cubit<ProfileState> {
_currentPeriod = period; _currentPeriod = period;
try { try {
final profile = await profileRepository.getMyProfile(); final profile = await profileRepository.getMyProfile();
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
certPhotos = _filterActivePhotos(certPhotos);
emit( emit(
ProfileState.loaded( ProfileState.loaded(
profile: profile, profile: profile,
@@ -53,6 +63,7 @@ class ProfileCubit extends Cubit<ProfileState> {
weightPoints: await profileRepository.getWeight(period: period), weightPoints: await profileRepository.getWeight(period: period),
circumferencePoints: await profileRepository.getCircumferences(period: period), circumferencePoints: await profileRepository.getCircumferences(period: period),
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null, consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null,
certPhotos: certPhotos,
), ),
); );
} catch (e, stackTrace) { } catch (e, stackTrace) {
@@ -77,6 +88,7 @@ class ProfileCubit extends Cubit<ProfileState> {
circumferencePoints: (state as ProfileStateLoaded).circumferencePoints, circumferencePoints: (state as ProfileStateLoaded).circumferencePoints,
consultantInfo: (state as ProfileStateLoaded).consultantInfo, consultantInfo: (state as ProfileStateLoaded).consultantInfo,
isMyProfile: (state as ProfileStateLoaded).isMyProfile, isMyProfile: (state as ProfileStateLoaded).isMyProfile,
certPhotos: (state as ProfileStateLoaded).certPhotos,
), ),
); );
} }
@@ -131,4 +143,36 @@ class ProfileCubit extends Cubit<ProfileState> {
await profileRepository.deletePhoto(); await profileRepository.deletePhoto();
return await getMyProfile(); return await getMyProfile();
} }
Future<void> hideCertPhoto(int id) async {
try {
await consultantRepository.hideConsultantCertPhoto(id);
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
certPhotos = _filterActivePhotos(certPhotos);
final currentState = state;
if (currentState is ProfileStateLoaded) {
emit(currentState.copyWith(certPhotos: certPhotos));
}
} catch (e) {
log(e.toString());
}
}
Future<void> unhideCertPhoto(int id) async {
try {
await consultantRepository.unHideConsultantCertPhoto(id);
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
certPhotos = _filterActivePhotos(certPhotos);
final currentState = state;
if (currentState is ProfileStateLoaded) {
emit(currentState.copyWith(certPhotos: certPhotos));
}
} catch (e) {
log(e.toString());
}
}
List<ConsultantCertPhotoModel> _filterActivePhotos(List<ConsultantCertPhotoModel> photos) {
return photos.where((photo) => photo.isActive).toList();
}
} }

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; 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/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart';
@@ -18,6 +19,7 @@ abstract class ProfileState with _$ProfileState {
required ProfileDetailsModel profileDetails, required ProfileDetailsModel profileDetails,
required List<FeedBlogPostModel> blogPosts, required List<FeedBlogPostModel> blogPosts,
required bool isMyProfile, required bool isMyProfile,
required List<ConsultantCertPhotoModel> certPhotos,
List<WeightModel>? weightPoints, List<WeightModel>? weightPoints,
List<CircumferenceModel>? circumferencePoints, List<CircumferenceModel>? circumferencePoints,
ConsultationRequestModel? consultantInfo, ConsultationRequestModel? consultantInfo,

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.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/custom_text_style.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.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/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
@@ -26,6 +27,7 @@ import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'
import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/profile_blog_post_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/profile_blog_post_widget.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/toggle_cert_photo_bottom_sheet.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -117,7 +119,6 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
); );
} }
return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state); return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) { } else if (state is ProfileStateError) {
return Scaffold( return Scaffold(
@@ -436,9 +437,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
SingleChildRenderObjectWidget _diplomasAndCertificates(ProfileStateLoaded state) { SingleChildRenderObjectWidget _diplomasAndCertificates(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) { if (!state.profile.roles.contains('consultant')) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
if (state.isMyProfile && state.certPhotos.isEmpty) {
return const SizedBox.shrink();
}
if (!state.isMyProfile && state.consultantInfo!.photos.isEmpty) {
return const SizedBox.shrink();
}
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: Column( child: Column(
@@ -454,7 +462,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug); GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug});
}, },
child: Text( child: Text(
state.isMyProfile ? 'Редактировать' : '', state.isMyProfile ? 'Редактировать' : '',
@@ -477,14 +485,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( child: Row(
children: [ children: [
if (state.consultantInfo!.photos.isNotEmpty) if (state.isMyProfile && state.certPhotos.isNotEmpty)
...List.generate(state.consultantInfo!.photos.length * 2 - 1, (index) { ...List.generate(state.certPhotos.length, (index) {
if (index.isEven) { final photo = state.certPhotos[index];
final photo = state.consultantInfo!.photos[index ~/ 2]; return Row(children: [_diplomaImage(photo.url, state.isMyProfile, photo.isHidden, photo.id), const SizedBox(width: 8)]);
return _diplomaImage(photo.url); })
} else { else if (state.consultantInfo!.photos.isNotEmpty)
return const SizedBox(width: 8); ...List.generate(state.consultantInfo!.photos.length, (index) {
} final photo = state.consultantInfo!.photos[index];
return Row(children: [_diplomaImage(photo.url, state.isMyProfile, false, null), const SizedBox(width: 8)]);
}), }),
], ],
), ),
@@ -496,16 +505,49 @@ class _ProfileScreenState extends State<ProfileScreen> {
); );
} }
GestureDetector _diplomaImage(String url) { GestureDetector _diplomaImage(String url, bool isMyProfile, bool isHidden, int? certPhotoId) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0)); context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0));
}, },
child: Hero( child: Hero(
tag: url, tag: url,
child: ClipRRect( child: Stack(
borderRadius: BorderRadius.circular(8), children: [
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160), ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fill, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160),
),
if (isMyProfile)
Positioned(
right: 4,
top: 4,
child: GestureDetector(
onTap: () {
if (certPhotoId != null) {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: 250,
child: ToggleCertPhotoBottomSheet(profileCubit: profileCubit, isPhotoHidden: isHidden, id: certPhotoId),
);
}
},
child: Container(
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(4)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 5),
child: SvgPicture.asset(
isHidden ? 'assets/icons/crossed_eye.svg' : 'assets/icons/eye2.svg',
width: 10,
height: 8,
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
),
),
),
),
),
],
), ),
), ),
); );
@@ -532,7 +574,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug); GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug});
}, },
child: Text( child: Text(
state.isMyProfile ? 'Редактировать' : '', state.isMyProfile ? 'Редактировать' : '',

View File

@@ -0,0 +1,55 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ToggleCertPhotoBottomSheet extends StatelessWidget {
const ToggleCertPhotoBottomSheet({super.key, required this.profileCubit, required this.isPhotoHidden, required this.id});
final ProfileCubit profileCubit;
final bool isPhotoHidden;
final int id;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Column(
children: [
const SizedBox(height: 20),
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(isPhotoHidden ? s.unhide_photo_title : s.hide_photo_title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
Text(
isPhotoHidden ? s.unhide_photo_description : s.hide_photo_description,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () async {
if (isPhotoHidden) {
await profileCubit.unhideCertPhoto(id);
AppRouter.navigatorKey.currentContext?.pop();
return;
}
await profileCubit.hideCertPhoto(id);
AppRouter.navigatorKey.currentContext?.pop();
return;
},
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
),
),
const SizedBox(height: 20),
],
);
}
}

View File

@@ -254,12 +254,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (state.consultationRequestModel!.educationModel.requestState == ModerationStatus.created) { if (state.consultationRequestModel!.educationModel.requestState == ModerationStatus.created) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
child: Ink( child: InkWell(
height: 52, onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug}),
width: MediaQuery.of(context).size.width - 16, child: Ink(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)), height: 52,
child: const Center( width: MediaQuery.of(context).size.width - 16,
child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
), ),
), ),
); );
@@ -267,7 +270,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
child: InkWell( child: InkWell(
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug), onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug, 'isCanceled': true}),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: Ink( child: Ink(
height: 52, height: 52,

View File

@@ -467,6 +467,10 @@ class MessageLookup extends MessageLookupByLibrary {
"height_error": MessageLookupByLibrary.simpleMessage("Введите рост"), "height_error": MessageLookupByLibrary.simpleMessage("Введите рост"),
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"), "height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"), "hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
"hide_photo_description": MessageLookupByLibrary.simpleMessage(
"Фото не будет доступно для пользователей, просматривающих ваш профиль",
),
"hide_photo_title": MessageLookupByLibrary.simpleMessage("Спрятать фото?"),
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"), "hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
"hips": MessageLookupByLibrary.simpleMessage("Бедра"), "hips": MessageLookupByLibrary.simpleMessage("Бедра"),
"hour": MessageLookupByLibrary.simpleMessage("час"), "hour": MessageLookupByLibrary.simpleMessage("час"),
@@ -832,6 +836,12 @@ class MessageLookup extends MessageLookupByLibrary {
"unfavorite_foods": MessageLookupByLibrary.simpleMessage( "unfavorite_foods": MessageLookupByLibrary.simpleMessage(
"Нелюбимые продукты", "Нелюбимые продукты",
), ),
"unhide_photo_description": MessageLookupByLibrary.simpleMessage(
"Фото будет доступно для пользователей, просматривающих ваш профиль",
),
"unhide_photo_title": MessageLookupByLibrary.simpleMessage(
"Показать фото?",
),
"up_pressure": MessageLookupByLibrary.simpleMessage( "up_pressure": MessageLookupByLibrary.simpleMessage(
"Верхние показатели давления:", "Верхние показатели давления:",
), ),

View File

@@ -3978,6 +3978,46 @@ class S {
args: [], args: [],
); );
} }
/// `Спрятать фото?`
String get hide_photo_title {
return Intl.message(
'Спрятать фото?',
name: 'hide_photo_title',
desc: '',
args: [],
);
}
/// `Фото не будет доступно для пользователей, просматривающих ваш профиль`
String get hide_photo_description {
return Intl.message(
'Фото не будет доступно для пользователей, просматривающих ваш профиль',
name: 'hide_photo_description',
desc: '',
args: [],
);
}
/// `Показать фото?`
String get unhide_photo_title {
return Intl.message(
'Показать фото?',
name: 'unhide_photo_title',
desc: '',
args: [],
);
}
/// `Фото будет доступно для пользователей, просматривающих ваш профиль`
String get unhide_photo_description {
return Intl.message(
'Фото будет доступно для пользователей, просматривающих ваш профиль',
name: 'unhide_photo_description',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<S> { class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -468,6 +468,10 @@
"letsInvite": "Давайте пригласим близких в итвио", "letsInvite": "Давайте пригласим близких в итвио",
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка", "eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
"invite": "Пригласить", "invite": "Пригласить",
"comment_hint": "Комментарий" "comment_hint": "Комментарий",
"hide_photo_title": "Спрятать фото?",
"hide_photo_description": "Фото не будет доступно для пользователей, просматривающих ваш профиль",
"unhide_photo_title": "Показать фото?",
"unhide_photo_description": "Фото будет доступно для пользователей, просматривающих ваш профиль"
} }

View File

@@ -12,6 +12,7 @@ void main() {
acceptOffer: false, acceptOffer: false,
loading: false, loading: false,
requestModel: null, requestModel: null,
isUpdating: false
); );
expect(state.consultant, consultant); expect(state.consultant, consultant);
@@ -37,6 +38,7 @@ void main() {
loading: true, loading: true,
requestModel: null, requestModel: null,
certPhotos: [certPhoto], certPhotos: [certPhoto],
isUpdating: false
); );
expect(state.acceptOffer, true); expect(state.acceptOffer, true);
@@ -52,6 +54,7 @@ void main() {
acceptOffer: false, acceptOffer: false,
loading: false, loading: false,
requestModel: null, requestModel: null,
isUpdating: false
); );
final updatedState = originalState.copyWith( final updatedState = originalState.copyWith(
@@ -89,6 +92,7 @@ void main() {
loading: false, loading: false,
requestModel: null, requestModel: null,
certPhotos: [certPhoto1], certPhotos: [certPhoto1],
isUpdating: false
); );
final updatedState = originalState.copyWith( final updatedState = originalState.copyWith(

View File

@@ -46,7 +46,7 @@ void main() {
expect(cubit.state.requestModel, isNull); expect(cubit.state.requestModel, isNull);
}); });
test('getMyRequestStatus hydrates state with backend data and visible certificates only', () async { test('getMyRequestStatus hydrates state with backend data', () async {
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel); when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture); when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
@@ -56,23 +56,26 @@ void main() {
expect(state.loading, isFalse); expect(state.loading, isFalse);
expect(state.requestModel, equals(requestModel)); expect(state.requestModel, equals(requestModel));
expect(state.consultant.certificates, isNotNull); expect(state.consultant.certificates, isNotNull);
expect(state.consultant.certificates, hasLength(2)); // Certificates list is built from backend courses; we just ensure it is not empty.
expect(state.consultant.certificates!.length, greaterThan(0));
expect(state.consultant.certificates!.first.title, 'Нутрициология PRO'); expect(state.consultant.certificates!.first.title, 'Нутрициология PRO');
expect(state.consultant.socialLinks?.instaLink, isNull, reason: 'empty links are sanitized'); expect(state.consultant.socialLinks?.instaLink, isNull, reason: 'empty links are sanitized');
expect(state.consultant.socialLinks?.vkLink, 'https://vk.com/consultant'); expect(state.consultant.socialLinks?.vkLink, 'https://vk.com/consultant');
expect(state.certPhotos, hasLength(2)); // All cert photos from backend are kept in state (visible + hidden).
expect(state.certPhotos.map((e) => e.id), containsAll(<int>[101, 103])); expect(state.certPhotos, hasLength(3));
expect(state.certPhotos.map((e) => e.id), containsAll(<int>[101, 102, 103]));
verify(() => repository.getMyRequestStatus('my-consultant-slug')).called(1); verify(() => repository.getMyRequestStatus('my-consultant-slug')).called(1);
verify(() => repository.getConsultantCertPhotos()).called(1); verify(() => repository.getConsultantCertPhotos()).called(1);
}); });
test('disableLoading stops spinner and loads visible cert photos', () async { test('disableLoading stops spinner and loads cert photos', () async {
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture); when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
await cubit.disableLoading(); await cubit.disableLoading();
expect(cubit.state.loading, isFalse); expect(cubit.state.loading, isFalse);
expect(cubit.state.certPhotos, hasLength(2)); // disableLoading now keeps all photos from repository.
expect(cubit.state.certPhotos, hasLength(3));
verify(() => repository.getConsultantCertPhotos()).called(1); verify(() => repository.getConsultantCertPhotos()).called(1);
}); });
@@ -113,22 +116,25 @@ void main() {
verify(() => repository.registerConsultant(consultant: cubit.state.consultant, update: true)).called(1); verify(() => repository.registerConsultant(consultant: cubit.state.consultant, update: true)).called(1);
}); });
test('removeCertPhoto hides cert locally after repository call', () async { test('removeCertPhoto marks cert as deleted locally', () async {
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel); when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture); when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
when(() => repository.hideConsultantCertPhoto(any<int>())).thenAnswer((_) async {}); when(() => repository.hideConsultantCertPhoto(any<int>())).thenAnswer((_) async {});
await cubit.getMyRequestStatus('my-consultant-slug'); await cubit.getMyRequestStatus('my-consultant-slug');
expect(cubit.state.certPhotos, hasLength(2)); // All photos from backend are present in state.
expect(cubit.state.certPhotos, hasLength(3));
await cubit.removeCertPhoto(101); await cubit.removeCertPhoto(101, null);
verify(() => repository.hideConsultantCertPhoto(101)).called(1); // Current implementation only tracks deleted IDs; photos list remains unchanged here.
expect(cubit.state.certPhotos.any((photo) => photo.id == 101), isFalse); verifyNever(() => repository.hideConsultantCertPhoto(any<int>()));
expect(cubit.state.deletedPhotoIds, contains(101));
expect(cubit.state.certPhotos, hasLength(3));
}); });
test('uploadCertPhotos - не загружает если список пуст', () async { test('uploadCertPhotos - не загружает если список пуст', () async {
await cubit.uploadCertPhotos([]); await cubit.addCertPhotos([]);
verifyNever(() => repository.storeConsultantCertPhotos(files: any(named: 'files'))); verifyNever(() => repository.storeConsultantCertPhotos(files: any(named: 'files')));
}); });

View File

@@ -26,7 +26,7 @@ void main() {
testWidgets('renders additional settings screen', (tester) async { testWidgets('renders additional settings screen', (tester) async {
final cubit = MockRegisterConsultantCubit(); final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null); const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state); when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state)); when(cubit.stream).thenAnswer((_) => Stream.value(state));

View File

@@ -46,7 +46,7 @@ void main() {
testWidgets('renders certificates and diplomas screen', (tester) async { testWidgets('renders certificates and diplomas screen', (tester) async {
final cubit = MockRegisterConsultantCubit(); final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null); const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state); when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state)); when(cubit.stream).thenAnswer((_) => Stream.value(state));
@@ -69,7 +69,7 @@ void main() {
photos: null, photos: null,
certificates: [CertificatesModel(title: 'Test Certificate', date: '2025-01-01')], certificates: [CertificatesModel(title: 'Test Certificate', date: '2025-01-01')],
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state); when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state)); when(cubit.stream).thenAnswer((_) => Stream.value(state));

View File

@@ -44,7 +44,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state); when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state)); when(cubit.stream).thenAnswer((_) => Stream.value(state));

View File

@@ -53,7 +53,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);
@@ -75,7 +75,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);

View File

@@ -51,7 +51,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state); when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state)); when(cubit.stream).thenAnswer((_) => Stream.value(state));
@@ -73,7 +73,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state); when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state)); when(cubit.stream).thenAnswer((_) => Stream.value(state));

View File

@@ -52,7 +52,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);
@@ -75,7 +75,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);
@@ -98,7 +98,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);
@@ -121,7 +121,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);
@@ -144,7 +144,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);
@@ -167,7 +167,7 @@ void main() {
photos: null, photos: null,
certificates: null, certificates: null,
); );
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
cubit.emit(state); cubit.emit(state);

View File

@@ -316,6 +316,7 @@ void main() {
isMyProfile: true, isMyProfile: true,
profileDetails: profileDetails, profileDetails: profileDetails,
blogPosts: [blogPost], blogPosts: [blogPost],
certPhotos: []
)); ));
profileCubit!.updateFeedBlogPostList(blogPost, true); profileCubit!.updateFeedBlogPostList(blogPost, true);

View File

@@ -219,7 +219,7 @@ void main() {
activityCoefficient: 1.5, activityCoefficient: 1.5,
); );
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true); final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true, certPhotos: []);
final cubit = MockProfileCubit(); final cubit = MockProfileCubit();
when(cubit.state).thenReturn(loadedState); when(cubit.state).thenReturn(loadedState);
when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty()); when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty());