Merchants section (#199)

This commit is contained in:
Aleksandra Usacheva 2020-10-23 21:31:58 +03:00 committed by GitHub
parent 12813b12e6
commit 6774128402
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 369 additions and 142 deletions

View File

@ -53,7 +53,7 @@ export class AppComponent implements OnInit {
route: '/payment-adjustment',
activateRoles: [PaymentAdjustmentRole.Create],
},
{ name: 'Parties', route: '/parties', activateRoles: [PartyRole.Get] },
{ name: 'Merchants', route: '/parties', activateRoles: [PartyRole.Get] },
{ name: 'Repairing', route: '/repairing', activateRoles: [DomainConfigRole.Checkout] },
{ name: 'Deposits', route: '/deposits', activateRoles: [DepositRole.Write] },
{

View File

@ -25,7 +25,6 @@ import { CoreModule } from './core/core.module';
import { DepositsModule } from './deposits/deposits.module';
import { DomainModule } from './domain';
import icons from './icons.json';
import { PartiesModule } from './parties/parties.module';
import { PartyModule as OldPartyModule } from './party/party.module';
import { PaymentAdjustmentModule } from './payment-adjustment/payment-adjustment.module';
import { PayoutsModule } from './payouts/payouts.module';
@ -33,6 +32,7 @@ import { RepairingModule } from './repairing/repairing.module';
import { OperationsModule } from './sections/operations/operations.module';
import { PartyModule } from './sections/party/party.module';
import { SearchClaimsModule } from './sections/search-claims/search-claims.module';
import { SearchPartiesModule } from './sections/search-parties/search-parties.module';
import { SettingsModule } from './settings';
import { ThemeManager, ThemeManagerModule, ThemeName } from './theme-manager';
@ -59,7 +59,6 @@ moment.locale('en');
ClaimModule,
PayoutsModule,
PaymentAdjustmentModule,
PartiesModule,
PartyModule,
DomainModule,
RepairingModule,
@ -68,6 +67,7 @@ moment.locale('en');
DepositsModule,
ClaimMgtModule,
PartyModule,
SearchPartiesModule,
OldPartyModule,
SearchClaimsModule,
OperationsModule,

View File

@ -1,26 +0,0 @@
<cc-card-container>
<mat-card>
<mat-card-subtitle> Search Parties </mat-card-subtitle>
<mat-card-content>
<form fxLayout="row" fxLayout.xs="column" fxLayoutGap="20px" [formGroup]="form">
<mat-form-field fxFlex="30" fxFlex.sm="70">
<input matInput placeholder="Party ID" formControlName="partyId" />
</mat-form-field>
<button
mat-raised-button
color="primary"
(click)="goToParty()"
[disabled]="!form.valid"
>
<span *ngIf="!isLoading">GO TO PARTY DETAILS</span>
<mat-progress-spinner
*ngIf="isLoading"
color="accent"
mode="indeterminate"
diameter="30"
></mat-progress-spinner>
</button>
</form>
</mat-card-content>
</mat-card>
</cc-card-container>

View File

@ -1,47 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { PartyService } from '../party/party.service';
import { PartiesService } from './parties.service';
@Component({
templateUrl: 'parties.component.html',
styleUrls: [],
providers: [PartiesService, PartyService],
})
export class PartiesComponent implements OnInit {
public form: FormGroup;
isLoading = false;
constructor(
private partiesService: PartiesService,
private partyService: PartyService,
private snackBar: MatSnackBar,
private router: Router
) {}
ngOnInit(): void {
this.form = this.partiesService.form;
}
goToParty() {
this.isLoading = true;
let { partyId } = this.form.value;
partyId = partyId.trim();
this.partyService.getParty(partyId).subscribe(
() => {
this.isLoading = false;
this.router.navigate(['party', partyId]);
},
() => {
this.isLoading = false;
this.snackBar
.open(`An error occurred while receiving party`, 'RETRY')
.onAction()
.subscribe(() => this.goToParty());
}
);
}
}

View File

@ -1,33 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { SharedPipesModule } from '@cc/app/shared/pipes';
import { CardContainerModule } from '@cc/components/card-container/card-container.module';
import { PartiesRoutingModule } from './parties-routing.module';
import { PartiesComponent } from './parties.component';
@NgModule({
imports: [
CommonModule,
PartiesRoutingModule,
FlexLayoutModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatProgressSpinnerModule,
SharedPipesModule,
CardContainerModule,
],
declarations: [PartiesComponent],
})
export class PartiesModule {}

View File

@ -1,17 +0,0 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Injectable()
export class PartiesService {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
}
private prepareForm(): FormGroup {
return this.fb.group({
partyId: ['', Validators.required],
});
}
}

View File

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { progress } from '@rbkmoney/partial-fetcher/dist/progress';
import { merge, of, Subject } from 'rxjs';
import { catchError, filter, map, switchMap } from 'rxjs/operators';
import { DeanonimusService } from '../../thrift-services/deanonimus';
import { SearchHit } from '../../thrift-services/deanonimus/gen-model/deanonimus';
import { PartiesSearchFiltersParams } from './parties-search-filters';
@Injectable()
export class FetchPartiesService {
private searchParties$: Subject<PartiesSearchFiltersParams> = new Subject();
private hasError$: Subject<any> = new Subject();
parties$ = this.searchParties$.pipe(
switchMap((params) =>
this.deanonimusService.searchParty(params).pipe(
catchError((_) => {
this.hasError$.next();
return of('error');
})
)
),
filter((r) => r !== 'error'),
map((hits: SearchHit[]) => hits.map((hit) => hit.party))
);
inProgress$ = progress(this.searchParties$, merge(this.parties$, this.hasError$));
constructor(private deanonimusService: DeanonimusService) {}
search(params: PartiesSearchFiltersParams) {
this.searchParties$.next(params);
}
}

View File

@ -0,0 +1,2 @@
export * from './parties-search-filters.module';
export * from './parties-search-filters-params';

View File

@ -0,0 +1,3 @@
export interface PartiesSearchFiltersParams {
text: string;
}

View File

@ -0,0 +1,14 @@
<form fxLayout="column" [formGroup]="form">
<mat-form-field fxFlexFill>
<input
matInput
placeholder="Search params"
formControlName="text"
type="string"
autocomplete="false"
/>
<mat-hint class="cc-caption">
Email, ID, INN, Registred name, Legal name, Trading name, Russian bank account
</mat-hint>
</mat-form-field>
</form>

View File

@ -0,0 +1,29 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PartiesSearchFiltersParams } from './parties-search-filters-params';
import { PartiesSearchFiltersService } from './parties-search-filters.service';
@Component({
selector: 'cc-parties-search-filters',
templateUrl: 'parties-search-filters.component.html',
providers: [PartiesSearchFiltersService],
})
export class PartiesSearchFiltersComponent implements OnInit {
@Input()
initParams: PartiesSearchFiltersParams;
@Output()
searchParamsChanged$: EventEmitter<any> = new EventEmitter();
form = this.partiesSearchFiltersService.form;
constructor(private partiesSearchFiltersService: PartiesSearchFiltersService) {
this.partiesSearchFiltersService.searchParamsChanged$.subscribe((params) =>
this.searchParamsChanged$.emit(params)
);
}
ngOnInit() {
this.partiesSearchFiltersService.init(this.initParams);
}
}

View File

@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { PartiesSearchFiltersComponent } from './parties-search-filters.component';
@NgModule({
imports: [FlexModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule],
exports: [PartiesSearchFiltersComponent],
declarations: [PartiesSearchFiltersComponent],
})
export class PartiesSearchFiltersModule {}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { debounceTime, filter, shareReplay } from 'rxjs/operators';
import { PartiesSearchFiltersParams } from './parties-search-filters-params';
@Injectable()
export class PartiesSearchFiltersService {
form = this.fb.group({
text: '',
});
searchParamsChanged$ = this.form.valueChanges.pipe(
debounceTime(600),
filter(() => this.form.valid),
shareReplay(1)
);
constructor(private fb: FormBuilder) {}
init(params: PartiesSearchFiltersParams) {
this.form.patchValue(params);
}
}

View File

@ -0,0 +1 @@
export * from './parties-table.module';

View File

@ -0,0 +1,36 @@
<table mat-table [dataSource]="parties" *ngIf="displayedColumns">
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef>Email</th>
<td mat-cell *matCellDef="let party">
{{ party.email }}
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let party">
{{ party.id }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="action-cell"></th>
<td mat-cell *matCellDef="let party" class="action-cell">
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
mat-menu-item
*ngFor="let action of partyActions"
(click)="menuItemSelected(action, party.id)"
>
{{ action | ccPartyActions }}
</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>

View File

@ -0,0 +1,7 @@
table {
width: 100%;
}
.action-cell {
width: 8px;
}

View File

@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { PartyID } from '../../../thrift-services/damsel/gen-model/domain';
import { Party } from '../../../thrift-services/deanonimus/gen-model/deanonimus';
import { PartyActions } from './party-actions';
import { PartyMenuItemEvent } from './party-menu-item-event';
@Component({
selector: 'cc-parties-table',
templateUrl: 'parties-table.component.html',
styleUrls: ['parties-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PartiesTableComponent {
@Input()
parties: Party[];
@Output()
menuItemSelected$: EventEmitter<PartyMenuItemEvent> = new EventEmitter();
partyActions = Object.keys(PartyActions);
displayedColumns = ['email', 'id', 'actions'];
menuItemSelected(action: string, partyID: PartyID) {
switch (action) {
case PartyActions.navigateToParty:
this.menuItemSelected$.emit({ action, partyID });
break;
default:
console.log('Wrong party action type.');
}
}
}

View File

@ -0,0 +1,16 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table';
import { PartiesTableComponent } from './parties-table.component';
import { PartyActionsPipe } from './party-actions.pipe';
@NgModule({
exports: [PartiesTableComponent],
declarations: [PartiesTableComponent, PartyActionsPipe],
imports: [MatTableModule, MatMenuModule, MatButtonModule, MatIconModule, CommonModule],
})
export class PartiesTableModule {}

View File

@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
import { PartyActions } from './party-actions';
const partyActionNames: { [N in PartyActions]: string } = {
[PartyActions.navigateToParty]: 'Merchant Details',
};
@Pipe({
name: 'ccPartyActions',
})
export class PartyActionsPipe implements PipeTransform {
transform(action: string): string {
return partyActionNames[action] || action;
}
}

View File

@ -0,0 +1,3 @@
export enum PartyActions {
navigateToParty = 'navigateToParty',
}

View File

@ -0,0 +1,7 @@
import { PartyID } from '../../../thrift-services/damsel/gen-model/domain';
import { PartyActions } from './party-actions';
export interface PartyMenuItemEvent {
action: PartyActions;
partyID: PartyID;
}

View File

@ -3,14 +3,14 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService, PartyRole } from '@cc/app/shared/services';
import { PartiesComponent } from './parties.component';
import { SearchPartiesComponent } from './search-parties.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'parties',
component: PartiesComponent,
component: SearchPartiesComponent,
canActivate: [AppAuthGuardService],
data: {
roles: [PartyRole.Get],
@ -20,4 +20,4 @@ import { PartiesComponent } from './parties.component';
],
exports: [RouterModule],
})
export class PartiesRoutingModule {}
export class SearchPartiesRoutingModule {}

View File

@ -0,0 +1,24 @@
<div class="parties-container" fxLayout="column" fxLayoutGap="24px">
<h3 class="cc-headline">Search merchants</h3>
<mat-card>
<mat-card-content>
<cc-parties-search-filters
[initParams]="initSearchParams$ | async"
(searchParamsChanged$)="searchParamsUpdated($event)"
></cc-parties-search-filters>
</mat-card-content>
<mat-card-footer *ngIf="inProgress$ | async">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-footer>
</mat-card>
<ng-container *ngIf="parties$ | async as parties">
<cc-empty-search-result *ngIf="parties.length === 0"></cc-empty-search-result>
<mat-card *ngIf="parties.length > 0" fxLayout="column" fxLayoutGap="16px">
<cc-parties-table
[parties]="parties"
(menuItemSelected$)="partyMenuItemSelected($event)"
></cc-parties-table>
</mat-card>
</ng-container>
</div>

View File

@ -0,0 +1,8 @@
.parties-container {
max-width: 936px;
margin: 24px auto;
}
router-outlet {
margin-bottom: 0 !important;
}

View File

@ -0,0 +1,37 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { FetchPartiesService } from './fetch-parties.service';
import { PartiesSearchFiltersParams } from './parties-search-filters';
import { PartyActions } from './parties-table/party-actions';
import { PartyMenuItemEvent } from './parties-table/party-menu-item-event';
import { SearchPartiesService } from './search-parties.service';
@Component({
templateUrl: 'search-parties.component.html',
styleUrls: ['search-parties.component.scss'],
providers: [SearchPartiesService, FetchPartiesService],
})
export class SearchPartiesComponent {
initSearchParams$ = this.partiesService.data$;
inProgress$ = this.fetchPartiesService.inProgress$;
parties$ = this.fetchPartiesService.parties$;
constructor(
private partiesService: SearchPartiesService,
private fetchPartiesService: FetchPartiesService,
private router: Router
) {}
searchParamsUpdated($event: PartiesSearchFiltersParams) {
this.partiesService.preserve($event);
this.fetchPartiesService.search($event);
}
partyMenuItemSelected(event: PartyMenuItemEvent) {
switch (event.action) {
case PartyActions.navigateToParty:
this.router.navigate([`/party/${event.partyID}`]);
}
}
}

View File

@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatCardModule } from '@angular/material/card';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
import { DeanonimusModule } from '../../thrift-services/deanonimus';
import { PartiesSearchFiltersModule } from './parties-search-filters';
import { PartiesTableModule } from './parties-table';
import { SearchPartiesRoutingModule } from './search-parties-routing.module';
import { SearchPartiesComponent } from './search-parties.component';
@NgModule({
imports: [
SearchPartiesRoutingModule,
FlexModule,
MatCardModule,
PartiesSearchFiltersModule,
PartiesTableModule,
CommonModule,
DeanonimusModule,
EmptySearchResultModule,
MatProgressBarModule,
],
declarations: [SearchPartiesComponent],
})
export class SearchPartiesModule {}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { QueryParamsStore } from '@cc/app/shared/services';
import { PartiesSearchFiltersParams } from './parties-search-filters';
@Injectable()
export class SearchPartiesService extends QueryParamsStore<PartiesSearchFiltersParams> {
constructor(protected route: ActivatedRoute, protected router: Router) {
super(router, route);
}
mapToData(queryParams: Params): PartiesSearchFiltersParams {
return queryParams as PartiesSearchFiltersParams;
}
mapToParams(data: PartiesSearchFiltersParams): Params {
return data;
}
}

View File

@ -1,12 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core';
import { MatTable } from '@angular/material/table';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import {
InvoiceID,
@ -28,8 +20,6 @@ export class PaymentsTableComponent {
@Input()
payments: StatPayment[];
@ViewChild(MatTable) table: MatTable<StatPayment>;
partyID: string;
@Input()

View File

@ -1,4 +1,5 @@
import { NgModule } from '@angular/core';
import { DeanonimusService } from './deanonimus.service';
@NgModule({

View File

@ -1,5 +1,6 @@
import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs';
import { KeycloakTokenInfoService } from '../../keycloak-token-info.service';
import { ThriftService } from '../thrift-service';
import { SearchHit } from './gen-model/deanonimus';
@ -11,6 +12,6 @@ export class DeanonimusService extends ThriftService {
super(zone, keycloakTokenInfoService, '/deanonimus', Deanonimus);
}
searchParty = (text: string): Observable<SearchHit[]> =>
this.toObservableAction('searchParty')(text);
searchParty = (params: { text: string }): Observable<SearchHit[]> =>
this.toObservableAction('searchParty')(params.text);
}