FE-1054: Upload documents step (#238)

This commit is contained in:
Rinat Arsaev 2020-05-29 16:29:29 +03:00 committed by GitHub
parent 7648fcd0b2
commit f22fd29354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 199 additions and 79 deletions

View File

@ -5,6 +5,14 @@ export class BasicError<T = any> {
constructor(public error: T) {}
}
export function isError<T>(value: T | BasicError<any>): value is BasicError<any> {
return value instanceof BasicError;
}
export function isPayload<T>(value: T | BasicError<any>): value is T {
return !isError(value);
}
export const replaceError = <T, E = any>(source: Observable<T>): Observable<T | BasicError<E>> =>
source.pipe(catchError((value) => of(new BasicError(value))));
@ -15,4 +23,4 @@ export const filterError = <E>(source: Observable<any | BasicError<E>>): Observa
);
export const filterPayload = <T>(source: Observable<T | BasicError<any>>): Observable<T> =>
source.pipe(filter((value) => !(value instanceof BasicError))) as Observable<T>;
source.pipe(filter(isPayload)) as Observable<T>;

View File

@ -1,7 +1,9 @@
import { ParsedAddressRF, Toponim } from '../../../../../api-codegen/aggr-proxy';
function getAddressPart(toponim: Toponim, isFullName = false): string {
return toponim ? `${isFullName ? toponim.topoFullName : toponim.topoShortName} ${toponim.topoValue}` : null;
return toponim
? [isFullName ? toponim.topoFullName : toponim.topoShortName, toponim.topoValue].filter((v) => !!v).join(' ')
: null;
}
export function getAddress(address: ParsedAddressRF): string {

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { shareReplay, switchMap } from 'rxjs/operators';
import { ClaimsService } from '../../../../api';
@Injectable()
export class ClaimService {
private loadClaim$ = new BehaviorSubject<void>(undefined);
cliam$ = combineLatest([this.route.params, this.loadClaim$]).pipe(
switchMap(([{ claimID }]) => this.claimsService.getClaimByID(claimID)),
shareReplay(1)
);
constructor(private route: ActivatedRoute, private claimsService: ClaimsService) {}
reloadCliam() {
this.loadClaim$.next();
}
}

View File

@ -0,0 +1 @@
export * from './claim.service';

View File

@ -9,6 +9,7 @@ import {
PlanningOperationsAndPayoutToolComponent,
RussianLegalOwnerComponent,
RussianPrivateEntityComponent,
UploadDocumentsComponent,
} from './forms';
import { StepName } from './step-flow';
@ -41,6 +42,10 @@ export const routes: Routes = [
path: StepName.PlanningOperationsAndPayoutTool,
component: PlanningOperationsAndPayoutToolComponent,
},
{
path: StepName.UploadDocuments,
component: UploadDocumentsComponent,
},
],
},
];

View File

@ -1,6 +1,8 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ClaimService } from './claim/claim.service';
import { DataFlowService } from './data-flow.service';
import { InitializeFormsService, UploadDocumentsService } from './forms';
import { QuestionaryStateService } from './questionary-state.service';
import { StepFlowService } from './step-flow';
@ -8,7 +10,7 @@ import { StepFlowService } from './step-flow';
selector: 'dsh-data-flow',
templateUrl: 'data-flow.component.html',
styleUrls: ['data-flow.component.scss'],
providers: [DataFlowService],
providers: [DataFlowService, ClaimService, InitializeFormsService, UploadDocumentsService],
})
export class DataFlowComponent implements OnInit, OnDestroy {
activeStep$ = this.stepFlowService.activeStep$;

View File

@ -8,11 +8,11 @@ import { ButtonModule } from '@dsh/components/buttons';
import { SpinnerModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
import { StateNavModule } from '@dsh/components/navigation';
import { ConfirmActionDialogModule } from '@dsh/components/popups';
import { QuestionaryModule } from '../../../api';
import { DataFlowRoutingModule } from './data-flow-routing.module';
import { DataFlowComponent } from './data-flow.component';
import { FinishOnboardingDialogComponent } from './finish-onboarding-dialog';
import { OnboardingFormsModule } from './forms';
import { HelpCardComponent } from './help-card';
import { QuestionaryStateService } from './questionary-state.service';
@ -36,16 +36,9 @@ import { ValidityService } from './validity';
SpinnerModule,
OnboardingFormsModule,
MatDialogModule,
ConfirmActionDialogModule,
],
declarations: [
DataFlowComponent,
HelpCardComponent,
StepCardComponent,
StepNavigationComponent,
StepLabelPipe,
FinishOnboardingDialogComponent,
],
declarations: [DataFlowComponent, HelpCardComponent, StepCardComponent, StepNavigationComponent, StepLabelPipe],
providers: [StepFlowService, ValidityService, QuestionaryStateService, ValidationCheckService],
entryComponents: [FinishOnboardingDialogComponent],
})
export class DataFlowModule {}

View File

@ -1,18 +0,0 @@
<ng-container *transloco="let t; scope: 'onboarding'; read: 'onboarding.finishOnboardingDialog'">
<h2 class="mat-dialog-title">{{ t.title }}</h2>
<mat-dialog-content fxLayout="column" fxLayoutGap="20px">
<div fxLayout="column" fxLayoutGap="10px">
<div class="mat-body-2">
{{ t.description1 }}
</div>
<div class="mat-body-1">- {{ t.passport }};</div>
<div class="mat-body-1">- {{ t.registrationDocument }}.</div>
</div>
<div class="mat-body-2">
{{ t.description2 }}
</div>
</mat-dialog-content>
<div class="leave-dialog-actions">
<button fxFlex dsh-button color="accent" (click)="close()">{{ t.close }}</button>
</div>
</ng-container>

View File

@ -1,3 +0,0 @@
.leave-dialog-actions {
margin-top: 30px;
}

View File

@ -1,15 +0,0 @@
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'dsh-finish-onboarding-dialog',
templateUrl: 'finish-onboarding-dialog.component.html',
styleUrls: ['finish-onboarding-dialog.component.scss'],
})
export class FinishOnboardingDialogComponent {
constructor(private dialogRef: MatDialogRef<FinishOnboardingDialogComponent>) {}
close() {
this.dialogRef.close();
}
}

View File

@ -1 +0,0 @@
export * from './finish-onboarding-dialog.component';

View File

@ -15,13 +15,13 @@ import { ButtonModule } from '@dsh/components/buttons';
import { FormControlsModule } from '@dsh/components/form-controls';
import { DaDataModule } from '../../../../dadata';
import { FileContainerModule } from './../../../claim-modification-containers/file-container/file-container.module';
import { BasicInfoComponent, BasicInfoService } from './basic-info';
import { BeneficialOwnersComponent, BeneficialOwnersService } from './beneficial-owners';
import {
FinancialAndEconomicActivityComponent,
FinancialAndEconomicActivityService,
} from './financial-and-economic-activity';
import { InitializeFormsService } from './initialize-forms.service';
import {
PlanningOperationsAndPayoutToolComponent,
PlanningOperationsAndPayoutToolService,
@ -43,6 +43,7 @@ import {
RussianDomesticPassportComponent,
RussianDomesticPassportService,
} from './subforms';
import { UploadDocumentsComponent } from './upload-documents';
@NgModule({
imports: [
@ -61,6 +62,7 @@ import {
TextMaskModule,
FormControlsModule,
DaDataModule,
FileContainerModule,
],
declarations: [
BasicInfoComponent,
@ -75,9 +77,9 @@ import {
PlanningOperationsAndPayoutToolComponent,
IndividualResidencyInfoComponent,
LegalResidencyInfoComponent,
UploadDocumentsComponent,
],
providers: [
InitializeFormsService,
BasicInfoService,
RussianLegalOwnerService,
RussianDomesticPassportService,

View File

@ -7,3 +7,4 @@ export * from './initialize-forms.service';
export * from './financial-and-economic-activity/financial-and-economic-activity.component';
export * from './beneficial-owners/beneficial-owners.component';
export * from './planning-operations-and-payout-tool/planning-operations-and-payout-tool.component';
export * from './upload-documents';

View File

@ -8,6 +8,7 @@ import { PlanningOperationsAndPayoutToolService } from './planning-operations-an
import { QuestionaryFormService } from './questionary-form.service';
import { RussianLegalOwnerService } from './russian-legal-owner';
import { RussianPrivateEntityService } from './russian-private-entity/russian-private-entity.service';
import { UploadDocumentsService } from './upload-documents/upload-documents.service';
@Injectable()
export class InitializeFormsService {
@ -21,7 +22,8 @@ export class InitializeFormsService {
private financialAndEconomicActivityService: FinancialAndEconomicActivityService,
private beneficialOwnersService: BeneficialOwnersService,
private planningOperationsAndPayoutToolService: PlanningOperationsAndPayoutToolService,
private russianPrivateEntityService: RussianPrivateEntityService
private russianPrivateEntityService: RussianPrivateEntityService,
private uploadDocumentsService: UploadDocumentsService
) {
this.initializeContainer = [
this.basicInfoService,
@ -30,6 +32,7 @@ export class InitializeFormsService {
this.beneficialOwnersService,
this.russianPrivateEntityService,
this.planningOperationsAndPayoutToolService,
this.uploadDocumentsService,
];
}

View File

@ -1,6 +1,6 @@
import { FormGroup } from '@angular/forms';
import { combineLatest, forkJoin, of, Subscription } from 'rxjs';
import { debounceTime, first, map, pluck, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { forkJoin, of, Subscription } from 'rxjs';
import { debounceTime, first, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { QuestionaryData } from '../../../../api-codegen/questionary';
import { QuestionaryStateService } from '../questionary-state.service';
@ -44,8 +44,12 @@ export abstract class QuestionaryFormService {
startFormValidityReporting(debounceMs = 300): Subscription {
return this.form$
.pipe(
switchMap((form) => combineLatest([of(form), form.statusChanges.pipe(startWith(form.status))])),
pluck(0, 'valid'),
switchMap((form) =>
form.statusChanges.pipe(
startWith(form.status),
map(() => form.valid)
)
),
debounceTime(debounceMs)
)
.subscribe((isValid) => this.validityService.setUpValidity(this.stepName, isValid));

View File

@ -0,0 +1,2 @@
export * from './upload-documents.component';
export * from './upload-documents.service';

View File

@ -0,0 +1,19 @@
<div
*transloco="let t; scope: 'onboarding'; read: 'onboarding.dataFlow.uploadDocuments'"
fxLayout="column"
fxLayoutGap="20px"
>
<div fxLayout="column" fxLayoutGap="10px">
<div class="mat-body-2">{{ t.requiredDocs }}:</div>
<div class="mat-body-1">- {{ t.passport }};</div>
<div class="mat-body-1">- {{ t.registrationDoc }}.</div>
</div>
<div class="mat-body-2">{{ t.uploadedDocs }}:</div>
<div *ngIf="!(fileUnits$ | async)?.length; else files" class="mat-body-1">{{ t.noDocs }}</div>
<ng-template #files>
<div fxLayout="column" fxLayoutGap="10px">
<dsh-file-container *ngFor="let unit of fileUnits$ | async" [unit]="unit"></dsh-file-container>
</div>
</ng-template>
<dsh-file-uploader (filesUploaded)="filesUploaded($event)"></dsh-file-uploader>
</div>

View File

@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { UploadDocumentsService } from './upload-documents.service';
@Component({
selector: 'dsh-upload-documents',
templateUrl: 'upload-documents.component.html',
})
export class UploadDocumentsComponent {
fileUnits$ = this.documentsService.fileUnits$;
constructor(private documentsService: UploadDocumentsService) {}
filesUploaded(fileIds: string[]) {
this.documentsService.filesUploaded(fileIds);
}
}

View File

@ -0,0 +1,75 @@
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { Observable, Subject } from 'rxjs';
import { map, pluck, share, switchMap, withLatestFrom } from 'rxjs/operators';
import { ClaimsService, createFileModificationUnit, takeFileModificationUnits } from '../../../../../api';
import { FileModificationUnit } from '../../../../../api-codegen/claim-management';
import { replaceError } from '../../../../../custom-operators';
import { ClaimService } from '../../claim';
import { QuestionaryStateService } from '../../questionary-state.service';
import { StepName } from '../../step-flow';
import { ValidationCheckService } from '../../validation-check';
import { ValidityService } from '../../validity';
import { QuestionaryFormService } from '../questionary-form.service';
import { filterError, filterPayload } from './../../../../../custom-operators/replace-error';
@Injectable()
export class UploadDocumentsService extends QuestionaryFormService {
private filesUploaded$ = new Subject<string[]>();
fileUnits$: Observable<FileModificationUnit[]> = this.claimService.cliam$.pipe(
pluck('changeset'),
map(takeFileModificationUnits)
);
constructor(
questionaryStateService: QuestionaryStateService,
validityService: ValidityService,
validationCheckService: ValidationCheckService,
private claimService: ClaimService,
private claimsService: ClaimsService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
) {
super(questionaryStateService, validityService, validationCheckService);
const uploadedFilesWithError$ = this.filesUploaded$.pipe(
map((fileIds) => fileIds.map(createFileModificationUnit)),
withLatestFrom(this.claimService.cliam$),
switchMap(([changeset, { id, revision }]) =>
this.claimsService.updateClaimByID(id, revision, changeset).pipe(replaceError)
),
share()
);
uploadedFilesWithError$.pipe(filterPayload).subscribe(() => this.claimService.reloadCliam());
uploadedFilesWithError$.pipe(filterError).subscribe(() =>
this.snackBar.open(this.transloco.translate('httpError'), 'OK', {
duration: 5000,
})
);
}
filesUploaded(fileIds: string[]) {
this.filesUploaded$.next(fileIds);
}
protected toForm() {
return new FormGroup({});
}
protected applyToQuestionaryData(data) {
return data;
}
protected getStepName(): StepName {
return StepName.UploadDocuments;
}
startFormValidityReporting() {
return this.fileUnits$
.pipe(map(({ length }) => !!length))
.subscribe((isValid) => this.validityService.setUpValidity(this.stepName, isValid));
}
}

View File

@ -2,10 +2,11 @@ import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, Observable, Subject } from 'rxjs';
import { map, pluck, shareReplay, switchMap, switchMapTo } from 'rxjs/operators';
import { filter, map, pluck, shareReplay, switchMap, switchMapTo } from 'rxjs/operators';
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
import { ClaimsService } from '../../../../api';
import { FinishOnboardingDialogComponent } from '../finish-onboarding-dialog';
import { QuestionaryStateService } from '../questionary-state.service';
import { StepFlowService } from '../step-flow';
import { ValidityService } from '../validity';
@ -37,29 +38,24 @@ export class StepCardService {
this.questionaryStateService.save();
this.stepFlowService.navigate(step);
});
this.finishFormFlow$
.pipe(
switchMap(() => this.dialog.open(ConfirmActionDialogComponent).afterClosed()),
filter((r) => r === 'confirm'),
switchMapTo(claimID$),
switchMap((claimID) => this.claimsService.getClaimByID(claimID)),
switchMap(({ id, revision }) => this.claimsService.requestReviewClaimByID(id, revision)),
switchMap(() =>
this.dialog
.open(FinishOnboardingDialogComponent, {
disableClose: true,
width: '600px',
})
.afterClosed()
),
switchMapTo(claimID$)
)
.subscribe((claimID) => this.router.navigate(['claim', claimID, 'documents']));
.subscribe((claimID) => {
this.finishFormFlow$.complete();
this.router.navigate(['claim', claimID, 'conversation']);
});
}
finishFormFlow() {
this.questionaryStateService.save();
this.finishFormFlow$.next();
this.finishFormFlow$.complete();
}
selectStepFlowIndex(index: number) {

View File

@ -6,6 +6,7 @@ const BasicStepFlow = [
StepName.FinancialAndEconomicActivity,
StepName.BeneficialOwners,
StepName.PlanningOperationsAndPayoutTool,
StepName.UploadDocuments,
];
const insertStepToBasicFlow = (step: StepName): StepName[] => BasicStepFlow.map((s) => (s === null ? step : s));

View File

@ -6,4 +6,5 @@ export enum StepName {
RussianPrivateEntity = 'russian-private-entity',
RussianLegalOwner = 'russian-legal-owner',
RussianIndividualEntity = 'russian-individual-entity',
UploadDocuments = 'upload-documents',
}

View File

@ -30,6 +30,9 @@ export class StepLabelPipe implements PipeTransform {
case StepName.RussianPrivateEntity:
path = 'russianPrivateEntity';
break;
case StepName.UploadDocuments:
path = 'uploadDocuments';
break;
}
return path ? this.transloco.translate(`onboarding.dataFlow.${pathSection}.${path}`) : path;
}

View File

@ -125,13 +125,14 @@
"planningOperationsAndPayoutTool": "Вывод средств",
"russianLegalOwner": "Сведения о юр. лице",
"russianPrivateEntity": "Сведения о физ. лице",
"uploadDocuments": "Загрузка документов",
"actionLabel": "Отправить на рассмотрение"
},
"stepTitle": {
"basicInfo": "Основные сведения",
"financialAndEconomicActivity": "Сведения о хозяйственной деятельности",
"beneficialOwners": "Сведения о бенефициарных владельцах (физлица)",
"documentsUpload": "Загрузка документов",
"uploadDocuments": "Загрузка документов",
"planningOperationsAndPayoutTool": "Планируемые операции и данные для вывода средств",
"russianLegalOwner": "Сведения о юридическом лице",
"russianPrivateEntity": "Сведения о физическом лице"
@ -139,6 +140,13 @@
"helpCard": {
"title": "Возникли вопросы?",
"content": "Напишите нам на почту help@rbk.money и оставьте свой телефон для связи."
},
"uploadDocuments": {
"requiredDocs": "Необходимо предоставить",
"passport": "Паспорт руководителя",
"registrationDoc": "Документ, подтверждающий право нахождения по фактическому адресу",
"uploadedDocs": "Документы, прикрепленные к заявке",
"noDocs": "Документы отсутствуют"
}
},
"documentUpload": {
@ -153,13 +161,5 @@
"cancelDocumentUploadTitle": "Подтвердите действие",
"cancelDocumentUploadDescription": "В случае отсутствия необходимых документов, мы не сможем одобрить заявку на подключение. Вы можете загрузить документы позже, в деталях заявки.",
"filesNotUploaded": "Произошла ошибка при загрузке файлов"
},
"finishOnboardingDialog": {
"title": "Заявка успешно отправлена на рассмотрение",
"description1": "Обращаем ваше внимание, что для подтверждения заявки необходимо будет предоставить следующие документы:",
"description2": "Загрузить документы и отслеживать изменения, вы сможете в разделе: 'Детали заявки'",
"passport": "Скан паспорта руководителя",
"registrationDocument": "Скан документа (свидетельство о праве собственности, договор аренды и т.п.), подтверждающего местонахождение юридического лица или индивидуального предпринимателя",
"close": "Перейти в детали заявки"
}
}