forked from eatvio/eatvio_app
Merge pull request 'consultant_scoring' (#1011) from misha/eatvio_app:consultant_scoring into consultant_scoring
Reviewed-on: eatvio/eatvio_app#1011
This commit is contained in:
@@ -265,7 +265,15 @@ class OtherRoutes {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Consultant routes
|
// Consultant routes
|
||||||
GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
|
GoRoute(
|
||||||
|
path: RouteConstants.becomeConsultantScreen,
|
||||||
|
name: 'becomeConsultantScreen',
|
||||||
|
builder: (context, state) {
|
||||||
|
final data = state.extra as Map<String, dynamic>?;
|
||||||
|
final slug = data?['slug'] as String?;
|
||||||
|
return BecomeConsultantScreen(slug: slug);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.stepperBecomeConsultantScreen,
|
path: RouteConstants.stepperBecomeConsultantScreen,
|
||||||
name: 'stepperBecomeConsultantScreen',
|
name: 'stepperBecomeConsultantScreen',
|
||||||
@@ -273,7 +281,12 @@ class OtherRoutes {
|
|||||||
final data = state.extra as Map<String, dynamic>;
|
final data = state.extra as Map<String, dynamic>;
|
||||||
final slug = data['slug'] as String?;
|
final slug = data['slug'] as String?;
|
||||||
final isCanceled = data['isCanceled'] as bool?;
|
final isCanceled = data['isCanceled'] as bool?;
|
||||||
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled);
|
final isNewRegistration = data['isNewRegistration'] as bool? ?? false;
|
||||||
|
return StepperBecomeConsultantScreen(
|
||||||
|
slug: slug,
|
||||||
|
isCanceled: isCanceled,
|
||||||
|
isNewRegistration: isNewRegistration,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
enum AlfaScoringStatus {
|
||||||
|
@JsonValue('none')
|
||||||
|
none,
|
||||||
|
@JsonValue('beneficiary_creating')
|
||||||
|
beneficiaryCreating,
|
||||||
|
@JsonValue('beneficiary_failed')
|
||||||
|
beneficiaryFailed,
|
||||||
|
@JsonValue('scoring_pending')
|
||||||
|
scoringPending,
|
||||||
|
@JsonValue('scoring_complete')
|
||||||
|
scoringComplete,
|
||||||
|
@JsonValue('scoring_failed')
|
||||||
|
scoringFailed;
|
||||||
|
|
||||||
|
static AlfaScoringStatus fromString(String? value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'beneficiary_creating':
|
||||||
|
return AlfaScoringStatus.beneficiaryCreating;
|
||||||
|
case 'beneficiary_failed':
|
||||||
|
return AlfaScoringStatus.beneficiaryFailed;
|
||||||
|
case 'scoring_pending':
|
||||||
|
return AlfaScoringStatus.scoringPending;
|
||||||
|
case 'scoring_complete':
|
||||||
|
return AlfaScoringStatus.scoringComplete;
|
||||||
|
case 'scoring_failed':
|
||||||
|
return AlfaScoringStatus.scoringFailed;
|
||||||
|
case 'none':
|
||||||
|
case null:
|
||||||
|
case '':
|
||||||
|
return AlfaScoringStatus.none;
|
||||||
|
default:
|
||||||
|
return AlfaScoringStatus.none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isProcessing =>
|
||||||
|
this == AlfaScoringStatus.beneficiaryCreating ||
|
||||||
|
this == AlfaScoringStatus.scoringPending;
|
||||||
|
|
||||||
|
bool get isFailed =>
|
||||||
|
this == AlfaScoringStatus.beneficiaryFailed ||
|
||||||
|
this == AlfaScoringStatus.scoringFailed;
|
||||||
|
|
||||||
|
bool get canRetry => isFailed;
|
||||||
|
|
||||||
|
bool get isComplete => this == AlfaScoringStatus.scoringComplete;
|
||||||
|
|
||||||
|
bool get isTerminal => isComplete || isFailed;
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
|
|||||||
|
|
||||||
const ConsultantCertPhotoModel._();
|
const ConsultantCertPhotoModel._();
|
||||||
|
|
||||||
factory ConsultantCertPhotoModel.fromJson(Map<String, dynamic> json) => _$ConsultantCertPhotoModelFromJson(json);
|
factory ConsultantCertPhotoModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ConsultantCertPhotoModelFromJson(json);
|
||||||
|
|
||||||
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';
|
||||||
|
|||||||
@@ -9,35 +9,50 @@ abstract class ConsultantProfileModel with _$ConsultantProfileModel {
|
|||||||
const ConsultantProfileModel._();
|
const ConsultantProfileModel._();
|
||||||
|
|
||||||
const factory ConsultantProfileModel({
|
const factory ConsultantProfileModel({
|
||||||
@JsonKey(name: 'has_medical_education', includeIfNull: false) bool? hasMedicalEducation,
|
@JsonKey(name: 'has_medical_education', includeIfNull: false)
|
||||||
@JsonKey(name: 'social_links', includeIfNull: false) SocialLinksModel? socialLinks,
|
bool? hasMedicalEducation,
|
||||||
|
@JsonKey(name: 'social_links', includeIfNull: false)
|
||||||
|
SocialLinksModel? socialLinks,
|
||||||
@JsonKey(name: 'cert_photos', includeIfNull: false) List<String>? photos,
|
@JsonKey(name: 'cert_photos', includeIfNull: false) List<String>? photos,
|
||||||
@JsonKey(name: 'completed_courses', includeIfNull: false) List<CertificatesModel>? certificates,
|
@JsonKey(name: 'completed_courses', includeIfNull: false)
|
||||||
|
List<CertificatesModel>? certificates,
|
||||||
@JsonKey(name: 'legal_type', includeIfNull: false) String? legalType,
|
@JsonKey(name: 'legal_type', includeIfNull: false) String? legalType,
|
||||||
@JsonKey(name: 'last_name', includeIfNull: false) String? lastName,
|
@JsonKey(name: 'last_name', includeIfNull: false) String? lastName,
|
||||||
@JsonKey(name: 'first_name', includeIfNull: false) String? firstName,
|
@JsonKey(name: 'first_name', includeIfNull: false) String? firstName,
|
||||||
@JsonKey(name: 'middle_name', includeIfNull: false) String? middleName,
|
@JsonKey(name: 'middle_name', includeIfNull: false) String? middleName,
|
||||||
@JsonKey(name: 'inn', includeIfNull: false) String? inn,
|
@JsonKey(name: 'inn', includeIfNull: false) String? inn,
|
||||||
@JsonKey(name: 'snils', includeIfNull: false) String? snils,
|
@JsonKey(name: 'snils', includeIfNull: false) String? snils,
|
||||||
@JsonKey(name: 'passport_series', includeIfNull: false) String? passportSeries,
|
@JsonKey(name: 'passport_series', includeIfNull: false)
|
||||||
@JsonKey(name: 'passport_number', includeIfNull: false) String? passportNumber,
|
String? passportSeries,
|
||||||
@JsonKey(name: 'passport_issue_date', includeIfNull: false) String? passportIssueDate,
|
@JsonKey(name: 'passport_number', includeIfNull: false)
|
||||||
@JsonKey(name: 'passport_issuer', includeIfNull: false) String? passportIssuer,
|
String? passportNumber,
|
||||||
@JsonKey(name: 'passport_issuer_code', includeIfNull: false) String? passportIssuerCode,
|
@JsonKey(name: 'passport_issue_date', includeIfNull: false)
|
||||||
|
String? passportIssueDate,
|
||||||
|
@JsonKey(name: 'passport_issuer', includeIfNull: false)
|
||||||
|
String? passportIssuer,
|
||||||
|
@JsonKey(name: 'passport_issuer_code', includeIfNull: false)
|
||||||
|
String? passportIssuerCode,
|
||||||
@JsonKey(name: 'birth_date', includeIfNull: false) String? birthDate,
|
@JsonKey(name: 'birth_date', includeIfNull: false) String? birthDate,
|
||||||
@JsonKey(name: 'birth_place', includeIfNull: false) String? birthPlace,
|
@JsonKey(name: 'birth_place', includeIfNull: false) String? birthPlace,
|
||||||
@JsonKey(name: 'registration_address', includeIfNull: false) String? registrationAddress,
|
@JsonKey(name: 'registration_address', includeIfNull: false)
|
||||||
@JsonKey(name: 'residence_address', includeIfNull: false) String? residenceAddress,
|
String? registrationAddress,
|
||||||
|
@JsonKey(name: 'residence_address', includeIfNull: false)
|
||||||
|
String? residenceAddress,
|
||||||
@JsonKey(name: 'citizenship', includeIfNull: false) String? citizenship,
|
@JsonKey(name: 'citizenship', includeIfNull: false) String? citizenship,
|
||||||
@JsonKey(name: 'tax_resident_us', includeIfNull: false) bool? taxResidentUs,
|
@JsonKey(name: 'tax_resident_us', includeIfNull: false) bool? taxResidentUs,
|
||||||
@JsonKey(name: 'foreign_tax_resident', includeIfNull: false) bool? foreignTaxResident,
|
@JsonKey(name: 'foreign_tax_resident', includeIfNull: false)
|
||||||
|
bool? foreignTaxResident,
|
||||||
@JsonKey(name: 'sbp_phone', includeIfNull: false) String? sbpPhone,
|
@JsonKey(name: 'sbp_phone', includeIfNull: false) String? sbpPhone,
|
||||||
@JsonKey(name: 'sbp_bank_member_id', includeIfNull: false) String? sbpBankMemberId,
|
@JsonKey(name: 'sbp_bank_member_id', includeIfNull: false)
|
||||||
@JsonKey(name: 'agreement_accepted', includeIfNull: false) bool? agreementAccepted,
|
String? sbpBankMemberId,
|
||||||
@JsonKey(name: 'frdo_agreement_accepted', includeIfNull: false) bool? frdoAgreementAccepted,
|
@JsonKey(name: 'agreement_accepted', includeIfNull: false)
|
||||||
|
bool? agreementAccepted,
|
||||||
|
@JsonKey(name: 'frdo_agreement_accepted', includeIfNull: false)
|
||||||
|
bool? frdoAgreementAccepted,
|
||||||
}) = _ConsultantProfileModel;
|
}) = _ConsultantProfileModel;
|
||||||
|
|
||||||
factory ConsultantProfileModel.fromJson(Map<String, Object?> json) => _$ConsultantProfileModelFromJson(json);
|
factory ConsultantProfileModel.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ConsultantProfileModelFromJson(json);
|
||||||
|
|
||||||
bool get hasValidPassportData {
|
bool get hasValidPassportData {
|
||||||
final last = lastName ?? '';
|
final last = lastName ?? '';
|
||||||
@@ -79,10 +94,12 @@ abstract class SocialLinksModel with _$SocialLinksModel {
|
|||||||
@JsonKey(name: 'tg_link', includeIfNull: false) String? tgLink,
|
@JsonKey(name: 'tg_link', includeIfNull: false) String? tgLink,
|
||||||
@JsonKey(name: 'fb_link', includeIfNull: false) String? fbLink,
|
@JsonKey(name: 'fb_link', includeIfNull: false) String? fbLink,
|
||||||
@JsonKey(name: 'insta_link', includeIfNull: false) String? instaLink,
|
@JsonKey(name: 'insta_link', includeIfNull: false) String? instaLink,
|
||||||
@JsonKey(name: 'personal_site_link', includeIfNull: false) String? personalSiteLink,
|
@JsonKey(name: 'personal_site_link', includeIfNull: false)
|
||||||
|
String? personalSiteLink,
|
||||||
}) = _SocialLinksModel;
|
}) = _SocialLinksModel;
|
||||||
|
|
||||||
factory SocialLinksModel.fromJson(Map<String, Object?> json) => _$SocialLinksModelFromJson(json);
|
factory SocialLinksModel.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SocialLinksModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -92,5 +109,6 @@ abstract class CertificatesModel with _$CertificatesModel {
|
|||||||
required String date,
|
required String date,
|
||||||
}) = _CertificatesModel;
|
}) = _CertificatesModel;
|
||||||
|
|
||||||
factory CertificatesModel.fromJson(Map<String, Object?> json) => _$CertificatesModelFromJson(json);
|
factory CertificatesModel.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$CertificatesModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.dart';
|
||||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
@@ -11,11 +12,13 @@ abstract class ConsultationRequestModel with _$ConsultationRequestModel {
|
|||||||
@JsonKey(name: 'rating_place') int? ratingPlace,
|
@JsonKey(name: 'rating_place') int? ratingPlace,
|
||||||
@JsonKey(name: 'education_resource') required EducationModel educationModel,
|
@JsonKey(name: 'education_resource') required EducationModel educationModel,
|
||||||
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
|
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
|
||||||
@JsonKey(name: 'canceled_moderation') CanceledModerationModel? canceledModeration,
|
@JsonKey(name: 'canceled_moderation')
|
||||||
|
CanceledModerationModel? canceledModeration,
|
||||||
@JsonKey(name: 'legal_entity_resource') LegalEntityModel? legalEntity,
|
@JsonKey(name: 'legal_entity_resource') LegalEntityModel? legalEntity,
|
||||||
}) = _ConsultationRequestModel;
|
}) = _ConsultationRequestModel;
|
||||||
|
|
||||||
factory ConsultationRequestModel.fromJson(Map<String, dynamic> json) => _$ConsultationRequestModelFromJson(json);
|
factory ConsultationRequestModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ConsultationRequestModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -28,7 +31,13 @@ abstract class LegalEntityModel with _$LegalEntityModel {
|
|||||||
String? inn,
|
String? inn,
|
||||||
String? snils,
|
String? snils,
|
||||||
String? state,
|
String? state,
|
||||||
@JsonKey(name: 'alfa_scoring_status') String? alfaScoringStatus,
|
@JsonKey(
|
||||||
|
name: 'alfa_scoring_status',
|
||||||
|
unknownEnumValue: AlfaScoringStatus.none,
|
||||||
|
defaultValue: AlfaScoringStatus.none,
|
||||||
|
)
|
||||||
|
@Default(AlfaScoringStatus.none)
|
||||||
|
AlfaScoringStatus alfaScoringStatus,
|
||||||
@JsonKey(name: 'alfa_scoring_request_id') String? alfaScoringRequestId,
|
@JsonKey(name: 'alfa_scoring_request_id') String? alfaScoringRequestId,
|
||||||
@JsonKey(name: 'last_name') String? lastName,
|
@JsonKey(name: 'last_name') String? lastName,
|
||||||
@JsonKey(name: 'first_name') String? firstName,
|
@JsonKey(name: 'first_name') String? firstName,
|
||||||
@@ -49,7 +58,8 @@ abstract class LegalEntityModel with _$LegalEntityModel {
|
|||||||
@JsonKey(name: 'sbp_bank_member_id') String? sbpBankMemberId,
|
@JsonKey(name: 'sbp_bank_member_id') String? sbpBankMemberId,
|
||||||
}) = _LegalEntityModel;
|
}) = _LegalEntityModel;
|
||||||
|
|
||||||
factory LegalEntityModel.fromJson(Map<String, dynamic> json) => _$LegalEntityModelFromJson(json);
|
factory LegalEntityModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LegalEntityModelFromJson(json);
|
||||||
|
|
||||||
/// Normalizes API date (`Y-m-d` or ISO datetime) to `Y-m-d`.
|
/// Normalizes API date (`Y-m-d` or ISO datetime) to `Y-m-d`.
|
||||||
static String? normalizeApiDate(String? value) {
|
static String? normalizeApiDate(String? value) {
|
||||||
@@ -62,25 +72,21 @@ abstract class LegalEntityModel with _$LegalEntityModel {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get normalizedPassportIssueDate => normalizeApiDate(passportIssueDate);
|
String? get normalizedPassportIssueDate =>
|
||||||
|
normalizeApiDate(passportIssueDate);
|
||||||
|
|
||||||
String? get normalizedBirthDate => normalizeApiDate(birthDate);
|
String? get normalizedBirthDate => normalizeApiDate(birthDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class CertPhotosModel with _$CertPhotosModel {
|
abstract class CertPhotosModel with _$CertPhotosModel {
|
||||||
const factory CertPhotosModel({
|
const factory CertPhotosModel({required String url}) = _CertPhotosModel;
|
||||||
required String url,
|
|
||||||
}) = _CertPhotosModel;
|
|
||||||
|
|
||||||
factory CertPhotosModel.fromJson(Map<String, dynamic> json) => _$CertPhotosModelFromJson(json);
|
factory CertPhotosModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CertPhotosModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ModerationStatus {
|
enum ModerationStatus { approved, created, canceled }
|
||||||
approved,
|
|
||||||
created,
|
|
||||||
canceled,
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class CanceledModerationModel with _$CanceledModerationModel {
|
abstract class CanceledModerationModel with _$CanceledModerationModel {
|
||||||
@@ -90,5 +96,6 @@ abstract class CanceledModerationModel with _$CanceledModerationModel {
|
|||||||
required List<String>? additional,
|
required List<String>? additional,
|
||||||
}) = _CanceledModerationModel;
|
}) = _CanceledModerationModel;
|
||||||
|
|
||||||
factory CanceledModerationModel.fromJson(Map<String, dynamic> json) => _$CanceledModerationModelFromJson(json);
|
factory CanceledModerationModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CanceledModerationModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ abstract class SbpBankModel with _$SbpBankModel {
|
|||||||
String? bic,
|
String? bic,
|
||||||
}) = _SbpBankModel;
|
}) = _SbpBankModel;
|
||||||
|
|
||||||
factory SbpBankModel.fromJson(Map<String, dynamic> json) => _$SbpBankModelFromJson(json);
|
factory SbpBankModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SbpBankModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,52 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/error_reporting/report_from.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/features/consultant/become_consultant/enums/alfa_scoring_status.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';
|
||||||
import 'package:eatvio_app/features/consultant/become_consultant/models/sbp_bank_model.dart';
|
import 'package:eatvio_app/features/consultant/become_consultant/models/sbp_bank_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';
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/utils/laravel_validation_errors.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
|
||||||
|
|
||||||
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||||
final ConsultantRepository consultantRepository;
|
final ConsultantRepository consultantRepository;
|
||||||
|
final Duration scoringPollInterval;
|
||||||
bool _update = false;
|
bool _update = false;
|
||||||
|
String? _userSlug;
|
||||||
|
Timer? _scoringPollTimer;
|
||||||
|
|
||||||
static const String individualPersonLegalType = 'individualPerson';
|
static const String individualPersonLegalType = 'individualPerson';
|
||||||
|
|
||||||
RegisterConsultantCubit({required this.consultantRepository})
|
RegisterConsultantCubit({
|
||||||
: super(
|
required this.consultantRepository,
|
||||||
const RegisterConsultantState(
|
this.scoringPollInterval = const Duration(seconds: 3),
|
||||||
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true, legalType: individualPersonLegalType),
|
}) : super(
|
||||||
loading: true,
|
const RegisterConsultantState(
|
||||||
acceptOffer: false,
|
consultant: ConsultantProfileModel(
|
||||||
acceptFrdoAgreement: false,
|
socialLinks: SocialLinksModel(),
|
||||||
isUpdating: false,
|
hasMedicalEducation: true,
|
||||||
),
|
legalType: individualPersonLegalType,
|
||||||
);
|
),
|
||||||
|
loading: true,
|
||||||
|
acceptOffer: false,
|
||||||
|
acceptFrdoAgreement: false,
|
||||||
|
isUpdating: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
String? get userSlug => _userSlug;
|
||||||
|
|
||||||
|
void setUserSlug(String? slug) {
|
||||||
|
if (slug != null && slug.isNotEmpty) {
|
||||||
|
_userSlug = slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<SbpBankModel>> _loadSbpBanks() async {
|
Future<List<SbpBankModel>> _loadSbpBanks() async {
|
||||||
try {
|
try {
|
||||||
@@ -45,7 +67,9 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
final name = bank.bankName.toLowerCase();
|
final name = bank.bankName.toLowerCase();
|
||||||
final shortName = (bank.shortName ?? '').toLowerCase();
|
final shortName = (bank.shortName ?? '').toLowerCase();
|
||||||
final bic = (bank.bic ?? '').toLowerCase();
|
final bic = (bank.bic ?? '').toLowerCase();
|
||||||
return name.contains(normalized) || shortName.contains(normalized) || bic.contains(normalized);
|
return name.contains(normalized) ||
|
||||||
|
shortName.contains(normalized) ||
|
||||||
|
bic.contains(normalized);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,21 +88,29 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
Future<void> getMyRequestStatus(String slug) async {
|
Future<void> getMyRequestStatus(String slug) async {
|
||||||
try {
|
try {
|
||||||
_update = true;
|
_update = true;
|
||||||
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
|
_userSlug = slug;
|
||||||
|
final myRequestStatus = await consultantRepository.getMyRequestStatus(
|
||||||
|
slug,
|
||||||
|
);
|
||||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||||
final sbpBanks = await _loadSbpBanks();
|
final sbpBanks = await _loadSbpBanks();
|
||||||
|
|
||||||
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
|
List<CertificatesModel>? certificates = myRequestStatus
|
||||||
final course = courseMap.values.first;
|
.educationModel
|
||||||
|
.completedCourses
|
||||||
|
?.map((courseMap) {
|
||||||
|
final course = courseMap.values.first;
|
||||||
|
|
||||||
return CertificatesModel(title: course.title, date: course.date);
|
return CertificatesModel(title: course.title, date: course.date);
|
||||||
}).toList();
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
String? sanitizeLink(String? link) {
|
String? sanitizeLink(String? link) {
|
||||||
return (link != null && link.isNotEmpty) ? link : null;
|
return (link != null && link.isNotEmpty) ? link : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final legal = myRequestStatus.legalEntity;
|
final legal = myRequestStatus.legalEntity;
|
||||||
|
final scoringStatus = legal?.alfaScoringStatus ?? AlfaScoringStatus.none;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
RegisterConsultantState(
|
RegisterConsultantState(
|
||||||
@@ -89,9 +121,12 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
|
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
|
||||||
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
|
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
|
||||||
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
|
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
|
||||||
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
|
personalSiteLink: sanitizeLink(
|
||||||
|
myRequestStatus.educationModel.personalSiteLink,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
|
hasMedicalEducation:
|
||||||
|
myRequestStatus.educationModel.hasMedicalEducation == 1,
|
||||||
legalType: legal?.type ?? individualPersonLegalType,
|
legalType: legal?.type ?? individualPersonLegalType,
|
||||||
lastName: _emptyToNull(legal?.lastName),
|
lastName: _emptyToNull(legal?.lastName),
|
||||||
firstName: _emptyToNull(legal?.firstName),
|
firstName: _emptyToNull(legal?.firstName),
|
||||||
@@ -120,13 +155,30 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
certPhotos: certPhotos,
|
certPhotos: certPhotos,
|
||||||
sbpBanks: sbpBanks,
|
sbpBanks: sbpBanks,
|
||||||
isUpdating: _update,
|
isUpdating: _update,
|
||||||
|
alfaScoringStatus: scoringStatus,
|
||||||
|
isScoringPolling: scoringStatus.isProcessing,
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
fieldErrors: const <String, String>{},
|
||||||
error: null,
|
error: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (scoringStatus.isProcessing) {
|
||||||
|
startScoringPoll();
|
||||||
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
emit(state.copyWith(loading: false, error: e.toString()));
|
final parsed = parseLaravelValidationErrors(e);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
loading: false,
|
||||||
|
error: parsed.summary,
|
||||||
|
fieldErrors: parsed.fieldErrors,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +189,9 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> disableLoading() async {
|
Future<void> disableLoading({String? slug}) async {
|
||||||
try {
|
try {
|
||||||
|
setUserSlug(slug);
|
||||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||||
final sbpBanks = await _loadSbpBanks();
|
final sbpBanks = await _loadSbpBanks();
|
||||||
emit(
|
emit(
|
||||||
@@ -147,6 +200,8 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
certPhotos: certPhotos,
|
certPhotos: certPhotos,
|
||||||
sbpBanks: sbpBanks,
|
sbpBanks: sbpBanks,
|
||||||
error: null,
|
error: null,
|
||||||
|
fieldErrors: const <String, String>{},
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
consultant: state.consultant.copyWith(
|
consultant: state.consultant.copyWith(
|
||||||
legalType: individualPersonLegalType,
|
legalType: individualPersonLegalType,
|
||||||
citizenship: state.consultant.citizenship,
|
citizenship: state.consultant.citizenship,
|
||||||
@@ -158,7 +213,15 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
emit(state.copyWith(loading: false, error: e.toString()));
|
final parsed = parseLaravelValidationErrors(e);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
loading: false,
|
||||||
|
error: parsed.summary,
|
||||||
|
fieldErrors: parsed.fieldErrors,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,11 +231,15 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
if (slug != null) {
|
if (slug != null) {
|
||||||
await getMyRequestStatus(slug);
|
await getMyRequestStatus(slug);
|
||||||
} else {
|
} else {
|
||||||
await disableLoading();
|
await disableLoading(slug: _userSlug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool? acceptOffer, bool? acceptFrdoAgreement]) {
|
void updateConsultantProfileModel(
|
||||||
|
ConsultantProfileModel consultant, [
|
||||||
|
bool? acceptOffer,
|
||||||
|
bool? acceptFrdoAgreement,
|
||||||
|
]) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
consultant: consultant,
|
consultant: consultant,
|
||||||
@@ -180,12 +247,19 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
acceptFrdoAgreement: acceptFrdoAgreement ?? state.acceptFrdoAgreement,
|
acceptFrdoAgreement: acceptFrdoAgreement ?? state.acceptFrdoAgreement,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
fieldErrors: const <String, String>{},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateAgreements({bool? acceptOffer, bool? acceptFrdoAgreement}) {
|
void updateAgreements({bool? acceptOffer, bool? acceptFrdoAgreement}) {
|
||||||
emit(state.copyWith(acceptOffer: acceptOffer ?? state.acceptOffer, acceptFrdoAgreement: acceptFrdoAgreement ?? state.acceptFrdoAgreement, error: null));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
acceptOffer: acceptOffer ?? state.acceptOffer,
|
||||||
|
acceptFrdoAgreement: acceptFrdoAgreement ?? state.acceptFrdoAgreement,
|
||||||
|
error: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasAcceptedRequiredAgreements {
|
bool get hasAcceptedRequiredAgreements {
|
||||||
@@ -202,7 +276,13 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
taxResidentUs: state.consultant.taxResidentUs ?? false,
|
taxResidentUs: state.consultant.taxResidentUs ?? false,
|
||||||
foreignTaxResident: state.consultant.foreignTaxResident ?? false,
|
foreignTaxResident: state.consultant.foreignTaxResident ?? false,
|
||||||
certificates: state.consultant.certificates
|
certificates: state.consultant.certificates
|
||||||
?.map((course) => CertificatesModel(title: course.title, date: LegalEntityModel.normalizeApiDate(course.date) ?? course.date))
|
?.map(
|
||||||
|
(course) => CertificatesModel(
|
||||||
|
title: course.title,
|
||||||
|
date:
|
||||||
|
LegalEntityModel.normalizeApiDate(course.date) ?? course.date,
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,40 +315,241 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return withLegalType.copyWith(agreementAccepted: state.acceptOffer, frdoAgreementAccepted: state.acceptFrdoAgreement);
|
return withLegalType.copyWith(
|
||||||
|
agreementAccepted: state.acceptOffer,
|
||||||
|
frdoAgreementAccepted: state.acceptFrdoAgreement,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> registerConsultant() async {
|
Future<void> registerConsultant() async {
|
||||||
emit(state.copyWith(error: null));
|
if (!state.consultant.hasValidPassportData) {
|
||||||
|
emit(state.copyWith(error: 'Заполните паспортные данные'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show scoring widget immediately while request / job are in progress.
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
error: null,
|
||||||
|
fieldErrors: const <String, String>{},
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
awaitingScoringOutcome: true,
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!state.consultant.hasValidPassportData) {
|
|
||||||
emit(state.copyWith(error: 'Заполните паспортные данные'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.newAddedPhotos.isNotEmpty) {
|
if (state.newAddedPhotos.isNotEmpty) {
|
||||||
await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos);
|
await consultantRepository.storeConsultantCertPhotos(
|
||||||
|
files: state.newAddedPhotos,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (state.deletedPhotoIds.isNotEmpty) {
|
if (state.deletedPhotoIds.isNotEmpty) {
|
||||||
for (final int id in state.deletedPhotoIds) {
|
for (final int id in state.deletedPhotoIds) {
|
||||||
await consultantRepository.deletePhoto(id);
|
await consultantRepository.deletePhoto(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await consultantRepository.registerConsultant(consultant: buildSubmitPayload(), update: _update);
|
await consultantRepository.registerConsultant(
|
||||||
|
consultant: buildSubmitPayload(),
|
||||||
|
update: _update,
|
||||||
|
);
|
||||||
|
_update = true;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isUpdating: true,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
isScoringPolling: true,
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
awaitingScoringOutcome: true,
|
||||||
|
newAddedPhotos: const <File>[],
|
||||||
|
deletedPhotoIds: const <int>[],
|
||||||
|
fieldErrors: const <String, String>{},
|
||||||
|
error: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
startScoringPoll();
|
||||||
|
} on DioException catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString());
|
||||||
|
final parsed = parseLaravelValidationErrors(e);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
error: parsed.summary,
|
||||||
|
fieldErrors: parsed.fieldErrors,
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
isScoringPolling: false,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.none,
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
emit(state.copyWith(error: e.toString()));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
error: e.toString(),
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
isScoringPolling: false,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.none,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> retryScoring() async {
|
||||||
|
if (!state.alfaScoringStatus.canRetry || state.scoringRetryInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
scoringRetryInProgress: true,
|
||||||
|
error: null,
|
||||||
|
fieldErrors: const <String, String>{},
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
isScoringPolling: true,
|
||||||
|
awaitingScoringOutcome: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_update = true;
|
||||||
|
try {
|
||||||
|
if (!state.consultant.hasValidPassportData) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
isScoringPolling: false,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.scoringFailed,
|
||||||
|
error: 'Заполните паспортные данные',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await consultantRepository.registerConsultant(
|
||||||
|
consultant: buildSubmitPayload(),
|
||||||
|
update: true,
|
||||||
|
);
|
||||||
|
startScoringPoll();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
isUpdating: true,
|
||||||
|
awaitingScoringOutcome: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on DioException catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString());
|
||||||
|
final parsed = parseLaravelValidationErrors(e);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
isScoringPolling: false,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.scoringFailed,
|
||||||
|
error: parsed.summary,
|
||||||
|
fieldErrors: parsed.fieldErrors,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString());
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
scoringRetryInProgress: false,
|
||||||
|
isScoringPolling: false,
|
||||||
|
awaitingScoringOutcome: false,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.scoringFailed,
|
||||||
|
error: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startScoringPoll() {
|
||||||
|
final slug = _userSlug;
|
||||||
|
if (slug == null || slug.isEmpty) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(isScoringPolling: false, awaitingScoringOutcome: false),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_stopScoringPoll();
|
||||||
|
emit(state.copyWith(isScoringPolling: true));
|
||||||
|
_scoringPollTimer = Timer.periodic(
|
||||||
|
scoringPollInterval,
|
||||||
|
(_) => _pollScoringStatus(slug),
|
||||||
|
);
|
||||||
|
unawaited(_pollScoringStatus(slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pollScoringStatus(String slug) async {
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final status = await consultantRepository.getMyRequestStatus(slug);
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final scoringStatus =
|
||||||
|
status.legalEntity?.alfaScoringStatus ?? AlfaScoringStatus.none;
|
||||||
|
// API stays on `none` until ProcessConsultantScoringJob starts
|
||||||
|
// (ScoringStarted → beneficiary_creating). Keep optimistic processing
|
||||||
|
// UI and continue polling until a real terminal/processing status arrives.
|
||||||
|
if (scoringStatus == AlfaScoringStatus.none) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
requestModel: status,
|
||||||
|
alfaScoringStatus: AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
isScoringPolling: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final isTerminal = scoringStatus.isTerminal;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
requestModel: status,
|
||||||
|
alfaScoringStatus: scoringStatus,
|
||||||
|
isScoringPolling: scoringStatus.isProcessing,
|
||||||
|
// Clear session flag on terminal so success sheet can fire once
|
||||||
|
// (listenWhen: previous.awaiting && !current.awaiting && complete).
|
||||||
|
awaitingScoringOutcome: isTerminal
|
||||||
|
? false
|
||||||
|
: state.awaitingScoringOutcome,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!scoringStatus.isProcessing) {
|
||||||
|
_stopScoringPoll();
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString());
|
||||||
|
// Keep polling on transient errors while scoring is in progress.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopScoringPoll() {
|
||||||
|
_scoringPollTimer?.cancel();
|
||||||
|
_scoringPollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> addCertPhotos(List<File> files) async {
|
Future<void> addCertPhotos(List<File> files) async {
|
||||||
if (files.isEmpty) {
|
if (files.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
|
final compressedFiles = await ImageCompressUtility.compressFilesList(
|
||||||
|
files,
|
||||||
|
);
|
||||||
|
|
||||||
emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles], error: null));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles],
|
||||||
|
error: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
@@ -282,7 +563,19 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
emit(state.copyWith(newAddedPhotos: state.newAddedPhotos.where((file) => file.uri.path != path).toList()));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
newAddedPhotos: state.newAddedPhotos
|
||||||
|
.where((file) => file.uri.path != path)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_stopScoringPoll();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.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_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';
|
||||||
@@ -17,10 +18,18 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
|
|||||||
required bool loading,
|
required bool loading,
|
||||||
required bool isUpdating,
|
required bool isUpdating,
|
||||||
ConsultationRequestModel? requestModel,
|
ConsultationRequestModel? requestModel,
|
||||||
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
|
@Default(<ConsultantCertPhotoModel>[])
|
||||||
|
List<ConsultantCertPhotoModel> certPhotos,
|
||||||
@Default(<SbpBankModel>[]) List<SbpBankModel> sbpBanks,
|
@Default(<SbpBankModel>[]) List<SbpBankModel> sbpBanks,
|
||||||
@Default(<File>[]) List<File> newAddedPhotos,
|
@Default(<File>[]) List<File> newAddedPhotos,
|
||||||
@Default(<int>[]) List<int> deletedPhotoIds,
|
@Default(<int>[]) List<int> deletedPhotoIds,
|
||||||
|
@Default(AlfaScoringStatus.none) AlfaScoringStatus alfaScoringStatus,
|
||||||
|
@Default(false) bool isScoringPolling,
|
||||||
|
@Default(false) bool scoringRetryInProgress,
|
||||||
|
|
||||||
|
/// True only after successful register/retry in this session until scoring is terminal.
|
||||||
|
@Default(false) bool awaitingScoringOutcome,
|
||||||
|
@Default(<String, String>{}) Map<String, String> fieldErrors,
|
||||||
String? error,
|
String? error,
|
||||||
}) = _RegisterConsultantState;
|
}) = _RegisterConsultantState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ class AdditionalSettingsScreen extends StatefulWidget {
|
|||||||
const AdditionalSettingsScreen({super.key});
|
const AdditionalSettingsScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdditionalSettingsScreen> createState() => _AdditionalSettingsScreenState();
|
State<AdditionalSettingsScreen> createState() =>
|
||||||
|
_AdditionalSettingsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
|
class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
|
||||||
@@ -17,7 +18,12 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if (context.read<RegisterConsultantCubit>().state.consultant.hasMedicalEducation != null) {
|
if (context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.hasMedicalEducation !=
|
||||||
|
null) {
|
||||||
haveMedicalDegree = context
|
haveMedicalDegree = context
|
||||||
.read<RegisterConsultantCubit>()
|
.read<RegisterConsultantCubit>()
|
||||||
.state
|
.state
|
||||||
@@ -52,7 +58,8 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
|
|||||||
value: publishDiplomasAndCertificatesInProfile,
|
value: publishDiplomasAndCertificatesInProfile,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
publishDiplomasAndCertificatesInProfile = !publishDiplomasAndCertificatesInProfile;
|
publishDiplomasAndCertificatesInProfile =
|
||||||
|
!publishDiplomasAndCertificatesInProfile;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -64,11 +71,15 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
haveMedicalDegree = !haveMedicalDegree;
|
haveMedicalDegree = !haveMedicalDegree;
|
||||||
});
|
});
|
||||||
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
|
context
|
||||||
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
|
.read<RegisterConsultantCubit>()
|
||||||
hasMedicalEducation: haveMedicalDegree,
|
.updateConsultantProfileModel(
|
||||||
),
|
context
|
||||||
);
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.copyWith(hasMedicalEducation: haveMedicalDegree),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class BecomeConsultantScreen extends StatefulWidget {
|
class BecomeConsultantScreen extends StatefulWidget {
|
||||||
const BecomeConsultantScreen({super.key});
|
final String? slug;
|
||||||
|
|
||||||
|
const BecomeConsultantScreen({super.key, this.slug});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BecomeConsultantScreen> createState() => _BecomeConsultantScreenState();
|
State<BecomeConsultantScreen> createState() => _BecomeConsultantScreenState();
|
||||||
@@ -17,7 +19,10 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
|
|||||||
title: const Text('Стать консультантом'),
|
title: const Text('Стать консультантом'),
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)),
|
leading: IconButton(
|
||||||
|
onPressed: () => GoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: const Padding(
|
body: const Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
@@ -32,16 +37,27 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
|
|||||||
|
|
||||||
InkWell _startRegistrationButton() {
|
InkWell _startRegistrationButton() {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}),
|
onTap: () => GoRouter.of(context).pushReplacementNamed(
|
||||||
|
'stepperBecomeConsultantScreen',
|
||||||
|
extra: {'slug': widget.slug, 'isNewRegistration': true},
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
height: 52,
|
height: 52,
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: appTheme.blueA400,
|
||||||
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Начать регистрацию',
|
'Начать регистрацию',
|
||||||
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
height: 1,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ class CertificatesAndDiplomasScreen extends StatefulWidget {
|
|||||||
const CertificatesAndDiplomasScreen({super.key});
|
const CertificatesAndDiplomasScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CertificatesAndDiplomasScreen> createState() => _CertificatesAndDiplomasScreenState();
|
State<CertificatesAndDiplomasScreen> createState() =>
|
||||||
|
_CertificatesAndDiplomasScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasScreen> {
|
class _CertificatesAndDiplomasScreenState
|
||||||
|
extends State<CertificatesAndDiplomasScreen> {
|
||||||
final scrollController = ScrollController();
|
final scrollController = ScrollController();
|
||||||
|
|
||||||
List<DiplomaEntity> diplomasList = [
|
List<DiplomaEntity> diplomasList = [
|
||||||
@@ -30,9 +32,15 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (context.read<RegisterConsultantCubit>().state.consultant.certificates != null) {
|
if (context.read<RegisterConsultantCubit>().state.consultant.certificates !=
|
||||||
|
null) {
|
||||||
diplomasList = [
|
diplomasList = [
|
||||||
...context.read<RegisterConsultantCubit>().state.consultant.certificates!.map(
|
...context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.certificates!
|
||||||
|
.map(
|
||||||
(e) => DiplomaEntity(
|
(e) => DiplomaEntity(
|
||||||
date: DateTime.tryParse(e.date),
|
date: DateTime.tryParse(e.date),
|
||||||
textEditingController: TextEditingController(text: e.title),
|
textEditingController: TextEditingController(text: e.title),
|
||||||
@@ -71,7 +79,8 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
ListView.separated(
|
ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) => _diplomaInfo(diploma: diplomasList[index]),
|
itemBuilder: (context, index) =>
|
||||||
|
_diplomaInfo(diploma: diplomasList[index]),
|
||||||
separatorBuilder: (context, index) => Padding(
|
separatorBuilder: (context, index) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: _divider(context),
|
child: _divider(context),
|
||||||
@@ -94,9 +103,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Column _diplomaInfo({
|
Column _diplomaInfo({required DiplomaEntity diploma}) {
|
||||||
required DiplomaEntity diploma,
|
|
||||||
}) {
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
@@ -112,7 +119,9 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width - (diploma.enableDeleteButton ? 92 : 32),
|
width:
|
||||||
|
MediaQuery.of(context).size.width -
|
||||||
|
(diploma.enableDeleteButton ? 92 : 32),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
onTap: onTapCalendar(context, diploma),
|
onTap: onTapCalendar(context, diploma),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
@@ -123,7 +132,9 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
title: Text(
|
title: Text(
|
||||||
diploma.date?.format('dd MMMM yyyy', 'ru') ?? 'Дата выдачи',
|
diploma.date?.format('dd MMMM yyyy', 'ru') ?? 'Дата выдачи',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: diploma.date == null ? Colors.grey.shade400 : Colors.black,
|
color: diploma.date == null
|
||||||
|
? Colors.grey.shade400
|
||||||
|
: Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
tileColor: const Color(0xFFF6F6F6),
|
tileColor: const Color(0xFFF6F6F6),
|
||||||
@@ -149,9 +160,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
color: const Color(0xFFF6F6F6),
|
color: const Color(0xFFF6F6F6),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(Icons.delete_outline_rounded),
|
||||||
Icons.delete_outline_rounded,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -191,16 +200,14 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
),
|
),
|
||||||
color: CupertinoColors.systemBackground.resolveFrom(context),
|
color: CupertinoColors.systemBackground.resolveFrom(context),
|
||||||
child: SafeArea(
|
child: SafeArea(top: false, child: child),
|
||||||
top: false,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkLast() {
|
void checkLast() {
|
||||||
if (diplomasList.last.textEditingController.text.isNotEmpty || diplomasList.last.date != null) {
|
if (diplomasList.last.textEditingController.text.isNotEmpty ||
|
||||||
|
diplomasList.last.date != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
diplomasList.add(
|
diplomasList.add(
|
||||||
DiplomaEntity(
|
DiplomaEntity(
|
||||||
@@ -231,17 +238,19 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
|||||||
|
|
||||||
void _saveState() {
|
void _saveState() {
|
||||||
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
|
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
|
||||||
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
|
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
|
||||||
certificates: diplomasList
|
certificates: diplomasList
|
||||||
.map(
|
.map(
|
||||||
(e) => CertificatesModel(
|
(e) => CertificatesModel(
|
||||||
title: e.textEditingController.text,
|
title: e.textEditingController.text,
|
||||||
date: e.date == null ? '' : DateFormat('yyyy-MM-dd').format(e.date!),
|
date: e.date == null
|
||||||
),
|
? ''
|
||||||
)
|
: DateFormat('yyyy-MM-dd').format(e.date!),
|
||||||
.toList()
|
|
||||||
.sublist(0, diplomasList.length - 1),
|
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
.toList()
|
||||||
|
.sublist(0, diplomasList.length - 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/const.dart';
|
import 'package:eatvio_app/core/const.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
|
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.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/widgets/scoring_status_widget.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -24,7 +26,8 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_frdoTapRecognizer = TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse(_frdoUrl));
|
_frdoTapRecognizer = TapGestureRecognizer()
|
||||||
|
..onTap = () async => await launchUrl(Uri.parse(_frdoUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -46,8 +49,22 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
child: Text(
|
||||||
|
'Последний шаг',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (state.awaitingScoringOutcome ||
|
||||||
|
state.isScoringPolling ||
|
||||||
|
state.alfaScoringStatus !=
|
||||||
|
AlfaScoringStatus.none) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const ScoringStatusWidget(),
|
||||||
|
],
|
||||||
if (state.requestModel?.canceledModeration != null) ...[
|
if (state.requestModel?.canceledModeration != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -64,12 +81,24 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
state.requestModel!.canceledModeration!.title,
|
state.requestModel!.canceledModeration!.title,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1.2),
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (state.requestModel!.canceledModeration!.additional != null) ...[
|
if (state.requestModel!.canceledModeration!.additional !=
|
||||||
|
null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
for (final item in state.requestModel!.canceledModeration!.additional!) ...[
|
for (final item
|
||||||
Text('• $item', style: const TextStyle(fontSize: 14, height: 1.3)),
|
in state
|
||||||
|
.requestModel!
|
||||||
|
.canceledModeration!
|
||||||
|
.additional!) ...[
|
||||||
|
Text(
|
||||||
|
'• $item',
|
||||||
|
style: const TextStyle(fontSize: 14, height: 1.3),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -98,9 +127,16 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
|||||||
value: state.acceptFrdoAgreement,
|
value: state.acceptFrdoAgreement,
|
||||||
title: Text.rich(
|
title: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
style: const TextStyle(fontSize: 18, height: 1.2, color: Color(0xFF323F4B)),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
height: 1.2,
|
||||||
|
color: Color(0xFF323F4B),
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(text: 'Обязательное требование: Один из моих документов об образовании есть в реестре '),
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
'Обязательное требование: Один из моих документов об образовании есть в реестре ',
|
||||||
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'ФРДО',
|
text: 'ФРДО',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -120,16 +156,31 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
|||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
child: _divider(context),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () async => await launchUrl(
|
onPressed: () async => await launchUrl(
|
||||||
Uri.parse(hidePlans ? '${sl<AppConfig>().baseUrl}/docs/privacy.pdf' : '${sl<AppConfig>().baseUrl}/oferta_bs.pdf'),
|
Uri.parse(
|
||||||
|
hidePlans
|
||||||
|
? '${sl<AppConfig>().baseUrl}/docs/privacy.pdf'
|
||||||
|
: '${sl<AppConfig>().baseUrl}/oferta_bs.pdf',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор оферты',
|
hidePlans
|
||||||
style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),
|
? 'Политика конфиденциальности и пользовательское соглашение'
|
||||||
|
: 'Договор оферты',
|
||||||
|
style: const TextStyle(
|
||||||
|
height: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Color(0xFF323F4B),
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -142,7 +193,10 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 0.4,
|
height: 0.4,
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black26,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||||
import 'package:eatvio_app/features/consultant/become_consultant/models/sbp_bank_model.dart';
|
import 'package:eatvio_app/features/consultant/become_consultant/models/sbp_bank_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/widgets/passport_field_with_error.dart';
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/presentation/widgets/passport_section_title.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
@@ -100,9 +103,7 @@ class _PassportDataScreenState extends State<PassportDataScreen> {
|
|||||||
birthPlaceController = TextEditingController(
|
birthPlaceController = TextEditingController(
|
||||||
text: consultant.birthPlace ?? '',
|
text: consultant.birthPlace ?? '',
|
||||||
);
|
);
|
||||||
citizenshipController = TextEditingController(
|
citizenshipController = TextEditingController(text: consultant.citizenship);
|
||||||
text: consultant.citizenship,
|
|
||||||
);
|
|
||||||
registrationAddressController = TextEditingController(
|
registrationAddressController = TextEditingController(
|
||||||
text: consultant.registrationAddress ?? '',
|
text: consultant.registrationAddress ?? '',
|
||||||
);
|
);
|
||||||
@@ -269,258 +270,321 @@ class _PassportDataScreenState extends State<PassportDataScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _sectionTitle(String title) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8, bottom: 12),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<RegisterConsultantCubit, RegisterConsultantState>(
|
return BlocListener<RegisterConsultantCubit, RegisterConsultantState>(
|
||||||
listenWhen: (previous, current) => previous.sbpBanks != current.sbpBanks,
|
listenWhen: (previous, current) => previous.sbpBanks != current.sbpBanks,
|
||||||
listener: (context, state) => _hydrateSbpBankNameIfNeeded(),
|
listener: (context, state) => _hydrateSbpBankNameIfNeeded(),
|
||||||
child: SingleChildScrollView(
|
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
||||||
padding: const EdgeInsets.only(
|
buildWhen: (previous, current) =>
|
||||||
bottom: 80,
|
previous.fieldErrors != current.fieldErrors,
|
||||||
right: 16,
|
builder: (context, state) {
|
||||||
left: 16,
|
final errors = state.fieldErrors;
|
||||||
top: 16,
|
return SingleChildScrollView(
|
||||||
),
|
padding: const EdgeInsets.only(
|
||||||
child: Column(
|
bottom: 80,
|
||||||
mainAxisSize: MainAxisSize.min,
|
right: 16,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
left: 16,
|
||||||
children: [
|
top: 16,
|
||||||
const Text(
|
|
||||||
'Паспортные данные',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
height: 1.1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
_sectionTitle('Персональные данные (как в паспорте)'),
|
child: Column(
|
||||||
CustomTextFormField(
|
mainAxisSize: MainAxisSize.min,
|
||||||
hintText: 'Фамилия',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
controller: lastNameController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Имя',
|
|
||||||
controller: firstNameController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Отчество',
|
|
||||||
controller: middleNameController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
_sectionTitle('Идентификация'),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'ИНН (12 цифр)',
|
|
||||||
controller: innController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
maxLength: 12,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'СНИЛС',
|
|
||||||
controller: snilsController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [snilsMask],
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Дата рождения (ДД.ММ.ГГГГ)',
|
|
||||||
controller: birthDateController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [dateMaskBirth],
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Место рождения',
|
|
||||||
controller: birthPlaceController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Гражданство (ISO, например RU)',
|
|
||||||
controller: citizenshipController,
|
|
||||||
maxLength: 2,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Адрес регистрации',
|
|
||||||
controller: registrationAddressController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Адрес проживания',
|
|
||||||
controller: residenceAddressController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
CheckboxListTile(
|
|
||||||
value: taxResidentUs,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
activeColor: appTheme.blueA400,
|
|
||||||
title: const Text(
|
|
||||||
'Я являюсь налоговым резидентом США',
|
|
||||||
style: TextStyle(fontSize: 16, height: 1.2),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() => taxResidentUs = value ?? false);
|
|
||||||
_syncToCubit();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
CheckboxListTile(
|
|
||||||
value: foreignTaxResident,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
activeColor: appTheme.blueA400,
|
|
||||||
title: const Text(
|
|
||||||
'Я являюсь иностранным налоговым резидентом',
|
|
||||||
style: TextStyle(fontSize: 16, height: 1.2),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() => foreignTaxResident = value ?? false);
|
|
||||||
_syncToCubit();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_sectionTitle('Паспорт'),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
const PassportScreenTitle(title: 'Паспортные данные'),
|
||||||
|
const PassportSectionTitle(
|
||||||
|
title: 'Персональные данные (как в паспорте)',
|
||||||
|
),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['last_name'],
|
||||||
child: CustomTextFormField(
|
child: CustomTextFormField(
|
||||||
hintText: 'Серия',
|
hintText: 'Фамилия',
|
||||||
controller: seriesController,
|
controller: lastNameController,
|
||||||
keyboardType: TextInputType.number,
|
hasError: errors.containsKey('last_name'),
|
||||||
maxLength: 4,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
onChanged: (_) => _syncToCubit(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
PassportFieldWithError(
|
||||||
|
error: errors['first_name'],
|
||||||
child: CustomTextFormField(
|
child: CustomTextFormField(
|
||||||
hintText: 'Номер',
|
hintText: 'Имя',
|
||||||
controller: numberController,
|
controller: firstNameController,
|
||||||
keyboardType: TextInputType.number,
|
hasError: errors.containsKey('first_name'),
|
||||||
maxLength: 6,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
onChanged: (_) => _syncToCubit(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['middle_name'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Отчество',
|
||||||
|
controller: middleNameController,
|
||||||
|
hasError: errors.containsKey('middle_name'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PassportSectionTitle(title: 'Идентификация'),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['inn'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'ИНН (12 цифр)',
|
||||||
|
controller: innController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 12,
|
||||||
|
hasError: errors.containsKey('inn'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['snils'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'СНИЛС',
|
||||||
|
controller: snilsController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [snilsMask],
|
||||||
|
hasError: errors.containsKey('snils'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['birth_date'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Дата рождения (ДД.ММ.ГГГГ)',
|
||||||
|
controller: birthDateController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [dateMaskBirth],
|
||||||
|
hasError: errors.containsKey('birth_date'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['birth_place'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Место рождения',
|
||||||
|
controller: birthPlaceController,
|
||||||
|
hasError: errors.containsKey('birth_place'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['citizenship'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Гражданство (ISO, например RU)',
|
||||||
|
controller: citizenshipController,
|
||||||
|
maxLength: 2,
|
||||||
|
hasError: errors.containsKey('citizenship'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['registration_address'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Адрес регистрации',
|
||||||
|
controller: registrationAddressController,
|
||||||
|
hasError: errors.containsKey('registration_address'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['residence_address'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Адрес проживания',
|
||||||
|
controller: residenceAddressController,
|
||||||
|
hasError: errors.containsKey('residence_address'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: taxResidentUs,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
activeColor: appTheme.blueA400,
|
||||||
|
title: Text(
|
||||||
|
'Я являюсь налоговым резидентом США',
|
||||||
|
style: TextStyles.textMMediumAuto.copyWith(
|
||||||
|
color: appTheme.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => taxResidentUs = value ?? false);
|
||||||
|
_syncToCubit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: foreignTaxResident,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
activeColor: appTheme.blueA400,
|
||||||
|
title: Text(
|
||||||
|
'Я являюсь иностранным налоговым резидентом',
|
||||||
|
style: TextStyles.textMMediumAuto.copyWith(
|
||||||
|
color: appTheme.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => foreignTaxResident = value ?? false);
|
||||||
|
_syncToCubit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const PassportSectionTitle(title: 'Паспорт'),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: PassportFieldWithError(
|
||||||
|
error: errors['passport_series'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Серия',
|
||||||
|
controller: seriesController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 4,
|
||||||
|
hasError: errors.containsKey('passport_series'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: PassportFieldWithError(
|
||||||
|
error: errors['passport_number'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Номер',
|
||||||
|
controller: numberController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 6,
|
||||||
|
hasError: errors.containsKey('passport_number'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['passport_issue_date'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Дата выдачи (ДД.ММ.ГГГГ)',
|
||||||
|
controller: issueDateController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [dateMaskIssue],
|
||||||
|
hasError: errors.containsKey('passport_issue_date'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['passport_issuer_code'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Код подразделения',
|
||||||
|
controller: issuerCodeController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [issuerCodeMask],
|
||||||
|
hasError: errors.containsKey('passport_issuer_code'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['passport_issuer'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Кем выдан',
|
||||||
|
controller: issuerController,
|
||||||
|
hasError: errors.containsKey('passport_issuer'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PassportSectionTitle(title: 'Реквизиты СБП'),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['sbp_phone'],
|
||||||
|
child: CustomTextFormField(
|
||||||
|
hintText: 'Телефон для выплат через СБП',
|
||||||
|
controller: sbpPhoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
inputFormatters: [phoneMask],
|
||||||
|
hasError: errors.containsKey('sbp_phone'),
|
||||||
|
onChanged: (_) => _syncToCubit(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
PassportFieldWithError(
|
||||||
|
error: errors['sbp_bank_member_id'],
|
||||||
|
child: TypeAheadField<SbpBankModel>(
|
||||||
|
debounceDuration: const Duration(milliseconds: 250),
|
||||||
|
controller: sbpBankController,
|
||||||
|
suggestionsController: sbpBankSuggestionsController,
|
||||||
|
builder: (context, controller, focusNode) =>
|
||||||
|
CustomTextFormField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
hintText: 'Банк получатель (поиск)',
|
||||||
|
hasError: errors.containsKey('sbp_bank_member_id'),
|
||||||
|
onChanged: _clearSbpBankIfNeeded,
|
||||||
|
),
|
||||||
|
suggestionsCallback: (query) async {
|
||||||
|
return context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.searchSbpBanks(query);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, bank) {
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
bank.bankName,
|
||||||
|
style: TextStyles.textMMedium.copyWith(
|
||||||
|
color: appTheme.black900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle:
|
||||||
|
bank.shortName == null || bank.shortName!.isEmpty
|
||||||
|
? null
|
||||||
|
: Text(
|
||||||
|
bank.shortName!,
|
||||||
|
style: TextStyles.textSMedium.copyWith(
|
||||||
|
color: appTheme.grey7B,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
decorationBuilder: (context, child) => Material(
|
||||||
|
type: MaterialType.card,
|
||||||
|
elevation: 4,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: appTheme.whiteA700,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
emptyBuilder: (context) => Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
'Ничего не найдено',
|
||||||
|
style: TextStyles.textSMedium.copyWith(
|
||||||
|
color: appTheme.grey7B,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSelected: _selectSbpBank,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Divider(height: 0.4, thickness: 0.4, color: appTheme.greyD9),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Данные используются для проверки и скоринга. Заполните поля в соответствии с паспортом РФ.',
|
||||||
|
style: TextStyles.textSMedium.copyWith(
|
||||||
|
color: appTheme.blueA400,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
);
|
||||||
CustomTextFormField(
|
},
|
||||||
hintText: 'Дата выдачи (ДД.ММ.ГГГГ)',
|
|
||||||
controller: issueDateController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [dateMaskIssue],
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Код подразделения',
|
|
||||||
controller: issuerCodeController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [issuerCodeMask],
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Кем выдан',
|
|
||||||
controller: issuerController,
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
_sectionTitle('Реквизиты СБП'),
|
|
||||||
CustomTextFormField(
|
|
||||||
hintText: 'Телефон для выплат через СБП',
|
|
||||||
controller: sbpPhoneController,
|
|
||||||
keyboardType: TextInputType.phone,
|
|
||||||
inputFormatters: [phoneMask],
|
|
||||||
onChanged: (_) => _syncToCubit(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TypeAheadField<SbpBankModel>(
|
|
||||||
debounceDuration: const Duration(milliseconds: 250),
|
|
||||||
controller: sbpBankController,
|
|
||||||
suggestionsController: sbpBankSuggestionsController,
|
|
||||||
builder: (context, controller, focusNode) => CustomTextFormField(
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
hintText: 'Банк получатель (поиск)',
|
|
||||||
onChanged: (value) {
|
|
||||||
_clearSbpBankIfNeeded(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
suggestionsCallback: (query) async {
|
|
||||||
return context.read<RegisterConsultantCubit>().searchSbpBanks(
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemBuilder: (context, bank) {
|
|
||||||
return ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: Text(
|
|
||||||
bank.bankName,
|
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
|
||||||
),
|
|
||||||
subtitle: bank.shortName == null || bank.shortName!.isEmpty
|
|
||||||
? null
|
|
||||||
: Text(
|
|
||||||
bank.shortName!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
decorationBuilder: (context, child) => Material(
|
|
||||||
type: MaterialType.card,
|
|
||||||
elevation: 4,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
emptyBuilder: (context) => const Padding(
|
|
||||||
padding: EdgeInsets.all(12),
|
|
||||||
child: Text(
|
|
||||||
'Ничего не найдено',
|
|
||||||
style: TextStyle(color: Colors.black54),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSelected: _selectSbpBank,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const Divider(height: 0.4, thickness: 0.4, color: Colors.black26),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Данные используются для проверки и скоринга. Заполните поля в соответствии с паспортом РФ.',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
height: 1.2,
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,32 +29,48 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Загрузите фото или сканы дипломов и сертификатов', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
const Text(
|
||||||
|
'Загрузите фото или сканы дипломов и сертификатов',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final photos = state.certPhotos.where((photo) => !state.deletedPhotoIds.contains(photo.id)).toList();
|
final photos = state.certPhotos
|
||||||
|
.where((photo) => !state.deletedPhotoIds.contains(photo.id))
|
||||||
|
.toList();
|
||||||
final addedPhotos = state.newAddedPhotos;
|
final addedPhotos = state.newAddedPhotos;
|
||||||
final totalPhotosCount = photos.length + addedPhotos.length;
|
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),
|
||||||
|
],
|
||||||
GridView.builder(
|
GridView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
crossAxisCount: 3,
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
childAspectRatio: 3 / 4,
|
crossAxisCount: 3,
|
||||||
crossAxisSpacing: 8,
|
childAspectRatio: 3 / 4,
|
||||||
mainAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
),
|
mainAxisSpacing: 8,
|
||||||
|
),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
if (index == totalPhotosCount) {
|
if (index == totalPhotosCount) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
onTap: () => _pickAndUploadPhotos(),
|
onTap: () => _pickAndUploadPhotos(),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: BoxDecoration(color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF6F6F6),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
child: Icon(Icons.add, color: appTheme.blueA400),
|
child: Icon(Icons.add, color: appTheme.blueA400),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -72,7 +88,8 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
|||||||
return _LocalCertPhotoCard(
|
return _LocalCertPhotoCard(
|
||||||
photo: localPhoto,
|
photo: localPhoto,
|
||||||
allPhotos: addedPhotos,
|
allPhotos: addedPhotos,
|
||||||
onRemove: () => _removePhoto(path: localPhoto.uri.path),
|
onRemove: () =>
|
||||||
|
_removePhoto(path: localPhoto.uri.path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +116,9 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
await context.read<RegisterConsultantCubit>().addCertPhotos(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(() {
|
||||||
@@ -119,7 +138,11 @@ class _CertPhotoCard extends StatelessWidget {
|
|||||||
final List<ConsultantCertPhotoModel> allPhotos;
|
final List<ConsultantCertPhotoModel> allPhotos;
|
||||||
final VoidCallback onRemove;
|
final VoidCallback onRemove;
|
||||||
|
|
||||||
const _CertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
|
const _CertPhotoCard({
|
||||||
|
required this.photo,
|
||||||
|
required this.allPhotos,
|
||||||
|
required this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -131,7 +154,11 @@ class _CertPhotoCard extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
PhotoViewScreen(imageUrls: allPhotos.map((e) => e.url).toList(), isLocal: false, index: allPhotos.indexOf(photo)),
|
PhotoViewScreen(
|
||||||
|
imageUrls: allPhotos.map((e) => e.url).toList(),
|
||||||
|
isLocal: false,
|
||||||
|
index: allPhotos.indexOf(photo),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -141,7 +168,10 @@ class _CertPhotoCard extends StatelessWidget {
|
|||||||
Ink(
|
Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
image: DecorationImage(fit: BoxFit.cover, image: NetworkImage(photo.url)),
|
image: DecorationImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: NetworkImage(photo.url),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -152,8 +182,15 @@ class _CertPhotoCard extends StatelessWidget {
|
|||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
|
decoration: const BoxDecoration(
|
||||||
child: const Icon(size: 20, Icons.close, color: Colors.black),
|
color: Color(0xFFF1F2F6),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
size: 20,
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -170,7 +207,11 @@ class _LocalCertPhotoCard extends StatelessWidget {
|
|||||||
final List<File> allPhotos;
|
final List<File> allPhotos;
|
||||||
final VoidCallback onRemove;
|
final VoidCallback onRemove;
|
||||||
|
|
||||||
const _LocalCertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
|
const _LocalCertPhotoCard({
|
||||||
|
required this.photo,
|
||||||
|
required this.allPhotos,
|
||||||
|
required this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -210,8 +251,15 @@ class _LocalCertPhotoCard extends StatelessWidget {
|
|||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
|
decoration: const BoxDecoration(
|
||||||
child: const Icon(size: 20, Icons.close, color: Colors.black),
|
color: Color(0xFFF1F2F6),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
size: 20,
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ class RegistrationAsScreen extends StatefulWidget {
|
|||||||
enum RegistrationAsType { physical, selfBusy, iP, ooo }
|
enum RegistrationAsType { physical, selfBusy, iP, ooo }
|
||||||
|
|
||||||
class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||||
static const RegistrationAsType registrationAsType = RegistrationAsType.physical;
|
static const RegistrationAsType registrationAsType =
|
||||||
|
RegistrationAsType.physical;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -24,7 +25,9 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
}
|
}
|
||||||
final cubit = context.read<RegisterConsultantCubit>();
|
final cubit = context.read<RegisterConsultantCubit>();
|
||||||
cubit.updateConsultantProfileModel(
|
cubit.updateConsultantProfileModel(
|
||||||
cubit.state.consultant.copyWith(legalType: RegisterConsultantCubit.individualPersonLegalType),
|
cubit.state.consultant.copyWith(
|
||||||
|
legalType: RegisterConsultantCubit.individualPersonLegalType,
|
||||||
|
),
|
||||||
cubit.state.acceptOffer,
|
cubit.state.acceptOffer,
|
||||||
cubit.state.acceptFrdoAgreement,
|
cubit.state.acceptFrdoAgreement,
|
||||||
);
|
);
|
||||||
@@ -41,7 +44,14 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text('Регистрация в качестве', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
child: Text(
|
||||||
|
'Регистрация в качестве',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Column(
|
Column(
|
||||||
@@ -59,7 +69,10 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
const Expanded(
|
const Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text('Физического лица', style: TextStyle(fontSize: 18, height: 1)),
|
child: Text(
|
||||||
|
'Физического лица',
|
||||||
|
style: TextStyle(fontSize: 18, height: 1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -77,7 +90,14 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
const Expanded(
|
const Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text('Самозанятого', style: TextStyle(fontSize: 18, height: 1, color: Colors.black38)),
|
child: Text(
|
||||||
|
'Самозанятого',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black38,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -95,7 +115,14 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
const Expanded(
|
const Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text('Индивидуального предпринимателя', style: TextStyle(fontSize: 18, height: 1, color: Colors.black38)),
|
child: Text(
|
||||||
|
'Индивидуального предпринимателя',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black38,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -113,17 +140,29 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
const Expanded(
|
const Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text('Общества с ограниченной ответственностью', style: TextStyle(fontSize: 18, height: 1, color: Colors.black38)),
|
child: Text(
|
||||||
|
'Общества с ограниченной ответственностью',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black38,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
child: _divider(context),
|
||||||
|
),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text('* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся'),
|
child: Text(
|
||||||
|
'* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -134,7 +173,10 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 0.4,
|
height: 0.4,
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black26,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,32 +19,47 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
|
|||||||
|
|
||||||
var maskFormatter = MaskTextInputFormatter(
|
var maskFormatter = MaskTextInputFormatter(
|
||||||
mask: 'https://########################################',
|
mask: 'https://########################################',
|
||||||
filter: {
|
filter: {'#': RegExp(r"[a-zA-Z. ]+")},
|
||||||
'#': RegExp(r"[a-zA-Z. ]+"),
|
|
||||||
},
|
|
||||||
type: MaskAutoCompletionType.lazy,
|
type: MaskAutoCompletionType.lazy,
|
||||||
);
|
);
|
||||||
var maskTgFormatter = MaskTextInputFormatter(
|
var maskTgFormatter = MaskTextInputFormatter(
|
||||||
mask: 'https://tg.me/########################################',
|
mask: 'https://tg.me/########################################',
|
||||||
filter: {
|
filter: {'#': RegExp(r"[a-zA-Z. ]+")},
|
||||||
'#': RegExp(r"[a-zA-Z. ]+"),
|
|
||||||
},
|
|
||||||
type: MaskAutoCompletionType.lazy,
|
type: MaskAutoCompletionType.lazy,
|
||||||
);
|
);
|
||||||
var maskVkFormatter = MaskTextInputFormatter(
|
var maskVkFormatter = MaskTextInputFormatter(
|
||||||
mask: 'https://vk.com/########################################',
|
mask: 'https://vk.com/########################################',
|
||||||
filter: {
|
filter: {'#': RegExp(r"[a-zA-Z. ]+")},
|
||||||
'#': RegExp(r"[a-zA-Z. ]+"),
|
|
||||||
},
|
|
||||||
type: MaskAutoCompletionType.lazy,
|
type: MaskAutoCompletionType.lazy,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
vkController.text = context.read<RegisterConsultantCubit>().state.consultant.socialLinks!.vkLink ?? '';
|
vkController.text =
|
||||||
telegramController.text = context.read<RegisterConsultantCubit>().state.consultant.socialLinks!.tgLink ?? '';
|
context
|
||||||
webSiteController.text = context.read<RegisterConsultantCubit>().state.consultant.socialLinks!.personalSiteLink ?? '';
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.socialLinks!
|
||||||
|
.vkLink ??
|
||||||
|
'';
|
||||||
|
telegramController.text =
|
||||||
|
context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.socialLinks!
|
||||||
|
.tgLink ??
|
||||||
|
'';
|
||||||
|
webSiteController.text =
|
||||||
|
context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.socialLinks!
|
||||||
|
.personalSiteLink ??
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -75,9 +90,7 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
|
|||||||
color: const Color(0xFFF6F6F6),
|
color: const Color(0xFFF6F6F6),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset('assets/icons/vk.svg'),
|
||||||
'assets/icons/vk.svg',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -105,9 +118,7 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
|
|||||||
color: const Color(0xFFF6F6F6),
|
color: const Color(0xFFF6F6F6),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset('assets/icons/telegram.svg'),
|
||||||
'assets/icons/telegram.svg',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -135,9 +146,7 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
|
|||||||
color: const Color(0xFFF6F6F6),
|
color: const Color(0xFFF6F6F6),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset('assets/icons/web.svg'),
|
||||||
'assets/icons/web.svg',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -164,25 +173,45 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
|
|||||||
String? personalSiteLink,
|
String? personalSiteLink,
|
||||||
}) {
|
}) {
|
||||||
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
|
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
|
||||||
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
|
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
|
||||||
socialLinks: context.read<RegisterConsultantCubit>().state.consultant.socialLinks?.copyWith(
|
socialLinks: context
|
||||||
tgLink: tgLink == null
|
.read<RegisterConsultantCubit>()
|
||||||
? context.read<RegisterConsultantCubit>().state.consultant.socialLinks?.tgLink
|
.state
|
||||||
: tgLink.isEmpty
|
.consultant
|
||||||
? null
|
.socialLinks
|
||||||
: tgLink,
|
?.copyWith(
|
||||||
vkLink: vkLink == null
|
tgLink: tgLink == null
|
||||||
? context.read<RegisterConsultantCubit>().state.consultant.socialLinks?.vkLink
|
? context
|
||||||
: vkLink.isEmpty
|
.read<RegisterConsultantCubit>()
|
||||||
? null
|
.state
|
||||||
: vkLink,
|
.consultant
|
||||||
personalSiteLink: personalSiteLink == null
|
.socialLinks
|
||||||
? context.read<RegisterConsultantCubit>().state.consultant.socialLinks?.personalSiteLink
|
?.tgLink
|
||||||
: personalSiteLink.isEmpty
|
: tgLink.isEmpty
|
||||||
? null
|
? null
|
||||||
: personalSiteLink,
|
: tgLink,
|
||||||
),
|
vkLink: vkLink == null
|
||||||
),
|
? context
|
||||||
);
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.socialLinks
|
||||||
|
?.vkLink
|
||||||
|
: vkLink.isEmpty
|
||||||
|
? null
|
||||||
|
: vkLink,
|
||||||
|
personalSiteLink: personalSiteLink == null
|
||||||
|
? context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.state
|
||||||
|
.consultant
|
||||||
|
.socialLinks
|
||||||
|
?.personalSiteLink
|
||||||
|
: personalSiteLink.isEmpty
|
||||||
|
? null
|
||||||
|
: personalSiteLink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
import 'package:eatvio_app/core/utils/image_constant.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/consultant/become_consultant/enums/alfa_scoring_status.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';
|
||||||
@@ -22,26 +23,37 @@ import 'package:lottie/lottie.dart';
|
|||||||
class StepperBecomeConsultantScreen extends StatefulWidget {
|
class StepperBecomeConsultantScreen extends StatefulWidget {
|
||||||
final String? slug;
|
final String? slug;
|
||||||
final bool? isCanceled;
|
final bool? isCanceled;
|
||||||
const StepperBecomeConsultantScreen({super.key, this.slug, this.isCanceled});
|
final bool isNewRegistration;
|
||||||
|
const StepperBecomeConsultantScreen({
|
||||||
|
super.key,
|
||||||
|
this.slug,
|
||||||
|
this.isCanceled,
|
||||||
|
this.isNewRegistration = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
|
State<StepperBecomeConsultantScreen> createState() =>
|
||||||
|
_StepperBecomeConsultantScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantScreen> {
|
class _StepperBecomeConsultantScreenState
|
||||||
|
extends State<StepperBecomeConsultantScreen> {
|
||||||
double progressValue = 0;
|
double progressValue = 0;
|
||||||
int step = 1;
|
int step = 1;
|
||||||
final pageController = PageController();
|
final pageController = PageController();
|
||||||
late final RegisterConsultantCubit registrationConsultantCubit;
|
late final RegisterConsultantCubit registrationConsultantCubit;
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
bool _successSheetShown = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.slug == null) {
|
if (widget.isNewRegistration || widget.slug == null) {
|
||||||
registrationConsultantCubit = sl<RegisterConsultantCubit>()..disableLoading();
|
registrationConsultantCubit = sl<RegisterConsultantCubit>()
|
||||||
|
..disableLoading(slug: widget.slug);
|
||||||
} else {
|
} else {
|
||||||
registrationConsultantCubit = sl<RegisterConsultantCubit>()..getMyRequestStatus(widget.slug!);
|
registrationConsultantCubit = sl<RegisterConsultantCubit>()
|
||||||
|
..getMyRequestStatus(widget.slug!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +71,97 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showSuccessSheet(BuildContext context) async {
|
||||||
|
if (_successSheetShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_successSheetShown = true;
|
||||||
|
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(
|
||||||
|
'Закрыть',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<RegisterConsultantCubit>(
|
return BlocProvider<RegisterConsultantCubit>(
|
||||||
create: (context) => registrationConsultantCubit,
|
create: (context) => registrationConsultantCubit,
|
||||||
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
child: BlocConsumer<RegisterConsultantCubit, RegisterConsultantState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.awaitingScoringOutcome &&
|
||||||
|
!current.awaitingScoringOutcome &&
|
||||||
|
current.alfaScoringStatus.isComplete,
|
||||||
|
listener: (context, state) {
|
||||||
|
_showSuccessSheet(context);
|
||||||
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
return const Scaffold(body: Loader());
|
return const Scaffold(body: Loader());
|
||||||
}
|
}
|
||||||
if (state.error != null && state.requestModel == null && widget.slug != null) {
|
if (state.error != null &&
|
||||||
|
state.requestModel == null &&
|
||||||
|
widget.slug != null &&
|
||||||
|
state.alfaScoringStatus == AlfaScoringStatus.none) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
@@ -87,11 +180,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
style: TextStyles.headingXLSemibold.copyWith(
|
||||||
|
color: appTheme.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: DataErrorWidget(
|
body: DataErrorWidget(
|
||||||
onReload: () => context.read<RegisterConsultantCubit>().reloadData(widget.slug),
|
onReload: () => context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.reloadData(widget.slug),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -100,8 +197,14 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
progressValue = 1 / totalSteps;
|
progressValue = 1 / totalSteps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final submitDisabled =
|
||||||
|
isLoading ||
|
||||||
|
state.isScoringPolling ||
|
||||||
|
state.scoringRetryInProgress;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
floatingActionButtonLocation:
|
||||||
|
FloatingActionButtonLocation.centerFloat,
|
||||||
floatingActionButton: Row(
|
floatingActionButton: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -113,20 +216,29 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
highlightElevation: 0,
|
highlightElevation: 0,
|
||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
backgroundColor: const Color(0xFFEAF2FD),
|
backgroundColor: const Color(0xFFEAF2FD),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
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 (widget.isCanceled == true && step != totalSteps)
|
if (widget.isCanceled == true && step != totalSteps)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final canceledModeration = state.requestModel?.canceledModeration;
|
final canceledModeration =
|
||||||
|
state.requestModel?.canceledModeration;
|
||||||
if (canceledModeration == null) return;
|
if (canceledModeration == null) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -137,12 +249,18 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (canceledModeration.additional != null)
|
if (canceledModeration.additional != null)
|
||||||
for (final item in canceledModeration.additional!) ...[
|
for (final item
|
||||||
|
in canceledModeration.additional!) ...[
|
||||||
Text('• $item'),
|
Text('• $item'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -150,11 +268,19 @@ 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(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: const Color(0x33FFCC00),
|
||||||
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -170,13 +296,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
highlightElevation: 0,
|
highlightElevation: 0,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
shape: RoundedRectangleBorder(
|
||||||
backgroundColor: isLoading
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
backgroundColor: submitDisabled
|
||||||
? Colors.grey
|
? Colors.grey
|
||||||
: widget.isCanceled == true && step != totalSteps
|
: widget.isCanceled == true && step != totalSteps
|
||||||
? const Color(0xFFEAF2FD)
|
? const Color(0xFFEAF2FD)
|
||||||
: appTheme.blueA400,
|
: appTheme.blueA400,
|
||||||
onPressed: isLoading
|
onPressed: submitDisabled
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
if (step == totalSteps) {
|
if (step == totalSteps) {
|
||||||
@@ -184,84 +312,73 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
actionsPadding: const EdgeInsets.symmetric(
|
||||||
content: const Text('Заполните паспортные данные'),
|
horizontal: 4,
|
||||||
|
),
|
||||||
|
content: const Text(
|
||||||
|
'Заполните паспортные данные',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Ок'),
|
child: const Text('Ок'),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (registrationConsultantCubit.hasAcceptedRequiredAgreements) {
|
if (registrationConsultantCubit
|
||||||
|
.hasAcceptedRequiredAgreements) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await registrationConsultantCubit.registerConsultant();
|
await registrationConsultantCubit
|
||||||
|
.registerConsultant();
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (registrationConsultantCubit.state.error != null) {
|
if (registrationConsultantCubit.state.error !=
|
||||||
return;
|
null) {
|
||||||
}
|
showDialog(
|
||||||
await showModalBottomSheet(
|
context: context,
|
||||||
useRootNavigator: true,
|
builder: (context) => AlertDialog(
|
||||||
showDragHandle: true,
|
actionsPadding:
|
||||||
context: context,
|
const EdgeInsets.symmetric(
|
||||||
builder: (context) => Container(
|
horizontal: 4,
|
||||||
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(
|
|
||||||
'Закрыть',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
content: Text(
|
||||||
|
registrationConsultantCubit
|
||||||
|
.state
|
||||||
|
.error!,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Ок'),
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
// Success sheet is shown by BlocListener when scoring completes.
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
actionsPadding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
content: Text(e.toString()),
|
content: Text(e.toString()),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Ок'),
|
child: const Text('Ок'),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -275,7 +392,9 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
actionsPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
state.isUpdating
|
state.isUpdating
|
||||||
? 'Примите договор публичной оферты'
|
? 'Примите договор публичной оферты'
|
||||||
@@ -295,17 +414,25 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
} else {
|
} else {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
pageController.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: isLoading
|
label: submitDisabled && step == totalSteps
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: widget.isCanceled == true && step != totalSteps
|
: widget.isCanceled == true && step != totalSteps
|
||||||
? Icon(Icons.arrow_forward_ios_outlined, color: appTheme.blueA400)
|
? Icon(
|
||||||
|
Icons.arrow_forward_ios_outlined,
|
||||||
|
color: appTheme.blueA400,
|
||||||
|
)
|
||||||
: Text(
|
: Text(
|
||||||
step == totalSteps ? 'Отправить' : 'Продолжить',
|
step == totalSteps ? 'Отправить' : 'Продолжить',
|
||||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
style: theme.textTheme.titleSmall!.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -314,11 +441,23 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
leading: Builder(
|
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,
|
centerTitle: true,
|
||||||
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
|
title: SvgPicture.asset(
|
||||||
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из $totalSteps'))],
|
ImageConstant.imgLogoEatvio,
|
||||||
|
height: 48,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: Text('шаг $step из $totalSteps'),
|
||||||
|
),
|
||||||
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -326,13 +465,18 @@ 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>(begin: 1 / totalSteps, end: progressValue),
|
tween: Tween<double>(
|
||||||
|
begin: 1 / totalSteps,
|
||||||
|
end: progressValue,
|
||||||
|
),
|
||||||
builder: (context, value, _) => LinearProgressIndicator(
|
builder: (context, value, _) => LinearProgressIndicator(
|
||||||
value: value,
|
value: value,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
minHeight: 8,
|
minHeight: 8,
|
||||||
backgroundColor: Colors.grey.shade100,
|
backgroundColor: Colors.grey.shade100,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
appTheme.blueA400,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Wraps a form field with [ValidationErrorWidget] when [error] is present.
|
||||||
|
/// Pass [hasError] into [CustomTextFormField] from the parent.
|
||||||
|
class PassportFieldWithError extends StatelessWidget {
|
||||||
|
const PassportFieldWithError({required this.child, this.error, super.key});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final message = error?.trim();
|
||||||
|
final showError = message != null && message.isNotEmpty;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
if (showError) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
ValidationErrorWidget(error: message),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class PassportSectionTitle extends StatelessWidget {
|
||||||
|
const PassportSectionTitle({required this.title, super.key});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, bottom: 12),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PassportScreenTitle extends StatelessWidget {
|
||||||
|
const PassportScreenTitle({required this.title, super.key});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
title,
|
||||||
|
style: TextStyles.textFS24FW700.copyWith(color: appTheme.black),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.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/create_diet/presentation/widgets/error_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class ScoringStatusWidget extends StatelessWidget {
|
||||||
|
const ScoringStatusWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.alfaScoringStatus != current.alfaScoringStatus ||
|
||||||
|
previous.isScoringPolling != current.isScoringPolling ||
|
||||||
|
previous.scoringRetryInProgress != current.scoringRetryInProgress ||
|
||||||
|
previous.awaitingScoringOutcome != current.awaitingScoringOutcome,
|
||||||
|
builder: (context, state) {
|
||||||
|
final isWaiting =
|
||||||
|
state.awaitingScoringOutcome || state.isScoringPolling;
|
||||||
|
final status = state.alfaScoringStatus;
|
||||||
|
final showProcessing = status.isProcessing ||
|
||||||
|
(isWaiting && !status.isTerminal);
|
||||||
|
|
||||||
|
if (status == AlfaScoringStatus.none && !isWaiting) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final displayStatus = showProcessing && !status.isProcessing
|
||||||
|
? AlfaScoringStatus.beneficiaryCreating
|
||||||
|
: status;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: appTheme.greyD9),
|
||||||
|
color: appTheme.whiteA700,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ScoringStatusHeader(status: displayStatus),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (showProcessing) const ScoringProcessingBody(),
|
||||||
|
if (status.isComplete) const ScoringCompleteBody(),
|
||||||
|
if (status.isFailed)
|
||||||
|
ScoringFailedBody(
|
||||||
|
retryInProgress: state.scoringRetryInProgress,
|
||||||
|
onRetry: () => context
|
||||||
|
.read<RegisterConsultantCubit>()
|
||||||
|
.retryScoring(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoringStatusHeader extends StatelessWidget {
|
||||||
|
const ScoringStatusHeader({required this.status, super.key});
|
||||||
|
|
||||||
|
final AlfaScoringStatus status;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Проверка данных',
|
||||||
|
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
ScoringStatusLabels.hintFor(status),
|
||||||
|
style: TextStyles.textSMedium.copyWith(
|
||||||
|
color: ScoringStatusLabels.colorFor(status),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoringProcessingBody extends StatelessWidget {
|
||||||
|
const ScoringProcessingBody({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: appTheme.blueA400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Идёт проверка…',
|
||||||
|
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoringCompleteBody extends StatelessWidget {
|
||||||
|
const ScoringCompleteBody({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle, color: appTheme.green, size: 22),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Проверка успешно завершена',
|
||||||
|
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoringFailedBody extends StatelessWidget {
|
||||||
|
const ScoringFailedBody({
|
||||||
|
required this.retryInProgress,
|
||||||
|
required this.onRetry,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool retryInProgress;
|
||||||
|
final VoidCallback onRetry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const ValidationErrorWidget(error: 'Не удалось пройти проверку'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 44,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: retryInProgress ? null : onRetry,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: appTheme.blueA400,
|
||||||
|
disabledBackgroundColor: appTheme.greyD9,
|
||||||
|
foregroundColor: appTheme.whiteA700,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: retryInProgress
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: appTheme.whiteA700,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Повторить',
|
||||||
|
style: TextStyles.textMSemibold.copyWith(
|
||||||
|
color: appTheme.whiteA700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract final class ScoringStatusLabels {
|
||||||
|
static String hintFor(AlfaScoringStatus status) {
|
||||||
|
return switch (status) {
|
||||||
|
AlfaScoringStatus.beneficiaryCreating => 'Создание профиля',
|
||||||
|
AlfaScoringStatus.scoringPending => 'Ожидание результата',
|
||||||
|
AlfaScoringStatus.scoringComplete => 'Успешно',
|
||||||
|
AlfaScoringStatus.beneficiaryFailed => 'Ошибка',
|
||||||
|
AlfaScoringStatus.scoringFailed => 'Ошибка',
|
||||||
|
AlfaScoringStatus.none => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color colorFor(AlfaScoringStatus status) {
|
||||||
|
return switch (status) {
|
||||||
|
AlfaScoringStatus.scoringComplete => appTheme.green,
|
||||||
|
AlfaScoringStatus.beneficiaryFailed ||
|
||||||
|
AlfaScoringStatus.scoringFailed => appTheme.red,
|
||||||
|
AlfaScoringStatus.beneficiaryCreating ||
|
||||||
|
AlfaScoringStatus.scoringPending => appTheme.blueA400,
|
||||||
|
AlfaScoringStatus.none => appTheme.grey7B,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,16 +12,25 @@ class ConsultantRepository {
|
|||||||
|
|
||||||
const ConsultantRepository({required this.consultantService});
|
const ConsultantRepository({required this.consultantService});
|
||||||
|
|
||||||
Future<void> registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async {
|
Future<void> registerConsultant({
|
||||||
|
required ConsultantProfileModel consultant,
|
||||||
|
bool update = false,
|
||||||
|
}) async {
|
||||||
if (update) {
|
if (update) {
|
||||||
return await consultantService.consultantUpdate(consultantModel: consultant);
|
return await consultantService.consultantUpdate(
|
||||||
|
consultantModel: consultant,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return await consultantService.consultantRegistration(consultantModel: consultant);
|
return await consultantService.consultantRegistration(
|
||||||
|
consultantModel: consultant,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Профиль консультанта (свой или чужой) — публичные данные без паспорта.
|
/// Профиль консультанта (свой или чужой) — публичные данные без паспорта.
|
||||||
Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async {
|
Future<ConsultationRequestModel> getConsultantRequestStatus(
|
||||||
|
String slug,
|
||||||
|
) async {
|
||||||
return await consultantService.getConsultantRequestStatus(slug: slug);
|
return await consultantService.getConsultantRequestStatus(slug: slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,12 +47,18 @@ class ConsultantRepository {
|
|||||||
return await consultantService.getConsultantCertPhotos();
|
return await consultantService.getConsultantCertPhotos();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({required List<File> files}) async {
|
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({
|
||||||
|
required List<File> files,
|
||||||
|
}) async {
|
||||||
final multipartFiles = <MultipartFile>[];
|
final multipartFiles = <MultipartFile>[];
|
||||||
|
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
final fileBytes = await file.readAsBytes();
|
final fileBytes = await file.readAsBytes();
|
||||||
final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
|
final multipartFile = MultipartFile.fromBytes(
|
||||||
|
fileBytes,
|
||||||
|
filename: file.path.split('/').last,
|
||||||
|
contentType: MediaType('application', 'octet-stream'),
|
||||||
|
);
|
||||||
multipartFiles.add(multipartFile);
|
multipartFiles.add(multipartFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +66,21 @@ class ConsultantRepository {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
|
return await consultantService.storeConsultantCertPhotos(
|
||||||
|
photos: multipartFiles,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> hideConsultantCertPhoto(int id) async {
|
Future<void> hideConsultantCertPhoto(int id) async {
|
||||||
await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
|
await consultantService.hideConsultantCertPhoto(
|
||||||
|
payload: {'cert_photo_id': id},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> unHideConsultantCertPhoto(int id) async {
|
Future<void> unHideConsultantCertPhoto(int id) async {
|
||||||
await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
|
await consultantService.unHideConsultantCertPhoto(
|
||||||
|
payload: {'cert_photo_id': id},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deletePhoto(int id) async {
|
Future<void> deletePhoto(int id) async {
|
||||||
|
|||||||
@@ -12,24 +12,35 @@ abstract class ConsultantService {
|
|||||||
factory ConsultantService(Dio dio, {String baseUrl}) = _ConsultantService;
|
factory ConsultantService(Dio dio, {String baseUrl}) = _ConsultantService;
|
||||||
|
|
||||||
@POST('/api/v2/consultant_request/registration')
|
@POST('/api/v2/consultant_request/registration')
|
||||||
Future<void> consultantRegistration({@Body() required ConsultantProfileModel consultantModel});
|
Future<void> consultantRegistration({
|
||||||
|
@Body() required ConsultantProfileModel consultantModel,
|
||||||
|
});
|
||||||
|
|
||||||
@POST('/api/consultant_request/photos/upload')
|
@POST('/api/consultant_request/photos/upload')
|
||||||
@MultiPart()
|
@MultiPart()
|
||||||
Future<List<String>> uploadPhotos({@Part(name: 'photos[]', contentType: 'multipart/form-data') List<MultipartFile>? photos});
|
Future<List<String>> uploadPhotos({
|
||||||
|
@Part(name: 'photos[]', contentType: 'multipart/form-data')
|
||||||
|
List<MultipartFile>? photos,
|
||||||
|
});
|
||||||
|
|
||||||
/// Публичный статус консультанта для просмотра профиля.
|
/// Публичный статус консультанта для просмотра профиля.
|
||||||
/// Без паспортных данных: `/api/profile/consultant/{slug}/status`.
|
/// Без паспортных данных: `/api/profile/consultant/{slug}/status`.
|
||||||
@GET('/api/profile/consultant/{slug}/status')
|
@GET('/api/profile/consultant/{slug}/status')
|
||||||
Future<ConsultationRequestModel> getConsultantRequestStatus({@Path() required String slug});
|
Future<ConsultationRequestModel> getConsultantRequestStatus({
|
||||||
|
@Path() required String slug,
|
||||||
|
});
|
||||||
|
|
||||||
/// Свой статус заявки для редактирования/модерации.
|
/// Свой статус заявки для редактирования/модерации.
|
||||||
/// С паспортными полями в `legal_entity_resource`: `/api/v2/consultant_request/{slug}/status`.
|
/// С паспортными полями в `legal_entity_resource`: `/api/v2/consultant_request/{slug}/status`.
|
||||||
@GET('/api/v2/consultant_request/{slug}/status')
|
@GET('/api/v2/consultant_request/{slug}/status')
|
||||||
Future<ConsultationRequestModel> getMyRequestStatus({@Path() required String slug});
|
Future<ConsultationRequestModel> getMyRequestStatus({
|
||||||
|
@Path() required String slug,
|
||||||
|
});
|
||||||
|
|
||||||
@PUT('/api/v2/consultant_request/update')
|
@PUT('/api/v2/consultant_request/update')
|
||||||
Future<void> consultantUpdate({@Body() required ConsultantProfileModel consultantModel});
|
Future<void> consultantUpdate({
|
||||||
|
@Body() required ConsultantProfileModel consultantModel,
|
||||||
|
});
|
||||||
|
|
||||||
@GET('/api/finances/sbp_banks')
|
@GET('/api/finances/sbp_banks')
|
||||||
Future<List<SbpBankModel>> getSbpBanks();
|
Future<List<SbpBankModel>> getSbpBanks();
|
||||||
@@ -40,14 +51,19 @@ abstract class ConsultantService {
|
|||||||
@POST('/api/v2/consultant_request/cert_photos/store')
|
@POST('/api/v2/consultant_request/cert_photos/store')
|
||||||
@MultiPart()
|
@MultiPart()
|
||||||
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({
|
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({
|
||||||
@Part(name: 'photos[]', contentType: 'multipart/form-data') required List<MultipartFile> photos,
|
@Part(name: 'photos[]', contentType: 'multipart/form-data')
|
||||||
|
required List<MultipartFile> photos,
|
||||||
});
|
});
|
||||||
|
|
||||||
@POST('/api/v2/consultant_request/cert_photos/hide')
|
@POST('/api/v2/consultant_request/cert_photos/hide')
|
||||||
Future<void> hideConsultantCertPhoto({@Body() required Map<String, dynamic> payload});
|
Future<void> hideConsultantCertPhoto({
|
||||||
|
@Body() required Map<String, dynamic> payload,
|
||||||
|
});
|
||||||
|
|
||||||
@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')
|
@POST('/api/v2/consultant_request/cert_photos/{id}/queue-for-deletion')
|
||||||
Future<void> deletePhoto({@Path() required int id});
|
Future<void> deletePhoto({@Path() required int id});
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
/// Parsed Laravel 422 validation payload for consultant registration UI.
|
||||||
|
class LaravelValidationErrors {
|
||||||
|
const LaravelValidationErrors({
|
||||||
|
required this.summary,
|
||||||
|
this.fieldErrors = const <String, String>{},
|
||||||
|
});
|
||||||
|
|
||||||
|
final String summary;
|
||||||
|
final Map<String, String> fieldErrors;
|
||||||
|
|
||||||
|
static const LaravelValidationErrors empty = LaravelValidationErrors(
|
||||||
|
summary: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get hasFieldErrors => fieldErrors.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _genericValidationSummary = 'Проверьте введённые данные';
|
||||||
|
const _genericNetworkSummary = 'Не удалось отправить данные. Попробуйте ещё раз';
|
||||||
|
|
||||||
|
/// Parses Laravel `errors` map and maps them to user-facing Russian texts.
|
||||||
|
/// Raw backend messages are never shown in the UI.
|
||||||
|
LaravelValidationErrors parseLaravelValidationErrors(Object error) {
|
||||||
|
if (error is! DioException) {
|
||||||
|
return const LaravelValidationErrors(summary: _genericNetworkSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = error.response?.data;
|
||||||
|
if (error.response?.statusCode == 422 && data is Map) {
|
||||||
|
final fieldErrors = <String, String>{};
|
||||||
|
final errors = data['errors'];
|
||||||
|
|
||||||
|
if (errors is Map) {
|
||||||
|
errors.forEach((key, value) {
|
||||||
|
final fieldKey = key.toString();
|
||||||
|
final raw = _rawErrorText(value);
|
||||||
|
fieldErrors[fieldKey] = mapConsultantValidationMessage(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
rawMessage: raw,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldErrors.isNotEmpty) {
|
||||||
|
final uniqueMessages = fieldErrors.values.toSet().toList();
|
||||||
|
return LaravelValidationErrors(
|
||||||
|
summary: uniqueMessages.join('\n'),
|
||||||
|
fieldErrors: fieldErrors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const LaravelValidationErrors(summary: _genericValidationSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const LaravelValidationErrors(summary: _genericNetworkSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _rawErrorText(Object? value) {
|
||||||
|
if (value is List && value.isNotEmpty) {
|
||||||
|
return value.map((e) => e.toString()).join(' ');
|
||||||
|
}
|
||||||
|
if (value is String) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps a backend validation message for [fieldKey] to a short Russian UI string.
|
||||||
|
String mapConsultantValidationMessage({
|
||||||
|
required String fieldKey,
|
||||||
|
required String rawMessage,
|
||||||
|
}) {
|
||||||
|
final field = fieldKey.split('.').last.toLowerCase();
|
||||||
|
final raw = rawMessage.toLowerCase();
|
||||||
|
|
||||||
|
final isUnique =
|
||||||
|
raw.contains('taken') ||
|
||||||
|
raw.contains('unique') ||
|
||||||
|
raw.contains('already') ||
|
||||||
|
raw.contains('уже существ') ||
|
||||||
|
raw.contains('уже занят');
|
||||||
|
final isRequired =
|
||||||
|
raw.contains('required') ||
|
||||||
|
raw.contains('обязательн') ||
|
||||||
|
raw.contains('required_if');
|
||||||
|
final isAccepted =
|
||||||
|
raw.contains('accepted') || field.contains('agreement');
|
||||||
|
final isUrl = raw.contains('url') || field.contains('link');
|
||||||
|
final isFormat =
|
||||||
|
raw.contains('format') ||
|
||||||
|
raw.contains('invalid') ||
|
||||||
|
raw.contains('regex') ||
|
||||||
|
raw.contains('digits') ||
|
||||||
|
raw.contains('size') ||
|
||||||
|
raw.contains('date') ||
|
||||||
|
raw.contains('before');
|
||||||
|
|
||||||
|
if (isAccepted) {
|
||||||
|
if (field.contains('frdo')) {
|
||||||
|
return 'Подтвердите согласие ФРДО';
|
||||||
|
}
|
||||||
|
return 'Примите условия оферты';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnique) {
|
||||||
|
return switch (field) {
|
||||||
|
'inn' => 'Такой ИНН существует',
|
||||||
|
'snils' => 'Такой СНИЛС существует',
|
||||||
|
'passport_number' || 'passport_series' => 'Такой паспорт существует',
|
||||||
|
'sbp_phone' => 'Такой телефон СБП существует',
|
||||||
|
_ => 'Такие данные уже существуют',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRequired) {
|
||||||
|
return switch (field) {
|
||||||
|
'inn' => 'Укажите ИНН',
|
||||||
|
'snils' => 'Укажите СНИЛС',
|
||||||
|
'last_name' => 'Укажите фамилию',
|
||||||
|
'first_name' => 'Укажите имя',
|
||||||
|
'middle_name' => 'Укажите отчество',
|
||||||
|
'passport_series' => 'Укажите серию паспорта',
|
||||||
|
'passport_number' => 'Укажите номер паспорта',
|
||||||
|
'passport_issue_date' => 'Укажите дату выдачи паспорта',
|
||||||
|
'passport_issuer' => 'Укажите, кем выдан паспорт',
|
||||||
|
'passport_issuer_code' => 'Укажите код подразделения',
|
||||||
|
'birth_date' => 'Укажите дату рождения',
|
||||||
|
'birth_place' => 'Укажите место рождения',
|
||||||
|
'registration_address' => 'Укажите адрес регистрации',
|
||||||
|
'residence_address' => 'Укажите адрес проживания',
|
||||||
|
'citizenship' => 'Укажите гражданство',
|
||||||
|
'sbp_phone' => 'Укажите телефон СБП',
|
||||||
|
'sbp_bank_member_id' => 'Выберите банк СБП',
|
||||||
|
'completed_courses' => 'Добавьте хотя бы один курс',
|
||||||
|
'title' => 'Укажите название курса',
|
||||||
|
'date' => 'Укажите дату курса',
|
||||||
|
'agreement_accepted' => 'Примите условия оферты',
|
||||||
|
'frdo_agreement_accepted' => 'Подтвердите согласие ФРДО',
|
||||||
|
_ => 'Заполните поле',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUrl) {
|
||||||
|
return 'Некорректная ссылка';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFormat || raw.isNotEmpty) {
|
||||||
|
return switch (field) {
|
||||||
|
'inn' => 'Некорректный ИНН',
|
||||||
|
'snils' => 'Некорректный СНИЛС',
|
||||||
|
'passport_series' => 'Некорректная серия паспорта',
|
||||||
|
'passport_number' => 'Некорректный номер паспорта',
|
||||||
|
'passport_issue_date' => 'Некорректная дата выдачи',
|
||||||
|
'passport_issuer_code' => 'Некорректный код подразделения',
|
||||||
|
'birth_date' => 'Некорректная дата рождения',
|
||||||
|
'citizenship' => 'Некорректный код гражданства',
|
||||||
|
'sbp_phone' => 'Некорректный телефон СБП',
|
||||||
|
'title' => 'Слишком короткое название курса',
|
||||||
|
'date' => 'Некорректная дата курса',
|
||||||
|
_ => 'Некорректное значение',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Проверьте поле';
|
||||||
|
}
|
||||||
@@ -238,7 +238,10 @@ 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('becomeConsultantScreen'),
|
onTap: () => GoRouter.of(context).pushNamed(
|
||||||
|
'becomeConsultantScreen',
|
||||||
|
extra: {'slug': state.profile.slug},
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
height: 52,
|
height: 52,
|
||||||
|
|||||||
@@ -33,4 +33,3 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,4 +100,3 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('toJson serializes all fields correctly', () {
|
test('toJson serializes all fields correctly', () {
|
||||||
const model = CertificatesModel(title: 'Certificate Title', date: '2024-01-15');
|
const model = CertificatesModel(
|
||||||
|
title: 'Certificate Title',
|
||||||
|
date: '2024-01-15',
|
||||||
|
);
|
||||||
|
|
||||||
final json = model.toJson();
|
final json = model.toJson();
|
||||||
|
|
||||||
@@ -105,7 +108,10 @@ void main() {
|
|||||||
|
|
||||||
test('toJson serializes all fields correctly', () {
|
test('toJson serializes all fields correctly', () {
|
||||||
const socialLinks = SocialLinksModel(vkLink: 'https://vk.com/user');
|
const socialLinks = SocialLinksModel(vkLink: 'https://vk.com/user');
|
||||||
const certificates = CertificatesModel(title: 'Course 1', date: '2024-01-01');
|
const certificates = CertificatesModel(
|
||||||
|
title: 'Course 1',
|
||||||
|
date: '2024-01-01',
|
||||||
|
);
|
||||||
const model = ConsultantProfileModel(
|
const model = ConsultantProfileModel(
|
||||||
hasMedicalEducation: true,
|
hasMedicalEducation: true,
|
||||||
socialLinks: socialLinks,
|
socialLinks: socialLinks,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.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/profile/models/profile_model.dart';
|
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -46,7 +47,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('fromJson handles null additional field', () {
|
test('fromJson handles null additional field', () {
|
||||||
final json = {'title': 'Test title', 'status': 'canceled', 'additional': null};
|
final json = {
|
||||||
|
'title': 'Test title',
|
||||||
|
'status': 'canceled',
|
||||||
|
'additional': null,
|
||||||
|
};
|
||||||
|
|
||||||
final model = CanceledModerationModel.fromJson(json);
|
final model = CanceledModerationModel.fromJson(json);
|
||||||
|
|
||||||
@@ -56,7 +61,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('toJson serializes all fields correctly', () {
|
test('toJson serializes all fields correctly', () {
|
||||||
const model = CanceledModerationModel(title: 'Test title', status: ModerationStatus.canceled, additional: ['reason1', 'reason2']);
|
const model = CanceledModerationModel(
|
||||||
|
title: 'Test title',
|
||||||
|
status: ModerationStatus.canceled,
|
||||||
|
additional: ['reason1', 'reason2'],
|
||||||
|
);
|
||||||
|
|
||||||
final json = model.toJson();
|
final json = model.toJson();
|
||||||
|
|
||||||
@@ -133,7 +142,7 @@ void main() {
|
|||||||
'inn': '123456789012',
|
'inn': '123456789012',
|
||||||
'snils': '123-456-789 00',
|
'snils': '123-456-789 00',
|
||||||
'state': 'created',
|
'state': 'created',
|
||||||
'alfa_scoring_status': 'pending',
|
'alfa_scoring_status': 'scoring_failed',
|
||||||
'alfa_scoring_request_id': 'abc',
|
'alfa_scoring_request_id': 'abc',
|
||||||
'passport_series': '1234',
|
'passport_series': '1234',
|
||||||
'passport_number': '567890',
|
'passport_number': '567890',
|
||||||
@@ -154,19 +163,35 @@ void main() {
|
|||||||
expect(model.legalEntity!.inn, '123456789012');
|
expect(model.legalEntity!.inn, '123456789012');
|
||||||
expect(model.legalEntity!.passportSeries, '1234');
|
expect(model.legalEntity!.passportSeries, '1234');
|
||||||
expect(model.legalEntity!.normalizedPassportIssueDate, '2020-01-15');
|
expect(model.legalEntity!.normalizedPassportIssueDate, '2020-01-15');
|
||||||
expect(model.legalEntity!.alfaScoringStatus, 'pending');
|
expect(
|
||||||
|
model.legalEntity!.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.scoringFailed,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('normalizeApiDate strips time from DateTime.toString format', () {
|
test('normalizeApiDate strips time from DateTime.toString format', () {
|
||||||
expect(LegalEntityModel.normalizeApiDate('2023-09-22 00:00:00.000'), '2023-09-22');
|
expect(
|
||||||
|
LegalEntityModel.normalizeApiDate('2023-09-22 00:00:00.000'),
|
||||||
|
'2023-09-22',
|
||||||
|
);
|
||||||
expect(LegalEntityModel.normalizeApiDate('2023-09-22'), '2023-09-22');
|
expect(LegalEntityModel.normalizeApiDate('2023-09-22'), '2023-09-22');
|
||||||
expect(LegalEntityModel.normalizeApiDate(null), isNull);
|
expect(LegalEntityModel.normalizeApiDate(null), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toJson serializes ConsultationRequestModel correctly', () {
|
test('toJson serializes ConsultationRequestModel correctly', () {
|
||||||
const educationModel = EducationModel(requestState: ModerationStatus.approved);
|
const educationModel = EducationModel(
|
||||||
const photos = [CertPhotosModel(url: 'https://example.com/photo1.jpg'), CertPhotosModel(url: 'https://example.com/photo2.jpg')];
|
requestState: ModerationStatus.approved,
|
||||||
const model = ConsultationRequestModel(ratingPlace: 5, educationModel: educationModel, photos: photos, canceledModeration: null);
|
);
|
||||||
|
const photos = [
|
||||||
|
CertPhotosModel(url: 'https://example.com/photo1.jpg'),
|
||||||
|
CertPhotosModel(url: 'https://example.com/photo2.jpg'),
|
||||||
|
];
|
||||||
|
const model = ConsultationRequestModel(
|
||||||
|
ratingPlace: 5,
|
||||||
|
educationModel: educationModel,
|
||||||
|
photos: photos,
|
||||||
|
canceledModeration: null,
|
||||||
|
);
|
||||||
|
|
||||||
final json = model.toJson();
|
final json = model.toJson();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ void main() {
|
|||||||
acceptOffer: false,
|
acceptOffer: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
requestModel: null,
|
requestModel: null,
|
||||||
isUpdating: false
|
isUpdating: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(state.consultant, consultant);
|
expect(state.consultant, consultant);
|
||||||
@@ -20,6 +20,8 @@ void main() {
|
|||||||
expect(state.loading, false);
|
expect(state.loading, false);
|
||||||
expect(state.requestModel, isNull);
|
expect(state.requestModel, isNull);
|
||||||
expect(state.certPhotos, isEmpty);
|
expect(state.certPhotos, isEmpty);
|
||||||
|
expect(state.awaitingScoringOutcome, isFalse);
|
||||||
|
expect(state.fieldErrors, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates instance with custom values', () {
|
test('creates instance with custom values', () {
|
||||||
@@ -38,7 +40,7 @@ void main() {
|
|||||||
loading: true,
|
loading: true,
|
||||||
requestModel: null,
|
requestModel: null,
|
||||||
certPhotos: [certPhoto],
|
certPhotos: [certPhoto],
|
||||||
isUpdating: false
|
isUpdating: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(state.acceptOffer, true);
|
expect(state.acceptOffer, true);
|
||||||
@@ -54,7 +56,7 @@ void main() {
|
|||||||
acceptOffer: false,
|
acceptOffer: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
requestModel: null,
|
requestModel: null,
|
||||||
isUpdating: false
|
isUpdating: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final updatedState = originalState.copyWith(
|
final updatedState = originalState.copyWith(
|
||||||
@@ -92,7 +94,7 @@ void main() {
|
|||||||
loading: false,
|
loading: false,
|
||||||
requestModel: null,
|
requestModel: null,
|
||||||
certPhotos: [certPhoto1],
|
certPhotos: [certPhoto1],
|
||||||
isUpdating: false
|
isUpdating: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final updatedState = originalState.copyWith(
|
final updatedState = originalState.copyWith(
|
||||||
@@ -100,9 +102,14 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(updatedState.certPhotos.length, 2);
|
expect(updatedState.certPhotos.length, 2);
|
||||||
expect(updatedState.certPhotos.first.url, 'https://example.com/photo1.jpg');
|
expect(
|
||||||
expect(updatedState.certPhotos.last.url, 'https://example.com/photo2.jpg');
|
updatedState.certPhotos.first.url,
|
||||||
|
'https://example.com/photo1.jpg',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
updatedState.certPhotos.last.url,
|
||||||
|
'https://example.com/photo2.jpg',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.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_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';
|
||||||
@@ -27,18 +29,47 @@ void main() {
|
|||||||
late List<SbpBankModel> sbpBanksFromFixture;
|
late List<SbpBankModel> sbpBanksFromFixture;
|
||||||
late ConsultantProfileModel consultantProfile;
|
late ConsultantProfileModel consultantProfile;
|
||||||
|
|
||||||
|
ConsultationRequestModel requestWithScoring(String scoringStatus) {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'consultant/consultation_request.json',
|
||||||
|
);
|
||||||
|
final legal = Map<String, dynamic>.from(
|
||||||
|
json['legal_entity_resource'] as Map,
|
||||||
|
);
|
||||||
|
legal['alfa_scoring_status'] = scoringStatus;
|
||||||
|
json['legal_entity_resource'] = legal;
|
||||||
|
return ConsultationRequestModel.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
repository = MockConsultantRepository();
|
repository = MockConsultantRepository();
|
||||||
cubit = RegisterConsultantCubit(consultantRepository: repository);
|
cubit = RegisterConsultantCubit(
|
||||||
requestModel = ConsultationRequestModel.fromJson(FixtureReader.readAsMap('consultant/consultation_request.json'));
|
consultantRepository: repository,
|
||||||
certPhotosFromFixture = FixtureReader.readAsList(
|
scoringPollInterval: const Duration(milliseconds: 30),
|
||||||
'consultant/consultant_cert_photos.json',
|
);
|
||||||
).map((item) => ConsultantCertPhotoModel.fromJson(item as Map<String, dynamic>)).toList();
|
requestModel = ConsultationRequestModel.fromJson(
|
||||||
sbpBanksFromFixture = FixtureReader.readAsList(
|
FixtureReader.readAsMap('consultant/consultation_request.json'),
|
||||||
'consultant/sbp_banks.json',
|
);
|
||||||
).map((item) => SbpBankModel.fromJson(item as Map<String, dynamic>)).toList();
|
certPhotosFromFixture =
|
||||||
consultantProfile = ConsultantProfileModel.fromJson(FixtureReader.readAsMap('consultant/consultant_profile_request.json'));
|
FixtureReader.readAsList('consultant/consultant_cert_photos.json')
|
||||||
when(() => repository.getSbpBanks()).thenAnswer((_) async => sbpBanksFromFixture);
|
.map(
|
||||||
|
(item) => ConsultantCertPhotoModel.fromJson(
|
||||||
|
item as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
sbpBanksFromFixture =
|
||||||
|
FixtureReader.readAsList('consultant/sbp_banks.json')
|
||||||
|
.map(
|
||||||
|
(item) => SbpBankModel.fromJson(item as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
consultantProfile = ConsultantProfileModel.fromJson(
|
||||||
|
FixtureReader.readAsMap('consultant/consultant_profile_request.json'),
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => repository.getSbpBanks(),
|
||||||
|
).thenAnswer((_) async => sbpBanksFromFixture);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@@ -50,11 +81,16 @@ void main() {
|
|||||||
expect(cubit.state.consultant.hasMedicalEducation, isTrue);
|
expect(cubit.state.consultant.hasMedicalEducation, isTrue);
|
||||||
expect(cubit.state.acceptOffer, isFalse);
|
expect(cubit.state.acceptOffer, isFalse);
|
||||||
expect(cubit.state.requestModel, isNull);
|
expect(cubit.state.requestModel, isNull);
|
||||||
|
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.none);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getMyRequestStatus hydrates state with backend data', () async {
|
test('getMyRequestStatus hydrates state with backend data', () async {
|
||||||
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
|
when(
|
||||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestModel);
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
|
||||||
await cubit.getMyRequestStatus('my-consultant-slug');
|
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||||
|
|
||||||
@@ -62,41 +98,90 @@ 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);
|
||||||
// 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!.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.consultant.legalType, 'individualPerson');
|
expect(state.consultant.legalType, 'individualPerson');
|
||||||
expect(state.consultant.inn, '123456789012');
|
expect(state.consultant.inn, '123456789012');
|
||||||
expect(state.consultant.passportSeries, '1234');
|
expect(state.consultant.passportSeries, '1234');
|
||||||
expect(state.consultant.passportIssueDate, '2020-01-15');
|
expect(state.consultant.passportIssueDate, '2020-01-15');
|
||||||
expect(state.consultant.birthPlace, 'г. Москва');
|
expect(state.consultant.birthPlace, 'г. Москва');
|
||||||
// All cert photos from backend are kept in state (visible + hidden).
|
|
||||||
expect(state.certPhotos, hasLength(3));
|
expect(state.certPhotos, hasLength(3));
|
||||||
expect(state.certPhotos.map((e) => e.id), containsAll(<int>[101, 102, 103]));
|
expect(
|
||||||
|
state.certPhotos.map((e) => e.id),
|
||||||
|
containsAll(<int>[101, 102, 103]),
|
||||||
|
);
|
||||||
expect(state.sbpBanks, hasLength(2));
|
expect(state.sbpBanks, hasLength(2));
|
||||||
expect(state.sbpBanks.first.bankName, 'Сбербанк');
|
expect(state.sbpBanks.first.bankName, 'Сбербанк');
|
||||||
verify(() => repository.getMyRequestStatus('my-consultant-slug')).called(1);
|
expect(state.alfaScoringStatus, AlfaScoringStatus.scoringFailed);
|
||||||
|
expect(state.alfaScoringStatus.canRetry, isTrue);
|
||||||
|
expect(state.isScoringPolling, isFalse);
|
||||||
|
expect(
|
||||||
|
state.awaitingScoringOutcome,
|
||||||
|
isFalse,
|
||||||
|
reason: 'hydrate must not arm success sheet',
|
||||||
|
);
|
||||||
|
verify(
|
||||||
|
() => repository.getMyRequestStatus('my-consultant-slug'),
|
||||||
|
).called(1);
|
||||||
verify(() => repository.getConsultantCertPhotos()).called(1);
|
verify(() => repository.getConsultantCertPhotos()).called(1);
|
||||||
verify(() => repository.getSbpBanks()).called(1);
|
verify(() => repository.getSbpBanks()).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('disableLoading stops spinner and loads cert photos', () async {
|
test(
|
||||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
'getMyRequestStatus with processing status starts scoring poll',
|
||||||
|
() async {
|
||||||
|
final pending = requestWithScoring('scoring_pending');
|
||||||
|
final complete = requestWithScoring('scoring_complete');
|
||||||
|
var calls = 0;
|
||||||
|
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((
|
||||||
|
_,
|
||||||
|
) async {
|
||||||
|
calls++;
|
||||||
|
return calls == 1 ? pending : complete;
|
||||||
|
});
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
|
||||||
await cubit.disableLoading();
|
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||||
|
|
||||||
|
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringPending);
|
||||||
|
expect(cubit.state.isScoringPolling, isTrue);
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.scoringComplete,
|
||||||
|
);
|
||||||
|
expect(cubit.state.isScoringPolling, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('disableLoading stops spinner and loads cert photos', () async {
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
|
||||||
|
await cubit.disableLoading(slug: 'my-consultant-slug');
|
||||||
|
|
||||||
expect(cubit.state.loading, isFalse);
|
expect(cubit.state.loading, isFalse);
|
||||||
// disableLoading now keeps all photos from repository.
|
|
||||||
expect(cubit.state.certPhotos, hasLength(3));
|
expect(cubit.state.certPhotos, hasLength(3));
|
||||||
expect(cubit.state.sbpBanks, hasLength(2));
|
expect(cubit.state.sbpBanks, hasLength(2));
|
||||||
|
expect(cubit.userSlug, 'my-consultant-slug');
|
||||||
verify(() => repository.getConsultantCertPhotos()).called(1);
|
verify(() => repository.getConsultantCertPhotos()).called(1);
|
||||||
verify(() => repository.getSbpBanks()).called(1);
|
verify(() => repository.getSbpBanks()).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('searchSbpBanks filters banks by name', () async {
|
test('searchSbpBanks filters banks by name', () async {
|
||||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
await cubit.disableLoading();
|
await cubit.disableLoading();
|
||||||
|
|
||||||
final results = cubit.searchSbpBanks('сбер');
|
final results = cubit.searchSbpBanks('сбер');
|
||||||
@@ -106,7 +191,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('findSbpBankById returns matching bank', () async {
|
test('findSbpBankById returns matching bank', () async {
|
||||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
await cubit.disableLoading();
|
await cubit.disableLoading();
|
||||||
|
|
||||||
final bank = cubit.findSbpBankById('100000000004');
|
final bank = cubit.findSbpBankById('100000000004');
|
||||||
@@ -114,42 +201,205 @@ void main() {
|
|||||||
expect(bank?.bankName, 'Т-Банк');
|
expect(bank?.bankName, 'Т-Банк');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updateConsultantProfileModel replaces consultant data and offer flag', () {
|
test(
|
||||||
cubit.updateConsultantProfileModel(consultantProfile, true);
|
'updateConsultantProfileModel replaces consultant data and offer flag',
|
||||||
|
() {
|
||||||
|
cubit.updateConsultantProfileModel(consultantProfile, true);
|
||||||
|
|
||||||
expect(cubit.state.consultant, consultantProfile);
|
expect(cubit.state.consultant, consultantProfile);
|
||||||
expect(cubit.state.acceptOffer, isTrue);
|
expect(cubit.state.acceptOffer, isTrue);
|
||||||
expect(cubit.state.loading, isFalse);
|
expect(cubit.state.loading, isFalse);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('registerConsultant forwards payload with update=false by default', () async {
|
test(
|
||||||
when(
|
'registerConsultant forwards payload with update=false by default',
|
||||||
() => repository.registerConsultant(
|
() async {
|
||||||
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
when(
|
||||||
update: any<bool>(named: 'update'),
|
() => repository.registerConsultant(
|
||||||
),
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
).thenAnswer((_) async {});
|
update: any<bool>(named: 'update'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
cubit.updateConsultantProfileModel(consultantProfile);
|
cubit.updateConsultantProfileModel(consultantProfile);
|
||||||
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
|
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
|
||||||
await cubit.registerConsultant();
|
await cubit.registerConsultant();
|
||||||
|
|
||||||
final captured = verify(
|
final captured =
|
||||||
() => repository.registerConsultant(
|
verify(
|
||||||
consultant: captureAny<ConsultantProfileModel>(named: 'consultant'),
|
() => repository.registerConsultant(
|
||||||
update: false,
|
consultant: captureAny<ConsultantProfileModel>(
|
||||||
),
|
named: 'consultant',
|
||||||
).captured.single as ConsultantProfileModel;
|
),
|
||||||
|
update: false,
|
||||||
|
),
|
||||||
|
).captured.single
|
||||||
|
as ConsultantProfileModel;
|
||||||
|
|
||||||
expect(captured.legalType, 'individualPerson');
|
expect(captured.legalType, 'individualPerson');
|
||||||
expect(captured.agreementAccepted, isTrue);
|
expect(captured.agreementAccepted, isTrue);
|
||||||
expect(captured.frdoAgreementAccepted, isTrue);
|
expect(captured.frdoAgreementAccepted, isTrue);
|
||||||
expect(captured.inn, isNotNull);
|
expect(captured.inn, isNotNull);
|
||||||
});
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.state.isScoringPolling,
|
||||||
|
isFalse,
|
||||||
|
reason: 'no slug for poll',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cubit.state.awaitingScoringOutcome,
|
||||||
|
isFalse,
|
||||||
|
reason: 'cannot await scoring without slug',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'getMyRequestStatus with scoring_complete does not arm awaiting flag',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestWithScoring('scoring_complete'));
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
|
||||||
|
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.scoringComplete,
|
||||||
|
);
|
||||||
|
expect(cubit.state.awaitingScoringOutcome, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'registerConsultant with slug starts scoring poll after submit',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
when(
|
||||||
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestWithScoring('scoring_complete'));
|
||||||
|
when(
|
||||||
|
() => repository.registerConsultant(
|
||||||
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
|
update: any<bool>(named: 'update'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await cubit.disableLoading(slug: 'my-consultant-slug');
|
||||||
|
cubit.updateConsultantProfileModel(consultantProfile);
|
||||||
|
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
|
||||||
|
await cubit.registerConsultant();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
);
|
||||||
|
expect(cubit.state.isScoringPolling, isTrue);
|
||||||
|
expect(cubit.state.awaitingScoringOutcome, isTrue);
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.scoringComplete,
|
||||||
|
);
|
||||||
|
expect(cubit.state.isScoringPolling, isFalse);
|
||||||
|
expect(
|
||||||
|
cubit.state.awaitingScoringOutcome,
|
||||||
|
isFalse,
|
||||||
|
reason: 'flag clears on terminal scoring for one-shot success sheet',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'scoring poll failed clears awaiting flag without leaving processing',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
when(
|
||||||
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestWithScoring('scoring_failed'));
|
||||||
|
when(
|
||||||
|
() => repository.registerConsultant(
|
||||||
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
|
update: any<bool>(named: 'update'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await cubit.disableLoading(slug: 'my-consultant-slug');
|
||||||
|
cubit.updateConsultantProfileModel(consultantProfile);
|
||||||
|
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
|
||||||
|
await cubit.registerConsultant();
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringFailed);
|
||||||
|
expect(cubit.state.isScoringPolling, isFalse);
|
||||||
|
expect(cubit.state.awaitingScoringOutcome, isFalse);
|
||||||
|
expect(cubit.state.alfaScoringStatus.canRetry, isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'scoring poll keeps processing UI while API still returns none',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
var calls = 0;
|
||||||
|
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((
|
||||||
|
_,
|
||||||
|
) async {
|
||||||
|
calls++;
|
||||||
|
if (calls <= 2) {
|
||||||
|
return requestWithScoring('none');
|
||||||
|
}
|
||||||
|
return requestWithScoring('scoring_complete');
|
||||||
|
});
|
||||||
|
when(
|
||||||
|
() => repository.registerConsultant(
|
||||||
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
|
update: any<bool>(named: 'update'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await cubit.disableLoading(slug: 'my-consultant-slug');
|
||||||
|
cubit.updateConsultantProfileModel(consultantProfile);
|
||||||
|
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
|
||||||
|
await cubit.registerConsultant();
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
);
|
||||||
|
expect(cubit.state.isScoringPolling, isTrue);
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 80));
|
||||||
|
expect(
|
||||||
|
cubit.state.alfaScoringStatus,
|
||||||
|
AlfaScoringStatus.scoringComplete,
|
||||||
|
);
|
||||||
|
expect(cubit.state.isScoringPolling, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('registerConsultant sends legal payload with update=true', () async {
|
test('registerConsultant sends legal payload with update=true', () async {
|
||||||
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
|
when(
|
||||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestModel);
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
when(
|
when(
|
||||||
() => repository.registerConsultant(
|
() => repository.registerConsultant(
|
||||||
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
@@ -160,12 +410,16 @@ void main() {
|
|||||||
await cubit.getMyRequestStatus('my-consultant-slug');
|
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||||
await cubit.registerConsultant();
|
await cubit.registerConsultant();
|
||||||
|
|
||||||
final captured = verify(
|
final captured =
|
||||||
() => repository.registerConsultant(
|
verify(
|
||||||
consultant: captureAny<ConsultantProfileModel>(named: 'consultant'),
|
() => repository.registerConsultant(
|
||||||
update: true,
|
consultant: captureAny<ConsultantProfileModel>(
|
||||||
),
|
named: 'consultant',
|
||||||
).captured.single as ConsultantProfileModel;
|
),
|
||||||
|
update: true,
|
||||||
|
),
|
||||||
|
).captured.single
|
||||||
|
as ConsultantProfileModel;
|
||||||
|
|
||||||
final json = captured.toJson();
|
final json = captured.toJson();
|
||||||
expect(json['legal_type'], 'individualPerson');
|
expect(json['legal_type'], 'individualPerson');
|
||||||
@@ -175,6 +429,40 @@ void main() {
|
|||||||
expect(json.containsKey('frdo_agreement_accepted'), isFalse);
|
expect(json.containsKey('frdo_agreement_accepted'), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('retryScoring calls update and restarts poll', () async {
|
||||||
|
when(
|
||||||
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestModel);
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
when(
|
||||||
|
() => repository.registerConsultant(
|
||||||
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
|
update: any<bool>(named: 'update'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||||
|
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringFailed);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
|
).thenAnswer((_) async => requestWithScoring('scoring_complete'));
|
||||||
|
await cubit.retryScoring();
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => repository.registerConsultant(
|
||||||
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
|
update: true,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringComplete);
|
||||||
|
expect(cubit.state.isScoringPolling, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('registerConsultant blocks submit without passport data', () async {
|
test('registerConsultant blocks submit without passport data', () async {
|
||||||
when(
|
when(
|
||||||
() => repository.registerConsultant(
|
() => repository.registerConsultant(
|
||||||
@@ -194,18 +482,59 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('registerConsultant maps Laravel 422 into fieldErrors', () async {
|
||||||
|
when(
|
||||||
|
() => repository.registerConsultant(
|
||||||
|
consultant: any<ConsultantProfileModel>(named: 'consultant'),
|
||||||
|
update: any<bool>(named: 'update'),
|
||||||
|
),
|
||||||
|
).thenThrow(
|
||||||
|
DioException(
|
||||||
|
requestOptions: RequestOptions(
|
||||||
|
path: '/api/v2/consultant_request/registration',
|
||||||
|
),
|
||||||
|
response: Response(
|
||||||
|
requestOptions: RequestOptions(
|
||||||
|
path: '/api/v2/consultant_request/registration',
|
||||||
|
),
|
||||||
|
statusCode: 422,
|
||||||
|
data: {
|
||||||
|
'message': 'The given data was invalid.',
|
||||||
|
'errors': {
|
||||||
|
'inn': ['The inn format is invalid.'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
type: DioExceptionType.badResponse,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
cubit.updateConsultantProfileModel(consultantProfile);
|
||||||
|
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
|
||||||
|
await cubit.registerConsultant();
|
||||||
|
|
||||||
|
expect(cubit.state.fieldErrors['inn'], 'Некорректный ИНН');
|
||||||
|
expect(cubit.state.error, contains('Некорректный ИНН'));
|
||||||
|
expect(cubit.state.awaitingScoringOutcome, isFalse);
|
||||||
|
expect(cubit.state.isScoringPolling, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('removeCertPhoto marks cert as deleted locally', () async {
|
test('removeCertPhoto marks cert as deleted locally', () async {
|
||||||
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
|
when(
|
||||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
() => repository.getMyRequestStatus(any<String>()),
|
||||||
when(() => repository.hideConsultantCertPhoto(any<int>())).thenAnswer((_) async {});
|
).thenAnswer((_) async => requestModel);
|
||||||
|
when(
|
||||||
|
() => repository.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotosFromFixture);
|
||||||
|
when(
|
||||||
|
() => repository.hideConsultantCertPhoto(any<int>()),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
await cubit.getMyRequestStatus('my-consultant-slug');
|
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||||
// All photos from backend are present in state.
|
|
||||||
expect(cubit.state.certPhotos, hasLength(3));
|
expect(cubit.state.certPhotos, hasLength(3));
|
||||||
|
|
||||||
await cubit.removeCertPhoto(101, null);
|
await cubit.removeCertPhoto(101, null);
|
||||||
|
|
||||||
// Current implementation only tracks deleted IDs; photos list remains unchanged here.
|
|
||||||
verifyNever(() => repository.hideConsultantCertPhoto(any<int>()));
|
verifyNever(() => repository.hideConsultantCertPhoto(any<int>()));
|
||||||
expect(cubit.state.deletedPhotoIds, contains(101));
|
expect(cubit.state.deletedPhotoIds, contains(101));
|
||||||
expect(cubit.state.certPhotos, hasLength(3));
|
expect(cubit.state.certPhotos, hasLength(3));
|
||||||
@@ -214,7 +543,9 @@ void main() {
|
|||||||
test('uploadCertPhotos - не загружает если список пуст', () async {
|
test('uploadCertPhotos - не загружает если список пуст', () async {
|
||||||
await cubit.addCertPhotos([]);
|
await cubit.addCertPhotos([]);
|
||||||
|
|
||||||
verifyNever(() => repository.storeConsultantCertPhotos(files: any(named: 'files')));
|
verifyNever(
|
||||||
|
() => repository.storeConsultantCertPhotos(files: any(named: 'files')),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updateConsultantProfileModel - обновляет без acceptOffer', () {
|
test('updateConsultantProfileModel - обновляет без acceptOffer', () {
|
||||||
@@ -225,4 +556,38 @@ void main() {
|
|||||||
expect(cubit.state.loading, isFalse);
|
expect(cubit.state.loading, isFalse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('AlfaScoringStatus', () {
|
||||||
|
test('fromString maps known values', () {
|
||||||
|
expect(
|
||||||
|
AlfaScoringStatus.fromString('beneficiary_creating'),
|
||||||
|
AlfaScoringStatus.beneficiaryCreating,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AlfaScoringStatus.fromString('scoring_pending'),
|
||||||
|
AlfaScoringStatus.scoringPending,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AlfaScoringStatus.fromString('scoring_complete'),
|
||||||
|
AlfaScoringStatus.scoringComplete,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AlfaScoringStatus.fromString('scoring_failed'),
|
||||||
|
AlfaScoringStatus.scoringFailed,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AlfaScoringStatus.fromString('beneficiary_failed'),
|
||||||
|
AlfaScoringStatus.beneficiaryFailed,
|
||||||
|
);
|
||||||
|
expect(AlfaScoringStatus.fromString(null), AlfaScoringStatus.none);
|
||||||
|
expect(AlfaScoringStatus.fromString('unknown'), AlfaScoringStatus.none);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('helpers', () {
|
||||||
|
expect(AlfaScoringStatus.scoringPending.isProcessing, isTrue);
|
||||||
|
expect(AlfaScoringStatus.scoringFailed.canRetry, isTrue);
|
||||||
|
expect(AlfaScoringStatus.scoringComplete.isComplete, isTrue);
|
||||||
|
expect(AlfaScoringStatus.scoringComplete.isTerminal, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,28 @@ void main() {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeHelper().themeData(),
|
theme: ThemeHelper().themeData(),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
|
body: BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
hasMedicalEducation: null,
|
||||||
|
socialLinks: null,
|
||||||
|
photos: null,
|
||||||
|
certificates: null,
|
||||||
|
);
|
||||||
|
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));
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ import 'certificates_and_diplomas_screen_smoke_test.mocks.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('CertificatesAndDiplomasScreen smoke', () {
|
group('CertificatesAndDiplomasScreen smoke', () {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
dotenv.loadFromString(
|
||||||
|
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await sl.reset();
|
await sl.reset();
|
||||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
sl.registerLazySingleton<AppConfig>(
|
||||||
|
() =>
|
||||||
|
AppConfig(buildType: AppBuildType.tests)
|
||||||
|
..setBaseUrl('http://localhost'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget wrap(Widget child) {
|
Widget wrap(Widget child) {
|
||||||
@@ -45,15 +51,30 @@ 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(
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
hasMedicalEducation: null,
|
||||||
|
socialLinks: null,
|
||||||
|
photos: null,
|
||||||
|
certificates: null,
|
||||||
|
);
|
||||||
|
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));
|
||||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
||||||
|
|
||||||
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen())));
|
await tester.pumpWidget(
|
||||||
|
BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: wrap(const CertificatesAndDiplomasScreen()),
|
||||||
|
),
|
||||||
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
@@ -67,16 +88,28 @@ void main() {
|
|||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: null,
|
socialLinks: null,
|
||||||
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,
|
||||||
|
isUpdating: 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));
|
||||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
||||||
|
|
||||||
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen())));
|
await tester.pumpWidget(
|
||||||
|
BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: wrap(const CertificatesAndDiplomasScreen()),
|
||||||
|
),
|
||||||
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
|||||||
@@ -19,50 +19,78 @@ import 'last_step_screen_test.mocks.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('LastStepScreen smoke', () {
|
group('LastStepScreen smoke', () {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
dotenv.loadFromString(
|
||||||
|
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await sl.reset();
|
await sl.reset();
|
||||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
sl.registerLazySingleton<AppConfig>(
|
||||||
|
() =>
|
||||||
|
AppConfig(buildType: AppBuildType.tests)
|
||||||
|
..setBaseUrl('http://localhost'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget wrap(Widget child, RegisterConsultantCubit cubit) {
|
Widget wrap(Widget child, RegisterConsultantCubit cubit) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeHelper().themeData(),
|
theme: ThemeHelper().themeData(),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
|
body: BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('renders last step screen with both agreements for new registration', (tester) async {
|
testWidgets(
|
||||||
final cubit = MockRegisterConsultantCubit();
|
'renders last step screen with both agreements for new registration',
|
||||||
const consultant = ConsultantProfileModel(
|
(tester) async {
|
||||||
hasMedicalEducation: null,
|
final cubit = MockRegisterConsultantCubit();
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
const consultant = ConsultantProfileModel(
|
||||||
photos: null,
|
hasMedicalEducation: null,
|
||||||
certificates: null,
|
socialLinks: SocialLinksModel(
|
||||||
);
|
vkLink: null,
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
|
photos: null,
|
||||||
|
certificates: null,
|
||||||
|
);
|
||||||
|
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));
|
||||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
|
|
||||||
await tester.pumpWidget(wrap(const LastStepScreen(), cubit));
|
await tester.pumpWidget(wrap(const LastStepScreen(), cubit));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.byType(LastStepScreen), findsOneWidget);
|
expect(find.byType(LastStepScreen), findsOneWidget);
|
||||||
expect(find.text('Последний шаг'), findsOneWidget);
|
expect(find.text('Последний шаг'), findsOneWidget);
|
||||||
expect(find.byType(CheckboxListTile), findsNWidgets(2));
|
expect(find.byType(CheckboxListTile), findsNWidgets(2));
|
||||||
expect(find.textContaining('ФРДО'), findsOneWidget);
|
expect(find.textContaining('ФРДО'), findsOneWidget);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('hides FRDO checkbox in update mode', (tester) async {
|
testWidgets('hides FRDO checkbox in update mode', (tester) async {
|
||||||
final cubit = MockRegisterConsultantCubit();
|
final cubit = MockRegisterConsultantCubit();
|
||||||
const consultant = ConsultantProfileModel();
|
const consultant = ConsultantProfileModel();
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: true);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: true,
|
||||||
|
);
|
||||||
|
|
||||||
when(cubit.state).thenReturn(state);
|
when(cubit.state).thenReturn(state);
|
||||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ void main() {
|
|||||||
late RegisterConsultantCubit cubit;
|
late RegisterConsultantCubit cubit;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
cubit = RegisterConsultantCubit(consultantRepository: MockConsultantRepository());
|
cubit = RegisterConsultantCubit(
|
||||||
|
consultantRepository: MockConsultantRepository(),
|
||||||
|
);
|
||||||
cubit.emit(
|
cubit.emit(
|
||||||
const RegisterConsultantState(
|
const RegisterConsultantState(
|
||||||
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel()),
|
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel()),
|
||||||
@@ -35,7 +37,10 @@ void main() {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeHelper().themeData(),
|
theme: ThemeHelper().themeData(),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
|
body: BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,18 @@ import 'stepper_become_consultant_screen_smoke_test.mocks.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('PhotoOrScansScreen smoke', () {
|
group('PhotoOrScansScreen smoke', () {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
dotenv.loadFromString(
|
||||||
|
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await sl.reset();
|
await sl.reset();
|
||||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
sl.registerLazySingleton<AppConfig>(
|
||||||
|
() =>
|
||||||
|
AppConfig(buildType: AppBuildType.tests)
|
||||||
|
..setBaseUrl('http://localhost'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget wrap(Widget child, RegisterConsultantCubit cubit) {
|
Widget wrap(Widget child, RegisterConsultantCubit cubit) {
|
||||||
@@ -38,7 +44,10 @@ void main() {
|
|||||||
],
|
],
|
||||||
supportedLocales: const [Locale('ru')],
|
supportedLocales: const [Locale('ru')],
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
|
body: BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,11 +58,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
@@ -61,7 +81,10 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(PhotoOrScansScreen), findsOneWidget);
|
expect(find.byType(PhotoOrScansScreen), findsOneWidget);
|
||||||
expect(find.text('Загрузите фото или сканы дипломов и сертификатов'), findsOneWidget);
|
expect(
|
||||||
|
find.text('Загрузите фото или сканы дипломов и сертификатов'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,11 +94,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ void main() {
|
|||||||
late RegisterConsultantCubit cubit;
|
late RegisterConsultantCubit cubit;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
cubit = RegisterConsultantCubit(consultantRepository: MockConsultantRepository());
|
cubit = RegisterConsultantCubit(
|
||||||
|
consultantRepository: MockConsultantRepository(),
|
||||||
|
);
|
||||||
cubit.emit(
|
cubit.emit(
|
||||||
const RegisterConsultantState(
|
const RegisterConsultantState(
|
||||||
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel()),
|
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel()),
|
||||||
@@ -35,7 +37,10 @@ void main() {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeHelper().themeData(),
|
theme: ThemeHelper().themeData(),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
|
body: BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ import 'social_networks_screen_smoke_test.mocks.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('SocialNetworksScreen smoke', () {
|
group('SocialNetworksScreen smoke', () {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
dotenv.loadFromString(
|
||||||
|
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await sl.reset();
|
await sl.reset();
|
||||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
sl.registerLazySingleton<AppConfig>(
|
||||||
|
() =>
|
||||||
|
AppConfig(buildType: AppBuildType.tests)
|
||||||
|
..setBaseUrl('http://localhost'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget wrap(Widget child) {
|
Widget wrap(Widget child) {
|
||||||
@@ -47,18 +53,34 @@ void main() {
|
|||||||
final cubit = MockRegisterConsultantCubit();
|
final cubit = MockRegisterConsultantCubit();
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: 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));
|
||||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
||||||
|
|
||||||
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const SocialNetworksScreen())));
|
await tester.pumpWidget(
|
||||||
|
BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: wrap(const SocialNetworksScreen()),
|
||||||
|
),
|
||||||
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
@@ -69,18 +91,34 @@ void main() {
|
|||||||
final cubit = MockRegisterConsultantCubit();
|
final cubit = MockRegisterConsultantCubit();
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: 'https://vk.com/test', tgLink: 'https://t.me/test', fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: 'https://vk.com/test',
|
||||||
|
tgLink: 'https://t.me/test',
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: 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));
|
||||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
|
||||||
|
|
||||||
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const SocialNetworksScreen())));
|
await tester.pumpWidget(
|
||||||
|
BlocProvider<RegisterConsultantCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: wrap(const SocialNetworksScreen()),
|
||||||
|
),
|
||||||
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ import 'stepper_become_consultant_screen_smoke_test.mocks.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('StepperBecomeConsultantScreen smoke', () {
|
group('StepperBecomeConsultantScreen smoke', () {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
dotenv.loadFromString(
|
||||||
|
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await sl.reset();
|
await sl.reset();
|
||||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
sl.registerLazySingleton<AppConfig>(
|
||||||
|
() =>
|
||||||
|
AppConfig(buildType: AppBuildType.tests)
|
||||||
|
..setBaseUrl('http://localhost'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget wrap(Widget child) {
|
Widget wrap(Widget child) {
|
||||||
@@ -48,11 +54,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
@@ -71,11 +88,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
@@ -94,11 +122,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
@@ -117,11 +156,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
@@ -140,11 +190,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
@@ -163,11 +224,22 @@ void main() {
|
|||||||
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
final cubit = RegisterConsultantCubit(consultantRepository: repository);
|
||||||
const consultant = ConsultantProfileModel(
|
const consultant = ConsultantProfileModel(
|
||||||
hasMedicalEducation: null,
|
hasMedicalEducation: null,
|
||||||
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
|
socialLinks: SocialLinksModel(
|
||||||
|
vkLink: null,
|
||||||
|
tgLink: null,
|
||||||
|
fbLink: null,
|
||||||
|
instaLink: null,
|
||||||
|
personalSiteLink: null,
|
||||||
|
),
|
||||||
photos: null,
|
photos: null,
|
||||||
certificates: null,
|
certificates: null,
|
||||||
);
|
);
|
||||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
const state = RegisterConsultantState(
|
||||||
|
consultant: consultant,
|
||||||
|
acceptOffer: false,
|
||||||
|
loading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
);
|
||||||
|
|
||||||
cubit.emit(state);
|
cubit.emit(state);
|
||||||
|
|
||||||
|
|||||||
@@ -37,42 +37,99 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
mockService = MockConsultantService();
|
mockService = MockConsultantService();
|
||||||
repository = ConsultantRepository(consultantService: mockService);
|
repository = ConsultantRepository(consultantService: mockService);
|
||||||
consultantProfile = ConsultantProfileModel.fromJson(FixtureReader.readAsMap('consultant/consultant_profile_request.json'));
|
consultantProfile = ConsultantProfileModel.fromJson(
|
||||||
consultationRequest = ConsultationRequestModel.fromJson(FixtureReader.readAsMap('consultant/consultation_request.json'));
|
FixtureReader.readAsMap('consultant/consultant_profile_request.json'),
|
||||||
certPhotos = FixtureReader.readAsList('consultant/consultant_cert_photos.json')
|
);
|
||||||
.map((item) => ConsultantCertPhotoModel.fromJson(item as Map<String, dynamic>))
|
consultationRequest = ConsultationRequestModel.fromJson(
|
||||||
.toList();
|
FixtureReader.readAsMap('consultant/consultation_request.json'),
|
||||||
|
);
|
||||||
|
certPhotos =
|
||||||
|
FixtureReader.readAsList('consultant/consultant_cert_photos.json')
|
||||||
|
.map(
|
||||||
|
(item) => ConsultantCertPhotoModel.fromJson(
|
||||||
|
item as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('registerConsultant delegates to consultantRegistration when update=false', () async {
|
test(
|
||||||
when(() => mockService.consultantRegistration(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel'))).thenAnswer((_) async {});
|
'registerConsultant delegates to consultantRegistration when update=false',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => mockService.consultantRegistration(
|
||||||
|
consultantModel: any<ConsultantProfileModel>(
|
||||||
|
named: 'consultantModel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
await repository.registerConsultant(consultant: consultantProfile);
|
await repository.registerConsultant(consultant: consultantProfile);
|
||||||
|
|
||||||
verify(() => mockService.consultantRegistration(consultantModel: consultantProfile)).called(1);
|
verify(
|
||||||
verifyNever(() => mockService.consultantUpdate(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel')));
|
() => mockService.consultantRegistration(
|
||||||
});
|
consultantModel: consultantProfile,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verifyNever(
|
||||||
|
() => mockService.consultantUpdate(
|
||||||
|
consultantModel: any<ConsultantProfileModel>(
|
||||||
|
named: 'consultantModel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('registerConsultant delegates to consultantUpdate when update=true', () async {
|
test(
|
||||||
when(() => mockService.consultantUpdate(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel'))).thenAnswer((_) async {});
|
'registerConsultant delegates to consultantUpdate when update=true',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => mockService.consultantUpdate(
|
||||||
|
consultantModel: any<ConsultantProfileModel>(
|
||||||
|
named: 'consultantModel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
await repository.registerConsultant(consultant: consultantProfile, update: true);
|
await repository.registerConsultant(
|
||||||
|
consultant: consultantProfile,
|
||||||
|
update: true,
|
||||||
|
);
|
||||||
|
|
||||||
verify(() => mockService.consultantUpdate(consultantModel: consultantProfile)).called(1);
|
verify(
|
||||||
verifyNever(() => mockService.consultantRegistration(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel')));
|
() =>
|
||||||
});
|
mockService.consultantUpdate(consultantModel: consultantProfile),
|
||||||
|
).called(1);
|
||||||
|
verifyNever(
|
||||||
|
() => mockService.consultantRegistration(
|
||||||
|
consultantModel: any<ConsultantProfileModel>(
|
||||||
|
named: 'consultantModel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('getConsultantRequestStatus returns request from service', () async {
|
test('getConsultantRequestStatus returns request from service', () async {
|
||||||
when(() => mockService.getConsultantRequestStatus(slug: any<String>(named: 'slug'))).thenAnswer((_) async => consultationRequest);
|
when(
|
||||||
|
() => mockService.getConsultantRequestStatus(
|
||||||
|
slug: any<String>(named: 'slug'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => consultationRequest);
|
||||||
|
|
||||||
final result = await repository.getConsultantRequestStatus('user-slug');
|
final result = await repository.getConsultantRequestStatus('user-slug');
|
||||||
|
|
||||||
expect(result, equals(consultationRequest));
|
expect(result, equals(consultationRequest));
|
||||||
verify(() => mockService.getConsultantRequestStatus(slug: 'user-slug')).called(1);
|
verify(
|
||||||
|
() => mockService.getConsultantRequestStatus(slug: 'user-slug'),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getMyRequestStatus returns request from service', () async {
|
test('getMyRequestStatus returns request from service', () async {
|
||||||
when(() => mockService.getMyRequestStatus(slug: any<String>(named: 'slug'))).thenAnswer((_) async => consultationRequest);
|
when(
|
||||||
|
() => mockService.getMyRequestStatus(slug: any<String>(named: 'slug')),
|
||||||
|
).thenAnswer((_) async => consultationRequest);
|
||||||
|
|
||||||
final result = await repository.getMyRequestStatus('my-slug');
|
final result = await repository.getMyRequestStatus('my-slug');
|
||||||
|
|
||||||
@@ -81,7 +138,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getConsultantCertPhotos returns photos from service', () async {
|
test('getConsultantCertPhotos returns photos from service', () async {
|
||||||
when(() => mockService.getConsultantCertPhotos()).thenAnswer((_) async => certPhotos);
|
when(
|
||||||
|
() => mockService.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => certPhotos);
|
||||||
|
|
||||||
final result = await repository.getConsultantCertPhotos();
|
final result = await repository.getConsultantCertPhotos();
|
||||||
|
|
||||||
@@ -101,42 +160,80 @@ void main() {
|
|||||||
verify(() => mockService.getSbpBanks()).called(1);
|
verify(() => mockService.getSbpBanks()).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('storeConsultantCertPhotos returns empty list when no files provided', () async {
|
test(
|
||||||
final result = await repository.storeConsultantCertPhotos(files: []);
|
'storeConsultantCertPhotos returns empty list when no files provided',
|
||||||
|
() async {
|
||||||
|
final result = await repository.storeConsultantCertPhotos(files: []);
|
||||||
|
|
||||||
expect(result, isEmpty);
|
expect(result, isEmpty);
|
||||||
verifyNever(() => mockService.storeConsultantCertPhotos(photos: any<List<MultipartFile>>(named: 'photos')));
|
verifyNever(
|
||||||
});
|
() => mockService.storeConsultantCertPhotos(
|
||||||
|
photos: any<List<MultipartFile>>(named: 'photos'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('storeConsultantCertPhotos uploads prepared files and returns response', () async {
|
test(
|
||||||
final mockFile = MockFile();
|
'storeConsultantCertPhotos uploads prepared files and returns response',
|
||||||
when(() => mockFile.readAsBytes()).thenAnswer((_) async => Uint8List.fromList([1, 2, 3]));
|
() async {
|
||||||
when(() => mockFile.path).thenReturn('/tmp/certificate.png');
|
final mockFile = MockFile();
|
||||||
when(() => mockService.storeConsultantCertPhotos(photos: any<List<MultipartFile>>(named: 'photos'))).thenAnswer((_) async => certPhotos);
|
when(
|
||||||
|
() => mockFile.readAsBytes(),
|
||||||
|
).thenAnswer((_) async => Uint8List.fromList([1, 2, 3]));
|
||||||
|
when(() => mockFile.path).thenReturn('/tmp/certificate.png');
|
||||||
|
when(
|
||||||
|
() => mockService.storeConsultantCertPhotos(
|
||||||
|
photos: any<List<MultipartFile>>(named: 'photos'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => certPhotos);
|
||||||
|
|
||||||
final result = await repository.storeConsultantCertPhotos(files: [mockFile]);
|
final result = await repository.storeConsultantCertPhotos(
|
||||||
|
files: [mockFile],
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, certPhotos);
|
expect(result, certPhotos);
|
||||||
final capturedArgument = verify(() => mockService.storeConsultantCertPhotos(photos: captureAny<List<MultipartFile>>(named: 'photos'))).captured.single as List<MultipartFile>;
|
final capturedArgument =
|
||||||
expect(capturedArgument, hasLength(1));
|
verify(
|
||||||
expect(capturedArgument.first.filename, 'certificate.png');
|
() => mockService.storeConsultantCertPhotos(
|
||||||
});
|
photos: captureAny<List<MultipartFile>>(named: 'photos'),
|
||||||
|
),
|
||||||
|
).captured.single
|
||||||
|
as List<MultipartFile>;
|
||||||
|
expect(capturedArgument, hasLength(1));
|
||||||
|
expect(capturedArgument.first.filename, 'certificate.png');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('hideConsultantCertPhoto sends payload to service', () async {
|
test('hideConsultantCertPhoto sends payload to service', () async {
|
||||||
when(() => mockService.hideConsultantCertPhoto(payload: any<Map<String, dynamic>>(named: 'payload'))).thenAnswer((_) async {});
|
when(
|
||||||
|
() => mockService.hideConsultantCertPhoto(
|
||||||
|
payload: any<Map<String, dynamic>>(named: 'payload'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
await repository.hideConsultantCertPhoto(42);
|
await repository.hideConsultantCertPhoto(42);
|
||||||
|
|
||||||
verify(() => mockService.hideConsultantCertPhoto(payload: {'cert_photo_id': 42})).called(1);
|
verify(
|
||||||
|
() =>
|
||||||
|
mockService.hideConsultantCertPhoto(payload: {'cert_photo_id': 42}),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unHideConsultantCertPhoto sends payload to service', () async {
|
test('unHideConsultantCertPhoto sends payload to service', () async {
|
||||||
when(() => mockService.unHideConsultantCertPhoto(payload: any<Map<String, dynamic>>(named: 'payload'))).thenAnswer((_) async {});
|
when(
|
||||||
|
() => mockService.unHideConsultantCertPhoto(
|
||||||
|
payload: any<Map<String, dynamic>>(named: 'payload'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
|
||||||
await repository.unHideConsultantCertPhoto(7);
|
await repository.unHideConsultantCertPhoto(7);
|
||||||
|
|
||||||
verify(() => mockService.unHideConsultantCertPhoto(payload: {'cert_photo_id': 7})).called(1);
|
verify(
|
||||||
|
() => mockService.unHideConsultantCertPhoto(
|
||||||
|
payload: {'cert_photo_id': 7},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ void main() {
|
|||||||
|
|
||||||
test('getMyRequestStatus - возвращает статус запроса', () async {
|
test('getMyRequestStatus - возвращает статус запроса', () async {
|
||||||
const slug = 'test-consultant';
|
const slug = 'test-consultant';
|
||||||
final json = FixtureReader.readAsMap('consultant/consultation_request.json');
|
final json = FixtureReader.readAsMap(
|
||||||
|
'consultant/consultation_request.json',
|
||||||
|
);
|
||||||
final expectedRequest = ConsultationRequestModel.fromJson(json);
|
final expectedRequest = ConsultationRequestModel.fromJson(json);
|
||||||
|
|
||||||
when(mockConsultantService.getMyRequestStatus(slug: slug))
|
when(
|
||||||
.thenAnswer((_) async => expectedRequest);
|
mockConsultantService.getMyRequestStatus(slug: slug),
|
||||||
|
).thenAnswer((_) async => expectedRequest);
|
||||||
|
|
||||||
final result = await mockConsultantService.getMyRequestStatus(slug: slug);
|
final result = await mockConsultantService.getMyRequestStatus(slug: slug);
|
||||||
|
|
||||||
@@ -33,25 +36,36 @@ void main() {
|
|||||||
verify(mockConsultantService.getMyRequestStatus(slug: slug)).called(1);
|
verify(mockConsultantService.getMyRequestStatus(slug: slug)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getConsultantRequestStatus - возвращает статус запроса консультанта', () async {
|
test(
|
||||||
const slug = 'test-consultant';
|
'getConsultantRequestStatus - возвращает статус запроса консультанта',
|
||||||
final json = FixtureReader.readAsMap('consultant/consultation_request.json');
|
() async {
|
||||||
final expectedRequest = ConsultationRequestModel.fromJson(json);
|
const slug = 'test-consultant';
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'consultant/consultation_request.json',
|
||||||
|
);
|
||||||
|
final expectedRequest = ConsultationRequestModel.fromJson(json);
|
||||||
|
|
||||||
when(mockConsultantService.getConsultantRequestStatus(slug: slug))
|
when(
|
||||||
.thenAnswer((_) async => expectedRequest);
|
mockConsultantService.getConsultantRequestStatus(slug: slug),
|
||||||
|
).thenAnswer((_) async => expectedRequest);
|
||||||
|
|
||||||
final result = await mockConsultantService.getConsultantRequestStatus(slug: slug);
|
final result = await mockConsultantService.getConsultantRequestStatus(
|
||||||
|
slug: slug,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isA<ConsultationRequestModel>());
|
expect(result, isA<ConsultationRequestModel>());
|
||||||
verify(mockConsultantService.getConsultantRequestStatus(slug: slug)).called(1);
|
verify(
|
||||||
});
|
mockConsultantService.getConsultantRequestStatus(slug: slug),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('getConsultantCertPhotos - возвращает фото сертификатов', () async {
|
test('getConsultantCertPhotos - возвращает фото сертификатов', () async {
|
||||||
final expectedPhotos = <ConsultantCertPhotoModel>[];
|
final expectedPhotos = <ConsultantCertPhotoModel>[];
|
||||||
|
|
||||||
when(mockConsultantService.getConsultantCertPhotos())
|
when(
|
||||||
.thenAnswer((_) async => expectedPhotos);
|
mockConsultantService.getConsultantCertPhotos(),
|
||||||
|
).thenAnswer((_) async => expectedPhotos);
|
||||||
|
|
||||||
final result = await mockConsultantService.getConsultantCertPhotos();
|
final result = await mockConsultantService.getConsultantCertPhotos();
|
||||||
|
|
||||||
@@ -60,16 +74,22 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('consultantRegistration - регистрирует консультанта', () async {
|
test('consultantRegistration - регистрирует консультанта', () async {
|
||||||
final profileJson = FixtureReader.readAsMap('consultant/consultant_profile_request.json');
|
final profileJson = FixtureReader.readAsMap(
|
||||||
|
'consultant/consultant_profile_request.json',
|
||||||
|
);
|
||||||
final profile = ConsultantProfileModel.fromJson(profileJson);
|
final profile = ConsultantProfileModel.fromJson(profileJson);
|
||||||
|
|
||||||
when(mockConsultantService.consultantRegistration(consultantModel: profile))
|
when(
|
||||||
.thenAnswer((_) async => Future.value());
|
mockConsultantService.consultantRegistration(consultantModel: profile),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await mockConsultantService.consultantRegistration(consultantModel: profile);
|
await mockConsultantService.consultantRegistration(
|
||||||
|
consultantModel: profile,
|
||||||
|
);
|
||||||
|
|
||||||
verify(mockConsultantService.consultantRegistration(consultantModel: profile)).called(1);
|
verify(
|
||||||
|
mockConsultantService.consultantRegistration(consultantModel: profile),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/features/consultant/become_consultant/utils/laravel_validation_errors.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('parseLaravelValidationErrors', () {
|
||||||
|
DioException dio422(Map<String, dynamic> data) {
|
||||||
|
return DioException(
|
||||||
|
requestOptions: RequestOptions(path: '/test'),
|
||||||
|
response: Response(
|
||||||
|
requestOptions: RequestOptions(path: '/test'),
|
||||||
|
statusCode: 422,
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
type: DioExceptionType.badResponse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('maps unique and format errors to Russian UI texts', () {
|
||||||
|
final parsed = parseLaravelValidationErrors(
|
||||||
|
dio422({
|
||||||
|
'message': 'The given data was invalid.',
|
||||||
|
'errors': {
|
||||||
|
'inn': ['The inn format is invalid.'],
|
||||||
|
'passport_number': ['The passport number has already been taken.'],
|
||||||
|
'snils': ['The snils has already been taken.'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsed.fieldErrors['inn'], 'Некорректный ИНН');
|
||||||
|
expect(parsed.fieldErrors['passport_number'], 'Такой паспорт существует');
|
||||||
|
expect(parsed.fieldErrors['snils'], 'Такой СНИЛС существует');
|
||||||
|
expect(parsed.summary, isNot(contains('The given data was invalid.')));
|
||||||
|
expect(parsed.summary, isNot(contains('already been taken')));
|
||||||
|
expect(parsed.summary, contains('Некорректный ИНН'));
|
||||||
|
expect(parsed.summary, contains('Такой паспорт существует'));
|
||||||
|
expect(parsed.hasFieldErrors, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maps required and agreement errors', () {
|
||||||
|
final parsed = parseLaravelValidationErrors(
|
||||||
|
dio422({
|
||||||
|
'errors': {
|
||||||
|
'last_name': ['The last name field is required.'],
|
||||||
|
'agreement_accepted': ['The agreement accepted must be accepted.'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsed.fieldErrors['last_name'], 'Укажите фамилию');
|
||||||
|
expect(parsed.fieldErrors['agreement_accepted'], 'Примите условия оферты');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to generic message for non-422 DioException', () {
|
||||||
|
final error = DioException(
|
||||||
|
requestOptions: RequestOptions(path: '/test'),
|
||||||
|
message: 'Network failed',
|
||||||
|
type: DioExceptionType.connectionError,
|
||||||
|
);
|
||||||
|
|
||||||
|
final parsed = parseLaravelValidationErrors(error);
|
||||||
|
|
||||||
|
expect(parsed.summary, 'Не удалось отправить данные. Попробуйте ещё раз');
|
||||||
|
expect(parsed.fieldErrors, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses generic text for non-Dio errors', () {
|
||||||
|
final parsed = parseLaravelValidationErrors(Exception('boom'));
|
||||||
|
|
||||||
|
expect(parsed.summary, 'Не удалось отправить данные. Попробуйте ещё раз');
|
||||||
|
expect(parsed.fieldErrors, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('mapConsultantValidationMessage', () {
|
||||||
|
test('unique inn and snils', () {
|
||||||
|
expect(
|
||||||
|
mapConsultantValidationMessage(
|
||||||
|
fieldKey: 'inn',
|
||||||
|
rawMessage: 'The inn has already been taken.',
|
||||||
|
),
|
||||||
|
'Такой ИНН существует',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
mapConsultantValidationMessage(
|
||||||
|
fieldKey: 'snils',
|
||||||
|
rawMessage: 'The snils has already been taken.',
|
||||||
|
),
|
||||||
|
'Такой СНИЛС существует',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"inn": "123456789012",
|
"inn": "123456789012",
|
||||||
"snils": "123-456-789 00",
|
"snils": "123-456-789 00",
|
||||||
"state": "created",
|
"state": "created",
|
||||||
"alfa_scoring_status": "pending",
|
"alfa_scoring_status": "scoring_failed",
|
||||||
"alfa_scoring_request_id": "abc-123",
|
"alfa_scoring_request_id": "abc-123",
|
||||||
"passport_series": "1234",
|
"passport_series": "1234",
|
||||||
"passport_number": "567890",
|
"passport_number": "567890",
|
||||||
|
|||||||
Reference in New Issue
Block a user