mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
IMP-23: Init payouts (#42)
This commit is contained in:
parent
eebb64595c
commit
bbfc9699bd
13
.idea/.gitignore
vendored
13
.idea/.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
**/*
|
||||
|
||||
!*.iml
|
||||
!jsLinters
|
||||
!inspectionProfiles
|
||||
!modules.xml
|
||||
!prettier.xml
|
||||
!vcs.xml
|
@ -3,6 +3,6 @@
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
<option name="myRunOnReformat" value="true" />
|
||||
<option name="myFilesPattern" value="{**/*,*}.{js,ts,jsx,tsx,json,yaml,yml}" />
|
||||
<option name="myFilesPattern" value="{**/*,*}.{js,ts,jsx,tsx,json,yaml,yml,html,scss,css}" />
|
||||
</component>
|
||||
</project>
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -28,6 +28,7 @@
|
||||
"@s-libs/micro-dash": "^13.1.0",
|
||||
"@s-libs/ng-core": "^13.1.0",
|
||||
"@s-libs/rxjs-core": "^13.1.0",
|
||||
"@vality/magista-proto": "^1.0.1-2ec22d8.0",
|
||||
"@vality/thrift-ts": "2.2.0-alpha",
|
||||
"@vality/woody": "^0.1.0",
|
||||
"angular-file": "3.0.1",
|
||||
@ -5266,6 +5267,11 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vality/magista-proto": {
|
||||
"version": "1.0.1-2ec22d8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/magista-proto/-/magista-proto-1.0.1-2ec22d8.0.tgz",
|
||||
"integrity": "sha512-+wQqTe3F3STDuGbuIg+GSEV/V+NFxXS5vbY+6STfUSG8a+dzq9Wgi+qMYgnNhDByH98utClmWJ/pxjPCpSm+Yg=="
|
||||
},
|
||||
"node_modules/@vality/thrift-ts": {
|
||||
"version": "2.2.0-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@vality/thrift-ts/-/thrift-ts-2.2.0-alpha.tgz",
|
||||
@ -26542,6 +26548,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vality/magista-proto": {
|
||||
"version": "1.0.1-2ec22d8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/magista-proto/-/magista-proto-1.0.1-2ec22d8.0.tgz",
|
||||
"integrity": "sha512-+wQqTe3F3STDuGbuIg+GSEV/V+NFxXS5vbY+6STfUSG8a+dzq9Wgi+qMYgnNhDByH98utClmWJ/pxjPCpSm+Yg=="
|
||||
},
|
||||
"@vality/thrift-ts": {
|
||||
"version": "2.2.0-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@vality/thrift-ts/-/thrift-ts-2.2.0-alpha.tgz",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"@s-libs/micro-dash": "^13.1.0",
|
||||
"@s-libs/ng-core": "^13.1.0",
|
||||
"@s-libs/rxjs-core": "^13.1.0",
|
||||
"@vality/magista-proto": "^1.0.1-2ec22d8.0",
|
||||
"@vality/thrift-ts": "2.2.0-alpha",
|
||||
"@vality/woody": "^0.1.0",
|
||||
"angular-file": "3.0.1",
|
||||
@ -115,4 +116,4 @@
|
||||
"ts-node": "~8.8.2",
|
||||
"typescript": "~4.5.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
const THRIFT_PROXY_CONFIG = {
|
||||
context: ['/v1', '/stat', '/fistful', '/papi', '/file_storage', '/deanonimus'],
|
||||
context: ['/v1', '/v3', '/stat', '/fistful', '/papi', '/file_storage', '/deanonimus'],
|
||||
target: '',
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
|
1
src/app/api/magista/index.ts
Normal file
1
src/app/api/magista/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './merchant-statistics.service';
|
16
src/app/api/magista/magista-instance-provider.service.ts
Normal file
16
src/app/api/magista/magista-instance-provider.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as context from '@vality/magista-proto/lib/magista/context.js';
|
||||
|
||||
import { ProviderSettings, ThriftInstanceProvider } from '../thrift-instance-provider';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MagistaInstanceProviderService extends ThriftInstanceProvider {
|
||||
protected getProviderSettings(): ProviderSettings {
|
||||
return {
|
||||
context,
|
||||
metadataLoad: () =>
|
||||
import('@vality/magista-proto/lib/metadata.json').then((m) => m.default),
|
||||
defaultNamespace: 'magista',
|
||||
};
|
||||
}
|
||||
}
|
33
src/app/api/magista/merchant-statistics.service.ts
Normal file
33
src/app/api/magista/merchant-statistics.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StatPayoutResponse, PayoutSearchQuery } from '@vality/magista-proto';
|
||||
import * as ThriftMerchantStatisticsService from '@vality/magista-proto/lib/magista/gen-nodejs/MerchantStatisticsService';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { ThriftConnector } from '@cc/app/api/thrift-connector';
|
||||
import { KeycloakTokenInfoService } from '@cc/app/shared/services';
|
||||
|
||||
import { MagistaInstanceProviderService } from './magista-instance-provider.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MerchantStatisticsService extends ThriftConnector {
|
||||
constructor(
|
||||
protected keycloakTokenInfoService: KeycloakTokenInfoService,
|
||||
private instanceProvider: MagistaInstanceProviderService
|
||||
) {
|
||||
super(keycloakTokenInfoService, ThriftMerchantStatisticsService, '/v3/stat');
|
||||
}
|
||||
|
||||
searchPayouts(payoutSearchQuery: PayoutSearchQuery): Observable<StatPayoutResponse> {
|
||||
return this.instanceProvider.methods$.pipe(
|
||||
switchMap(({ toPlainObject, toThriftInstance }) =>
|
||||
this.callThriftServiceMethod<StatPayoutResponse>(
|
||||
'SearchPayouts',
|
||||
toThriftInstance('PayoutSearchQuery', payoutSearchQuery)
|
||||
).pipe(map((v) => toPlainObject('StatPayoutResponse', v)))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -12,19 +12,26 @@ export abstract class ThriftInstanceProvider {
|
||||
methods$: Observable<ProviderMethods>;
|
||||
|
||||
constructor(private thriftMetaLoader: ThriftMetaLoader) {
|
||||
const { metadataName, defaultNamespace, context } = this.getProviderSettings();
|
||||
this.methods$ = this.thriftMetaLoader.get(metadataName).pipe(
|
||||
map((metadata) => ({
|
||||
toPlainObject: partial(thriftInstanceToObject, metadata, defaultNamespace),
|
||||
toThriftInstance: partial(
|
||||
createThriftInstance,
|
||||
metadata,
|
||||
context,
|
||||
defaultNamespace
|
||||
),
|
||||
})),
|
||||
first()
|
||||
);
|
||||
const {
|
||||
metadataName,
|
||||
metadataLoad,
|
||||
defaultNamespace,
|
||||
context,
|
||||
} = this.getProviderSettings();
|
||||
this.methods$ = this.thriftMetaLoader
|
||||
.get(metadataName || defaultNamespace, metadataLoad)
|
||||
.pipe(
|
||||
map((metadata) => ({
|
||||
toPlainObject: partial(thriftInstanceToObject, metadata, defaultNamespace),
|
||||
toThriftInstance: partial(
|
||||
createThriftInstance,
|
||||
metadata,
|
||||
context,
|
||||
defaultNamespace
|
||||
),
|
||||
})),
|
||||
first()
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract getProviderSettings(): ProviderSettings;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { from, Observable, ObservableInput } from 'rxjs';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { ThriftAstMetadata } from '../../thrift-services';
|
||||
@ -11,12 +11,13 @@ export class ThriftMetaLoader {
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
get(name: string): Observable<ThriftAstMetadata[]> {
|
||||
get(name: string, load?: () => ObservableInput<any>): Observable<ThriftAstMetadata[]> {
|
||||
const req = this.requests[name];
|
||||
return req
|
||||
? req
|
||||
: (this.requests[name] = this.http
|
||||
.get<ThriftAstMetadata[]>(`assets/api-meta/${name}.json`)
|
||||
.pipe(shareReplay(1)));
|
||||
: (this.requests[name] = (load
|
||||
? from(load())
|
||||
: this.http.get<ThriftAstMetadata[]>(`assets/api-meta/${name}.json`)
|
||||
).pipe(shareReplay(1)));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { ObservableInput } from 'rxjs';
|
||||
|
||||
import { ThriftInstanceContext } from '../../../thrift-services';
|
||||
|
||||
export interface ProviderSettings {
|
||||
context: ThriftInstanceContext;
|
||||
metadataName: string;
|
||||
defaultNamespace: string;
|
||||
metadataName?: string;
|
||||
metadataLoad?: () => ObservableInput<any>;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { AppAuthGuardService } from '@cc/app/shared/services';
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/payouts',
|
||||
redirectTo: '/old-payouts',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
],
|
||||
|
@ -40,7 +40,7 @@ export class AppComponent implements OnInit {
|
||||
private getMenuItems() {
|
||||
const menuItems = [
|
||||
{ name: 'Domain config', route: '/domain', activateRoles: [DomainConfigRole.Checkout] },
|
||||
{ name: 'Payouts', route: '/payouts', activateRoles: [PayoutRole.Read] },
|
||||
{ name: 'Payouts', route: '/old-payouts', activateRoles: [PayoutRole.Read] },
|
||||
{ name: 'Claims', route: '/claims', activateRoles: [ClaimManagementRole.GetClaims] },
|
||||
{
|
||||
name: 'Payment adjustment',
|
||||
|
@ -1,9 +1,5 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import {
|
||||
MAT_MOMENT_DATE_ADAPTER_OPTIONS,
|
||||
MAT_MOMENT_DATE_FORMATS,
|
||||
MomentDateAdapter,
|
||||
} from '@angular/material-moment-adapter';
|
||||
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
|
||||
@ -15,9 +11,14 @@ import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import * as moment from 'moment';
|
||||
|
||||
import { KeycloakTokenInfoModule } from '@cc/app/shared/services';
|
||||
import 'moment/locale/ru';
|
||||
|
||||
import {
|
||||
KeycloakTokenInfoModule,
|
||||
QUERY_PARAMS_SERIALIZERS,
|
||||
MomentUtcDateAdapter,
|
||||
} from '@cc/app/shared/services';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { ClaimModule } from './claim/claim.module';
|
||||
@ -25,18 +26,21 @@ import { CoreModule } from './core/core.module';
|
||||
import { DomainModule } from './domain';
|
||||
import icons from './icons.json';
|
||||
import { NotFoundModule } from './not-found';
|
||||
import { PayoutsModule } from './payouts/payouts.module';
|
||||
import { PayoutsModule as OldPayoutsModule } from './payouts/payouts.module';
|
||||
import { RepairingModule } from './repairing/repairing.module';
|
||||
import { DomainConfigModule } from './sections/domain-config';
|
||||
import { OperationsModule } from './sections/operations/operations.module';
|
||||
import { PartyModule } from './sections/party/party.module';
|
||||
import { PaymentAdjustmentModule } from './sections/payment-adjustment/payment-adjustment.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 { SettingsModule } from './settings';
|
||||
import { ThemeManager, ThemeManagerModule, ThemeName } from './theme-manager';
|
||||
import {
|
||||
DEFAULT_DIALOG_CONFIG,
|
||||
DEFAULT_MAT_DATE_FORMATS,
|
||||
DEFAULT_QUERY_PARAMS_SERIALIZERS,
|
||||
DEFAULT_SEARCH_LIMIT,
|
||||
DEFAULT_SMALL_SEARCH_LIMIT,
|
||||
DIALOG_CONFIG,
|
||||
@ -63,7 +67,7 @@ moment.locale('en');
|
||||
MatSidenavModule,
|
||||
MatListModule,
|
||||
ClaimModule,
|
||||
PayoutsModule,
|
||||
OldPayoutsModule,
|
||||
PaymentAdjustmentModule,
|
||||
DomainModule,
|
||||
RepairingModule,
|
||||
@ -75,17 +79,19 @@ moment.locale('en');
|
||||
OperationsModule,
|
||||
DomainConfigModule,
|
||||
KeycloakTokenInfoModule,
|
||||
PayoutsModule,
|
||||
// It is important that NotFoundModule module should be last
|
||||
NotFoundModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
|
||||
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: DEFAULT_MAT_DATE_FORMATS },
|
||||
{ provide: DateAdapter, useClass: MomentUtcDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_LOCALE, useValue: 'en' },
|
||||
{ provide: SEARCH_LIMIT, useValue: DEFAULT_SEARCH_LIMIT },
|
||||
{ provide: SMALL_SEARCH_LIMIT, useValue: DEFAULT_SMALL_SEARCH_LIMIT },
|
||||
{ provide: DIALOG_CONFIG, useValue: DEFAULT_DIALOG_CONFIG },
|
||||
{ provide: QUERY_PARAMS_SERIALIZERS, useValue: DEFAULT_QUERY_PARAMS_SERIALIZERS },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
@ -9,7 +9,7 @@ import { PayoutsComponent } from './payouts.component';
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: 'payouts',
|
||||
path: 'old-payouts',
|
||||
component: PayoutsComponent,
|
||||
canActivate: [AppAuthGuardService],
|
||||
data: {
|
||||
|
@ -82,5 +82,6 @@ import { SearchFormComponent } from './search-form/search-form.component';
|
||||
ConfirmPayoutsComponent,
|
||||
],
|
||||
providers: [PayoutsService],
|
||||
exports: [PayoutsTableComponent],
|
||||
})
|
||||
export class PayoutsModule {}
|
||||
|
@ -0,0 +1,40 @@
|
||||
<div [formGroup]="control" gdGap="16px" gdColumns="1fr 1fr 1fr 1fr">
|
||||
<mat-form-field>
|
||||
<mat-label>Date Range</mat-label>
|
||||
<mat-date-range-input [rangePicker]="picker">
|
||||
<input matStartDate matInput formControlName="fromTime" placeholder="Start Date" />
|
||||
<input matEndDate matInput formControlName="toTime" placeholder="End Date" />
|
||||
</mat-date-range-input>
|
||||
<mat-datepicker-toggle matSuffix [for]="picker">
|
||||
<mat-icon matDatepickerToggleIcon>keyboard_arrow_down</mat-icon>
|
||||
</mat-datepicker-toggle>
|
||||
<mat-date-range-picker #picker></mat-date-range-picker>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Payout ID" formControlName="payoutId" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select multiple formControlName="payoutStatuses" placeholder="Payout Status">
|
||||
<mat-option *ngFor="let status of statuses" [value]="status">
|
||||
{{ status }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="payoutType" placeholder="Payout Tool Type">
|
||||
<mat-option [value]="null">any</mat-option>
|
||||
<mat-option *ngFor="let type of types" [value]="payoutToolType[type]">
|
||||
{{ type }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<cc-merchant-field formControlName="partyId"></cc-merchant-field>
|
||||
<mat-form-field gdColumn="auto/span 3">
|
||||
<mat-label>Shops</mat-label>
|
||||
<mat-select multiple formControlName="shops" [disabled]="!control.value.partyId">
|
||||
<mat-option *ngFor="let shop of shops$ | async" [value]="shop.id">
|
||||
{{ shop.details.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
@ -0,0 +1,60 @@
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { FormBuilder } from '@ngneat/reactive-forms';
|
||||
import { PayoutToolType } from '@vality/magista-proto';
|
||||
import { Party, Shop } from '@vality/magista-proto/lib/domain';
|
||||
import { PayoutStatus } from '@vality/magista-proto/lib/payout_manager';
|
||||
import { Moment } from 'moment';
|
||||
import * as moment from 'moment';
|
||||
import { of } from 'rxjs';
|
||||
import { share, startWith, switchMap } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
createValidatedAbstractControlProviders,
|
||||
ValidatedWrappedAbstractControlSuperclass,
|
||||
} from '@cc/utils/forms';
|
||||
|
||||
import { PartyService } from '../../../../papi/party.service';
|
||||
|
||||
export interface PayoutsSearchForm {
|
||||
payoutId: string;
|
||||
partyId: Party['id'];
|
||||
fromTime: Moment;
|
||||
toTime: Moment;
|
||||
shops: Shop['id'][];
|
||||
payoutStatuses: (keyof PayoutStatus)[];
|
||||
payoutType: PayoutToolType;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cc-payouts-search-form',
|
||||
templateUrl: './payouts-search-form.component.html',
|
||||
providers: createValidatedAbstractControlProviders(PayoutsSearchFormComponent),
|
||||
})
|
||||
export class PayoutsSearchFormComponent extends ValidatedWrappedAbstractControlSuperclass<
|
||||
PayoutsSearchForm
|
||||
> {
|
||||
control = this.fb.group<PayoutsSearchForm>({
|
||||
payoutId: null,
|
||||
partyId: null,
|
||||
fromTime: [moment().subtract(1, 'year').startOf('d'), Validators.required],
|
||||
toTime: [moment().endOf('d'), Validators.required],
|
||||
shops: null,
|
||||
payoutStatuses: null,
|
||||
payoutType: null,
|
||||
});
|
||||
shops$ = this.control.controls.partyId.valueChanges.pipe(
|
||||
startWith(this.control.value.partyId),
|
||||
switchMap((partyId) => (partyId ? this.partyService.getShops(partyId) : of([]))),
|
||||
share()
|
||||
);
|
||||
statuses: PayoutsSearchForm['payoutStatuses'] = ['unpaid', 'paid', 'cancelled', 'confirmed'];
|
||||
types: string[] = Object.values(PayoutToolType).filter(
|
||||
(v) => typeof v === 'string'
|
||||
) as string[];
|
||||
payoutToolType = PayoutToolType;
|
||||
|
||||
constructor(private fb: FormBuilder, private partyService: PartyService, injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
<table mat-table [dataSource]="payouts">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Payout ID</th>
|
||||
<td mat-cell *matCellDef="let payout">{{ payout.id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="party">
|
||||
<th mat-header-cell *matHeaderCellDef>Party ID</th>
|
||||
<td mat-cell *matCellDef="let payout">{{ payout.party_id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="shop">
|
||||
<th mat-header-cell *matHeaderCellDef>Shop</th>
|
||||
<td mat-cell *matCellDef="let payout">{{ payout.shop_id | shopName: payout.party_id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="createdAt">
|
||||
<th mat-header-cell *matHeaderCellDef>Created At</th>
|
||||
<td mat-cell *matCellDef="let payout">
|
||||
{{ payout.created_at | date: 'dd.MM.yyyy HH:mm:ss' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let payout">
|
||||
<cc-status [color]="getColorByStatus(payout.status | ccUnionKey)">{{
|
||||
payout.status | ccUnionKey
|
||||
}}</cc-status>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="amount">
|
||||
<th mat-header-cell *matHeaderCellDef>Amount</th>
|
||||
<td mat-cell *matCellDef="let payout">
|
||||
{{ payout.amount | ccFormatAmount }}
|
||||
{{ payout.currency_symbolic_code | ccCurrency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="fee">
|
||||
<th mat-header-cell *matHeaderCellDef>Fee</th>
|
||||
<td mat-cell *matCellDef="let payout">
|
||||
{{ payout.fee | ccFormatAmount }}
|
||||
{{ payout.currency_symbolic_code | ccCurrency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="payoutToolType">
|
||||
<th mat-header-cell *matHeaderCellDef>Payout Tool Type</th>
|
||||
<td mat-cell *matCellDef="let payout">
|
||||
{{ payout.payout_tool_info | ccUnionKey }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="action-cell"></th>
|
||||
<td mat-cell *matCellDef="let payout" class="action-cell">
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="navigateToPayout(payout.id)">
|
||||
Details
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
@ -0,0 +1,9 @@
|
||||
:host {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
width: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { PayoutID, PayoutStatus, StatPayout } from '@vality/magista-proto';
|
||||
|
||||
import { StatusColor } from '@cc/app/shared/components/status/types/status-color';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-payouts-table',
|
||||
templateUrl: './payouts-table.component.html',
|
||||
styleUrls: ['./payouts-table.component.scss'],
|
||||
})
|
||||
export class PayoutsTableComponent {
|
||||
@Input() payouts: StatPayout[];
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'id',
|
||||
'party',
|
||||
'shop',
|
||||
'createdAt',
|
||||
'status',
|
||||
'amount',
|
||||
'fee',
|
||||
'payoutToolType',
|
||||
'actions',
|
||||
];
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
async navigateToPayout(id: PayoutID) {
|
||||
await this.router.navigate([`/payouts/${id}`]);
|
||||
}
|
||||
|
||||
getColorByStatus(status: keyof PayoutStatus) {
|
||||
switch (status) {
|
||||
case 'confirmed':
|
||||
return StatusColor.Success;
|
||||
case 'paid':
|
||||
return StatusColor.Pending;
|
||||
case 'cancelled':
|
||||
case 'unpaid':
|
||||
return StatusColor.Warn;
|
||||
default:
|
||||
return StatusColor.Neutral;
|
||||
}
|
||||
}
|
||||
}
|
1
src/app/sections/payouts/index.ts
Normal file
1
src/app/sections/payouts/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './payouts.module';
|
23
src/app/sections/payouts/payouts-routing.module.ts
Normal file
23
src/app/sections/payouts/payouts-routing.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AppAuthGuardService } from '@cc/app/shared/services';
|
||||
|
||||
import { PayoutsComponent } from './payouts.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: 'payouts',
|
||||
component: PayoutsComponent,
|
||||
canActivate: [AppAuthGuardService],
|
||||
data: {
|
||||
roles: ['payout:read'],
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PayoutsRoutingModule {}
|
32
src/app/sections/payouts/payouts.component.html
Normal file
32
src/app/sections/payouts/payouts.component.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px">
|
||||
<div fxLayout="row" fxLayoutAlign="space-between">
|
||||
<h1 class="cc-headline">Payouts</h1>
|
||||
<div><button mat-button>CREATE</button></div>
|
||||
</div>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<cc-payouts-search-form
|
||||
[formControl]="control"
|
||||
(ngModelChange)="search($event)"
|
||||
></cc-payouts-search-form>
|
||||
</mat-card-content>
|
||||
<mat-card-footer *ngIf="inProgress$ | async">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</mat-card-footer>
|
||||
</mat-card>
|
||||
<ng-container *ngIf="payouts$ | async as payouts">
|
||||
<cc-empty-search-result *ngIf="payouts.length === 0"></cc-empty-search-result>
|
||||
<mat-card *ngIf="payouts.length > 0" fxLayout="column" fxLayoutGap="18px">
|
||||
<cc-payouts-table [payouts]="payouts"></cc-payouts-table>
|
||||
<button
|
||||
fxFlex="100"
|
||||
mat-button
|
||||
*ngIf="hasMore$ | async"
|
||||
(click)="fetchMore()"
|
||||
[disabled]="inProgress$ | async"
|
||||
>
|
||||
{{ (inProgress$ | async) ? 'LOADING...' : 'SHOW MORE' }}
|
||||
</button>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
</div>
|
4
src/app/sections/payouts/payouts.component.scss
Normal file
4
src/app/sections/payouts/payouts.component.scss
Normal file
@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding: 24px 16px;
|
||||
}
|
58
src/app/sections/payouts/payouts.component.ts
Normal file
58
src/app/sections/payouts/payouts.component.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { FormControl } from '@ngneat/reactive-forms';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
|
||||
import { QueryParamsService } from '@cc/app/shared/services';
|
||||
|
||||
import { PayoutsSearchForm } from './components/payouts-search-form/payouts-search-form.component';
|
||||
import { FetchPayoutsService, SearchParams } from './services/fetch-payouts.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-payouts',
|
||||
templateUrl: './payouts.component.html',
|
||||
styleUrls: ['./payouts.component.scss'],
|
||||
providers: [FetchPayoutsService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PayoutsComponent {
|
||||
control = new FormControl<PayoutsSearchForm>(this.qp.params);
|
||||
inProgress$ = this.fetchPayoutsService.doAction$;
|
||||
payouts$ = this.fetchPayoutsService.searchResult$;
|
||||
hasMore$ = this.fetchPayoutsService.hasMore$;
|
||||
|
||||
constructor(
|
||||
private fetchPayoutsService: FetchPayoutsService,
|
||||
private qp: QueryParamsService<PayoutsSearchForm>
|
||||
) {}
|
||||
|
||||
fetchMore() {
|
||||
this.fetchPayoutsService.fetchMore();
|
||||
}
|
||||
|
||||
search(value: PayoutsSearchForm) {
|
||||
void this.qp.set(value);
|
||||
this.fetchPayoutsService.search(
|
||||
omitBy(
|
||||
{
|
||||
common_search_query_params: omitBy(
|
||||
{
|
||||
from_time: value.fromTime?.utc()?.format(),
|
||||
to_time: value.toTime?.utc()?.format(),
|
||||
party_id: value.partyId,
|
||||
shop_ids: value.shops,
|
||||
},
|
||||
isNil
|
||||
),
|
||||
payout_id: value.payoutId,
|
||||
// TODO: Should be enum
|
||||
payout_statuses: value.payoutStatuses?.map((key) => ({ [key]: {} })),
|
||||
payout_type: value.payoutType,
|
||||
},
|
||||
isNil
|
||||
) as SearchParams
|
||||
);
|
||||
}
|
||||
}
|
52
src/app/sections/payouts/payouts.module.ts
Normal file
52
src/app/sections/payouts/payouts.module.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
import { StatusModule } from '@cc/app/shared/components';
|
||||
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
|
||||
import { ApiModelPipesModule, CommonPipesModule, ThriftPipesModule } from '@cc/app/shared/pipes';
|
||||
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
|
||||
|
||||
import { PayoutsSearchFormComponent } from './components/payouts-search-form/payouts-search-form.component';
|
||||
import { PayoutsTableComponent } from './components/payouts-table/payouts-table.component';
|
||||
import { PayoutsRoutingModule } from './payouts-routing.module';
|
||||
import { PayoutsComponent } from './payouts.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PayoutsComponent, PayoutsTableComponent, PayoutsSearchFormComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
PayoutsRoutingModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatProgressBarModule,
|
||||
EmptySearchResultModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
FormsModule,
|
||||
MerchantFieldModule,
|
||||
FlexLayoutModule,
|
||||
ReactiveFormsModule,
|
||||
MatSelectModule,
|
||||
MatDatepickerModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
MatMenuModule,
|
||||
ApiModelPipesModule,
|
||||
CommonPipesModule,
|
||||
ThriftPipesModule,
|
||||
StatusModule,
|
||||
],
|
||||
})
|
||||
export class PayoutsModule {}
|
45
src/app/sections/payouts/services/fetch-payouts.service.ts
Normal file
45
src/app/sections/payouts/services/fetch-payouts.service.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StatPayout, PayoutSearchQuery } from '@vality/magista-proto';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Overwrite } from 'utility-types';
|
||||
|
||||
import { MerchantStatisticsService } from '@cc/app/api/magista';
|
||||
import { FetchResult, PartialFetcher } from '@cc/app/shared/services';
|
||||
|
||||
export type SearchParams = Overwrite<
|
||||
PayoutSearchQuery,
|
||||
{
|
||||
common_search_query_params: Omit<
|
||||
PayoutSearchQuery['common_search_query_params'],
|
||||
'continuation_token' | 'limit'
|
||||
>;
|
||||
}
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class FetchPayoutsService extends PartialFetcher<StatPayout, SearchParams> {
|
||||
constructor(private merchantStatisticsService: MerchantStatisticsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected fetch(
|
||||
params: SearchParams,
|
||||
continuationToken: string
|
||||
): Observable<FetchResult<StatPayout>> {
|
||||
return this.merchantStatisticsService
|
||||
.searchPayouts({
|
||||
...params,
|
||||
common_search_query_params: {
|
||||
...params.common_search_query_params,
|
||||
continuation_token: continuationToken,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
map(({ continuation_token, payouts }) => ({
|
||||
result: payouts,
|
||||
continuationToken: continuation_token,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
@ -22,8 +22,8 @@ export class SearchClaimsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
search({ merchant, ...v }: ClaimSearchForm): void {
|
||||
this.searchClaimService.search({ ...v, party_id: merchant?.id });
|
||||
search(v: ClaimSearchForm): void {
|
||||
this.searchClaimService.search(v);
|
||||
}
|
||||
|
||||
fetchMore(): void {
|
||||
|
@ -18,6 +18,6 @@
|
||||
<cc-merchant-field
|
||||
*ngIf="!hideMerchantSearch"
|
||||
fxFlex
|
||||
formControlName="merchant"
|
||||
formControlName="party_id"
|
||||
></cc-merchant-field>
|
||||
</form>
|
||||
|
@ -32,7 +32,7 @@ export class ClaimSearchFormComponent implements OnInit {
|
||||
form = this.fb.group<ClaimSearchForm>({
|
||||
statuses: null,
|
||||
claim_id: null,
|
||||
merchant: null,
|
||||
party_id: null,
|
||||
});
|
||||
|
||||
claimStatuses: (keyof ClaimStatus)[] = [
|
||||
@ -50,20 +50,13 @@ export class ClaimSearchFormComponent implements OnInit {
|
||||
this.form.valueChanges
|
||||
.pipe(debounceTime(600), map(removeEmptyProperties), untilDestroyed(this))
|
||||
.subscribe((value) => {
|
||||
const { merchant, ...v } = value;
|
||||
void this.router.navigate([location.pathname], {
|
||||
queryParams: Object.assign(v, !!merchant?.id && { merchantId: merchant?.id }),
|
||||
});
|
||||
void this.router.navigate([location.pathname], { queryParams: value });
|
||||
this.valueChanges.emit(formValueToSearchParams(value));
|
||||
});
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
take(1),
|
||||
map(queryParamsToFormValue),
|
||||
map(({ merchantId, ...v }) => ({
|
||||
...v,
|
||||
merchant: merchantId ? { id: merchantId } : null,
|
||||
})),
|
||||
map(removeEmptyProperties),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { PartyID } from '@cc/app/api/damsel/gen-model/domain';
|
||||
|
||||
import { ClaimStatus } from '../../../papi/model';
|
||||
import { ClaimID } from '../../../thrift-services/damsel/gen-model/claim_management';
|
||||
import { Party } from '../../../thrift-services/deanonimus/gen-model/deanonimus';
|
||||
|
||||
export interface ClaimSearchForm {
|
||||
claim_id?: ClaimID;
|
||||
statuses?: ClaimStatus[];
|
||||
merchant?: Party;
|
||||
claim_id: ClaimID;
|
||||
statuses: ClaimStatus[];
|
||||
party_id: PartyID;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
[label]="label || 'Merchant'"
|
||||
[required]="required"
|
||||
[options]="options$ | async"
|
||||
[formControl]="formControl"
|
||||
[formControl]="control"
|
||||
(searchChange)="this.searchChange$.next($event)"
|
||||
isExternalSearch
|
||||
></cc-select-search-field>
|
||||
|
@ -15,10 +15,10 @@ import {
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { PartyID } from '@cc/app/api/damsel/gen-model/domain';
|
||||
import { Option } from '@cc/components/select-search-field';
|
||||
|
||||
import { DeanonimusService } from '../../../thrift-services/deanonimus';
|
||||
import { Party } from '../../../thrift-services/deanonimus/gen-model/deanonimus';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
@ -27,13 +27,14 @@ import { Party } from '../../../thrift-services/deanonimus/gen-model/deanonimus'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [provideValueAccessor(MerchantFieldComponent)],
|
||||
})
|
||||
export class MerchantFieldComponent extends WrappedFormControlSuperclass<Party> implements OnInit {
|
||||
export class MerchantFieldComponent extends WrappedFormControlSuperclass<PartyID>
|
||||
implements OnInit {
|
||||
@Input() label: string;
|
||||
@Input() @coerceBoolean required: boolean;
|
||||
|
||||
formControl = new FormControl<Party>();
|
||||
incomingValue$ = new Subject<Partial<Party>>();
|
||||
options$ = new ReplaySubject<Option<Party>[]>(1);
|
||||
control = new FormControl<PartyID>();
|
||||
incomingValue$ = new Subject<Partial<PartyID>>();
|
||||
options$ = new ReplaySubject<Option<PartyID>[]>(1);
|
||||
searchChange$ = new Subject<string>();
|
||||
|
||||
constructor(
|
||||
@ -47,24 +48,23 @@ export class MerchantFieldComponent extends WrappedFormControlSuperclass<Party>
|
||||
ngOnInit(): void {
|
||||
this.incomingValue$
|
||||
.pipe(
|
||||
withLatestFrom(this.options$.pipe(startWith<Option<Party>[]>([]))),
|
||||
withLatestFrom(this.options$.pipe(startWith<Option<PartyID>[]>([]))),
|
||||
switchMap(([value, options]) => {
|
||||
if (!value?.id) return of<Party>(null);
|
||||
const v = options.find((o) => o.value.id === value.id);
|
||||
if (!value) return of<PartyID>(null);
|
||||
const v = options.find((o) => o.value === value);
|
||||
if (v) return of(v.value);
|
||||
return this.searchOptions(value?.id).pipe(
|
||||
return this.searchOptions(value).pipe(
|
||||
tap((options) => this.options$.next(options)),
|
||||
map(
|
||||
(options) =>
|
||||
options?.find((o) => o.value.id === this.formControl.value?.id)
|
||||
?.value || null
|
||||
options?.find((o) => o.value === this.control.value)?.value || null
|
||||
)
|
||||
);
|
||||
}),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((v) => this.formControl.setValue(v));
|
||||
this.formControl.valueChanges.subscribe((v) => this.emitOutgoingValue(v));
|
||||
.subscribe((v) => this.control.setValue(v));
|
||||
this.control.valueChanges.subscribe((v) => this.emitOutgoingValue(v));
|
||||
this.searchChange$
|
||||
.pipe(
|
||||
debounceTime(600),
|
||||
@ -74,14 +74,14 @@ export class MerchantFieldComponent extends WrappedFormControlSuperclass<Party>
|
||||
.subscribe((options) => this.options$.next(options));
|
||||
}
|
||||
|
||||
handleIncomingValue(partyLike: Partial<Party>): void {
|
||||
this.formControl.setValue(partyLike as Party);
|
||||
this.incomingValue$.next(partyLike);
|
||||
handleIncomingValue(partyId: PartyID): void {
|
||||
this.control.setValue(partyId);
|
||||
this.incomingValue$.next(partyId);
|
||||
}
|
||||
|
||||
private searchOptions(str: string): Observable<Option<Party>[]> {
|
||||
private searchOptions(str: string): Observable<Option<PartyID>[]> {
|
||||
return this.deanonimusService.searchParty(str).pipe(
|
||||
map((parties) => parties.map((p) => ({ label: p.party.email, value: p.party }))),
|
||||
map((parties) => parties.map((p) => ({ label: p.party.email, value: p.party.id }))),
|
||||
catchError((err) => {
|
||||
this.snackBar.open('Search error', 'OK', { duration: 2000 });
|
||||
console.error(err);
|
||||
|
@ -4,3 +4,5 @@ export * from './fetch-parties.service';
|
||||
export * from './keycloak-token-info';
|
||||
export * from './user-info-based-id-generator';
|
||||
export * from './partial-fetcher';
|
||||
export * from './query-params';
|
||||
export * from './moment-utc-date-adapter';
|
||||
|
1
src/app/shared/services/moment-utc-date-adapter/index.ts
Normal file
1
src/app/shared/services/moment-utc-date-adapter/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './moment-utc-date-adapter';
|
@ -0,0 +1,36 @@
|
||||
import { Inject, Injectable, Optional } from '@angular/core';
|
||||
import { MomentDateAdapter } from '@angular/material-moment-adapter';
|
||||
import { MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { Moment } from 'moment';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* https://github.com/angular/components/issues/7167#issuecomment-385416948
|
||||
*/
|
||||
export class MomentUtcDateAdapter extends MomentDateAdapter {
|
||||
constructor(@Optional() @Inject(MAT_DATE_LOCALE) dateLocale: string) {
|
||||
super(dateLocale);
|
||||
}
|
||||
|
||||
createDate(year: number, month: number, date: number): Moment {
|
||||
// Moment.js will create an invalid date if any of the components are out of bounds, but we
|
||||
// explicitly check each case so we can throw more descriptive errors.
|
||||
if (month < 0 || month > 11) {
|
||||
throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
|
||||
}
|
||||
|
||||
if (date < 1) {
|
||||
throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
|
||||
}
|
||||
|
||||
const result = moment.utc({ year, month, date }).locale(this.locale);
|
||||
|
||||
// If the result isn't valid, the date must have been out of bounds for this month.
|
||||
if (!result.isValid()) {
|
||||
throw Error(`Invalid date "${date}" for month with index "${month}".`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -5,6 +5,9 @@ import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { removeEmptyProperties } from '@cc/utils/remove-empty-properties';
|
||||
|
||||
/**
|
||||
* @deprecated use QueryParamsService
|
||||
*/
|
||||
export abstract class QueryParamsStore<D> {
|
||||
data$: Observable<Partial<D>> = this.route.queryParams.pipe(
|
||||
distinctUntilChanged(isEqual),
|
||||
|
2
src/app/shared/services/query-params/index.ts
Normal file
2
src/app/shared/services/query-params/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './query-params.service';
|
||||
export * from './utils/query-params-serializers';
|
77
src/app/shared/services/query-params/query-params.service.ts
Normal file
77
src/app/shared/services/query-params/query-params.service.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { Inject, Injectable, Optional } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import negate from 'lodash-es/negate';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, shareReplay, startWith } from 'rxjs/operators';
|
||||
|
||||
import { isEmptyValue } from '@cc/app/shared/utils';
|
||||
|
||||
import { Serializer } from './types/serializer';
|
||||
import { deserializeQueryParam } from './utils/deserialize-query-param';
|
||||
import { QUERY_PARAMS_SERIALIZERS } from './utils/query-params-serializers';
|
||||
import { serializeQueryParam } from './utils/serialize-query-param';
|
||||
|
||||
type Options = {
|
||||
filter?: (param: unknown, key: string) => boolean;
|
||||
};
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class QueryParamsService<Params> {
|
||||
params$: Observable<Params> = this.route.queryParams.pipe(
|
||||
map((params) => this.deserialize(params)),
|
||||
startWith(this.params),
|
||||
distinctUntilChanged<Params>(isEqual),
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
);
|
||||
|
||||
get params(): Params {
|
||||
return this.deserialize(this.route.snapshot.queryParams);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@Optional() @Inject(QUERY_PARAMS_SERIALIZERS) private serializers?: Serializer[]
|
||||
) {
|
||||
// Angular @Optional not support TS syntax: `serializers: Serializer[] = []`
|
||||
if (!this.serializers) {
|
||||
this.serializers = [];
|
||||
}
|
||||
}
|
||||
|
||||
async set(params: Params, options?: Options): Promise<boolean> {
|
||||
return await this.router.navigate([], { queryParams: this.serialize(params, options) });
|
||||
}
|
||||
|
||||
async patch(param: Partial<Params>): Promise<boolean> {
|
||||
return await this.set({ ...this.params, ...param });
|
||||
}
|
||||
|
||||
async init(param: Params): Promise<boolean> {
|
||||
return await this.set({ ...param, ...this.params });
|
||||
}
|
||||
|
||||
private serialize(
|
||||
params: Params,
|
||||
{ filter = negate(isEmptyValue) }: Options = {}
|
||||
): { [key: string]: string } {
|
||||
return Object.entries(params).reduce((acc, [k, v]) => {
|
||||
if (filter(v, k)) acc[k] = serializeQueryParam(v, this.serializers);
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
}
|
||||
|
||||
private deserialize(params: { [key: string]: string }): Params {
|
||||
return Object.entries(params).reduce((acc, [k, v]) => {
|
||||
try {
|
||||
acc[k] = deserializeQueryParam<Params[keyof Params]>(v, this.serializers);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return acc;
|
||||
}, {} as Params);
|
||||
}
|
||||
}
|
6
src/app/shared/services/query-params/types/serializer.ts
Normal file
6
src/app/shared/services/query-params/types/serializer.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type Serializer<T = unknown> = {
|
||||
id: string;
|
||||
serialize: (v: T) => string;
|
||||
deserialize: (v: string) => T;
|
||||
recognize: (v: T) => boolean;
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import { Serializer } from '../types/serializer';
|
||||
|
||||
export function deserializeQueryParam<P>(value: string, serializers: Serializer[] = []): P {
|
||||
const serializer = serializers.find((s) => value.startsWith(s.id));
|
||||
return (serializer
|
||||
? serializer.deserialize(value.slice(serializer.id.length))
|
||||
: JSON.parse(value || '')) as P;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
import { Serializer } from '../types/serializer';
|
||||
|
||||
export const QUERY_PARAMS_SERIALIZERS = new InjectionToken<Serializer[]>(
|
||||
'query params serializers'
|
||||
);
|
@ -0,0 +1,6 @@
|
||||
import { Serializer } from '../types/serializer';
|
||||
|
||||
export function serializeQueryParam(value: unknown, serializers: Serializer[] = []): string {
|
||||
const serializer = serializers.find((s) => s.recognize(value));
|
||||
return serializer ? serializer.id + serializer.serialize(value) : JSON.stringify(value);
|
||||
}
|
@ -3,3 +3,4 @@ export * from './component-changes';
|
||||
export * from './polling-conditions';
|
||||
export * from './sort-units';
|
||||
export * from './deposit-status';
|
||||
export * from './is-empty-value';
|
||||
|
6
src/app/shared/utils/is-empty-value.ts
Normal file
6
src/app/shared/utils/is-empty-value.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
|
||||
export function isEmptyValue(v: unknown): boolean {
|
||||
return typeof v === 'object' ? isEmpty(v) : isNil(v);
|
||||
}
|
@ -1 +1 @@
|
||||
export type ThriftInstanceContext = { [N in string]: any };
|
||||
export type ThriftInstanceContext = { [N in string]: unknown };
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { MatDateFormats } from '@angular/material/core';
|
||||
import { MatDialogConfig } from '@angular/material/dialog';
|
||||
import { Moment } from 'moment';
|
||||
import * as moment from 'moment';
|
||||
|
||||
import { Serializer } from '@cc/app/shared/services/query-params/types/serializer';
|
||||
|
||||
export const SEARCH_LIMIT = new InjectionToken<number>('searchLimit');
|
||||
export const DEFAULT_SEARCH_LIMIT = 10;
|
||||
@ -23,3 +28,24 @@ export const DEFAULT_DIALOG_CONFIG: DialogConfig = {
|
||||
medium: { ...BASE_CONFIG, width: '552px' },
|
||||
large: { ...BASE_CONFIG, width: '648px' },
|
||||
};
|
||||
|
||||
export const DEFAULT_QUERY_PARAMS_SERIALIZERS: Serializer[] = [
|
||||
{
|
||||
id: 'moment',
|
||||
serialize: (date: Moment) => date.utc().format(),
|
||||
deserialize: (value) => moment(value),
|
||||
recognize: (value) => moment.isMoment(value),
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_MAT_DATE_FORMATS: MatDateFormats = {
|
||||
parse: {
|
||||
dateInput: ['l', 'LL'],
|
||||
},
|
||||
display: {
|
||||
dateInput: 'DD.MM.YYYY',
|
||||
monthYearLabel: 'DD.MM.YYYY',
|
||||
dateA11yLabel: 'DD.MM.YYYY',
|
||||
monthYearA11yLabel: 'DD.MM.YYYY',
|
||||
},
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ async function execWithLog(cmd: string) {
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error === null) {
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
res(stdout);
|
||||
} else {
|
||||
console.error(error);
|
||||
|
Loading…
Reference in New Issue
Block a user