IMP-63: Support new wallet modifications and others (#231)

This commit is contained in:
Rinat Arsaev 2023-06-19 13:01:04 +04:00 committed by GitHub
parent 99a65e917b
commit 12628346bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 361 additions and 525 deletions

14
package-lock.json generated
View File

@ -30,7 +30,7 @@
"@vality/dominant-cache-proto": "2.0.1-99f38c9.0",
"@vality/fistful-proto": "2.0.1-4ff4ea3.0",
"@vality/magista-proto": "2.0.1-cf0eff8.0",
"@vality/ng-core": "0.7.1-pr-26-5b97f36.0",
"@vality/ng-core": "15.0.0",
"@vality/payout-manager-proto": "2.0.1-b079679.0",
"@vality/repairer-proto": "2.0.1-8f7973d.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",
@ -6602,9 +6602,9 @@
"integrity": "sha512-59ncaJpt7tXFLOq9KrDu4OgrDQr9vTQ3j30T0hjN+ZIsPBsE+lld/pGKASWLLQfwvTtvp9laAuKgQGX9GuvIiQ=="
},
"node_modules/@vality/ng-core": {
"version": "0.7.1-pr-26-5b97f36.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-0.7.1-pr-26-5b97f36.0.tgz",
"integrity": "sha512-d95e80Y6yrPSGmfqVQWmREPEH8TVEDqaobphcroa9a+FHzjexh14+mDh3jxcgYlsuLMI4j1+yBXMDwTqCWXMcw==",
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-15.0.0.tgz",
"integrity": "sha512-7ux9yH80Bu7QnONFzmY8BSlxCZojvervVuecZdLAhvCo0/W6/NysOjzt3skSsi4S9I0vEtfaJ2kYpFqfqLjuJA==",
"dependencies": {
"@ng-matero/extensions": "^15.0.0",
"@s-libs/js-core": "^15.2.0",
@ -27589,9 +27589,9 @@
"integrity": "sha512-59ncaJpt7tXFLOq9KrDu4OgrDQr9vTQ3j30T0hjN+ZIsPBsE+lld/pGKASWLLQfwvTtvp9laAuKgQGX9GuvIiQ=="
},
"@vality/ng-core": {
"version": "0.7.1-pr-26-5b97f36.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-0.7.1-pr-26-5b97f36.0.tgz",
"integrity": "sha512-d95e80Y6yrPSGmfqVQWmREPEH8TVEDqaobphcroa9a+FHzjexh14+mDh3jxcgYlsuLMI4j1+yBXMDwTqCWXMcw==",
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-15.0.0.tgz",
"integrity": "sha512-7ux9yH80Bu7QnONFzmY8BSlxCZojvervVuecZdLAhvCo0/W6/NysOjzt3skSsi4S9I0vEtfaJ2kYpFqfqLjuJA==",
"requires": {
"@ng-matero/extensions": "^15.0.0",
"@s-libs/js-core": "^15.2.0",

View File

@ -39,7 +39,7 @@
"@vality/dominant-cache-proto": "2.0.1-99f38c9.0",
"@vality/fistful-proto": "2.0.1-4ff4ea3.0",
"@vality/magista-proto": "2.0.1-cf0eff8.0",
"@vality/ng-core": "0.7.1-pr-26-5b97f36.0",
"@vality/ng-core": "15.0.0",
"@vality/payout-manager-proto": "2.0.1-b079679.0",
"@vality/repairer-proto": "2.0.1-8f7973d.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",

View File

@ -1,8 +1,8 @@
import { ClaimStatus as ClaimStatusUnion } from '@vality/domain-proto/claim_management';
import { ClaimStatus } from '@vality/domain-proto/claim_management';
import { enumerate } from '@cc/utils';
export const CLAIM_STATUSES = enumerate<keyof ClaimStatusUnion>()(
export const CLAIM_STATUSES = enumerate<keyof ClaimStatus>()(
'pending',
'review',
'pending_acceptance',
@ -10,13 +10,3 @@ export const CLAIM_STATUSES = enumerate<keyof ClaimStatusUnion>()(
'denied',
'revoked'
);
/** @deprecated use CLAIM_STATUS - it checks for the occurrence of all elements */
export enum ClaimStatus {
Pending = 'pending',
Review = 'review',
Denied = 'denied',
Revoked = 'revoked',
Accepted = 'accepted',
PendingAcceptance = 'pending_acceptance',
}

View File

@ -3,12 +3,12 @@ import { KeycloakService } from 'keycloak-angular';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { ROUTING_CONFIG as CLAIMS_ROUTING_CONFIG } from './sections/claims/routing-config';
import { ROUTING_CONFIG as DEPOSITS_ROUTING_CONFIG } from './sections/deposits/routing-config';
import { ROUTING_CONFIG as DOMAIN_ROUTING_CONFIG } from './sections/domain/routing-config';
import { ROUTING_CONFIG as PAYMENTS_ROUTING_CONFIG } from './sections/payments/routing-config';
import { ROUTING_CONFIG as PAYOUTS_ROUTING_CONFIG } from './sections/payouts/payouts/routing-config';
import { ROUTING_CONFIG as REPAIRING_ROUTING_CONFIG } from './sections/repairing/routing-config';
import { ROUTING_CONFIG as CLAIMS_ROUTING_CONFIG } from './sections/search-claims/routing-config';
import { ROUTING_CONFIG as PARTIES_ROUTING_CONFIG } from './sections/search-parties/routing-config';
import { ROUTING_CONFIG as SOURCES_ROUTING_CONFIG } from './sections/sources/routing-config';
import { ROUTING_CONFIG as WALLETS_ROUTING_CONFIG } from './sections/wallets/routing-config';

View File

@ -28,8 +28,8 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import icons from './icons.json';
import { ClaimsModule } from './sections/claims/claims.module';
import { PayoutsModule } from './sections/payouts';
import { SearchClaimsModule } from './sections/search-claims/search-claims.module';
import { SearchPartiesModule } from './sections/search-parties/search-parties.module';
import { SectionsModule } from './sections/sections.module';
import {
@ -68,7 +68,7 @@ export let AppInjector: Injector;
MatSidenavModule,
MatListModule,
SearchPartiesModule,
SearchClaimsModule,
ClaimsModule,
KeycloakTokenInfoModule,
PayoutsModule,
SectionsModule,

View File

@ -1,20 +1,26 @@
<cc-page-layout *ngIf="claim$ | async as claim" description="#{{ claim.id }}" title="Claim">
<cc-page-layout-actions>
<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-actions *ngIf="claim$ | async as claim">
<cc-status [color]="statusColor[claim.status | ccUnionKey]">{{
claim.status | ccUnionKey | keyTitle | titlecase
}}</cc-status>
</cc-page-layout-actions>
<div gdColumns="1fr" gdGap="16px">
<div *ngIf="party$ | async as party" fxLayoutAlign="space-between center">
<span class="mat-h3 mat-secondary-text">
<a class="mat-secondary-text" routerLink="/claims">Claims</a> / Claim details
</span>
<span class="mat-h3 mat-secondary-text">
{{ party.contact_info.email }} (#{{ party.id }})
</span>
</div>
</div>
<div *ngIf="claim$ | async as claim" gdColumns="1fr" gdGap="24px">
<h2 class="mat-headline-5">Changeset</h2>
<div *ngIf="isLoading$ | async; else timeline" fxLayoutAlign="center">
@ -78,6 +84,13 @@
(claimChanged)="reloadClaim()"
></cc-modification-unit-timeline-item>
</ng-container>
<cc-modification-unit-timeline-item
*ngSwitchDefault
[claim]="claim"
[modificationUnit]="modificationUnit"
isChangeable
(claimChanged)="reloadClaim()"
></cc-modification-unit-timeline-item>
</ng-container>
</cc-timeline>
</ng-template>

View File

@ -1,22 +1,18 @@
import { Modification } from '@vality/domain-proto/claim_management';
import isObject from 'lodash-es/isObject';
import { getUnionKey } from '../../../../utils';
import { MODIFICATIONS_NAME_TREE } from './types/modifications-name-tree';
export function getModificationName(modification: Modification) {
let value: unknown = modification;
let name: unknown = MODIFICATIONS_NAME_TREE;
while (value) {
if (typeof value === 'object') {
const key = Object.keys(value).find((k) => Object.keys(name).includes(k));
value = value[key];
name = name[key];
}
if (!name) {
console.error('Unknown modification:', modification);
return 'Unknown Modification';
}
if (typeof name === 'string') {
return name + ' Modification';
}
let currentValue: unknown = modification;
let currentName: unknown = MODIFICATIONS_NAME_TREE;
while (isObject(currentName) && isObject(currentValue)) {
const key = Object.keys(currentValue).find((k) => Object.keys(currentName).includes(k));
currentValue = currentValue?.[key];
currentName = currentName?.[key];
}
return typeof currentName === 'string'
? `${currentName} Modification`
: getUnionKey(modification);
}

View File

@ -1,85 +1,8 @@
import {
CashRegisterModificationUnit,
ClaimModification,
CommentModificationUnit,
ContractAdjustmentModificationUnit,
ContractModificationUnit,
ContractorModificationUnit,
DocumentModificationUnit,
FileModificationUnit,
Modification,
PartyModification,
PayoutToolModificationUnit,
ShopModificationUnit,
StatusModificationUnit,
WalletModificationUnit,
WalletModification,
IdentityModification,
} from '@vality/domain-proto/claim_management';
import { Overwrite } from 'utility-types';
import { Modification } from '@vality/domain-proto/claim_management';
type OverwriteAll<T extends object, U extends { [N in keyof T]-?: unknown }> = U;
type ModificationsNameTree<T> = { [N in keyof T]?: ModificationsNameTree<T[N]> | string };
type ModificationsName<T extends object> = {
[P in keyof T]-?: string;
};
type ModificationUnitsName<
T extends { modification: object },
O extends { [N in keyof T['modification']]?: unknown } = Record<never, never>
> = {
modification: Overwrite<ModificationsName<T['modification']>, O>;
};
type ModificationsNameTree = OverwriteAll<
Modification,
{
claim_modification: OverwriteAll<
ClaimModification,
{
document_modification: ModificationUnitsName<DocumentModificationUnit>;
file_modification: ModificationUnitsName<FileModificationUnit>;
comment_modification: ModificationUnitsName<CommentModificationUnit>;
status_modification: ModificationUnitsName<StatusModificationUnit>;
external_info_modification: string;
}
>;
party_modification: OverwriteAll<
PartyModification,
{
contractor_modification: ModificationUnitsName<ContractorModificationUnit>;
contract_modification: ModificationUnitsName<
ContractModificationUnit,
{
adjustment_modification: ModificationUnitsName<ContractAdjustmentModificationUnit>;
payout_tool_modification: ModificationUnitsName<PayoutToolModificationUnit>;
}
>;
shop_modification: ModificationUnitsName<
ShopModificationUnit,
{
cash_register_modification_unit: ModificationUnitsName<CashRegisterModificationUnit>;
}
>;
wallet_modification: ModificationUnitsName<WalletModificationUnit>;
}
>;
identity_modification: OverwriteAll<
IdentityModification,
{
creation: string;
}
>;
wallet_modification: OverwriteAll<
WalletModification,
{
creation: string;
account_creation: string;
}
>;
}
>;
export const MODIFICATIONS_NAME_TREE: ModificationsNameTree = {
export const MODIFICATIONS_NAME_TREE: ModificationsNameTree<Modification> = {
claim_modification: {
document_modification: {
modification: {
@ -154,16 +77,19 @@ export const MODIFICATIONS_NAME_TREE: ModificationsNameTree = {
},
wallet_modification: {
modification: {
creation: 'Wallet Creation',
account_creation: 'Wallet Account Creation',
creation: 'Old Wallet Creation',
account_creation: 'Old Wallet Account Creation',
},
},
},
identity_modification: {
modification: {
creation: 'Identity Creation',
},
},
wallet_modification: {
modification: {
creation: 'Wallet Creation',
account_creation: 'Wallet Account Creation',
},
},
};

View File

@ -3,15 +3,15 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { ClaimsComponent } from './claims.component';
import { ROUTING_CONFIG } from './routing-config';
import { SearchClaimsComponent } from './search-claims.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'claims',
component: SearchClaimsComponent,
component: ClaimsComponent,
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
},
@ -19,4 +19,4 @@ import { SearchClaimsComponent } from './search-claims.component';
],
exports: [RouterModule],
})
export class SearchClaimsComponentRouting {}
export class ClaimsComponentRouting {}

View File

@ -0,0 +1,10 @@
<v-table
[columns]="columns"
[data]="data"
[hasMore]="hasMore"
[progress]="isLoading"
(more)="more.emit()"
(update)="update.emit($event)"
>
<v-table-actions><ng-content></ng-content></v-table-actions>
</v-table>

View File

@ -0,0 +1,72 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { Claim, ClaimStatus } from '@vality/domain-proto/claim_management';
import { Column, LoadOptions, TagColumn } from '@vality/ng-core';
import startCase from 'lodash-es/startCase';
import { map } from 'rxjs/operators';
import { getUnionKey } from '../../../../utils';
import { PartiesStoreService } from '../../../api/payment-processing';
@Component({
selector: 'cc-claims-table',
templateUrl: './claims-table.component.html',
styleUrls: ['./claims-table.component.scss'],
})
export class ClaimsTableComponent {
@Input() data!: Claim[];
@Input() isLoading?: boolean | null;
@Input() hasMore?: boolean | null;
@Output() update = new EventEmitter<LoadOptions>();
@Output() more = new EventEmitter<void>();
columns: Column<Claim>[] = [
{ field: 'id', pinned: 'left' },
{
field: 'party',
description: 'party_id',
formatter: (claim) =>
this.partiesStoreService.get(claim.party_id).pipe(map((p) => p.contact_info.email)),
},
{
field: 'status',
type: 'tag',
formatter: (claim) => getUnionKey(claim.status),
typeParameters: {
label: (claim) => startCase(getUnionKey(claim.status)),
tags: {
pending: { color: 'pending' },
review: { color: 'pending' },
pending_acceptance: { color: 'pending' },
accepted: { color: 'success' },
denied: { color: 'warn' },
},
},
} as TagColumn<Claim, keyof ClaimStatus>,
'revision',
{ field: 'created_at', type: 'datetime' },
{ field: 'updated_at', type: 'datetime' },
{
field: 'operation',
header: '',
type: 'menu',
pinned: 'right',
width: '0',
typeParameters: {
items: [
{
label: 'Details',
click: (claim) => this.navigateToClaim(claim.party_id, claim.id),
},
],
},
},
];
constructor(private router: Router, private partiesStoreService: PartiesStoreService) {}
navigateToClaim(partyId: string, claimID: number) {
void this.router.navigate([`/party/${partyId}/claim/${claimID}`]);
}
}

View File

@ -0,0 +1,35 @@
<cc-page-layout title="Claims">
<cc-page-layout-actions
><v-more-filters-button [filters]="filters"></v-more-filters-button
></cc-page-layout-actions>
<v-filters #filters [active]="active">
<ng-template [formGroup]="filtersForm">
<mat-form-field>
<input
autocomplete="off"
formControlName="claim_id"
matInput
placeholder="Claim ID"
type="number"
/>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="statuses" multiple placeholder="Claim statuses">
<mat-option *ngFor="let status of claimStatuses" [value]="status">{{
status | keyTitle | titlecase
}}</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"
(more)="more()"
(update)="load($event)"
>
<button color="primary" mat-raised-button (click)="create()">Create</button>
</cc-claims-table>
</cc-page-layout>

View File

@ -0,0 +1,64 @@
import { Component, OnInit } from '@angular/core';
import { NonNullableFormBuilder } from '@angular/forms';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
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 { CLAIM_STATUSES } from '../../api/claim-management';
import { CreateClaimDialogComponent } from './components/create-claim-dialog/create-claim-dialog.component';
import { FetchClaimsService } from './fetch-claims.service';
@UntilDestroy()
@Component({
templateUrl: './claims.component.html',
})
export class ClaimsComponent implements OnInit {
isLoading$ = this.fetchClaimsService.isLoading$;
claims$ = this.fetchClaimsService.result$;
hasMore$ = this.fetchClaimsService.hasMore$;
claimStatuses = CLAIM_STATUSES;
filtersForm = this.fb.group({
party_id: undefined as string,
claim_id: undefined as number,
statuses: [[] as string[]],
});
active = 0;
private selectedPartyId: PartyID;
constructor(
private fetchClaimsService: FetchClaimsService,
private dialogService: DialogService,
private fb: NonNullableFormBuilder,
private qp: QueryParamsService<ClaimsComponent['filtersForm']['value']>
) {}
ngOnInit(): void {
this.filtersForm.patchValue(this.qp.params);
this.filtersForm.valueChanges
.pipe(startWith(null), debounceTime(500), untilDestroyed(this))
.subscribe(() => {
this.load();
});
}
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;
}
more(): void {
this.fetchClaimsService.more();
}
create() {
this.dialogService.open(CreateClaimDialogComponent, { partyId: this.selectedPartyId });
}
}

View File

@ -14,25 +14,22 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { ActionsModule, DialogModule } from '@vality/ng-core';
import { ActionsModule, DialogModule, TableModule, FiltersModule } from '@vality/ng-core';
import { ClaimSearchFormModule, PageLayoutModule } from '@cc/app/shared/components';
import { PageLayoutModule } from '@cc/app/shared/components';
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
import { ApiModelPipesModule, ThriftPipesModule } from '@cc/app/shared/pipes';
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
import { TableModule } from '@cc/components/table';
import { ClaimsComponentRouting } from './claims-routing.module';
import { ClaimsTableComponent } from './claims-table/claims-table.component';
import { ClaimsComponent } from './claims.component';
import { CreateClaimDialogComponent } from './components/create-claim-dialog/create-claim-dialog.component';
import { SearchClaimsComponentRouting } from './search-claims-routing.module';
import { SearchClaimsComponent } from './search-claims.component';
import { SearchClaimsService } from './search-claims.service';
import { ClaimMailPipePipe } from './search-table/claim-mail-pipe.pipe';
import { SearchTableComponent } from './search-table/search-table.component';
@NgModule({
imports: [
CommonModule,
SearchClaimsComponentRouting,
ClaimsComponentRouting,
MatButtonModule,
MatCardModule,
MatDialogModule,
@ -47,7 +44,6 @@ import { SearchTableComponent } from './search-table/search-table.component';
ReactiveFormsModule,
FlexLayoutModule,
MatExpansionModule,
ClaimSearchFormModule,
EmptySearchResultModule,
ApiModelPipesModule,
ThriftPipesModule,
@ -56,13 +52,9 @@ import { SearchTableComponent } from './search-table/search-table.component';
DialogModule,
MerchantFieldModule,
PageLayoutModule,
TableModule,
FiltersModule,
],
declarations: [
SearchClaimsComponent,
SearchTableComponent,
ClaimMailPipePipe,
CreateClaimDialogComponent,
],
providers: [SearchClaimsService],
declarations: [ClaimsComponent, ClaimsTableComponent, CreateClaimDialogComponent],
})
export class SearchClaimsModule {}
export class ClaimsModule {}

View File

@ -1,37 +1,42 @@
import { Injectable } from '@angular/core';
import { Claim, ClaimSearchQuery } from '@vality/domain-proto/claim_management';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FetchSuperclass, FetchResult, FetchOptions, NotifyLogService } from '@vality/ng-core';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { ClaimManagementService } from '@cc/app/api/claim-management';
import { FetchResult, PartialFetcher } from '@cc/app/shared/services';
@Injectable()
export class SearchClaimsService extends PartialFetcher<
@Injectable({ providedIn: 'root' })
export class FetchClaimsService extends FetchSuperclass<
Claim,
Omit<ClaimSearchQuery, 'continuation_token' | 'limit'>
> {
private readonly searchLimit = 10;
constructor(private claimManagementService: ClaimManagementService) {
constructor(
private claimManagementService: ClaimManagementService,
private log: NotifyLogService
) {
super();
}
protected fetch(
params: Omit<ClaimSearchQuery, 'continuation_token' | 'limit'>,
continuationToken: string
{ size, continuationToken }: FetchOptions
): Observable<FetchResult<Claim>> {
return this.claimManagementService
.SearchClaims({
...params,
continuation_token: continuationToken,
limit: this.searchLimit,
} as ClaimSearchQuery)
limit: size,
})
.pipe(
map((r) => ({
result: r.result,
continuationToken: r.continuation_token,
}))
})),
catchError((err) => {
this.log.errorOperation(err, 'receive', 'claims');
return of({ result: [] });
})
);
}
}

View File

@ -48,14 +48,13 @@ export class PaymentsTableComponent {
{
field: 'status',
type: 'tag',
formatter: (data) => startCase(getUnionKey(data.status)),
formatter: (data) => getUnionKey(data.status),
typeParameters: {
value: (data) => getUnionKey(data.status),
label: (data) => startCase(getUnionKey(data.status)),
tags: {
pending: { color: 'pending' },
processed: { color: 'pending' },
captured: { color: 'success' },
cancelled: {},
refunded: { color: 'success' },
failed: { color: 'warn' },
charged_back: { color: 'success' },
@ -86,7 +85,7 @@ export class PaymentsTableComponent {
field: 'menu',
header: '',
pinned: 'right',
maxWidth: 0,
width: '0',
type: 'menu',
typeParameters: {
items: [

View File

@ -1,25 +0,0 @@
<cc-page-layout title="Claims">
<cc-page-layout-actions>
<button color="primary" mat-raised-button (click)="create()">Create</button>
</cc-page-layout-actions>
<mat-card>
<mat-card-content>
<cc-claim-search-form (valueChanges)="search($event)"></cc-claim-search-form>
</mat-card-content>
<mat-card-footer *ngIf="doAction$ | async">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-footer>
</mat-card>
<ng-container *ngIf="claims$ | async as claims">
<cc-empty-search-result *ngIf="claims.length === 0"></cc-empty-search-result>
<mat-card *ngIf="claims.length > 0" fxLayout="column" fxLayoutGap="18px">
<cc-search-table [claims]="claims"></cc-search-table>
<cc-show-more-button
*ngIf="hasMore$ | async"
[inProgress]="doAction$ | async"
(click)="fetchMore()"
>
</cc-show-more-button>
</mat-card>
</ng-container>
</cc-page-layout>

View File

@ -1,46 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PartyID } from '@vality/domain-proto/domain';
import { DialogService, cleanPrimitiveProps, clean } from '@vality/ng-core';
import { ClaimSearchForm } from '@cc/app/shared/components';
import { CreateClaimDialogComponent } from './components/create-claim-dialog/create-claim-dialog.component';
import { SearchClaimsService } from './search-claims.service';
@Component({
templateUrl: './search-claims.component.html',
})
export class SearchClaimsComponent implements OnInit {
doAction$ = this.searchClaimService.doAction$;
claims$ = this.searchClaimService.searchResult$;
hasMore$ = this.searchClaimService.hasMore$;
private selectedPartyId: PartyID;
constructor(
private searchClaimService: SearchClaimsService,
private snackBar: MatSnackBar,
private dialogService: DialogService
) {}
ngOnInit(): void {
this.searchClaimService.errors$.subscribe((e) =>
this.snackBar.open(`An error occurred while search claims (${String(e)})`, 'OK')
);
}
search(v: ClaimSearchForm): void {
this.selectedPartyId = v?.party_id;
this.searchClaimService.search(
cleanPrimitiveProps({ ...v, statuses: clean(v.statuses?.map((s) => ({ [s]: {} }))) })
);
}
fetchMore(): void {
this.searchClaimService.fetchMore();
}
create() {
this.dialogService.open(CreateClaimDialogComponent, { partyId: this.selectedPartyId });
}
}

View File

@ -1,19 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Claim } from '@vality/domain-proto/claim_management';
@Pipe({
name: 'ccClaimMail',
})
export class ClaimMailPipePipe implements PipeTransform {
transform(value: Claim): string {
let res = 'Unknown';
const changeSet = value.changeset;
if (changeSet.length > 0) {
const modificationUnit = changeSet[0];
res = modificationUnit.user_info.email;
}
return res;
}
}

View File

@ -1,52 +0,0 @@
<table [dataSource]="claims" mat-table>
<ng-container matColumnDef="claimID">
<th *matHeaderCellDef fxHide.sm fxHide.xs mat-header-cell>Claim ID</th>
<td *matCellDef="let claim" fxHide.sm fxHide.xs mat-cell>{{ claim.id }}</td>
</ng-container>
<ng-container matColumnDef="party">
<th *matHeaderCellDef mat-header-cell>Party</th>
<td *matCellDef="let claim" mat-cell>
<div>{{ claim | ccClaimMail }}</div>
<div class="mat-caption party-id">{{ claim.party_id }}</div>
</td>
</ng-container>
<ng-container matColumnDef="status">
<th *matHeaderCellDef mat-header-cell>Status</th>
<td *matCellDef="let claim" mat-cell>
{{ claim.status | ccClaimStatusThrift | ccClaimStatus }}
</td>
</ng-container>
<ng-container matColumnDef="revision">
<th *matHeaderCellDef mat-header-cell>Revision</th>
<td *matCellDef="let claim" mat-cell>
{{ claim.revision }}
</td>
</ng-container>
<ng-container matColumnDef="updatedAt">
<th *matHeaderCellDef fxHide.sm fxHide.xs mat-header-cell>Updated At</th>
<td *matCellDef="let claim" fxHide.sm fxHide.xs mat-cell>
{{ claim.updated_at | date : 'dd.MM.yyyy HH:mm:ss' }}
</td>
</ng-container>
<ng-container matColumnDef="createdAt">
<th *matHeaderCellDef fxHide.sm fxHide.xs mat-header-cell>Created At</th>
<td *matCellDef="let claim" fxHide.sm fxHide.xs mat-cell>
{{ claim.created_at | date : 'dd.MM.yyyy HH:mm:ss' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="action-cell" mat-header-cell></th>
<td *matCellDef="let claim" class="action-cell" mat-cell>
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="navigateToClaim(claim.party_id, claim.id)">
Details
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let claim; columns: displayedColumns" mat-row></tr>
</table>

View File

@ -1,29 +0,0 @@
import { Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { Claim } from '@vality/domain-proto/claim_management';
@Component({
selector: 'cc-search-table',
templateUrl: './search-table.component.html',
styleUrls: ['./search-table.component.scss'],
})
export class SearchTableComponent {
@Input()
claims: Claim[];
displayedColumns = [
'claimID',
'party',
'status',
'revision',
'updatedAt',
'createdAt',
'actions',
];
constructor(private router: Router) {}
navigateToClaim(partyId: string, claimID: number) {
void this.router.navigate([`/party/${partyId}/claim/${claimID}`]);
}
}

View File

@ -1,10 +1,9 @@
<v-dialog title="Create Source">
<cc-metadata-form
<cc-fistful-thrift-form
[formControl]="control"
[metadata]="metadata$ | async"
namespace="source"
type="SourceParams"
></cc-metadata-form>
></cc-fistful-thrift-form>
<v-dialog-actions>
<button [disabled]="control.invalid" color="primary" mat-raised-button (click)="create()">
Create

View File

@ -1,8 +1,6 @@
import { Component, Injector } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ThriftAstMetadata } from '@vality/fistful-proto';
import { DialogSuperclass } from '@vality/ng-core';
import { from } from 'rxjs';
import { FistfulAdminService } from '../../../api/fistful-admin';
import { NotificationService } from '../../../shared/services/notification';
@ -14,9 +12,6 @@ import { NotificationErrorService } from '../../../shared/services/notification-
})
export class CreateSourceComponent extends DialogSuperclass<void> {
control = new FormControl();
metadata$ = from(
import('@vality/fistful-proto/metadata.json').then((m) => m.default as ThriftAstMetadata[])
);
constructor(
injector: Injector,

View File

@ -10,6 +10,7 @@ import { ActionsModule, DialogModule, TableModule } from '@vality/ng-core';
import { EmptySearchResultModule } from '../../../components/empty-search-result';
import { PageLayoutModule } from '../../shared';
import { FistfulThriftFormComponent } from '../../shared/components/fistful-thrift-form';
import { MetadataFormModule } from '../../shared/components/metadata-form';
import { CreateSourceComponent } from './create-source/create-source.component';
import { SourcesRoutingModule } from './sources-routing.module';
@ -31,6 +32,7 @@ import { SourcesComponent } from './sources.component';
MetadataFormModule,
ReactiveFormsModule,
PageLayoutModule,
FistfulThriftFormComponent,
],
declarations: [SourcesComponent, CreateSourceComponent],
})

View File

@ -1,23 +0,0 @@
<form [formGroup]="form" fxLayout="row" fxLayout.xs="column" fxLayoutGap="16px">
<mat-form-field fxFlex>
<mat-select formControlName="statuses" multiple placeholder="Claim statuses">
<mat-option *ngFor="let status of claimStatuses" [value]="status">{{
status | ccClaimStatus
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<input
autocomplete="off"
formControlName="claim_id"
matInput
placeholder="Claim ID"
type="number"
/>
</mat-form-field>
<cc-merchant-field
*ngIf="!hideMerchantSearch"
formControlName="party_id"
fxFlex
></cc-merchant-field>
</form>

View File

@ -1,57 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { coerceBoolean } from 'coerce-property';
import { debounceTime, map, take } from 'rxjs/operators';
import { CLAIM_STATUSES } from '@cc/app/api/claim-management';
import { removeEmptyProperties } from '@cc/utils/remove-empty-properties';
import { ClaimSearchForm } from './claim-search-form';
import { queryParamsToFormValue } from './query-params-to-form-value';
@UntilDestroy()
@Component({
selector: 'cc-claim-search-form',
templateUrl: 'claim-search-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClaimSearchFormComponent implements OnInit {
@Input() @coerceBoolean hideMerchantSearch = false;
@Output() valueChanges = new EventEmitter<ClaimSearchForm>();
form = this.fb.group<ClaimSearchForm>({
statuses: null,
claim_id: null,
party_id: null,
});
claimStatuses = CLAIM_STATUSES;
constructor(private route: ActivatedRoute, private router: Router, private fb: FormBuilder) {}
ngOnInit(): void {
this.form.valueChanges
.pipe(debounceTime(600), map(removeEmptyProperties), untilDestroyed(this))
.subscribe((value) => {
void this.router.navigate([location.pathname], { queryParams: value });
this.valueChanges.emit(value as never);
});
this.route.queryParams
.pipe(
take(1),
map(queryParamsToFormValue),
map(removeEmptyProperties),
untilDestroyed(this)
)
.subscribe((v) => this.form.patchValue(v));
}
}

View File

@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
import { ApiModelPipesModule } from '../../pipes';
import { ClaimSearchFormComponent } from './claim-search-form.component';
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
MatInputModule,
MatSelectModule,
ReactiveFormsModule,
ApiModelPipesModule,
MerchantFieldModule,
],
declarations: [ClaimSearchFormComponent],
exports: [ClaimSearchFormComponent],
})
export class ClaimSearchFormModule {}

View File

@ -1,8 +0,0 @@
import { ClaimID, ClaimStatus } from '@vality/domain-proto/claim_management';
import { PartyID } from '@vality/domain-proto/domain';
export interface ClaimSearchForm {
claim_id: ClaimID;
statuses: (keyof ClaimStatus)[];
party_id: PartyID;
}

View File

@ -1,3 +0,0 @@
export * from './claim-search-form.module';
export * from './claim-search-form.component';
export * from './claim-search-form';

View File

@ -1,13 +0,0 @@
import { Params } from '@angular/router';
import pickBy from 'lodash-es/pickBy';
import { wrapValuesToArray } from '@cc/utils/wrap-values-to-array';
const statusesAndPrimitives = (v, k) =>
k === 'statuses' && (typeof v === 'string' || typeof v === 'number');
export const queryParamsToFormValue = (params: Params) => ({
...params,
// Query param ?statuses=accepted will be present as { statuses: 'accepted' } in form. Selector value must be an array in multiple-selection mode.
...wrapValuesToArray(pickBy(params, statusesAndPrimitives)),
});

View File

@ -0,0 +1,7 @@
<cc-metadata-form
[extensions]="extensions"
[formControl]="control"
[metadata]="metadata$ | async"
[namespace]="namespace"
[type]="type"
></cc-metadata-form>

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { ThriftAstMetadata } from '@vality/fistful-proto';
import { FormControlSuperclass, createControlProviders } from '@vality/ng-core';
import { from, of } from 'rxjs';
import short from 'short-uuid';
import { MetadataFormModule, isTypeWithAliases, MetadataFormExtension } from '../metadata-form';
@Component({
standalone: true,
selector: 'cc-fistful-thrift-form',
templateUrl: './fistful-thrift-form.component.html',
providers: createControlProviders(() => FistfulThriftFormComponent),
imports: [CommonModule, ReactiveFormsModule, MetadataFormModule],
})
export class FistfulThriftFormComponent extends FormControlSuperclass<unknown> {
@Input() namespace: string;
@Input() type: string;
metadata$ = from(
import('@vality/fistful-proto/metadata.json').then((m) => m.default as ThriftAstMetadata[])
);
extensions: MetadataFormExtension[] = [
{
determinant: (data) => of(isTypeWithAliases(data, 'SourceID', 'fistful')),
extension: () => of({ generate: () => of(short().uuid()), isIdentifier: true }),
},
];
}

View File

@ -0,0 +1 @@
export * from './fistful-thrift-form.component';

View File

@ -1,4 +1,3 @@
export * from './claim-search-form';
export * from './party-modification-creator';
export * from './party-modification-forms';
export * from './status';

View File

@ -3,9 +3,11 @@
class="mat-headline-4 mat-no-margin"
fxLayout="row"
fxLayoutAlign="start center"
fxLayoutGap="8px"
fxLayoutGap="4px"
>
<mat-icon *ngIf="isBackAvailable" class="back" (click)="back()">arrow_back</mat-icon>
<button *ngIf="isBackAvailable" mat-icon-button (click)="back()">
<mat-icon>arrow_back</mat-icon>
</button>
<div>
{{ title }}
<span class="mat-secondary-text">{{ description }}</span>
@ -15,6 +17,22 @@
<ng-content select="cc-page-layout-actions"></ng-content>
</div>
</div>
<div *ngIf="path?.length" class="mat-h3 mat-secondary-text" fxLayout fxLayoutGap="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" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
</div>

View File

@ -1,5 +1,6 @@
import { Location } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Params } from '@angular/router';
@Component({
selector: 'cc-page-layout',
@ -11,9 +12,16 @@ export class PageLayoutComponent {
@Input() title!: string;
@Input() description?: string;
@Input() progress?: boolean;
@Input() path?: {
label: string;
link?: unknown[] | string | null | undefined;
queryParams?: Params | null;
tooltip?: string;
}[];
// 1 and 2 is default history length
isBackAvailable = window.history.length > 2;
isBackAvailable =
window.history.length > 2 && window.location.pathname.split('/').slice(1).length > 1;
constructor(private location: Location) {}

View File

@ -1,14 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { FlexModule, GridModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { PageLayoutActionsComponent } from './components/page-layout-actions/page-layout-actions.component';
import { PageLayoutComponent } from './page-layout.component';
@NgModule({
imports: [CommonModule, FlexModule, MatIconModule, MatProgressSpinnerModule],
imports: [
CommonModule,
FlexModule,
MatIconModule,
MatProgressSpinnerModule,
GridModule,
RouterLink,
MatButtonModule,
MatTooltipModule,
],
declarations: [PageLayoutComponent, PageLayoutActionsComponent],
exports: [PageLayoutComponent, PageLayoutActionsComponent],
})

View File

@ -1,13 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ClaimStatus as UnionClaimStatus } from '@vality/domain-proto/claim_management';
import { extractClaimStatus } from '../../utils';
@Pipe({
name: 'ccClaimStatusThrift',
})
export class ClaimStatusThriftPipe implements PipeTransform {
transform(value: UnionClaimStatus): string {
return extractClaimStatus(value);
}
}

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { ClaimStatusThriftPipe } from './claim-status-thrift.pipe';
import { KeyTitlePipe } from './key-title.pipe';
import { MapUnionPipe } from './map-union.pipe';
import { ThriftInt64Pipe } from './thrift-int64.pipe';
@ -9,7 +8,6 @@ import { UnionKeyPipe } from './union-key.pipe';
import { UnionValuePipe } from './union-value.pipe';
const PIPES = [
ClaimStatusThriftPipe,
MapUnionPipe,
ThriftInt64Pipe,
ThriftViewPipe,

View File

@ -1,18 +0,0 @@
import { ClaimStatus as UnionClaimStatus } from '@vality/domain-proto/claim_management';
import { ClaimStatus } from '@cc/app/api/claim-management';
import { getUnionKey } from '@cc/utils/get-union-key';
export const CLAIM_STATUS_BY_UNION_CLAIM_STATUS: {
[name in keyof UnionClaimStatus]-?: ClaimStatus;
} = {
accepted: ClaimStatus.Accepted,
denied: ClaimStatus.Denied,
revoked: ClaimStatus.Revoked,
pending: ClaimStatus.Pending,
review: ClaimStatus.Review,
pending_acceptance: ClaimStatus.PendingAcceptance,
};
export const extractClaimStatus = (status: UnionClaimStatus): ClaimStatus =>
CLAIM_STATUS_BY_UNION_CLAIM_STATUS[getUnionKey(status)];

View File

@ -1,4 +1,3 @@
export * from './extract-claim-status';
export * from './component-changes';
export * from './polling-conditions';
export * from './deposit-status';

View File

@ -1,16 +1,12 @@
@use '@angular/material' as mat;
@use '@vality/ng-core' as v;
@import './all-component-themes';
@include v.core();
$app-theme: v.create-light-theme();
@include v.core();
@include v.all-component-themes($app-theme);
@include all-component-themes($app-theme);
@include v.typography-hierarchy($app-theme);
@include v.app($app-theme);
.cc-code {