feat(consultant): update consultant info

This commit is contained in:
mso7nueh
2025-06-02 16:05:47 +03:00
parent 8a9a913420
commit f0a184b800
10 changed files with 392 additions and 261 deletions

View File

@@ -293,7 +293,12 @@ class AppRouter {
),
GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
GoRoute(
path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', builder: (context, state) => const StepperBecomeConsultantScreen()),
path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
GoRoute(

View File

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

View File

@@ -8,7 +8,7 @@ part 'consultation_request_model.g.dart';
@freezed
abstract class ConsultationRequestModel with _$ConsultationRequestModel {
const factory ConsultationRequestModel({
@JsonKey(name: 'rating_place') required int ratingPlace,
@JsonKey(name: 'rating_place') int? ratingPlace,
@JsonKey(name: 'education_resource') required EducationModel educationModel,
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
}) = _ConsultationRequestModel;

View File

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

View File

@@ -8,5 +8,6 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
const factory RegisterConsultantState({
required ConsultantProfileModel consultant,
required bool acceptOffer,
required bool loading,
}) = _RegisterConsultantState;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -449,12 +449,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
},
child: Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
@@ -544,12 +549,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
},
child: Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],