forked from eatvio/eatvio_app
feat(consultant_registration): added new consultant registration flow
This commit is contained in:
@@ -220,7 +220,12 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.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.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||
|
||||
@@ -22,6 +22,9 @@ abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
|
||||
|
||||
static const String visibleGroup = 'user_certs';
|
||||
static const String hiddenGroup = 'hidden_user_certs';
|
||||
static const String pendingGroup = 'pending_user_certs';
|
||||
|
||||
bool get isHidden => group == hiddenGroup;
|
||||
|
||||
bool get isActive => group == visibleGroup || group == hiddenGroup;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
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/presentation/bloc/register_consultant_cubit/register_consultant_state.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),
|
||||
loading: true,
|
||||
acceptOffer: false,
|
||||
isUpdating: false,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> getMyRequestStatus(String slug) async {
|
||||
_update = true;
|
||||
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
|
||||
final certPhotos = await _fetchVisibleCertPhotos();
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
|
||||
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
|
||||
final course = courseMap.values.first;
|
||||
@@ -51,12 +51,13 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
loading: false,
|
||||
requestModel: myRequestStatus,
|
||||
certPhotos: certPhotos,
|
||||
isUpdating: _update
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> disableLoading() async {
|
||||
final certPhotos = await _fetchVisibleCertPhotos();
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
emit(state.copyWith(loading: false, certPhotos: certPhotos));
|
||||
}
|
||||
|
||||
@@ -65,33 +66,33 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Future<void> uploadCertPhotos(List<File> files) async {
|
||||
Future<void> addCertPhotos(List<File> files) async {
|
||||
if (files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
await consultantRepository.hideConsultantCertPhoto(id);
|
||||
emit(state.copyWith(certPhotos: state.certPhotos.where((photo) => photo.id != id).toList()));
|
||||
}
|
||||
|
||||
Future<List<ConsultantCertPhotoModel>> _fetchVisibleCertPhotos() async {
|
||||
try {
|
||||
final photos = await consultantRepository.getConsultantCertPhotos();
|
||||
return _onlyVisible(photos);
|
||||
} catch (e) {
|
||||
return [];
|
||||
Future<void> removeCertPhoto(int? id, String? path) async {
|
||||
if (id != null) {
|
||||
emit(state.copyWith(deletedPhotoIds: [...state.deletedPhotoIds, id]));
|
||||
return;
|
||||
}
|
||||
if (path != null) {
|
||||
emit(state.copyWith(newAddedPhotos: state.newAddedPhotos.where((file) => file.uri.path != path).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
List<ConsultantCertPhotoModel> _onlyVisible(List<ConsultantCertPhotoModel> photos) {
|
||||
return photos.where((photo) => !photo.isHidden).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_profile_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 bool acceptOffer,
|
||||
required bool loading,
|
||||
required bool isUpdating,
|
||||
ConsultationRequestModel? requestModel,
|
||||
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
|
||||
@Default(<File>[]) List<File> newAddedPhotos,
|
||||
@Default(<int>[]) List<int> deletedPhotoIds,
|
||||
}) = _RegisterConsultantState;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
||||
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(
|
||||
children: [
|
||||
if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)],
|
||||
@@ -47,7 +49,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == photos.length) {
|
||||
if (index == totalPhotosCount) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => _pickAndUploadPhotos(),
|
||||
@@ -57,10 +59,25 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
),
|
||||
);
|
||||
} 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 {
|
||||
// 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 {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -92,8 +109,8 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removePhoto(int id) async {
|
||||
await context.read<RegisterConsultantCubit>().removeCertPhoto(id);
|
||||
Future<void> _removePhoto({int? id, String? path}) async {
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -19,8 +18,8 @@ import 'package:lottie/lottie.dart';
|
||||
|
||||
class StepperBecomeConsultantScreen extends StatefulWidget {
|
||||
final String? slug;
|
||||
|
||||
const StepperBecomeConsultantScreen({super.key, this.slug});
|
||||
final bool? isCanceled;
|
||||
const StepperBecomeConsultantScreen({super.key, this.slug, this.isCanceled});
|
||||
|
||||
@override
|
||||
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
|
||||
@@ -63,25 +62,17 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: const Color(0xFFEAF2FD),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
||||
});
|
||||
},
|
||||
label: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
if (state.requestModel?.educationModel.requestState == ModerationStatus.canceled)
|
||||
if (widget.isCanceled == true)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
@@ -94,15 +85,10 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
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'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -110,26 +96,18 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
child: Ink(
|
||||
height: 48,
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0x33FFCC00),
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF323F4B), height: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
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.8,
|
||||
padding: const EdgeInsets.only(left: 4, right: 16),
|
||||
@@ -138,18 +116,19 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: isLoading
|
||||
? Colors.grey
|
||||
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? const Color(0xFFEAF2FD)
|
||||
: appTheme.blueA400,
|
||||
: widget.isCanceled == true
|
||||
? const Color(0xFFEAF2FD)
|
||||
: appTheme.blueA400,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (step == 6) {
|
||||
if (widget.isCanceled == true) {
|
||||
return;
|
||||
}
|
||||
if (registrationConsultantCubit.state.acceptOffer) {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
@@ -170,27 +149,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
const Text(
|
||||
'Спасибо!\nВаши данные отправлены на модерацию',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Lottie.asset(
|
||||
'assets/lottie_animations/success.json',
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
@@ -201,18 +168,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Закрыть',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -263,24 +223,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
} else {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
setState(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
||||
});
|
||||
}
|
||||
},
|
||||
label: isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: appTheme.blueA400,
|
||||
)
|
||||
: Text(
|
||||
'Продолжить',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
: widget.isCanceled == true
|
||||
? Icon(Icons.arrow_forward_ios_outlined, color: appTheme.blueA400)
|
||||
: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -288,23 +239,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
],
|
||||
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(
|
||||
@@ -312,10 +251,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 1 / 6,
|
||||
end: progressValue,
|
||||
),
|
||||
tween: Tween<double>(begin: 1 / 6, end: progressValue),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -347,9 +283,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Scaffold(
|
||||
body: Loader(),
|
||||
);
|
||||
return const Scaffold(body: Loader());
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -90,4 +90,13 @@ class ConsultantRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deletePhoto(int id) async {
|
||||
try {
|
||||
await consultantService.deletePhoto(id: id);
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,7 @@ abstract class ConsultantService {
|
||||
|
||||
@POST('/api/v2/consultant_request/cert_photos/un_hide')
|
||||
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});
|
||||
}
|
||||
|
||||
@@ -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/utils/toasts.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/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
|
||||
@@ -24,13 +25,20 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
try {
|
||||
final profile = await profileRepository.getProfileBySlug(slug);
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
final isMyProfile = myProfile.slug == profile.slug;
|
||||
List<ConsultantCertPhotoModel> certPhotos = [];
|
||||
if (isMyProfile) {
|
||||
certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
certPhotos = _filterActivePhotos(certPhotos);
|
||||
}
|
||||
emit(
|
||||
ProfileState.loaded(
|
||||
profile: profile,
|
||||
isMyProfile: myProfile.slug == profile.slug,
|
||||
isMyProfile: isMyProfile,
|
||||
profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
|
||||
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
|
||||
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null,
|
||||
certPhotos: certPhotos,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -44,6 +52,8 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
_currentPeriod = period;
|
||||
try {
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
certPhotos = _filterActivePhotos(certPhotos);
|
||||
emit(
|
||||
ProfileState.loaded(
|
||||
profile: profile,
|
||||
@@ -53,6 +63,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
weightPoints: await profileRepository.getWeight(period: period),
|
||||
circumferencePoints: await profileRepository.getCircumferences(period: period),
|
||||
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null,
|
||||
certPhotos: certPhotos,
|
||||
),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
@@ -77,6 +88,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
circumferencePoints: (state as ProfileStateLoaded).circumferencePoints,
|
||||
consultantInfo: (state as ProfileStateLoaded).consultantInfo,
|
||||
isMyProfile: (state as ProfileStateLoaded).isMyProfile,
|
||||
certPhotos: (state as ProfileStateLoaded).certPhotos,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -131,4 +143,36 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
await profileRepository.deletePhoto();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/feed/models/feed_blog_post_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 List<FeedBlogPostModel> blogPosts,
|
||||
required bool isMyProfile,
|
||||
required List<ConsultantCertPhotoModel> certPhotos,
|
||||
List<WeightModel>? weightPoints,
|
||||
List<CircumferenceModel>? circumferencePoints,
|
||||
ConsultationRequestModel? consultantInfo,
|
||||
|
||||
@@ -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/theme/custom_text_style.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/loader.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/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/toggle_cert_photo_bottom_sheet.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/theme/theme_helper.dart';
|
||||
@@ -117,7 +119,6 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state);
|
||||
} else if (state is ProfileStateError) {
|
||||
return Scaffold(
|
||||
@@ -436,9 +437,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
SingleChildRenderObjectWidget _diplomasAndCertificates(ProfileStateLoaded state) {
|
||||
if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) {
|
||||
if (!state.profile.roles.contains('consultant')) {
|
||||
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(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
@@ -454,7 +462,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug});
|
||||
},
|
||||
child: Text(
|
||||
state.isMyProfile ? 'Редактировать' : '',
|
||||
@@ -477,14 +485,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
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);
|
||||
}
|
||||
if (state.isMyProfile && state.certPhotos.isNotEmpty)
|
||||
...List.generate(state.certPhotos.length, (index) {
|
||||
final photo = state.certPhotos[index];
|
||||
return Row(children: [_diplomaImage(photo.url, state.isMyProfile, photo.isHidden, photo.id), const SizedBox(width: 8)]);
|
||||
})
|
||||
else if (state.consultantInfo!.photos.isNotEmpty)
|
||||
...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(
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0));
|
||||
},
|
||||
child: Hero(
|
||||
tag: url,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160),
|
||||
child: Stack(
|
||||
children: [
|
||||
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(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug});
|
||||
},
|
||||
child: Text(
|
||||
state.isMyProfile ? 'Редактировать' : '',
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -254,12 +254,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (state.consultationRequestModel!.educationModel.requestState == 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)),
|
||||
child: InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug}),
|
||||
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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -267,7 +270,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
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),
|
||||
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug, 'isCanceled': true}),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
|
||||
@@ -467,6 +467,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"height_error": MessageLookupByLibrary.simpleMessage("Введите рост"),
|
||||
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||
"hide_photo_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Фото не будет доступно для пользователей, просматривающих ваш профиль",
|
||||
),
|
||||
"hide_photo_title": MessageLookupByLibrary.simpleMessage("Спрятать фото?"),
|
||||
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
||||
"hips": MessageLookupByLibrary.simpleMessage("Бедра"),
|
||||
"hour": MessageLookupByLibrary.simpleMessage("час"),
|
||||
@@ -832,6 +836,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"unfavorite_foods": MessageLookupByLibrary.simpleMessage(
|
||||
"Нелюбимые продукты",
|
||||
),
|
||||
"unhide_photo_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Фото будет доступно для пользователей, просматривающих ваш профиль",
|
||||
),
|
||||
"unhide_photo_title": MessageLookupByLibrary.simpleMessage(
|
||||
"Показать фото?",
|
||||
),
|
||||
"up_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Верхние показатели давления:",
|
||||
),
|
||||
|
||||
@@ -3978,6 +3978,46 @@ class S {
|
||||
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> {
|
||||
|
||||
@@ -468,6 +468,10 @@
|
||||
"letsInvite": "Давайте пригласим близких в итвио",
|
||||
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
|
||||
"invite": "Пригласить",
|
||||
"comment_hint": "Комментарий"
|
||||
"comment_hint": "Комментарий",
|
||||
"hide_photo_title": "Спрятать фото?",
|
||||
"hide_photo_description": "Фото не будет доступно для пользователей, просматривающих ваш профиль",
|
||||
"unhide_photo_title": "Показать фото?",
|
||||
"unhide_photo_description": "Фото будет доступно для пользователей, просматривающих ваш профиль"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user