TD-882: New party page (#341)

This commit is contained in:
Rinat Arsaev 2024-03-21 13:08:38 +07:00 committed by GitHub
parent fc1335e8e0
commit d7741ae064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 749 additions and 517 deletions

8
package-lock.json generated
View File

@ -25,7 +25,7 @@
"@vality/fistful-proto": "2.0.1-6600be9.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "^17.2.1-pr-57-3adeb57.0",
"@vality/ng-core": "17.2.1-pr-57-943cc8a.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",
@ -6454,9 +6454,9 @@
"integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ=="
},
"node_modules/@vality/ng-core": {
"version": "17.2.1-pr-57-3adeb57.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-57-3adeb57.0.tgz",
"integrity": "sha512-h8/U6pUrJDMTXUj2HL9xC6KjlVa1kNj20DcI79gaoYmh//4L/U+y6vF75ieOeZW8MUFDD9WcVGfrwsRFWOntqQ==",
"version": "17.2.1-pr-57-943cc8a.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-57-943cc8a.0.tgz",
"integrity": "sha512-o7WVumfCORuG+NgzKCyEH4E5UKiF2eESHvqzJRe3GSXFkUD69NWnxx0qmGNIelR4jqy1O/JXd1bEuqWg6xobLA==",
"dependencies": {
"@angular/material-date-fns-adapter": "^17.2.0",
"@ng-matero/extensions": "^17.1.0",

View File

@ -33,7 +33,7 @@
"@vality/fistful-proto": "2.0.1-6600be9.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "^17.2.1-pr-57-3adeb57.0",
"@vality/ng-core": "17.2.1-pr-57-943cc8a.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",

View File

@ -2,17 +2,7 @@
<mat-sidenav-container autosize class="container">
<mat-sidenav fixedInViewport="true" fixedTopGap="64" mode="side" opened role="navigation">
<mat-nav-list>
<ng-container *ngFor="let group of menuItemsGroups$ | async; let i = index">
<mat-divider *ngIf="i !== 0"></mat-divider>
<mat-list-item
*ngFor="let item of group"
[routerLink]="item.route"
[routerLinkActive]="['active']"
>{{ item.name }}
</mat-list-item>
</ng-container>
</mat-nav-list>
<v-nav [links]="links$ | async" exact style="display: block"></v-nav>
</mat-sidenav>
<mat-sidenav-content class="content" role="main">
<router-outlet></router-outlet>

View File

@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { Link } from '@vality/ng-core';
import { KeycloakService } from 'keycloak-angular';
import sortBy from 'lodash-es/sortBy';
import { from, Observable } from 'rxjs';
import { shareReplay, map } from 'rxjs/operators';
import { shareReplay, map, startWith } from 'rxjs/operators';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { AppAuthGuardService, Services } from '@cc/app/shared/services';
import { environment } from '../environments/environment';
@ -28,9 +29,8 @@ import { SidenavInfoService } from './shared/components/sidenav-info';
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
menuItemsGroups$: Observable<{ name: string; route: string }[][]> = from(
this.keycloakService.loadUserProfile(),
).pipe(
links$: Observable<Link[][]> = from(this.keycloakService.loadUserProfile()).pipe(
startWith(null),
map(() => this.getMenuItemsGroups()),
shareReplay({ refCount: true, bufferSize: 1 }),
);
@ -58,80 +58,80 @@ export class AppComponent {
}
private getMenuItemsGroups() {
const menuItems = [
const menuItems: (Link & { services: Services[] })[][] = [
[
{
name: 'Domain config',
route: '/domain',
label: 'Domain config',
url: '/domain',
services: DOMAIN_ROUTING_CONFIG.services,
},
{
name: 'Terminals',
route: '/terminals',
label: 'Terminals',
url: '/terminals',
services: TERMINALS_ROUTING_CONFIG.services,
},
{
name: 'Repairing',
route: '/repairing',
label: 'Repairing',
url: '/repairing',
services: REPAIRING_ROUTING_CONFIG.services,
},
{
name: 'Sources',
route: '/sources',
label: 'Sources',
url: '/sources',
services: SOURCES_ROUTING_CONFIG.services,
},
],
[
{
name: 'Merchants',
route: '/parties',
label: 'Merchants',
url: '/parties',
services: PARTIES_ROUTING_CONFIG.services,
},
{
name: 'Shops',
route: '/shops',
label: 'Shops',
url: '/shops',
services: SHOPS_ROUTING_CONFIG.services,
},
{
name: 'Wallets',
route: '/wallets',
label: 'Wallets',
url: '/wallets',
services: WALLETS_ROUTING_CONFIG.services,
},
{
name: 'Claims',
route: '/claims',
label: 'Claims',
url: '/claims',
services: CLAIMS_ROUTING_CONFIG.services,
},
],
sortBy(
[
{
name: 'Payments',
route: '/payments',
label: 'Payments',
url: '/payments',
services: PAYMENTS_ROUTING_CONFIG.services,
},
{
name: 'Payouts',
route: '/payouts',
label: 'Payouts',
url: '/payouts',
services: PAYOUTS_ROUTING_CONFIG.services,
},
{
name: 'Chargebacks',
route: '/chargebacks',
label: 'Chargebacks',
url: '/chargebacks',
services: WALLETS_ROUTING_CONFIG.services,
},
{
name: 'Deposits',
route: '/deposits',
label: 'Deposits',
url: '/deposits',
services: DEPOSITS_ROUTING_CONFIG.services,
},
{
name: 'Withdrawals',
route: '/withdrawals',
label: 'Withdrawals',
url: '/withdrawals',
services: WITHDRAWALS_ROUTING_CONFIG.services,
},
],
'name',
'label',
),
];
return menuItems.map((group) =>

View File

@ -13,7 +13,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InputMaskModule } from '@ngneat/input-mask';
import { QUERY_PARAMS_SERIALIZERS } from '@vality/ng-core';
import { NavComponent, QUERY_PARAMS_SERIALIZERS } from '@vality/ng-core';
import { MonacoEditorModule } from 'ngx-monaco-editor-v2';
import { KeycloakTokenInfoModule } from '@cc/app/shared/services';
@ -72,6 +72,7 @@ export let AppInjector: Injector;
SectionsModule,
SidenavInfoComponent,
ToolbarComponent,
NavComponent,
MonacoEditorModule.forRoot(),
// TODO: hack for metadata datetime 😡
MatDatepickerModule,

View File

@ -1,6 +1,27 @@
<mat-toolbar class="toolbar" color="primary">
<span>Control Center</span><span class="spacer"></span><span>{{ username$ | async }}</span>
<button [matMenuTriggerFor]="userMenu" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<div class="logo">Control Center</div>
<div class="search">
<cc-merchant-field
[formControl]="partyIdControl"
appearance="outline"
hint=""
label="Search merchant"
size="small"
></cc-merchant-field>
<mat-icon
*ngIf="partyIdControl.value"
[cdkCopyToClipboard]="partyIdControl.value"
class="copy"
(cdkCopyToClipboardCopied)="copyNotify($event)"
>content_copy</mat-icon
>
</div>
<div class="user">
<span>{{ username$ | async }}</span>
<button [matMenuTriggerFor]="userMenu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-menu #userMenu="matMenu"><button mat-menu-item (click)="logout()">Logout</button></mat-menu>

View File

@ -1,8 +1,52 @@
.toolbar {
position: fixed;
z-index: 2;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
& > * {
display: flex;
align-items: center;
}
}
.spacer {
flex: 1 1 auto;
.search {
align-self: end;
margin-bottom: -9px;
justify-self: center;
position: relative;
.copy {
cursor: pointer;
position: absolute;
right: -291px;
top: 8px;
}
::ng-deep .mdc-floating-label {
pointer-events: none !important;
}
::ng-deep & > * {
min-width: 280px;
}
::ng-deep * {
color: #fff !important;
}
::ng-deep .mdc-notched-outline * {
border-color: rgba(255, 255, 255, 0.5) !important;
}
::ng-deep .ng-spinner-loader {
border-color: rgba(255, 255, 255, 0.2);
border-left-color: #fff;
}
--mtx-select-enabled-arrow-color: #fff;
}
.user {
justify-self: end;
}

View File

@ -1,29 +1,92 @@
import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { Router } from '@angular/router';
import { UrlService } from '@vality/ng-core';
import { KeycloakService } from 'keycloak-angular';
import { from } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { map, shareReplay, distinctUntilChanged } from 'rxjs/operators';
import { MerchantFieldModule } from '../../../shared/components/merchant-field';
@Component({
selector: 'cc-toolbar',
standalone: true,
imports: [MatButtonModule, MatIconModule, MatMenuModule, MatToolbarModule, CommonModule],
imports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
MatToolbarModule,
CommonModule,
MerchantFieldModule,
FormsModule,
ReactiveFormsModule,
CdkCopyToClipboard,
],
templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.scss',
})
export class ToolbarComponent {
export class ToolbarComponent implements OnInit {
username$ = from(this.keycloakService.loadUserProfile()).pipe(
map(() => this.keycloakService.getUsername()),
shareReplay({ refCount: true, bufferSize: 1 }),
);
partyIdControl = new FormControl<string>(this.getPartyId());
constructor(private keycloakService: KeycloakService) {}
constructor(
private keycloakService: KeycloakService,
private router: Router,
private urlService: UrlService,
private destroyRef: DestroyRef,
private snackBar: MatSnackBar,
) {}
ngOnInit() {
this.partyIdControl.valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
.subscribe((partyId) => {
if (partyId) {
if (this.getPartyId() !== partyId) {
void this.router.navigate([`/party/${partyId}`]);
}
} else {
void this.router.navigate([`/parties`]);
}
});
this.urlService.path$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((path) => {
const partyId = this.getPartyId(path);
if (partyId) {
if (partyId !== this.partyIdControl.value) {
this.partyIdControl.setValue(partyId, { emitEvent: false });
}
} else if (this.partyIdControl.value) {
this.partyIdControl.setValue(null, { emitEvent: false });
}
});
}
logout() {
void this.keycloakService.logout();
}
copyNotify(res: boolean) {
this.snackBar.open(
res
? `Party Id #${this.partyIdControl.value} copied`
: `Party Id not copied, select and copy yourself: ${this.partyIdControl.value}`,
'OK',
{ duration: res ? 2_000 : 60_000 },
);
}
private getPartyId(path: string[] = this.urlService.path) {
return path[0] === 'party' && path[1] ? path[1] : null;
}
}

View File

@ -1,20 +1,4 @@
<cc-page-layout
[path]="[
{ label: 'Merchants', link: '/parties' },
{
label: (party$ | async)?.contact_info?.email,
tooltip: (party$ | async)?.id,
link: ['/party', (party$ | async)?.id]
},
{
label: 'Merchant claims',
link: '/claims',
queryParams: { party_id: '&quot;' + (party$ | async)?.id + '&quot;' }
}
]"
description="#{{ (claim$ | async)?.id }}"
title="Claim"
>
<cc-page-layout description="#{{ (claim$ | async)?.id }}" title="Claim">
<cc-page-layout-actions *ngIf="claim$ | async as claim">
<cc-status [color]="statusColor[claim.status | ccUnionKey]">{{
claim.status | ccUnionKey | keyTitle | titlecase

View File

@ -10,7 +10,7 @@ import { ROUTING_CONFIG } from './routing-config';
imports: [
RouterModule.forChild([
{
path: 'claims',
path: '',
component: ClaimsComponent,
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,

View File

@ -1,5 +1,5 @@
<v-table
[columns]="columns"
[columns]="columns()"
[data]="data"
[hasMore]="hasMore"
[progress]="isLoading"

View File

@ -1,11 +1,19 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import {
Component,
Input,
Output,
EventEmitter,
booleanAttribute,
input,
computed,
} from '@angular/core';
import { Router } from '@angular/router';
import { Claim, ClaimStatus } from '@vality/domain-proto/claim_management';
import { Column, LoadOptions, TagColumn, createOperationColumn } from '@vality/ng-core';
import isObject from 'lodash-es/isObject';
import startCase from 'lodash-es/startCase';
import { getUnionKey } from '../../../../utils';
import { PartiesStoreService } from '../../../api/payment-processing';
import { createPartyColumn } from '../../../shared';
@Component({
@ -17,11 +25,17 @@ export class ClaimsTableComponent {
@Input() data!: Claim[];
@Input() isLoading?: boolean | null;
@Input() hasMore?: boolean | null;
noParty = input(false, { transform: booleanAttribute });
@Output() update = new EventEmitter<LoadOptions>();
@Output() more = new EventEmitter<void>();
columns: Column<Claim>[] = [
columns = computed<Column<Claim>[]>(() =>
this.sourceColumns.filter(
(c) => (isObject(c) && c?.field !== 'party_id') || !this.noParty(),
),
);
private sourceColumns: Column<Claim>[] = [
{ field: 'id', link: (d) => this.getClaimLink(d.party_id, d.id) },
createPartyColumn('party_id'),
{
@ -51,10 +65,7 @@ export class ClaimsTableComponent {
]),
];
constructor(
private router: Router,
private partiesStoreService: PartiesStoreService,
) {}
constructor(private router: Router) {}
navigateToClaim(partyId: string, claimID: number) {
void this.router.navigate([this.getClaimLink(partyId, claimID)]);

View File

@ -4,6 +4,10 @@
></cc-page-layout-actions>
<v-filters #filters [active]="active">
<ng-template [formGroup]="filtersForm">
<cc-merchant-field
*ngIf="!(party$ | async)"
formControlName="party_id"
></cc-merchant-field>
<mat-form-field>
<input
autocomplete="off"
@ -20,13 +24,13 @@
}}</mat-option>
</mat-select>
</mat-form-field>
<cc-merchant-field formControlName="party_id"></cc-merchant-field>
</ng-template>
</v-filters>
<cc-claims-table
[data]="claims$ | async"
[hasMore]="hasMore$ | async"
[isLoading]="isLoading$ | async"
[noParty]="!!(party$ | async)"
(more)="more()"
(update)="load($event)"
>

View File

@ -4,15 +4,17 @@ import { NonNullableFormBuilder } from '@angular/forms';
import { PartyID } from '@vality/domain-proto/domain';
import { DialogService, LoadOptions, QueryParamsService, clean } from '@vality/ng-core';
import { debounceTime } from 'rxjs';
import { startWith } from 'rxjs/operators';
import { startWith, take } from 'rxjs/operators';
import { CLAIM_STATUSES } from '../../api/claim-management';
import { PartyStoreService } from '../party';
import { CreateClaimDialogComponent } from './components/create-claim-dialog/create-claim-dialog.component';
import { FetchClaimsService } from './fetch-claims.service';
@Component({
templateUrl: './claims.component.html',
providers: [PartyStoreService],
})
export class ClaimsComponent implements OnInit {
isLoading$ = this.fetchClaimsService.isLoading$;
@ -25,6 +27,7 @@ export class ClaimsComponent implements OnInit {
statuses: [[] as string[]],
});
active = 0;
party$ = this.partyStoreService.party$;
private selectedPartyId: PartyID;
@ -34,6 +37,7 @@ export class ClaimsComponent implements OnInit {
private fb: NonNullableFormBuilder,
private qp: QueryParamsService<ClaimsComponent['filtersForm']['value']>,
private destroyRef: DestroyRef,
private partyStoreService: PartyStoreService,
) {}
ngOnInit(): void {
@ -48,11 +52,17 @@ export class ClaimsComponent implements OnInit {
load(options?: LoadOptions): void {
const filters = clean(this.filtersForm.value);
void this.qp.set(filters);
this.fetchClaimsService.load(
{ ...filters, statuses: filters.statuses?.map((status) => ({ [status]: {} })) || [] },
options,
);
this.active = Object.keys(filters).length;
this.partyStoreService.party$.pipe(take(1)).subscribe((p) => {
this.fetchClaimsService.load(
clean({
party_id: p ? p.id : undefined,
...filters,
statuses: filters.statuses?.map((status) => ({ [status]: {} })) || [],
}),
options,
);
});
}
more(): void {

View File

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

View File

@ -13,12 +13,7 @@ import { ROUTING_CONFIG } from './routing-config';
path: '',
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
children: [
{
path: '',
component: DomainInfoComponent,
},
],
component: DomainInfoComponent,
},
]),
],

View File

@ -1,9 +1,8 @@
<div style="display: flex; flex-direction: column; gap: 24px">
<div class="mat-headline-4 mat-no-margin">Shops</div>
<cc-page-layout title="Shops">
<cc-shops-table
[progress]="progress$ | async"
[shops]="shopsParty$ | async"
noPartyColumn
(update)="update()"
></cc-shops-table>
</div>
</cc-page-layout>

View File

@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { InputFieldModule } from '@vality/ng-core';
import { PageLayoutModule } from '../../shared';
import { ShopsTableComponent } from '../../shared/components/shops-table';
import { PartyShopsRoutingModule } from './party-shops-routing.module';
@ -16,6 +17,7 @@ import { PartyShopsComponent } from './party-shops.component';
ReactiveFormsModule,
InputFieldModule,
ShopsTableComponent,
PageLayoutModule,
],
declarations: [PartyShopsComponent],
})

View File

@ -0,0 +1,2 @@
export * from './party-store.service';
export * from './party.module';

View File

@ -25,7 +25,28 @@ import { ROUTING_CONFIG } from './routing-config';
loadChildren: () =>
import('../routing-rules').then((m) => m.RoutingRulesModule),
},
{ path: '', redirectTo: 'shops', pathMatch: 'full' },
{
path: 'claims',
loadChildren: () => import('../claims').then((m) => m.ClaimsModule),
},
{
path: 'claim',
redirectTo: 'claims',
},
{
path: 'wallets',
loadChildren: () =>
import('../wallets/wallets.module').then((m) => m.WalletsModule),
},
{
path: 'wallet',
redirectTo: 'wallets',
},
{
path: '',
redirectTo: 'shops',
pathMatch: 'full',
},
],
},
]),

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Party } from '@vality/domain-proto/domain';
import { NotifyLogService } from '@vality/ng-core';
import { EMPTY, Observable, of } from 'rxjs';
import {
startWith,
switchMap,
catchError,
shareReplay,
distinctUntilChanged,
} from 'rxjs/operators';
import { PartyManagementService } from '../../api/payment-processing';
@Injectable()
export class PartyStoreService {
party$: Observable<Party | Pick<Party, 'id'> | null> = this.route.params.pipe(
startWith(this.route.snapshot.params),
switchMap(({ partyID }) =>
partyID
? this.partyManagementService.Get(partyID).pipe(
catchError((err) => {
this.log.error(err);
return EMPTY;
}),
)
: of(null),
),
startWith(this.partyId ? { id: this.partyId } : null),
distinctUntilChanged(),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private get partyId() {
return this.route.snapshot.params.partyID;
}
constructor(
private route: ActivatedRoute,
private partyManagementService: PartyManagementService,
private log: NotifyLogService,
) {}
}

View File

@ -1,24 +1,35 @@
<div class="party-container" style="display: flex; flex-direction: column; gap: 24px">
<div style="display: flex; place-content: stretch space-between">
<h3 class="mat-body-1">
{{ (party$ | async)?.contact_info?.email }}
</h3>
<h4 class="mat-secondary-text mat-body-1">{{ (party$ | async)?.id }}</h4>
<mat-toolbar *ngIf="party$ | async as party" style="height: 48px; padding: 0 24px">
<div style="display: flex; gap: 8px; align-items: center">
<!-- <div class="mat-body-2 mat-no-margin">-->
<!-- {{ party?.contact_info?.email }}-->
<!-- </div>-->
<v-tag
*ngIf="party?.blocking"
[color]="(party?.blocking | ccUnionKey) === 'blocked' ? 'warn' : 'success'"
style="margin-top: 8px"
>{{ party?.blocking | ccUnionKey | titlecase }}</v-tag
>
<v-tag
*ngIf="party?.suspension"
[color]="(party?.suspension | ccUnionKey) === 'suspended' ? 'warn' : 'success'"
style="margin-top: 8px"
>{{ party?.suspension | ccUnionKey | titlecase }}</v-tag
>
</div>
</mat-toolbar>
<div style="display: flex; flex-direction: column; gap: 24px">
<nav mat-tab-nav-bar>
<a
#rla="routerLinkActive"
*ngFor="let link of links"
[active]="rla.isActive || (activeLinkByFragment$ | async) === link"
[routerLink]="link.url"
mat-tab-link
routerLinkActive
>
{{ link.name }}
</a>
</nav>
<router-outlet></router-outlet>
</div>
</div>
<mat-sidenav-container autosize>
<mat-sidenav-content style="overflow: unset"
><router-outlet></router-outlet
></mat-sidenav-content>
<mat-sidenav
[fixedTopGap]="64 + 48"
[opened]="!(sidenavInfoService.opened$ | async)"
fixedInViewport="true"
mode="side"
position="end"
style="background: transparent; border: none; padding: 24px 0 24px 0"
>
<v-nav [links]="links" type="secondary"></v-nav
></mat-sidenav>
</mat-sidenav-container>

View File

@ -1,7 +0,0 @@
.party-container {
margin: 24px;
}
router-outlet {
margin-bottom: 0 !important;
}

View File

@ -1,89 +1,57 @@
import { Component } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { NotifyLogService } from '@vality/ng-core';
import { EMPTY } from 'rxjs';
import { catchError, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { Link } from '@vality/ng-core';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { AppAuthGuardService, Services } from '@cc/app/shared/services';
import { PartyManagementService } from '../../api/payment-processing';
import { ROUTING_CONFIG as SHOPS_ROUTING_CONFIG } from '../party-shops/routing-config';
import { SidenavInfoService } from '../../shared/components/sidenav-info';
import { ROUTING_CONFIG as CLAIMS_CONFIG } from '../claims/routing-config';
import { ROUTING_CONFIG as RULESET_ROUTING_CONFIG } from '../routing-rules/party-routing-ruleset/routing-config';
import { SHOPS_ROUTING_CONFIG } from '../shops';
import { ROUTING_CONFIG as WALLETS_ROUTING_CONFIG } from '../wallets/routing-config';
import { PartyStoreService } from './party-store.service';
interface PartyLink extends Link {
services?: Services[];
}
@Component({
templateUrl: 'party.component.html',
styleUrls: ['party.component.scss'],
providers: [PartyStoreService],
})
export class PartyComponent {
links = this.getLinks();
activeLinkByFragment$ = this.router.events.pipe(
filter((e) => e instanceof NavigationEnd),
startWith(undefined),
map(() => this.findLinkWithMaxActiveFragments()),
shareReplay(1),
);
party$ = this.route.params.pipe(
startWith(this.route.snapshot.params),
switchMap(({ partyID }) => this.partyManagementService.Get(partyID)),
catchError((err) => {
this.log.error(err);
return EMPTY;
}),
startWith({ id: this.route.snapshot.params.partyID }),
shareReplay({ refCount: true, bufferSize: 1 }),
);
links: PartyLink[] = [
{
label: 'Shops',
url: 'shops',
services: SHOPS_ROUTING_CONFIG.services,
},
{
label: 'Wallets',
url: 'wallets',
services: WALLETS_ROUTING_CONFIG.services,
},
{
label: 'Claims',
url: 'claims',
services: CLAIMS_CONFIG.services,
},
{
label: 'Payment Routing Rules',
url: 'routing-rules/payment',
services: RULESET_ROUTING_CONFIG.services,
},
{
label: 'Withdrawal Routing Rules',
url: 'routing-rules/withdrawal',
services: RULESET_ROUTING_CONFIG.services,
},
].filter((item) => this.appAuthGuardService.userHasSomeServiceMethods(item.services));
party$ = this.partyStoreService.party$;
constructor(
private route: ActivatedRoute,
private router: Router,
private appAuthGuardService: AppAuthGuardService,
private partyManagementService: PartyManagementService,
private log: NotifyLogService,
protected sidenavInfoService: SidenavInfoService,
private partyStoreService: PartyStoreService,
) {}
private getLinks() {
const links = [
{
name: 'Shops',
url: 'shops',
otherActiveUrlFragments: ['shop'],
services: SHOPS_ROUTING_CONFIG.services,
},
{
name: 'Payment Routing Rules',
url: 'routing-rules/payment',
services: RULESET_ROUTING_CONFIG.services,
},
{
name: 'Withdrawal Routing Rules',
url: 'routing-rules/withdrawal',
services: RULESET_ROUTING_CONFIG.services,
},
];
return links.filter((item) =>
this.appAuthGuardService.userHasSomeServiceMethods(item.services),
);
}
private activeFragments(fragments: string[]): number {
if (fragments?.length) {
const ulrFragments = this.router.url.split('/');
if (
ulrFragments.filter((fragment) => fragments.includes(fragment)).length ===
fragments.length
) {
return fragments.length;
}
}
return 0;
}
private findLinkWithMaxActiveFragments() {
return this.links.reduce(([maxLink, maxActiveFragments], link) => {
const activeFragments = this.activeFragments(link.otherActiveUrlFragments);
return maxActiveFragments > activeFragments
? [maxLink, maxActiveFragments]
: [link, activeFragments];
}, [])?.[0];
}
}

View File

@ -1,15 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbar } from '@angular/material/toolbar';
import { NavComponent, TagModule } from '@vality/ng-core';
import { PageLayoutModule } from '../../shared';
import { PageLayoutModule, ThriftPipesModule } from '../../shared';
import { PartyRouting } from './party-routing.module';
import { PartyComponent } from './party.component';
@NgModule({
imports: [PartyRouting, CommonModule, MatTabsModule, MatButtonModule, PageLayoutModule],
imports: [
PartyRouting,
CommonModule,
MatTabsModule,
MatButtonModule,
PageLayoutModule,
NavComponent,
MatSidenavModule,
MatToolbar,
TagModule,
ThriftPipesModule,
],
declarations: [PartyComponent],
})
export class PartyModule {}

View File

@ -32,6 +32,11 @@ import { ROUTING_CONFIG } from './routing-config';
},
],
},
{
path: '',
redirectTo: 'payment',
pathMatch: 'prefix',
},
]),
],
})

View File

@ -1,13 +1,14 @@
<div style="display: flex; flex-direction: column; gap: 24px">
<div style="display: flex; place-content: center space-between; align-items: center; gap: 8px">
<div class="mat-headline-4 mat-no-margin">Party delegate rulesets</div>
<button color="primary" mat-button (click)="attachNewRuleset()">Attach new ruleset</button>
</div>
<cc-page-layout
[progress]="isLoading$ | async"
[title]="(routingRulesTypeService.routingRulesType$ | async | titlecase) + ' Routing Rules'"
>
<cc-page-layout-actions>
<button color="primary" mat-raised-button (click)="attachNewRuleset()">Add</button>
</cc-page-layout-actions>
<cc-routing-rules-list
[data]="data$ | async"
[displayedColumns]="displayedColumns"
[progress]="isLoading$ | async"
(toDetails)="navigateToPartyRuleset($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
</div>
</cc-page-layout>

View File

@ -1,14 +1,14 @@
import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router';
import { DialogService } from '@vality/ng-core';
import { DialogService, NotifyLogService } from '@vality/ng-core';
import { first, map } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { RoutingRulesType } from '@cc/app/sections/routing-rules/types/routing-rules-type';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
import { handleError } from '../../../../utils/operators/handle-error';
import { RoutingRulesTypeService } from '../routing-rules-type.service';
import { RoutingRulesService } from '../services/routing-rules';
import { AttachNewRulesetDialogComponent } from './attach-new-ruleset-dialog';
@ -18,7 +18,7 @@ import { PartyDelegateRulesetsService } from './party-delegate-rulesets.service'
selector: 'cc-party-delegate-rulesets',
templateUrl: 'party-delegate-rulesets.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [PartyDelegateRulesetsService],
providers: [PartyDelegateRulesetsService, RoutingRulesTypeService],
})
export class PartyDelegateRulesetsComponent {
displayedColumns = [
@ -60,9 +60,10 @@ export class PartyDelegateRulesetsComponent {
private router: Router,
private dialogService: DialogService,
private domainStoreService: DomainStoreService,
private notificationErrorService: NotificationErrorService,
private log: NotifyLogService,
private route: ActivatedRoute,
private destroyRef: DestroyRef,
protected routingRulesTypeService: RoutingRulesTypeService,
) {}
attachNewRuleset() {
@ -72,10 +73,7 @@ export class PartyDelegateRulesetsComponent {
type: this.route.snapshot.params.type,
})
.afterClosed()
.pipe(
handleError(this.notificationErrorService.error),
takeUntilDestroyed(this.destroyRef),
)
.pipe(handleError(this.log.error), takeUntilDestroyed(this.destroyRef))
.subscribe();
}

View File

@ -17,9 +17,9 @@ import { DialogModule } from '@vality/ng-core';
import { DetailsItemModule } from '@cc/components/details-item';
import { PageLayoutModule } from '../../../shared';
import { ChangeTargetDialogModule } from '../change-target-dialog';
import { RoutingRulesListModule } from '../routing-rules-list';
import { RoutingRulesetHeaderModule } from '../routing-ruleset-header';
import { TargetRulesetFormModule } from '../target-ruleset-form';
import { AttachNewRulesetDialogComponent } from './attach-new-ruleset-dialog';
@ -31,7 +31,6 @@ const EXPORTED_DECLARATIONS = [PartyDelegateRulesetsComponent, AttachNewRulesetD
@NgModule({
imports: [
PartyDelegateRulesetsRoutingModule,
RoutingRulesetHeaderModule,
MatButtonModule,
CommonModule,
RouterModule,
@ -51,6 +50,7 @@ const EXPORTED_DECLARATIONS = [PartyDelegateRulesetsComponent, AttachNewRulesetD
TargetRulesetFormModule,
RoutingRulesListModule,
DialogModule,
PageLayoutModule,
],
declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS,

View File

@ -1,39 +1,30 @@
<ng-container *ngIf="isLoading$ | async; else loaded">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-container>
<ng-template #loaded>
<div *ngIf="partyRuleset$ | async as partyRuleset; else emptyPartyDelegate" class="content">
<cc-routing-ruleset-header
[backTo]="
'party/' + (partyID$ | async) + '/routing-rules/' + (routingRulesType$ | async)
"
[refID]="partyRuleset.ref.id"
(add)="addPartyRule()"
>
Party routing rules
</cc-routing-ruleset-header>
<cc-routing-rules-list
*ngIf="(routingRulesType$ | async) === 'payment'"
[data]="shopsData$ | async"
[displayedColumns]="shopsDisplayedColumns"
(toDetails)="navigateToDelegate($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
<cc-routing-rules-list
*ngIf="(routingRulesType$ | async) === 'withdrawal'"
[data]="walletsData$ | async"
[displayedColumns]="walletsDisplayedColumns"
(toDetails)="navigateToDelegate($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
</div>
<ng-template #emptyPartyDelegate>
<div class="empty-party-delegate">
<div class="mat-display-1">Routing rules not found</div>
<button class="init" color="primary" mat-raised-button (click)="initialize()">
Initialize
</button>
</div>
</ng-template>
</ng-template>
<cc-page-layout
[id]="(partyRuleset$ | async)?.ref?.id"
[progress]="isLoading$ | async"
[upLink]="[
'/party/' +
(partyID$ | async) +
'/routing-rules/' +
(routingRulesTypeService.routingRulesType$ | async)
]"
title="Party Routing Rules"
(idLinkClick)="openRefId()"
>
<cc-page-layout-actions>
<button [disabled]="isLoading$ | async" color="primary" mat-raised-button (click)="add()">
{{ (partyRuleset$ | async) ? 'Add' : 'Init' }}
</button>
</cc-page-layout-actions>
<cc-routing-rules-list
*ngIf="(routingRulesTypeService.routingRulesType$ | async) === 'payment'"
[data]="shopsData$ | async"
[displayedColumns]="shopsDisplayedColumns"
(toDetails)="navigateToDelegate($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
<cc-routing-rules-list
*ngIf="(routingRulesTypeService.routingRulesType$ | async) === 'withdrawal'"
[data]="walletsData$ | async"
[displayedColumns]="walletsDisplayedColumns"
(toDetails)="navigateToDelegate($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
</cc-page-layout>

View File

@ -1,9 +1,3 @@
.content {
display: flex;
flex-direction: column;
gap: 24px;
}
.empty-party-delegate {
display: flex;
flex-direction: column;

View File

@ -2,12 +2,14 @@ import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router';
import { DialogService, DialogResponseStatus } from '@vality/ng-core';
import { combineLatest, Observable } from 'rxjs';
import { filter, map, pluck, shareReplay, startWith, switchMap, take } from 'rxjs/operators';
import { combineLatest } from 'rxjs';
import { filter, map, shareReplay, startWith, switchMap, take } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { RoutingRulesType } from '../types/routing-rules-type';
import { SidenavInfoService } from '../../../shared/components/sidenav-info';
import { DomainObjectCardComponent } from '../../../shared/components/thrift-api-crud';
import { RoutingRulesTypeService } from '../routing-rules-type.service';
import { AddPartyRoutingRuleDialogComponent } from './add-party-routing-rule-dialog';
import { InitializeRoutingRulesDialogComponent } from './initialize-routing-rules-dialog';
@ -17,15 +19,11 @@ import { PartyRoutingRulesetService } from './party-routing-ruleset.service';
selector: 'cc-party-routing-ruleset',
templateUrl: 'party-routing-ruleset.component.html',
styleUrls: ['party-routing-ruleset.component.scss'],
providers: [PartyRoutingRulesetService],
providers: [PartyRoutingRulesetService, RoutingRulesTypeService],
})
export class PartyRoutingRulesetComponent {
partyRuleset$ = this.partyRoutingRulesetService.partyRuleset$;
partyID$ = this.partyRoutingRulesetService.partyID$;
routingRulesType$ = this.route.params.pipe(
startWith(this.route.snapshot.params),
pluck('type'),
) as Observable<RoutingRulesType>;
isLoading$ = this.domainStoreService.isLoading$;
shopsDisplayedColumns = [
@ -60,6 +58,7 @@ export class PartyRoutingRulesetComponent {
};
}),
),
startWith([]),
takeUntilDestroyed(this.destroyRef),
shareReplay(1),
);
@ -87,6 +86,7 @@ export class PartyRoutingRulesetComponent {
};
}),
),
startWith([]),
takeUntilDestroyed(this.destroyRef),
shareReplay(1),
);
@ -98,9 +98,45 @@ export class PartyRoutingRulesetComponent {
private route: ActivatedRoute,
private domainStoreService: DomainStoreService,
private destroyRef: DestroyRef,
private sidenavInfoService: SidenavInfoService,
protected routingRulesTypeService: RoutingRulesTypeService,
) {}
initialize() {
add() {
this.partyRuleset$.pipe(take(1)).subscribe((partyRuleset) => {
if (partyRuleset) {
this.addPartyRule();
} else {
this.initialize();
}
});
}
navigateToDelegate(parentRefId: number, delegateIdx: number) {
this.partyRoutingRulesetService.partyRuleset$
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe((ruleset) =>
this.router.navigate([
'party',
this.route.snapshot.params.partyID,
'routing-rules',
this.route.snapshot.params.type,
parentRefId,
'delegate',
ruleset?.data?.decisions?.delegates?.[delegateIdx]?.ruleset?.id,
]),
);
}
openRefId() {
this.partyRuleset$.pipe(take(1), filter(Boolean)).subscribe(({ ref }) => {
this.sidenavInfoService.toggle(DomainObjectCardComponent, {
ref: { routing_rules: { id: Number(ref.id) } },
});
});
}
private initialize() {
combineLatest([
this.partyRoutingRulesetService.partyID$,
this.partyRoutingRulesetService.refID$,
@ -121,12 +157,12 @@ export class PartyRoutingRulesetComponent {
});
}
addPartyRule() {
private addPartyRule() {
combineLatest([
this.partyRoutingRulesetService.refID$,
this.partyRoutingRulesetService.shops$,
this.partyRoutingRulesetService.wallets$,
this.routingRulesType$,
this.routingRulesTypeService.routingRulesType$,
this.partyRoutingRulesetService.partyID$,
])
.pipe(
@ -152,20 +188,4 @@ export class PartyRoutingRulesetComponent {
},
});
}
navigateToDelegate(parentRefId: number, delegateIdx: number) {
this.partyRoutingRulesetService.partyRuleset$
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe((ruleset) =>
this.router.navigate([
'party',
this.route.snapshot.params.partyID,
'routing-rules',
this.route.snapshot.params.type,
parentRefId,
'delegate',
ruleset?.data?.decisions?.delegates?.[delegateIdx]?.ruleset?.id,
]),
);
}
}

View File

@ -16,9 +16,9 @@ import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { PageLayoutModule } from '../../../shared';
import { ChangeTargetDialogModule } from '../change-target-dialog';
import { RoutingRulesListModule } from '../routing-rules-list';
import { RoutingRulesetHeaderModule } from '../routing-ruleset-header';
import { AddPartyRoutingRuleDialogModule } from './add-party-routing-rule-dialog';
import { InitializeRoutingRulesDialogModule } from './initialize-routing-rules-dialog';
@ -43,12 +43,12 @@ import { PartyRoutingRulesetComponent } from './party-routing-ruleset.component'
MatSelectModule,
MatRadioModule,
MatExpansionModule,
RoutingRulesetHeaderModule,
AddPartyRoutingRuleDialogModule,
InitializeRoutingRulesDialogModule,
MatProgressBarModule,
ChangeTargetDialogModule,
RoutingRulesListModule,
PageLayoutModule,
],
declarations: [PartyRoutingRulesetComponent],
})

View File

@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { startWith, map } from 'rxjs/operators';
import { getEnumValues } from '../../../utils';
import { RoutingRulesType } from './types/routing-rules-type';
@Injectable()
export class RoutingRulesTypeService {
routingRulesType$ = this.route.params.pipe(
startWith(this.route.snapshot.params),
map((p) => (getEnumValues(RoutingRulesType).includes(p.type) ? p.type : null)),
) as Observable<RoutingRulesType>;
constructor(private route: ActivatedRoute) {}
}

View File

@ -1 +0,0 @@
export * from './routing-ruleset-header.module';

View File

@ -1,24 +0,0 @@
<div style="display: flex; flex-direction: column; gap: 8px">
<div style="display: flex; place-content: center space-between; align-items: center; gap: 8px">
<div
class="mat-headline-4 title"
style="display: flex; place-content: center flex-start; align-items: center; gap: 8px"
>
<mat-icon *ngIf="backTo" class="back" (click)="navigateBack()">arrow_back</mat-icon>
<div><ng-content></ng-content></div>
</div>
<button mat-button (click)="add.emit($event)">Add rule</button>
</div>
<div *ngIf="description" class="mat-h3 cc-routing-rules-caption">
{{ description }}
</div>
<div class="mat-caption cc-routing-rules-caption">
<span
class="mat-secondary-text"
style="cursor: pointer; text-decoration: underline"
(click)="openRule()"
>
Ruleset ref ID: {{ refID }}</span
>
</div>
</div>

View File

@ -1,7 +0,0 @@
.title {
margin: 0;
}
.back {
cursor: pointer;
}

View File

@ -1,43 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { SidenavInfoService } from '../../../shared/components/sidenav-info';
import { DomainObjectCardComponent } from '../../../shared/components/thrift-api-crud';
@Component({
selector: 'cc-routing-ruleset-header',
templateUrl: 'routing-ruleset-header.component.html',
styleUrls: ['routing-ruleset-header.component.scss'],
})
export class RoutingRulesetHeaderComponent {
@Input() refID: string;
@Input() description?: string;
@Input() backTo?: string;
@Output() add = new EventEmitter();
get queryParams() {
return {
types: JSON.stringify(['RoutingRulesObject']),
sidenav: JSON.stringify({
id: 'domainObject',
inputs: { ref: { routing_rules: { id: this.refID } } },
}),
};
}
constructor(
private router: Router,
private sidenavInfoService: SidenavInfoService,
) {}
navigateBack() {
void this.router.navigate([this.backTo]);
}
openRule() {
this.sidenavInfoService.toggle(DomainObjectCardComponent, {
ref: { routing_rules: { id: Number(this.refID) } },
});
}
}

View File

@ -1,14 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { RouterLink } from '@angular/router';
import { RoutingRulesetHeaderComponent } from './routing-ruleset-header.component';
@NgModule({
imports: [CommonModule, MatIconModule, MatButtonModule, RouterLink],
declarations: [RoutingRulesetHeaderComponent],
exports: [RoutingRulesetHeaderComponent],
})
export class RoutingRulesetHeaderModule {}

View File

@ -1,20 +1,19 @@
<div style="display: flex; flex-direction: column; gap: 24px">
<cc-routing-ruleset-header
[backTo]="
'party/' +
<cc-page-layout
[id]="(shopRuleset$ | async)?.ref?.id"
[title]="((routingRulesType$ | async) === 'payment' ? 'Shop' : 'Wallet') + ' Routing Rules'"
[upLink]="[
'/party/' +
(partyID$ | async) +
'/routing-rules/' +
(routingRulesType$ | async) +
'/' +
(partyRulesetRefID$ | async)
"
[description]="(shop$ | async)?.details?.name"
[refID]="(shopRuleset$ | async)?.ref?.id"
(add)="addShopRule()"
>
Routing rules
</cc-routing-ruleset-header>
]"
(idLinkClick)="openRefId()"
>
<cc-page-layout-actions>
<button color="primary" mat-raised-button (click)="addShopRule()">Add</button>
</cc-page-layout-actions>
<v-table
[columns]="columns"
[data]="(candidates$ | async) || []"
@ -26,4 +25,4 @@
sortOnFront
(rowDropped)="drop($event)"
></v-table>
</div>
</cc-page-layout>

View File

@ -13,11 +13,14 @@ import {
} from '@vality/ng-core';
import cloneDeep from 'lodash-es/cloneDeep';
import { Observable, combineLatest, filter } from 'rxjs';
import { first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { first, map, switchMap, withLatestFrom, take } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { RoutingRulesType } from '@cc/app/sections/routing-rules/types/routing-rules-type';
import { DomainThriftFormDialogComponent } from '@cc/app/shared/components/thrift-api-crud';
import {
DomainThriftFormDialogComponent,
DomainObjectCardComponent,
} from '@cc/app/shared/components/thrift-api-crud';
import { objectToJSON } from '../../../../utils';
import { createPredicateColumn } from '../../../shared';
@ -285,4 +288,12 @@ export class RoutingRulesetComponent {
},
});
}
openRefId() {
this.shopRuleset$.pipe(take(1), filter(Boolean)).subscribe(({ ref }) => {
this.sidenavInfoService.toggle(DomainObjectCardComponent, {
ref: { routing_rules: { id: Number(ref.id) } },
});
});
}
}

View File

@ -19,8 +19,8 @@ import { TableModule, DialogModule } from '@vality/ng-core';
import { DomainThriftViewerComponent } from '@cc/app/shared/components/thrift-api-crud';
import { PageLayoutModule } from '../../../shared';
import { ThriftViewerModule } from '../../../shared/components/thrift-viewer';
import { RoutingRulesetHeaderModule } from '../routing-ruleset-header';
import { ChangeCandidatesPrioritiesDialogComponent } from './components/change-candidates-priorities-dialog/change-candidates-priorities-dialog.component';
import { RoutingRulesetRoutingModule } from './routing-ruleset-routing.module';
@ -44,12 +44,12 @@ import { RoutingRulesetComponent } from './routing-ruleset.component';
MatSelectModule,
MatRadioModule,
MatExpansionModule,
RoutingRulesetHeaderModule,
MatAutocompleteModule,
TableModule,
DomainThriftViewerComponent,
ThriftViewerModule,
DialogModule,
PageLayoutModule,
],
declarations: [RoutingRulesetComponent, ChangeCandidatesPrioritiesDialogComponent],
})

View File

@ -15,6 +15,10 @@ import { SearchPartiesComponent } from './search-parties.component';
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
},
{
path: 'party',
redirectTo: 'parties',
},
]),
],
exports: [RouterModule],

View File

@ -11,8 +11,8 @@ const ROUTES: Routes = [
loadChildren: () => import('./party/party.module').then((m) => m.PartyModule),
},
{
path: 'party',
loadChildren: () => import('./party/party.module').then((m) => m.PartyModule),
path: 'claims',
loadChildren: () => import('./claims').then((m) => m.ClaimsModule),
},
{
path: 'party/:partyID',

View File

@ -12,19 +12,22 @@
</cc-page-layout-actions>
<v-filters *ngIf="isFilterControl.value" [active]="active" merge (clear)="filtersForm.reset()">
<ng-template [formGroup]="filtersForm">
<cc-merchant-field formControlName="party_id"></cc-merchant-field>
<cc-merchant-field
*ngIf="!(party$ | async)"
formControlName="party_id"
></cc-merchant-field>
<v-list-field formControlName="wallet_id" label="Wallet IDs"></v-list-field>
<mat-form-field>
<mat-label>Identity ID</mat-label>
<input formControlName="identity_id" matInput />
</mat-form-field>
<cc-currency-field formControlName="currency_code"></cc-currency-field>
<v-list-field formControlName="wallet_id" label="Wallet IDs"></v-list-field>
</ng-template>
</v-filters>
<v-table
*ngIf="isFilterControl.value"
[columns]="filterColumns"
[columns]="filterColumns$ | async"
[data]="filterWallets$ | async"
[hasMore]="filterHasMore$ | async"
[progress]="filtersLoading$ | async"

View File

@ -1,4 +1,12 @@
import { Component, OnInit, Inject, ViewChild, DestroyRef } from '@angular/core';
import {
Component,
OnInit,
Inject,
ViewChild,
DestroyRef,
Injector,
runInInjectionContext,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl } from '@angular/forms';
import { SearchWalletHit } from '@vality/deanonimus-proto/internal/deanonimus';
@ -17,7 +25,7 @@ import {
} from '@vality/ng-core';
import isNil from 'lodash-es/isNil';
import { of } from 'rxjs';
import { map, shareReplay, catchError, debounceTime } from 'rxjs/operators';
import { map, shareReplay, catchError, debounceTime, take } from 'rxjs/operators';
import { MemoizeExpiring } from 'typescript-memoize';
import { WalletParams } from '@cc/app/api/fistful-stat/query-dsl/types/wallet';
@ -26,6 +34,7 @@ import { ManagementService } from '@cc/app/api/wallet';
import { IdentityManagementService } from '../../api/identity';
import { createCurrencyColumn, createPartyColumn } from '../../shared';
import { DEBOUNCE_TIME_MS } from '../../tokens';
import { PartyStoreService } from '../party';
import { FetchWalletsTextService } from './fetch-wallets-text.service';
import { FetchWalletsService } from './fetch-wallets.service';
@ -33,7 +42,7 @@ import { FetchWalletsService } from './fetch-wallets.service';
@Component({
selector: 'cc-wallets',
templateUrl: './wallets.component.html',
providers: [FetchWalletsService, FetchWalletsTextService],
providers: [FetchWalletsService, FetchWalletsTextService, PartyStoreService],
})
export class WalletsComponent implements OnInit {
isFilterControl = new FormControl(1);
@ -45,43 +54,56 @@ export class WalletsComponent implements OnInit {
fullTextSearchWallets$ = this.fetchWalletsTextService.result$;
fullTextSearchLoading$ = this.fetchWalletsTextService.isLoading$;
filterColumns: Column<StatWallet>[] = [
{ field: 'id' },
{ field: 'name' },
'currency_symbolic_code',
'identity_id',
{ field: 'created_at', type: 'datetime' },
createCurrencyColumn<StatWallet>(
'balance',
(d) => this.getBalance(d.id).pipe(map((b) => b.current)),
(d) => this.getBalance(d.id).pipe(map((b) => b.currency.symbolic_code)),
{ lazy: true },
filterColumns$ = this.partyStoreService.party$.pipe(
map((party) =>
runInInjectionContext(this.injector, () => [
{ field: 'id' },
{ field: 'name' },
'currency_symbolic_code',
'identity_id',
{ field: 'created_at', type: 'datetime' },
createCurrencyColumn<StatWallet>(
'balance',
(d) => this.getBalance(d.id).pipe(map((b) => b.current)),
(d) => this.getBalance(d.id).pipe(map((b) => b.currency.symbolic_code)),
{ lazy: true },
),
createCurrencyColumn<StatWallet>(
'hold',
(d) => this.getBalance(d.id).pipe(map((b) => b.current - b.expected_min)),
(d) => this.getBalance(d.id).pipe(map((b) => b.currency.symbolic_code)),
{ lazy: true },
),
createCurrencyColumn<StatWallet>(
'expected_min',
(d) => this.getBalance(d.id).pipe(map((b) => b.expected_min)),
(d) => this.getBalance(d.id).pipe(map((b) => b.currency.symbolic_code)),
{ lazy: true },
),
{
field: 'contract_id',
formatter: (d) =>
this.getIdentity(d.identity_id).pipe(
map((identity) => identity.contract_id),
),
lazy: true,
},
...(party
? []
: [
createPartyColumn<StatWallet>(
'party',
(d) =>
this.getIdentity(d.identity_id).pipe(
map((identity) => identity.party_id),
),
undefined,
{ lazy: true },
),
]),
]),
),
createCurrencyColumn<StatWallet>(
'hold',
(d) => this.getBalance(d.id).pipe(map((b) => b.current - b.expected_min)),
(d) => this.getBalance(d.id).pipe(map((b) => b.currency.symbolic_code)),
{ lazy: true },
),
createCurrencyColumn<StatWallet>(
'expected_min',
(d) => this.getBalance(d.id).pipe(map((b) => b.expected_min)),
(d) => this.getBalance(d.id).pipe(map((b) => b.currency.symbolic_code)),
{ lazy: true },
),
{
field: 'contract_id',
formatter: (d) =>
this.getIdentity(d.identity_id).pipe(map((identity) => identity.contract_id)),
lazy: true,
},
createPartyColumn(
'party',
(d) => this.getIdentity(d.identity_id).pipe(map((identity) => identity.party_id)),
undefined,
{ lazy: true },
),
];
);
fullTextSearchColumns: Column<SearchWalletHit>[] = [
{ field: 'wallet.id' },
{ field: 'wallet.name' },
@ -118,6 +140,7 @@ export class WalletsComponent implements OnInit {
active = 0;
@ViewChild(FiltersComponent) filters!: FiltersComponent;
typeQp = this.qp.createNamespace<{ isFilter: boolean }>('type');
party$ = this.partyStoreService.party$;
constructor(
private fetchWalletsService: FetchWalletsService,
@ -129,6 +152,8 @@ export class WalletsComponent implements OnInit {
@Inject(DEBOUNCE_TIME_MS) private debounceTimeMs: number,
private destroyRef: DestroyRef,
private identityManagementService: IdentityManagementService,
private partyStoreService: PartyStoreService,
private injector: Injector,
) {}
ngOnInit() {
@ -152,8 +177,16 @@ export class WalletsComponent implements OnInit {
filterSearch(opts?: UpdateOptions) {
const props = clean(this.filtersForm.value);
this.fetchWalletsService.load(props, opts);
this.active = countProps(props);
this.partyStoreService.party$.pipe(take(1)).subscribe((p) => {
this.fetchWalletsService.load(
clean({
party_id: p ? p.id : undefined,
...props,
}),
opts,
);
});
}
filterMore() {

View File

@ -1,9 +1,12 @@
<v-select-field
[appearance]="appearance"
[formControl]="control"
[hint]="hint"
[label]="label || 'Merchant'"
[options]="options$ | async"
[progress]="!!(progress$ | async)"
[required]="required"
[size]="size"
externalSearch
(searchChange)="this.searchChange$.next($event)"
></v-select-field>

View File

@ -6,9 +6,17 @@ import {
NotifyLogService,
FormControlSuperclass,
createControlProviders,
getValueChanges,
} from '@vality/ng-core';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { catchError, debounceTime, map, switchMap, tap, startWith } from 'rxjs/operators';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject, merge } from 'rxjs';
import {
catchError,
debounceTime,
map,
switchMap,
tap,
distinctUntilChanged,
} from 'rxjs/operators';
import { DeanonimusService } from '@cc/app/api/deanonimus';
@ -23,6 +31,9 @@ export class MerchantFieldComponent
{
@Input() label: string;
@Input({ transform: booleanAttribute }) required: boolean;
@Input() size?: string;
@Input() appearance?: string;
@Input() hint?: string;
options$ = new ReplaySubject<Option<PartyID>[]>(1);
searchChange$ = new Subject<string>();
@ -37,9 +48,9 @@ export class MerchantFieldComponent
}
ngAfterViewInit() {
this.searchChange$
merge(getValueChanges(this.control), this.searchChange$)
.pipe(
startWith(this.control.value),
distinctUntilChanged(),
tap(() => {
this.options$.next([]);
this.progress$.next(true);

View File

@ -1,42 +1,60 @@
<div class="header">
<h1
class="mat-headline-4 mat-no-margin"
style="display: flex; place-content: center flex-start; align-items: center; gap: 4px"
>
<button *ngIf="isBackAvailable" mat-icon-button (click)="back()">
<mat-icon>arrow_back</mat-icon>
</button>
<div>
{{ title }}
<span class="mat-secondary-text">{{ description }}</span>
<div [ngClass]="{ wrapper__offset: !noOffset }" class="wrapper">
<div *ngIf="title" style="display: flex; flex-direction: column; gap: 8px">
<div *ngIf="(path$ | async)?.length > 1" class="mat-caption mat-secondary-text">
<ng-container *ngFor="let p of path$ | async; let index = index">
<span
[ngClass]="{ 'mat-link': index !== (path$ | async).length - 1 }"
[routerLink]="p.url"
[title]="p.url"
>{{ p.label }}</span
>
<span *ngIf="index !== (path$ | async).length - 1"> / </span>
</ng-container>
</div>
</h1>
<div class="mat-headline-4 mat-no-margin">
<ng-content select="cc-page-layout-actions"></ng-content>
<div class="header">
<h1
class="mat-headline-4 mat-no-margin"
style="
display: flex;
place-content: center flex-start;
align-items: center;
gap: 4px;
"
>
<button *ngIf="isBackAvailable()" mat-icon-button (click)="back()">
<mat-icon>{{ upLink() ? 'arrow_upward' : 'arrow_back' }}</mat-icon>
</button>
<div>
{{ title }}
<span class="mat-secondary-text"
>{{ description }}
<ng-container *ngIf="id">
<span
*ngIf="idLink() || idLinkClick.observed; else idText"
[routerLink]="idLink()"
class="mat-action"
(click)="idLinkClick.emit($event)"
><ng-container *ngTemplateOutlet="idText"></ng-container
></span>
<ng-template #idText>{{ id ? '#' + id : '' }}</ng-template>
</ng-container>
</span>
</div>
</h1>
<div class="mat-headline-4 mat-no-margin">
<ng-content select="cc-page-layout-actions"></ng-content>
</div>
</div>
</div>
<div
*ngIf="progress; else content"
style="display: flex; place-content: center; align-items: center"
>
<mat-spinner diameter="64"></mat-spinner>
</div>
</div>
<div *ngIf="path?.length" class="mat-h3 mat-secondary-text" style="display: flex; gap: 4px">
<ng-container *ngFor="let pathPart of path; let idx = index">
<div [matTooltip]="pathPart.tooltip">
<a
*ngIf="pathPart.link; else onlyLabel"
[queryParams]="pathPart?.queryParams"
[routerLink]="pathPart.link"
class="mat-secondary-text"
>{{ pathPart.label }}</a
>
<ng-template #onlyLabel>{{ pathPart.label }}</ng-template>
</div>
<div *ngIf="idx !== path.length - 1">/</div></ng-container
>
</div>
<div
*ngIf="progress; else content"
style="display: flex; place-content: center; align-items: center"
>
<mat-spinner diameter="64"></mat-spinner>
</div>
<ng-template #content>
<ng-content></ng-content>
</ng-template>

View File

@ -1,7 +1,10 @@
:host {
.wrapper {
&__offset {
padding: 24px;
}
display: grid;
gap: 24px;
padding: 24px;
.header {
display: flex;

View File

@ -1,6 +1,17 @@
import { Location } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Params } from '@angular/router';
import {
ChangeDetectionStrategy,
Component,
Input,
booleanAttribute,
input,
computed,
Output,
EventEmitter,
} from '@angular/core';
import { Router } from '@angular/router';
import { UrlService } from '@vality/ng-core';
import { map } from 'rxjs/operators';
@Component({
selector: 'cc-page-layout',
@ -11,21 +22,49 @@ import { Params } from '@angular/router';
export class PageLayoutComponent {
@Input() title!: string;
@Input() description?: string;
@Input() id?: string;
@Input() progress?: boolean;
@Input() path?: {
label: string;
link?: unknown[] | string | null | undefined;
queryParams?: Params | null;
tooltip?: string;
}[];
@Input({ transform: booleanAttribute }) noOffset = false;
// 1 and 2 is default history length
isBackAvailable =
window.history.length > 2 && window.location.pathname.split('/').slice(1).length > 1;
@Output() idLinkClick = new EventEmitter<MouseEvent>();
constructor(private location: Location) {}
backLink = input<unknown[]>();
upLink = input<unknown[]>();
idLink = input<unknown[]>();
isBackAvailable = computed(
() =>
this.backLink() ||
this.upLink() ||
// 1 and 2 is default history length
(window.history.length > 2 && window.location.pathname.split('/').slice(1).length > 1),
);
path$ = this.urlService.path$.pipe(
map((path) => {
return path
.reduce(
(acc, p) => {
acc.push({ url: [...(acc.at(-1)?.url || ['']), p], label: p });
return acc;
},
[] as { url: string[]; label: string }[],
)
.map((v) => ({ ...v, url: v.url.join('/') }));
}),
);
constructor(
private location: Location,
private router: Router,
private urlService: UrlService,
) {}
back() {
this.location.back();
if (this.backLink() || this.upLink()) {
void this.router.navigate(this.backLink() || this.upLink());
} else {
this.location.back();
}
}
}

View File

@ -1,14 +1,14 @@
import { Injectable, Type, Inject, Optional } from '@angular/core';
import { Router } from '@angular/router';
import {
QueryParamsService,
QueryParamsNamespace,
getPossiblyAsyncObservable,
PossiblyAsync,
UrlService,
} from '@vality/ng-core';
import isEqual from 'lodash-es/isEqual';
import { BehaviorSubject } from 'rxjs';
import { filter, startWith, map, distinctUntilChanged } from 'rxjs/operators';
import { filter, map } from 'rxjs/operators';
import { SIDENAV_INFO_COMPONENTS, SidenavInfoComponents } from './tokens';
@ -24,7 +24,7 @@ export class SidenavInfoService {
private qp!: QueryParamsNamespace<{ id?: string; inputs?: Record<PropertyKey, unknown> }>;
constructor(
router: Router,
urlService: UrlService,
private qps: QueryParamsService,
@Optional()
@Inject(SIDENAV_INFO_COMPONENTS)
@ -33,17 +33,9 @@ export class SidenavInfoService {
if (!this.sidenavInfoComponents) {
this.sidenavInfoComponents = sidenavInfoComponents ?? {};
}
router.events
.pipe(
startWith(null),
filter(() => router.navigated),
map(() => router.url?.split('?', 1)[0].split('#', 1)[0]),
distinctUntilChanged(),
filter(() => !!this.component$.value),
)
.subscribe(() => {
this.close();
});
urlService.url$.pipe(filter(() => !!this.component$.value)).subscribe(() => {
this.close();
});
this.qp = this.qps.createNamespace('sidenav');
this.qp.params$
.pipe(