Move terminals page to table v2 (#402)
Some checks failed
Main / Deploy (push) Has been cancelled
Main / Notify (push) Has been cancelled

This commit is contained in:
Rinat Arsaev 2024-10-09 17:27:05 +09:00 committed by GitHub
parent 6a76549c59
commit 753ad0d727
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 275 additions and 144 deletions

8
package-lock.json generated
View File

@ -26,7 +26,7 @@
"@vality/fistful-proto": "2.0.1-88e69a5.0",
"@vality/machinegun-proto": "1.0.1-3decc8f.0",
"@vality/magista-proto": "2.0.2-ec1bdb9.0",
"@vality/ng-core": "^18.4.1-pr-69-2efb51f.0",
"@vality/ng-core": "18.4.1-pr-74-55d4acd.0",
"@vality/ng-thrift": "18.0.1-pr-13-bdb6d51.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/scrooge-proto": "0.1.1-9ce7fc6.0",
@ -5969,9 +5969,9 @@
"integrity": "sha512-XWF7qM/CARRAey0scGVhfGU6jNq+UdlGE2mg3jn4eIFDuIWQJqsT+Bah300RBUrl+XgFsmj95C6HWRfeA5Q8kw=="
},
"node_modules/@vality/ng-core": {
"version": "18.4.1-pr-69-2efb51f.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-18.4.1-pr-69-2efb51f.0.tgz",
"integrity": "sha512-q01uhPxW88y9/rpPGfOLSPqsCL65ORZGcsxBd/2Rta+5wqLTSMlzz++gm13BmLUPKc8HvaRKDacOEfktVfCG3Q==",
"version": "18.4.1-pr-74-55d4acd.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-18.4.1-pr-74-55d4acd.0.tgz",
"integrity": "sha512-EkGTPth5+HA0MLc+BYLIV5ie7xnwBrXftTxoVJw+5zlcWoLm5V1+10AGz6wmhX4VIunFFZR/p9XUNAhNqvluVw==",
"dependencies": {
"@angular/material-date-fns-adapter": "^18.2.2",
"@ng-matero/extensions": "^18.2.0",

View File

@ -35,7 +35,7 @@
"@vality/fistful-proto": "2.0.1-88e69a5.0",
"@vality/machinegun-proto": "1.0.1-3decc8f.0",
"@vality/magista-proto": "2.0.2-ec1bdb9.0",
"@vality/ng-core": "^18.4.1-pr-69-2efb51f.0",
"@vality/ng-core": "18.4.1-pr-74-55d4acd.0",
"@vality/ng-thrift": "18.0.1-pr-13-bdb6d51.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/scrooge-proto": "0.1.1-9ce7fc6.0",

View File

@ -3,11 +3,11 @@
"extends": ["local>valitydev/.github:renovate-config"],
"packageRules": [
{
"groupName": "Minor Updates",
"groupName": "Minor",
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "Major Updates",
"groupName": "Major",
"matchUpdateTypes": ["major"]
}
]

View File

@ -30,14 +30,20 @@ export class AccountBalancesStoreService {
);
}
getTerminalBalances(id: string | number, providerId?: string | number) {
getTerminalBalances(
id: string | number,
providerId?: string | number,
): Observable<AccountBalance[]> {
if (isNil(providerId)) {
return of([]);
}
return this.balances$.pipe(
map((balances) =>
balances.filter(
(b) =>
!isNil(providerId) &&
b.provider.id === String(providerId) &&
b.terminal.id === String(id),
b.terminal.id === String(id) &&
b.balance,
),
),
);

View File

@ -47,10 +47,10 @@ export class DepositDetailsComponent implements OnInit {
map((c) => ({
value: formatCurrency(
amount,
c.data.symbolic_code,
c.symbolic_code,
'long',
this._locale,
c.data.exponent,
c.exponent,
),
})),
),

View File

@ -82,7 +82,6 @@ export class DomainObjectsTableComponent implements OnInit {
{
field: 'id',
cell: (d) => ({ value: getDomainObjectDetails(d.obj).id }),
sort: true,
sticky: 'start',
},
{
@ -91,17 +90,14 @@ export class DomainObjectsTableComponent implements OnInit {
value: getDomainObjectDetails(d.obj).label,
click: () => this.details(d),
}),
sort: true,
style: { width: 0 },
},
{
field: 'description',
cell: (d) => ({ value: getDomainObjectDetails(d.obj).description }),
sort: true,
},
{
field: 'type',
sort: true,
cell: (d) => ({ value: startCase(d.type) }),
hidden: getValueChanges(this.typesControl).pipe(map((t) => t.length <= 1)),
},

View File

@ -35,10 +35,10 @@ export class PaymentDetailsComponent {
map((c) => ({
value: formatCurrency(
amount,
c.data.symbolic_code,
c.symbolic_code,
'long',
this._locale,
c.data.exponent,
c.exponent,
),
})),
),

View File

@ -1,16 +1,14 @@
<cc-page-layout title="Terminals">
<cc-page-layout fullHeight title="Terminals">
<cc-page-layout-actions>
<button color="primary" mat-raised-button (click)="create()">Create</button>
</cc-page-layout-actions>
<v-table
<v-table2
[(sort)]="sort"
[columns]="columns"
[data]="data$ | async"
[filterByColumns]="[]"
[progress]="progress$ | async"
name="terminals"
sortOnFront
standaloneFilter
(update)="update()"
></v-table>
></v-table2>
</cc-page-layout>

View File

@ -1,20 +1,24 @@
import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Component } from '@angular/core';
import { Sort } from '@angular/material/sort';
import { TerminalObject } from '@vality/domain-proto/domain';
import { Column, DialogService } from '@vality/ng-core';
import { of } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { DialogService, Column2 } from '@vality/ng-core';
import { map } from 'rxjs/operators';
import { DomainStoreService } from '../../api/domain-config';
import { AccountBalancesStoreService } from '../../api/terminal-balance';
import { createPredicateColumn, createCurrenciesColumn } from '../../shared';
import { SidenavInfoService } from '../../shared/components/sidenav-info';
import { TerminalBalancesCardComponent } from '../../shared/components/terminal-balances-card/terminal-balances-card.component';
import { TerminalDelegatesCardComponent } from '../../shared/components/terminal-delegates-card/terminal-delegates-card.component';
import {
DomainObjectCardComponent,
CreateDomainObjectDialogComponent,
getDomainObjectDetails,
} from '../../shared/components/thrift-api-crud';
import {
createCurrencyColumn,
createPredicateColumn,
createDomainObjectColumn,
} from '../../shared/utils/table2';
import { getTerminalShopWalletDelegates } from './utils/get-terminal-shop-wallet-delegates';
@ -23,78 +27,86 @@ import { getTerminalShopWalletDelegates } from './utils/get-terminal-shop-wallet
templateUrl: './terminals.component.html',
})
export class TerminalsComponent {
columns: Column<TerminalObject>[] = [
{ field: 'ref.id', sortable: true },
columns: Column2<TerminalObject>[] = [
{ field: 'ref.id', sticky: 'start' },
{
field: 'data.name',
description: 'data.description',
sortable: true,
click: (d) => {
cell: (d) => ({
description: getDomainObjectDetails({ terminal: d }).description,
click: () => {
this.sidenavInfoService.toggle(DomainObjectCardComponent, {
ref: { terminal: d.ref },
});
},
}),
},
{
field: 'data.provider_ref.id',
description: 'data.provider_ref.id',
createDomainObjectColumn((d) => ({ ref: { provider: d.data.provider_ref } }), {
header: 'Provider',
formatter: (d) => this.getProvider(d).pipe(map((p) => p?.data?.name || '')),
sortable: true,
click: (d) => {
this.getProvider(d)
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe((provider) => {
if (!provider) {
return;
}
this.sidenavInfoService.toggle(DomainObjectCardComponent, {
ref: { provider: provider.ref },
});
});
},
},
createPredicateColumn('payments global allow', (d) => d.data.terms?.payments?.global_allow),
}),
createPredicateColumn((d) => ({ predicate: d.data.terms?.payments?.global_allow }), {
header: 'Payments Global Allow',
}),
createPredicateColumn(
'withdrawals global allow',
(d) => d.data.terms?.wallet?.withdrawals?.global_allow,
(d) => ({ predicate: d.data.terms?.wallet?.withdrawals?.global_allow }),
{
header: 'Withdrawals Global Allow',
},
),
{
field: 'delegates',
formatter: (d) =>
this.getTerminalShopWalletDelegates(d).pipe(map((r) => r.length || '')),
click: (d) => {
cell: (d) =>
this.getTerminalShopWalletDelegates(d).pipe(
map((r) => ({
value: r.length || '',
click: () => {
this.sidenavInfoService.toggle(TerminalDelegatesCardComponent, {
ref: d.ref,
});
},
},
createCurrenciesColumn<TerminalObject>(
'balances',
(d) =>
this.accountBalancesStoreService
.getTerminalBalances(d.ref.id, d.data.provider_ref.id)
.pipe(
map((b) =>
b.map((a) => ({
amount: a.balance.amount,
symbolicCode: a.balance.currency_code,
})),
),
},
createCurrencyColumn(
(d) =>
this.accountBalancesStoreService
.getTerminalBalances(d.ref.id, d.data.provider_ref?.id)
.pipe(
map((balances) => ({
values: balances.map((a) => ({
amount: a.balance.amount,
code: a.balance.currency_code,
})),
isSum: true,
})),
),
{
sortable: true,
tooltip: (d) =>
header: 'Balances (Summarized)',
cell: (d) => ({
click: () => {
this.toggleBalancesCard(d);
},
}),
},
),
createCurrencyColumn(
(d) =>
this.accountBalancesStoreService
.getTerminalBalances(d.ref.id, d.data.provider_ref.id)
.getTerminalBalances(d.ref.id, d.data.provider_ref?.id)
.pipe(
map((accountBalance) =>
accountBalance
.sort((a, b) => b.balance.amount - a.balance.amount)
.map((a) => a.account_id)
.join(', '),
),
map((balances) => ({
values: balances.map((a) => ({
amount: a.balance.amount,
code: a.balance.currency_code,
})),
})),
),
{
header: 'Balances',
cell: (d) => ({
click: () => {
this.toggleBalancesCard(d);
},
}),
},
),
];
@ -105,7 +117,6 @@ export class TerminalsComponent {
constructor(
private domainStoreService: DomainStoreService,
private sidenavInfoService: SidenavInfoService,
private destroyRef: DestroyRef,
private dialogService: DialogService,
private accountBalancesStoreService: AccountBalancesStoreService,
) {}
@ -120,21 +131,16 @@ export class TerminalsComponent {
});
}
private getProvider(terminalObj: TerminalObject) {
return terminalObj.data.provider_ref
? this.domainStoreService
.getObjects('provider')
.pipe(
map((providers) =>
providers.find((p) => p.ref.id === terminalObj.data.provider_ref.id),
),
)
: of(null);
}
private getTerminalShopWalletDelegates(terminalObj: TerminalObject) {
return this.domainStoreService
.getObjects('routing_rules')
.pipe(map((rules) => getTerminalShopWalletDelegates(rules, terminalObj)));
}
private toggleBalancesCard(d: TerminalObject) {
this.sidenavInfoService.toggle(TerminalBalancesCardComponent, {
terminalId: d.ref.id,
providerId: d.data.provider_ref.id,
});
}
}

View File

@ -0,0 +1,3 @@
<cc-card title="Terminal #{{ terminalId() }} balances">
<v-table2 [(sort)]="sort" [columns]="columns" [data]="balances$ | async"></v-table2>
</cc-card>

View File

@ -0,0 +1,41 @@
import { CommonModule } from '@angular/common';
import { Component, input } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { Sort } from '@angular/material/sort';
import { TableModule, Column2 } from '@vality/ng-core';
import { AccountBalance } from '@vality/scrooge-proto/internal/account_balance';
import { combineLatest } from 'rxjs';
import { switchMap, shareReplay } from 'rxjs/operators';
import { AccountBalancesStoreService } from '../../../api/terminal-balance';
import { createCurrencyColumn } from '../../utils/table2';
import { CardComponent } from '../sidenav-info/components/card/card.component';
import { DomainThriftViewerComponent } from '../thrift-api-crud';
@Component({
selector: 'cc-terminal-balances-card',
standalone: true,
imports: [CommonModule, CardComponent, DomainThriftViewerComponent, TableModule],
templateUrl: './terminal-balances-card.component.html',
})
export class TerminalBalancesCardComponent {
terminalId = input<number>();
providerId = input<number>();
columns: Column2<AccountBalance>[] = [
{ field: 'account_id' },
createCurrencyColumn((d) => ({ code: d.balance.currency_code, amount: d.balance.amount }), {
header: 'Balance',
field: 'balance',
}),
{ field: 'last_updated', cell: { type: 'datetime' } },
];
balances$ = combineLatest([toObservable(this.terminalId), toObservable(this.providerId)]).pipe(
switchMap(([terminalId, providerId]) =>
this.accountBalancesStoreService.getTerminalBalances(terminalId, providerId),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
sort: Sort = { active: 'account_id', direction: 'asc' };
constructor(private accountBalancesStoreService: AccountBalancesStoreService) {}
}

View File

@ -112,7 +112,7 @@ export function getDomainObjectValueDetailsFn(key: keyof DomainObject): GetDomai
}
export function getDomainObjectDetails(o: DomainObject): DomainObjectDetails {
if (!o || !getUnionValue(o)) {
if (!o || !getUnionValue(o)?.ref || !getUnionValue(o)?.data) {
return { id: null, label: '', description: '', type: '' };
}
const result = getDomainObjectValueDetailsFn(getUnionKey(o))(getUnionValue(o));

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { toMajorByExponent, toMinorByExponent } from '@vality/ng-core';
import { map, first } from 'rxjs/operators';
import { map, first, shareReplay } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
@ -8,27 +8,28 @@ import { DomainStoreService } from '@cc/app/api/domain-config';
providedIn: 'root',
})
export class AmountCurrencyService {
currencies$ = this.domainStoreService.getObjects('currency').pipe(
map((currencies) => new Map(currencies.map((c) => [c.ref.symbolic_code, c.data]))),
shareReplay(1),
);
constructor(private domainStoreService: DomainStoreService) {}
toMajor(amount: number, symbolicCode: string) {
return this.getCurrency(symbolicCode).pipe(
first(),
map((currency) => toMajorByExponent(amount, currency?.data?.exponent)),
map((currency) => toMajorByExponent(amount, currency?.exponent)),
);
}
toMinor(amount: number, symbolicCode: string) {
return this.getCurrency(symbolicCode).pipe(
first(),
map((currency) => toMinorByExponent(amount, currency?.data?.exponent)),
map((currency) => toMinorByExponent(amount, currency?.exponent)),
);
}
getCurrency(symbolicCode: string) {
return this.domainStoreService
.getObjects('currency')
.pipe(
map((currencies) => currencies.find((c) => c.ref.symbolic_code === symbolicCode)),
);
return this.currencies$.pipe(map((currencies) => currencies.get(symbolicCode)));
}
}

View File

@ -37,7 +37,7 @@ export function createCurrencyColumn<T extends object>(
exponent: (d: T) =>
getPossiblyAsyncObservable(selectSymbolicCode(d)).pipe(
switchMap((code) => amountCurrencyService.getCurrency(code)),
map((c) => c?.data?.exponent),
map((c) => c?.exponent),
),
},
...params,

View File

@ -1,28 +1,70 @@
import { inject } from '@angular/core';
import { createColumn } from '@vality/ng-core';
import isNil from 'lodash-es/isNil';
import { of, concat, EMPTY } from 'rxjs';
import { map } from 'rxjs/operators';
import { getCurrencySymbol } from '@angular/common';
import { inject, LOCALE_ID } from '@angular/core';
import { createColumn, formatCurrency } from '@vality/ng-core';
import { groupBy, uniq } from 'lodash-es';
import { of, combineLatest } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { DomainStoreService } from '../../../api/domain-config';
import { AmountCurrencyService } from '../../services';
export const createCurrencyColumn = createColumn(
({ amount, code }: { amount: number; code: string }) => {
interface CurrencyValue {
amount: number;
code: string;
}
function formatCurrencyValue(value: CurrencyValue) {
const amountCurrencyService = inject(AmountCurrencyService);
if (isNil(amount)) {
const locale = inject(LOCALE_ID);
return amountCurrencyService.getCurrency(value.code).pipe(
map((currencyObj) =>
formatCurrency(value.amount, value.code, 'long', locale, currencyObj?.exponent),
),
startWith(
(value.amount === 0 ? '0' : '…') +
' ' +
getCurrencySymbol(value.code, 'narrow', locale),
),
);
}
function formatCurrencyValues(values: CurrencyValue[], separator = ' | ') {
return combineLatest(values.map(formatCurrencyValue)).pipe(map((v) => v.join(separator)));
}
export const createCurrencyColumn = createColumn(
(currencyValue: CurrencyValue | { values: CurrencyValue[]; isSum?: boolean }) => {
const isSum = 'isSum' in currencyValue ? currencyValue.isSum : false;
const currencyValues = ('values' in currencyValue ? currencyValue.values : [currencyValue])
.filter(Boolean)
.sort((a, b) => b.amount - a.amount);
if (!currencyValues?.length) {
return of(undefined);
}
return concat(
amount === 0 ? of(0) : EMPTY,
amountCurrencyService.getCurrency(code).pipe(map((c) => c?.data?.exponent)),
).pipe(
map((exponent) => ({
value: amount,
type: 'currency',
params: {
code,
exponent,
const currencyValuesByCode = groupBy(currencyValues, 'code');
let currencyValuesByCodeList = uniq(currencyValues.map((v) => v.code)).map(
(code) => currencyValuesByCode[code],
);
if (isSum) {
currencyValuesByCodeList = currencyValuesByCodeList.map((g) =>
g.reduce(
(sum, v) => {
sum[0].amount += v.amount;
return sum;
},
[{ code: g[0].code, amount: 0 }],
),
);
}
const domainStoreService = inject(DomainStoreService);
return combineLatest([
combineLatest(currencyValuesByCodeList.map((g) => formatCurrencyValues(g))),
domainStoreService.isLoading$,
]).pipe(
map(([currencyValueStrings, inProgress]) => ({
value: currencyValueStrings[0],
description: currencyValueStrings.slice(1).join('; '),
inProgress,
})),
);
},

View File

@ -1,7 +1,7 @@
import { inject } from '@angular/core';
import { Reference } from '@vality/domain-proto/internal/domain';
import { createColumn } from '@vality/ng-core';
import { getUnionKey, getUnionValue } from '@vality/ng-thrift';
import { getUnionValue, getUnionKey } from '@vality/ng-thrift';
import { map, startWith } from 'rxjs/operators';
import { DomainStoreService } from '../../../api/domain-config';
@ -12,12 +12,12 @@ import {
} from '../../components/thrift-api-crud';
export const createDomainObjectColumn = createColumn(({ ref }: { ref: Reference }) => {
const sourceObj = {
[getUnionKey(ref)]: { ref: getUnionValue(ref), data: {} },
};
return inject(DomainStoreService)
.getObject(ref)
.pipe(
startWith({
[getUnionKey(ref)]: { ref: getUnionValue(ref), data: {} },
}),
map((obj) => ({
value: getDomainObjectDetails(obj).label || '',
description: getDomainObjectDetails(obj).id || '',
@ -25,5 +25,13 @@ export const createDomainObjectColumn = createColumn(({ ref }: { ref: Reference
inject(SidenavInfoService).toggle(DomainObjectCardComponent, { ref });
},
})),
startWith({
value: getDomainObjectDetails(sourceObj).label || '',
description: getDomainObjectDetails(sourceObj).id || '',
click: () => {
inject(SidenavInfoService).toggle(DomainObjectCardComponent, { ref });
},
inProgress: true,
}),
);
});

View File

@ -13,18 +13,22 @@ export const createPartyColumn = createColumn(
? of(params.partyName)
: inject(PartiesStoreService)
.get(id)
.pipe(
map((party) => party.contact_info.registration_email),
startWith(''),
);
return partyName$.pipe(
map((partyName) => ({
value: partyName,
.pipe(map((party) => party.contact_info.registration_email));
const partyCell = {
description: id,
link: () => {
void inject(Router).navigate([`/party/${id}`]);
},
};
return partyName$.pipe(
map((partyName) => ({
...partyCell,
value: partyName,
})),
startWith({
...partyCell,
inProgress: true,
}),
);
},
{ header: 'Party' },

View File

@ -0,0 +1,18 @@
import { Predicate } from '@vality/domain-proto/domain';
import { createColumn } from '@vality/ng-core';
import { formatPredicate } from '../table';
export const createPredicateColumn = createColumn(
({ predicate }: { predicate: Predicate }) => {
const value = formatPredicate(predicate);
return {
value,
color: {
True: 'success',
False: 'warn',
}[value],
};
},
{ header: 'Predicate' },
);

View File

@ -1,7 +1,7 @@
import { inject } from '@angular/core';
import { createColumn } from '@vality/ng-core';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';
import { PartiesStoreService } from '../../../api/payment-processing';
import { ShopCardComponent } from '../../components/shop-card/shop-card.component';
@ -16,14 +16,21 @@ export const createShopColumn = createColumn(
.get(partyId)
.pipe(map((party) => party.shops.get(shopId).details.name));
const sidenavInfoService = inject(SidenavInfoService);
return shopName$.pipe(
map((shopName) => ({
value: shopName,
const shopCell = {
description: shopId,
click: () => {
sidenavInfoService.toggle(ShopCardComponent, { id: shopId, partyId });
},
};
return shopName$.pipe(
map((shopName) => ({
...shopCell,
value: shopName,
})),
startWith({
...shopCell,
inProgress: true,
}),
);
},
{ header: 'Shop' },

View File

@ -4,3 +4,4 @@ export * from './create-wallet-column';
export * from './create-contract-column';
export * from './create-domain-object-column';
export * from './create-currency-column';
export * from './create-predicate-column';