FE-756: FE-763: Withdrawal registry reports & Deposits init (#242)

This commit is contained in:
Rinat Arsaev 2019-02-25 16:38:16 +05:00 committed by GitHub
parent 781616ed3e
commit 004a34091b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1300 additions and 51 deletions

View File

@ -16,6 +16,7 @@ import { UrlShortenerService } from './url-shortener.service';
import { ClaimService } from './claim.service';
import { CustomerService } from './customer.service';
import { DownloadService } from './download.service';
import { WAPIModule } from './wapi/wapi.module';
@NgModule({
providers: [
@ -35,6 +36,7 @@ import { DownloadService } from './download.service';
ClaimService,
CustomerService,
DownloadService
]
],
imports: [WAPIModule]
})
export class BackendModule {}

View File

@ -0,0 +1,3 @@
export * from './to-search-params';
export * from './applyMixins';
export * from './to-utc';

View File

@ -0,0 +1,20 @@
import { isDate, reduce } from 'lodash';
import { toUTC } from './to-utc';
export function toCreateParams(params: object): object {
return reduce(
params,
(acc, value, key) => {
if (value) {
if (isDate(value)) {
return { ...acc, [key]: toUTC(value) };
} else {
return { ...acc, [key]: value };
}
} else {
return acc;
}
},
{}
);
}

View File

@ -0,0 +1,19 @@
import { URLSearchParams } from '@angular/http';
import { toString, forEach, isNumber, isDate } from 'lodash';
import { toUTC } from './to-utc';
export function toSearchParams(params: object): URLSearchParams {
const result = new URLSearchParams();
forEach(params, (value, field) => {
if (value) {
if (isDate(value)) {
result.set(field, toUTC(value));
} else if (isNumber(value)) {
result.set(field, toString(value));
} else {
result.set(field, value);
}
}
});
return result;
}

View File

@ -0,0 +1,6 @@
import moment = require('moment');
export const toUTC = (date: Date): string =>
moment(date)
.utc()
.format();

View File

@ -25,6 +25,8 @@ import {
Withdrawal,
WithdrawalSearchResult
} from './model';
import { SearchDeposits } from './wapi/requests/search-deposits-params';
import { SearchDepositResult } from './wapi/requests/search-deposits-result';
@Injectable()
export class SearchService {
@ -86,6 +88,11 @@ export class SearchService {
.map(res => res.json());
}
public searchWalletDeposits(depositsParams: SearchDeposits): Observable<SearchDepositResult> {
const search = this.toSearchParams(depositsParams);
return this.http.get(`${this.config.wapiUrl}/deposits`, { search }).map(res => res.json());
}
public searchWalletWithdrawal(withdrawalID: string): Observable<Withdrawal> {
return this.http
.get(`${this.config.wapiUrl}/withdrawals/${withdrawalID}`)

View File

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { KoffingHttp } from '../koffing-http.service';
import { ConfigService } from '../config.service';
import { DownloadFilePathParams, DownloadFileParams } from './requests/download-file-params';
import { File } from './model/file';
import { toSearchParams } from '../helpers';
@Injectable()
export class DownloadFileService {
constructor(private http: KoffingHttp, private config: ConfigService) {}
public downloadFile(
{ fileID }: DownloadFilePathParams,
queryParams: DownloadFileParams
): Observable<File> {
return this.http
.post(
`${this.config.wapiUrl}/files/${fileID}/download`,
{},
{
search: toSearchParams(queryParams)
}
)
.map(res => res.json());
}
}

View File

@ -0,0 +1,25 @@
export enum DepositStatus {
Pending = 'Pending',
Succeeded = 'Succeeded',
Failed = 'Failed'
}
export interface Deposit {
id: string;
createdAt: string;
wallet: string;
source: string;
body: {
amount: number;
currency: string;
};
fee: {
amount: number;
currency: string;
};
externalID: string;
status: DepositStatus;
failure: {
code: string;
};
}

View File

@ -0,0 +1,4 @@
export interface File {
url: string;
expiresAt: string;
}

View File

@ -0,0 +1,25 @@
export enum ReportType {
withdrawalRegistry = 'withdrawalRegistry'
}
export enum ReportStatus {
pending = 'pending',
created = 'created',
canceled = 'canceled'
}
export interface Report {
id: number;
createdAt: string;
fromTime: string;
toTime: string;
status: ReportStatus;
type: ReportType;
files: string[];
}
export interface SearchReportParams {
fromTime: string;
toTime: string;
type: ReportType;
}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { KoffingHttp } from '../koffing-http.service';
import { ConfigService } from '../config.service';
import { Report } from './model/report';
import { GetReportsQuery, GetReportsPath } from './requests/get-reports-params';
import { toSearchParams } from '../helpers';
import { CreateReportPath, CreateReportQuery } from './requests/create-report-params';
import { toCreateParams } from '../helpers/to-create-params';
@Injectable()
export class ReportsService {
constructor(private http: KoffingHttp, private config: ConfigService) {}
public getReports(
{ identityID }: GetReportsPath,
queryParams: GetReportsQuery
): Observable<Report[]> {
return this.http
.get(`${this.config.wapiUrl}/identities/${identityID}/reports`, {
search: toSearchParams(queryParams)
})
.map(res => res.json());
}
public createReport(
{ identityID }: CreateReportPath,
queryParams: CreateReportQuery
): Observable<Report> {
return this.http
.post(
`${this.config.wapiUrl}/identities/${identityID}/reports`,
toCreateParams(queryParams)
)
.map(res => res.json());
}
}

View File

@ -0,0 +1,11 @@
import { ReportType } from '../model/report';
export interface CreateReportPath {
identityID: string;
}
export interface CreateReportQuery {
fromTime: Date;
toTime: Date;
reportType?: ReportType;
}

View File

@ -0,0 +1,7 @@
export interface DownloadFilePathParams {
fileID: string;
}
export interface DownloadFileParams {
expiresAt: string;
}

View File

@ -0,0 +1,11 @@
import { ReportType } from '../model/report';
export interface GetReportsPath {
identityID: string;
}
export interface GetReportsQuery {
fromTime: Date;
toTime: Date;
type?: ReportType;
}

View File

@ -0,0 +1,16 @@
import { DepositStatus } from '../model/deposit';
export interface SearchDeposits {
walletID?: string;
identityID?: string;
depositID?: string;
sourceID?: string;
status?: DepositStatus;
createdAtFrom?: string;
createdAtTo?: string;
amountFrom?: number;
amountTo?: number;
currencyID?: string;
limit: number;
continuationToken?: string;
}

View File

@ -0,0 +1,6 @@
import { Deposit } from '../model/deposit';
export interface SearchDepositResult {
continuationToken: string;
result: Deposit[];
}

View File

@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { ReportsService } from './reports.service';
import { DownloadFileService } from './download-file.service';
@NgModule({
providers: [ReportsService, DownloadFileService]
})
export class WAPIModule {}

View File

@ -18,6 +18,7 @@ import { PaymentStatusPipe } from './payment-statuses.pipe';
import { CurrencyPipe } from './currency.pipe';
import { StepperComponent } from './stepper/stepper.component';
import { WithdrawalStatusPipe } from './withdrawal-status.pipe';
import { DepositStatusPipe } from './deposit-status.pipe';
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, CalendarModule],
@ -35,7 +36,8 @@ import { WithdrawalStatusPipe } from './withdrawal-status.pipe';
PaymentStatusPipe,
CurrencyPipe,
WithdrawalStatusPipe,
StepperComponent
StepperComponent,
DepositStatusPipe
],
exports: [
SelectComponent,
@ -51,7 +53,8 @@ import { WithdrawalStatusPipe } from './withdrawal-status.pipe';
PaymentStatusPipe,
CurrencyPipe,
WithdrawalStatusPipe,
StepperComponent
StepperComponent,
DepositStatusPipe
],
providers: [EventPollerService]
})

View File

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DepositStatus } from 'koffing/backend/wapi/model/deposit';
import { DepositStatusLabel } from '../deposits/deposit-status-label';
@Pipe({
name: 'kofDepositStatus'
})
export class DepositStatusPipe implements PipeTransform {
public transform(input: DepositStatus): string {
return DepositStatusLabel[input] || input;
}
}

View File

@ -0,0 +1,41 @@
form.form-horizontal.form-label-left.css-form
.form-group(*ngIf="deposit?.id")
.col-xs-12.col-sm-4
label.text-left Идентификатор пополнения:
.col-xs-12.col-sm-8
div {{deposit?.id}}
.form-group(*ngIf="deposit?.status")
.col-xs-12.col-sm-4
label.text-left Статус:
.col-xs-12.col-sm-8
div.label.label-default([ngClass]="getLabelClass(deposit?.status)") {{deposit?.status | kofDepositStatus}}
.form-group
.col-xs-12.col-sm-4
label.text-left Идентификатор кошелька:
.col-xs-12.col-sm-8
div {{deposit?.wallet}}
.form-group
.col-xs-12.col-sm-4
label.text-left Идентификатор источника денежных средств:
.col-xs-12.col-sm-8
div {{deposit?.source}}
.form-group
.col-xs-12.col-sm-4
label.text-left Дата и время создания:
.col-xs-12.col-sm-8
div {{deposit?.createdAt | date: "dd.MM.yyyy HH:mm:ss"}}
.form-group
.col-xs-12.col-sm-4
label.text-left Объем пополнения средств:
.col-xs-12.col-sm-8
div {{deposit?.body.amount | kofRoubleCurrency}} {{deposit.body.currency | kofCurrency}}
.form-group(*ngIf="deposit?.fee")
.col-xs-12.col-sm-4
label.text-left Коммисия:
.col-xs-12.col-sm-8
div {{deposit?.fee.amount | kofRoubleCurrency}} {{deposit.fee.currency | kofCurrency}}
.form-group(*ngIf="deposit?.failure")
.col-xs-12.col-sm-4
label.text-left Причина ошибки:
.col-xs-12.col-sm-8
div {{deposit.failure.code}}

View File

@ -0,0 +1,20 @@
import { Component, Input } from '@angular/core';
import { DepositStatus, Deposit } from 'koffing/backend/wapi/model/deposit';
@Component({
selector: 'kof-deposit-details',
templateUrl: 'deposit-details.component.pug'
})
export class DepositDetailsComponent {
@Input()
public deposit: Deposit;
public getLabelClass(status: DepositStatus) {
return {
'label-success': status === DepositStatus.Succeeded,
'label-danger': status === DepositStatus.Failed,
'label-warning': status === DepositStatus.Pending
};
}
}

View File

@ -0,0 +1,16 @@
kof-loading([isLoading]="!deposit && !depositNotFound")
.row(*ngIf="deposit || depositNotFound")
.col-xs-12.col-md-10.col-md-offset-1(*ngIf="!depositNotFound")
.x_panel
.x_title
.row
.col-xs-6
h5 Пополнение
.col-xs-6
button.btn.btn-default.btn-sm.pull-right((click)="back()") Назад
.x_content
kof-deposit-details([deposit]="deposit")
.col-xs-12.col-md-6.col-md-offset-3(*ngIf="depositNotFound")
.x_panel
.x_content
h5.text-center Пополнение не найдено

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { DepositService } from './deposit.service';
import { Deposit } from 'koffing/backend/wapi/model/deposit';
@Component({
templateUrl: 'deposit.component.pug',
providers: [DepositService]
})
export class DepositComponent implements OnInit {
public deposit: Deposit;
public depositNotFound: boolean = false;
constructor(private depositService: DepositService) {}
public ngOnInit() {
this.depositService.depositSubject.subscribe(
(deposit: Deposit) => (this.deposit = deposit),
() => (this.depositNotFound = true)
);
}
public back() {
this.depositService.back();
}
}

View File

@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from 'koffing/common/common.module';
import { DepositComponent } from './deposit.component';
import { DepositDetailsComponent } from './deposit-details/deposit-details.component';
@NgModule({
imports: [BrowserModule, CommonModule],
declarations: [DepositComponent, DepositDetailsComponent]
})
export class DepositModule {}

View File

@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { isUndefined } from 'lodash';
import { SearchService } from 'koffing/backend/search.service';
import { Deposit } from 'koffing/backend/wapi/model/deposit';
@Injectable()
export class DepositService {
public depositSubject: Subject<Deposit> = new Subject();
private deposit: Deposit;
constructor(
private searchService: SearchService,
private route: ActivatedRoute,
private router: Router
) {
Observable.combineLatest(this.route.parent.params, this.route.params).subscribe(result => {
this.searchDeposit(result[1].depositID);
});
}
public back() {
this.router.navigate(['wallets', 'deposits']);
}
private searchDeposit(depositID: string) {
const searchRetries = 20;
this.searchService
.searchWalletDeposits({ limit: 1, depositID })
.repeatWhen(notifications =>
notifications.delay(1000).takeWhile((value, retries) => {
if (retries === searchRetries) {
this.depositSubject.error({ message: 'Deposit not found' });
}
return isUndefined(this.deposit) && retries < searchRetries;
})
)
.subscribe(searchResult => {
if (searchResult) {
this.deposit = searchResult[0];
this.depositSubject.next(this.deposit);
this.depositSubject.complete();
}
});
}
}

View File

@ -0,0 +1,7 @@
import { DepositStatus } from 'koffing/backend/wapi/model/deposit';
export const DepositStatusLabel = {
[DepositStatus.Pending]: 'В процессе',
[DepositStatus.Succeeded]: 'Успешно',
[DepositStatus.Failed]: 'Ошибка'
};

View File

@ -0,0 +1,28 @@
div.ui-calendar.search-form {
width: 100%;
}
p-calendar.search-form > span {
width: 100%;
}
p-calendar.search-form input.ui-inputtext {
width: 100%;
height: 34px;
margin: 0;
padding: 6px 12px;
}
p-calendar.has-error input.ui-inputtext {
border-color: #a94442;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
input.has-error {
border-color: #a94442;
}
input.ng-dirty.ng-invalid {
border-color: #a94442;
}

View File

@ -0,0 +1,8 @@
.row
.col-xs-12
kof-deposits-search-form((onSearch)="onSearch($event)")
.ln_solid
.row
.col-xs-12
kof-deposits-search-result([deposits]="deposits")
kof-stepper((onChange)="onChangePage($event)", [hasNext]="hasNext()", [page]="page + 1")

View File

@ -0,0 +1,59 @@
import { Component } from '@angular/core';
import { Subject } from 'rxjs';
import { DepositsTableService } from './deposits-table.service';
import { SearchService } from 'koffing/backend/search.service';
import { SearchFormService } from './search-form/search-form.service';
import { Deposit } from 'koffing/backend/wapi/model/deposit';
@Component({
selector: 'kof-wallets-deposits',
templateUrl: 'deposits-table.component.pug',
styleUrls: ['deposits-table.component.less'],
providers: [DepositsTableService, SearchFormService]
})
export class DepositsTableComponent {
public page: number = 0;
public limit: number = 20;
public deposits: Subject<Deposit[]> = new Subject();
private continuationTokens: string[] = [];
private formValue: any;
constructor(
private depositsTableService: DepositsTableService,
private searchService: SearchService
) {}
public reset() {
this.continuationTokens = [];
this.page = 0;
}
public onSearch(fromValue: any) {
this.reset();
this.formValue = fromValue;
this.search();
}
public hasNext() {
return !!this.continuationTokens[this.page + 1];
}
public onChangePage(num: number) {
this.search(num);
}
private search(num: number = 0) {
this.page += num;
const continuationToken = this.continuationTokens[this.page];
const request = this.depositsTableService.toSearchParams(
this.limit,
continuationToken,
this.formValue
);
this.searchService.searchWalletDeposits(request).subscribe(response => {
this.continuationTokens[this.page + 1] = response.continuationToken;
this.deposits.next(response.result);
});
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { toMinor } from 'koffing/common/amount-utils';
import { SearchDeposits } from 'koffing/backend/wapi/requests/search-deposits-params';
@Injectable()
export class DepositsTableService {
public toSearchParams(
limit: number,
continuationToken: string,
formParams: any
): SearchDeposits {
return {
limit,
walletID: formParams.walletID,
continuationToken,
identityID: formParams.identityID,
sourceID: formParams.sourceID,
depositID: formParams.depositID,
status: formParams.status,
createdAtFrom: formParams.createdAtFrom,
createdAtTo: formParams.createdAtTo,
amountFrom: toMinor(formParams.amountFrom),
amountTo: toMinor(formParams.amountTo),
currencyID: formParams.currencyID
};
}
}

View File

@ -0,0 +1,7 @@
.reset-button-container {
padding-top: 10px;
}
.toggle-button-container {
padding-top: 20px;
}

View File

@ -0,0 +1,66 @@
form([formGroup]="searchForm", novalidate)
.row
.col-sm-2.col-xs-12
.form-group
label ID кошелька:
input.form-control(formControlName="walletID")
.col-sm-2.col-xs-12
.form-group
label ID личности владельца:
input.form-control(formControlName="identityID")
.col-sm-2.col-xs-12
.form-group
label ID ввода:
input.form-control(formControlName="depositID")
.col-sm-3.col-xs-12
.form-group
label Статус вывода
kof-select(
[items]="depositStatuses",
[placeholder]="'Любой статус'",
formControlName="status")
.col-sm-3.col-xs-12
.toggle-button-container
button.btn.btn-default((click)="toggleAdditionalParamsVisible()") ...
div(*ngIf="additionalParamsVisible")
.row
.col-sm-4.col-xs-12
.form-group
label Временной интервал:
.row
.col-sm-6.col-xs-12
.ui-calendar.search-form
p-calendar.ui-inputtext.search-form(
formControlName="createdAtFrom",
dateFormat = "dd.mm.yy",
[maxDate]="searchForm.controls.createdAtTo.value",
readonlyInput="true")
.col-sm-6.col-xs-12
.ui-calendar.search-form
p-calendar.ui-inputtext.search-form(
formControlName="createdAtTo",
dateFormat = "dd.mm.yy",
[minDate]="searchForm.controls.createdAtFrom.value",
readonlyInput="true")
.col-sm-2.col-xs-12
.form-group
label ID источника средств:
input.form-control(formControlName="sourceID")
.col-sm-2.col-xs-12
.form-group
label Сумма от:
input.form-control(formControlName="amountFrom", type="number")
.col-sm-2.col-xs-12
.form-group
label Сумма до:
input.form-control(formControlName="amountTo", type="number")
.col-sm-2.col-xs-12
.form-group
label Валюта:
input.form-control(formControlName="currencyID")
.row
.col-sm-9.col-xs-12
.reset-button-container
button.btn.btn-default((click)="reset()") Сбросить параметры поиска

View File

@ -0,0 +1,43 @@
import { Component, Output, OnInit, EventEmitter } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { map } from 'lodash';
import { SelectItem } from 'koffing/common/select/select-item';
import { SearchFormService } from './search-form.service';
import { DepositStatusLabel } from '../../deposit-status-label';
@Component({
selector: 'kof-deposits-search-form',
templateUrl: 'search-form.component.pug',
providers: [SearchFormService],
styleUrls: ['search-form.component.less']
})
export class SearchFormComponent implements OnInit {
@Output()
public onSearch: EventEmitter<any> = new EventEmitter<any>();
public searchForm: FormGroup;
public additionalParamsVisible: boolean;
public depositStatuses: SelectItem[];
constructor(private searchFormService: SearchFormService) {}
public ngOnInit() {
this.depositStatuses = map(DepositStatusLabel, (name, key) => new SelectItem(key, name));
this.searchForm = this.searchFormService.searchForm;
this.onSearch.emit(this.searchForm.value);
this.searchForm.valueChanges
.filter(() => this.searchForm.status === 'VALID')
.debounceTime(300)
.subscribe(value => this.onSearch.emit(value));
this.additionalParamsVisible = this.searchFormService.hasFormAdditionalParams();
}
public reset() {
this.onSearch.emit(this.searchFormService.reset());
}
public toggleAdditionalParamsVisible() {
this.additionalParamsVisible = !this.additionalParamsVisible;
}
}

View File

@ -0,0 +1,92 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { mapValues, isEqual, chain, keys, difference, clone } from 'lodash';
import { ActivatedRoute, Params, Router } from '@angular/router';
import * as moment from 'moment';
@Injectable()
export class SearchFormService {
public searchForm: FormGroup;
private urlDateFormat = 'YYYY-MM-DD';
private defaultValues: any;
private mainSearchFields = ['walletID', 'identityID', 'status', 'depositID'];
constructor(private fb: FormBuilder, private router: Router, private route: ActivatedRoute) {
this.searchForm = this.initForm();
this.route.queryParams.subscribe(queryParams => this.updateFormValue(queryParams));
this.searchForm.valueChanges.subscribe(values => this.updateQueryParams(values));
}
public hasFormAdditionalParams(): boolean {
const formFields = chain(this.searchForm.getRawValue())
.map((value: string, key: string) => (isEqual(value, '') ? null : key))
.filter(mapped => mapped !== null && mapped !== 'limit')
.value();
const defaultFields = keys(this.defaultValues);
return difference(formFields, defaultFields, this.mainSearchFields).length > 0;
}
public reset(): any {
this.searchForm.reset(this.defaultValues);
return this.defaultValues;
}
private updateFormValue(queryParams: Params) {
if (isEqual(queryParams, {})) {
this.updateQueryParams(this.defaultValues);
} else {
this.searchForm.patchValue(this.queryParamsToFormValue(queryParams));
}
}
private updateQueryParams(value: any) {
const queryParams = this.formValueToQueryParams(value);
this.router.navigate(['wallets', 'deposits'], { queryParams });
}
private initForm(): FormGroup {
const form = this.fb.group({
walletID: '',
identityID: '',
sourceID: '',
depositID: '',
status: '',
createdAtFrom: moment()
.subtract(1, 'month')
.startOf('day')
.toDate(),
createdAtTo: moment()
.endOf('day')
.toDate(),
amountFrom: '',
amountTo: '',
currencyID: '',
continuationToken: '',
limit: [20, Validators.required]
});
this.defaultValues = clone(form.value);
return form;
}
private formValueToQueryParams(formValue: any): Params {
const mapped = mapValues(formValue, value => (isEqual(value, '') ? null : value));
return {
...mapped,
createdAtFrom: moment(formValue.createdAtFrom).format(this.urlDateFormat),
createdAtTo: moment(formValue.createdAtTo).format(this.urlDateFormat)
};
}
private queryParamsToFormValue(params: Params): any {
return {
...params,
createdAtFrom: moment(params.createdAtFrom)
.startOf('day')
.toDate(),
createdAtTo: moment(params.createdAtTo)
.endOf('day')
.toDate()
};
}
}

View File

@ -0,0 +1,22 @@
table.table.table-striped
thead
tr
th ID выплаты
th Сумма пополнения
th Комиссия
th Сумма зачисления
th Дата создания
th Статус
th
tbody(*ngFor="let deposit of deposits | async")
tr
td {{deposit.id}}
td {{deposit.body.amount | kofRoubleCurrency}} {{deposit.body.currency | kofCurrency}}
td {{deposit.fee.amount | kofRoubleCurrency }} {{deposit.fee.currency | kofCurrency }}
td {{deposit.body.amount - deposit.fee.amount | kofRoubleCurrency}} {{deposit.body.currency | kofCurrency}}
td {{deposit.createdAt | date: "dd.MM.yyyy HH:mm:ss"}}
td
span.label.label-default([ngClass]="getLabelClass(deposit.status)") {{deposit.status | kofDepositStatus}}
td
.pull-right
button.btn.btn-xs.btn-default((click)="gotToDepositDetails(deposit.id)") Детали

View File

@ -0,0 +1,28 @@
import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Router } from '@angular/router';
import { DepositStatus, Deposit } from 'koffing/backend/wapi/model/deposit';
@Component({
selector: 'kof-deposits-search-result',
templateUrl: 'search-result.component.pug'
})
export class SearchResultComponent {
@Input()
public deposits: Observable<Deposit[]>;
constructor(private router: Router) {}
public getLabelClass(status: string) {
return {
'label-success': status === DepositStatus.Succeeded,
'label-danger': status === DepositStatus.Failed,
'label-warning': status === DepositStatus.Pending
};
}
public gotToDepositDetails(depositID: string) {
this.router.navigate(['wallets', 'deposits', depositID]);
}
}

View File

View File

@ -0,0 +1,9 @@
.row
.col-xs-12.col-md-10.col-md-offset-1
.x_panel
.x_title
h5 Поиск пополнений
.x_content
.row
.col-xs-12
kof-wallets-deposits

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
templateUrl: 'deposits.component.pug',
styleUrls: ['deposits.component.less']
})
export class DepositsComponent {}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { CalendarModule } from 'primeng/primeng';
import { CommonModule } from 'koffing/common/common.module';
import { DepositsComponent } from './deposits.component';
import { DepositsTableComponent } from './deposits-table/deposits-table.component';
import { SearchFormComponent } from './deposits-table/search-form/search-form.component';
import { SearchResultComponent } from './deposits-table/search-result/search-result.component';
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, CommonModule, CalendarModule],
declarations: [
DepositsComponent,
DepositsTableComponent,
SearchFormComponent,
SearchResultComponent
]
})
export class DepositsModule {}

View File

@ -2,7 +2,7 @@ kof-http-error-handle
div([ngClass]="{'nav-md': !isSidebarOpened, 'nav-sm': isSidebarOpened}")
.container.body
.main_container
kof-shop-top-panel
kof-top-panel
kof-sidebar
.right_col
.outlet

View File

@ -1,11 +0,0 @@
.top_nav
nav.nav_menu
.nav.toggle.toggle-menu
a((click)="toggleMenu()")
i.fa.fa-bars
.nav.toggle.logo
a([routerLink]=["/"])
| RBKmoney
.nav.toggle.shop-selector
kof-shop-selector
kof-top-panel-actions

View File

@ -1,15 +0,0 @@
import { Component } from '@angular/core';
import { ToggleMenuBroadcaster } from 'koffing/broadcaster';
@Component({
selector: 'kof-shop-top-panel',
templateUrl: 'shop-top-panel.component.pug'
})
export class ShopTopPanelComponent {
constructor(private toggleMenuBroadcaster: ToggleMenuBroadcaster) {}
public toggleMenu() {
this.toggleMenuBroadcaster.fire();
}
}

View File

@ -3,7 +3,6 @@
border: none;
font-weight: 500;
cursor: pointer;
text-decoration: underline;
-webkit-appearance: none;
-moz-appearance: none;
padding: 8px 15px;

View File

@ -1,2 +1,3 @@
select.shop-selector([(ngModel)]="selected")
option(value="", disabled="disabled") Магазины
option(*ngFor="let item of selectorItems", [value]="item.value") {{item.label}}

View File

@ -12,7 +12,7 @@ import { SelectItem } from './select-item';
export class ShopSelectorComponent implements OnInit {
public selectorItems: SelectItem[];
private selectedShopID: string;
private selectedShopID: string = '';
constructor(private shopSelectorService: ShopSelectorService) {}

View File

@ -26,15 +26,19 @@ export class ShopSelectorService {
public getActiveShopID(): string {
const routeShopID = this.route.snapshot.params['shopID'];
return routeShopID ? routeShopID : this.getFromStorage(this.shops);
return routeShopID ? routeShopID : '';
}
public navigateToShop(shopID: string) {
ShopIDStorage.set(shopID);
const hasChildren = this.route.children.length > 0;
const childRoute = hasChildren ? this.route.children[0].routeConfig.path : 'invoices';
const childComponents = childRoute.split('/');
this.router.navigate(['shop', shopID].concat(childComponents));
let childRoute = ['invoices'];
if (
this.route.children.length &&
this.route.children[0].parent.routeConfig.path === 'shops'
) {
childRoute = this.route.children[0].routeConfig.path.split('/');
}
this.router.navigate(['shop', shopID].concat(childRoute));
}
private toSelectorItems(shops: Shop[]): SelectItem[] {

View File

@ -1,5 +1,4 @@
.top-panel-breadcrumb {
width: 50%;
display: table-cell;
vertical-align: middle;
background: none;
@ -10,3 +9,14 @@
display: none;
}
}
.top-panel__link.top-panel__link.top-panel__link {
@media (max-width: 600px) {
display: none;
}
}
/deep/.top-panel__link:hover *,
/deep/.top-panel__link_active * {
text-decoration: underline;
}

View File

@ -1,8 +1,14 @@
.top_nav
nav.nav_menu
.nav.toggle.toggle-menu
a((click)="toggleMenu()")
i.fa.fa-bars
.nav.toggle.logo
a([routerLink]=["/"])
| RBKmoney
a([routerLink]=["/"]) RBKmoney
div([class]="'nav toggle shop-selector top-panel__link' + (activeLink === links.shop ? ' top-panel__link_active' : '')")
kof-shop-selector
div([class]="'nav toggle top-panel__link' + (activeLink === links.wallets ? ' top-panel__link_active' : '')")
a([routerLink]=["/wallets"]) Кошельки
ol.breadcrumb.top-panel-breadcrumb(*ngIf="breadcrumbConfig.length > 1")
li(*ngFor="let config of breadcrumbConfig", [ngClass]="{'active': !config.routerLink}")
a(*ngIf="config.routerLink", [routerLink]="config.routerLink") {{config.label}}

View File

@ -1,6 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { BreadcrumbBroadcaster, BreadcrumbConfig } from 'koffing/broadcaster';
import {
ToggleMenuBroadcaster,
BreadcrumbConfig,
BreadcrumbBroadcaster
} from 'koffing/broadcaster';
enum Links {
shop = 'shop',
wallets = 'wallets'
}
@Component({
selector: 'kof-top-panel',
@ -9,6 +19,8 @@ import { BreadcrumbBroadcaster, BreadcrumbConfig } from 'koffing/broadcaster';
})
export class TopPanelComponent implements OnInit {
public breadcrumbConfig: BreadcrumbConfig[] = [];
private activeLink: Links;
private links = Links;
private initialBreadcrumbConfig: BreadcrumbConfig[] = [
{
@ -17,11 +29,29 @@ export class TopPanelComponent implements OnInit {
}
];
constructor(private breadcrumbBroadcaster: BreadcrumbBroadcaster) {}
constructor(
private toggleMenuBroadcaster: ToggleMenuBroadcaster,
private breadcrumbBroadcaster: BreadcrumbBroadcaster,
private router: Router
) {
this.router.events.subscribe(e => {
if (e instanceof NavigationEnd) {
for (const link in Links) {
if (e.url.search(new RegExp(`^/${link}`, 'g')) !== -1) {
this.activeLink = link as Links;
}
}
}
});
}
public ngOnInit() {
this.breadcrumbBroadcaster.on().subscribe((config: BreadcrumbConfig[]) => {
this.breadcrumbConfig = this.initialBreadcrumbConfig.concat(config);
});
}
public toggleMenu() {
this.toggleMenuBroadcaster.fire();
}
}

View File

@ -7,7 +7,15 @@
a(routerLink="/wallets/list")
i.fa.fa-suitcase
| Кошельки
//- li([routerLinkActive]="['active']")
//- a(routerLink="/wallets/deposits")
//- i.fa.fa-level-down
//- | Пополнения
li([routerLinkActive]="['active']")
a(routerLink="/wallets/withdrawals")
i.fa.fa-level-up
| Выводы
li([routerLinkActive]="['active']")
a(routerLink="/wallets/documents")
i.fa.fa-file-o
| Документы

View File

@ -20,10 +20,15 @@ import { DocumentsComponent } from 'koffing/documents/documents.component';
import { InitCreateShopComponent } from 'koffing/management/init-create-shop/init-create-shop.component';
import { ReportsComponent } from 'koffing/documents/reports/reports.component';
import { ReportType } from 'koffing/backend';
import { ReportType as WalletsReportType } from 'koffing/backend/wapi/model/report';
import { WalletsComponent } from 'koffing/wallets/wallets.component';
import { WithdrawalComponent } from 'koffing/withdrawal/withdrawal.component';
import { WalletsContainerComponent } from './components/wallets-container/wallets-container.component';
import { WithdrawalsComponent } from 'koffing/withdrawals/withdrawals.component';
import { WalletsReportsComponent } from 'koffing/wallets-documents/reports/wallets-reports.component';
import { WalletsDocumentsComponent } from 'koffing/wallets-documents/wallets-documents.component';
import { DepositsComponent } from 'koffing/deposits/deposits.component';
import { DepositComponent } from 'koffing/deposit/deposit.component';
@NgModule({
imports: [
@ -129,6 +134,14 @@ import { WithdrawalsComponent } from 'koffing/withdrawals/withdrawals.component'
path: 'list',
component: WalletsComponent
},
{
path: 'deposits',
component: DepositsComponent
},
{
path: 'deposits/:depositID',
component: DepositComponent
},
{
path: 'withdrawals',
component: WithdrawalsComponent
@ -136,6 +149,21 @@ import { WithdrawalsComponent } from 'koffing/withdrawals/withdrawals.component'
{
path: 'withdrawals/:withdrawalID',
component: WithdrawalComponent
},
{
path: 'documents',
component: WalletsDocumentsComponent,
children: [
{
path: '',
redirectTo: 'reports/' + WalletsReportType.withdrawalRegistry,
pathMatch: 'full'
},
{
path: 'reports/:type',
component: WalletsReportsComponent
}
]
}
]
}

View File

@ -20,16 +20,18 @@ import { AnalyticsModule } from 'koffing/analytics/analytics.module';
import { ShopInfoModule } from 'koffing/shop-info/shop-info.module';
import { PayoutsModule } from 'koffing/payouts/payouts.module';
import { LandingContainerComponent } from './components/landing-container/landing-container.component';
import { TopPanelComponent } from './components/top-panel/top-panel.component';
import { TopPanelActionsComponent } from './components/top-panel-actions/top-panel-actions.component';
import { ShopSelectorComponent } from 'koffing/root/components/shop-container/shop-top-panel/shop-selector/shop-selector.component';
import { ShopTopPanelComponent } from 'koffing/root/components/shop-container/shop-top-panel/shop-top-panel.component';
import { ShopSelectorComponent } from 'koffing/root/components/top-panel/shop-selector/shop-selector.component';
import { TopPanelComponent } from 'koffing/root/components/top-panel/top-panel.component';
import { ShopContainerComponent } from './components/shop-container/shop-container.component';
import { WalletsModule } from 'koffing/wallets/wallets.module';
import { WithdrawalModule } from 'koffing/withdrawal/withdrawal.module';
import { WalletsContainerComponent } from './components/wallets-container/wallets-container.component';
import { WalletsSidebarComponent } from './components/wallets-container/wallets-sidebar/wallets-sidebar.component';
import { WithdrawalsModule } from 'koffing/withdrawals/withdrawals.module';
import { WalletsDocumentsModule } from 'koffing/wallets-documents/wallets-documents.module';
import { DepositsModule } from 'koffing/deposits/deposits.module';
import { DepositModule } from 'koffing/deposit/deposit.module';
@NgModule({
imports: [
@ -51,17 +53,19 @@ import { WithdrawalsModule } from 'koffing/withdrawals/withdrawals.module';
PayoutsModule,
WalletsModule,
WithdrawalModule,
WithdrawalsModule
WithdrawalsModule,
WalletsDocumentsModule,
DepositsModule,
DepositModule
],
declarations: [
ContainerComponent,
LandingContainerComponent,
ShopContainerComponent,
SidebarComponent,
ShopTopPanelComponent,
TopPanelComponent,
HttpErrorHandleComponent,
ShopSelectorComponent,
TopPanelComponent,
TopPanelActionsComponent,
WalletsContainerComponent,
WalletsSidebarComponent

View File

@ -0,0 +1,10 @@
div(*ngFor="let file of files")
form.form-horizontal.css-form.form-list
.form-group
.col-xs-12.col-sm-2
label.text-left Имя файла:
.col-xs-12.col-sm-10
div {{file}}
.form-group
.col-xs-12
.btn.btn-sm.btn-primary((click)="downloadFile(file, file)") Загрузить

View File

@ -0,0 +1,43 @@
import { Component, Input } from '@angular/core';
import * as moment from 'moment';
import { DownloadFileService } from 'koffing/backend/wapi/download-file.service';
import { Report } from 'koffing/backend/wapi/model/report';
@Component({
selector: 'kof-report-files',
templateUrl: 'report-files.component.pug'
})
export class ReportFilesComponent {
@Input()
public files: Report['files'];
@Input()
public reportID: number;
constructor(private downloadFileService: DownloadFileService) {}
public downloadFile(fileID: string, fileName: string) {
this.downloadFileService
.downloadFile(
{ fileID },
{
expiresAt: moment()
.add(1, 'minute')
.utc()
.format()
}
)
.subscribe(file => this.download(fileName, file.url));
}
private download(fileName: string, url: string) {
const a: any = document.createElement('a');
document.body.appendChild(a);
a.style = 'display: none';
a.href = url;
a.download = fileName;
a.click();
a.parentNode.removeChild(a);
}
}

View File

@ -0,0 +1,11 @@
import { Report } from 'koffing/backend/wapi/model/report';
export class ReportTableItem {
public report: Report;
public isVisible: boolean;
constructor(report: Report, isVisible: boolean) {
this.report = report;
this.isVisible = isVisible;
}
}

View File

@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'kofReportStatus'
})
export class ReportStatusPipe implements PipeTransform {
private STATUSES = {
pending: 'В процессе формирования',
created: 'Сформирован'
};
public transform(input: string): string {
const status = this.STATUSES[input];
return status ? status : input;
}
}

View File

@ -0,0 +1,21 @@
table.table.table-striped
thead
tr
th Идентификатор
th.hidden-xs Временной интервал
th Время создания
th Статус
th
tbody(*ngFor="let item of filtered(reportItems)")
tr
td {{item.report.id}}
td.hidden-xs {{item.report.fromTime | date: "dd.MM.yyyy"}} - {{item.report.toTime | date: "dd.MM.yyyy"}}
td {{item.report.createdAt | date: "dd.MM.yyyy HH:mm:ss"}}
td {{item.report.status | kofReportStatus}}
td
.pull-right(*ngIf="item.report.status === reportStatuses.created")
button.btn.btn-xs.btn-default((click)="toggleFilesPanel(item)")
| {{item.isVisible ? 'Скрыть' : 'Показать'}} файлы
tr(*ngIf="item.isVisible")
td.row(colspan="5")
kof-report-files([files]="item.report.files", [reportID]="item.report.id")

View File

@ -0,0 +1,38 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { get } from 'lodash';
import { Report, ReportStatus } from 'koffing/backend/wapi/model/report';
import { ReportTableItem } from './report-item';
import { ReportsFilter } from '../wallets-reports-filter';
@Component({
selector: 'kof-search-reports-result',
templateUrl: 'search-reports-result.component.pug'
})
export class SearchReportsResultComponent implements OnInit {
@Input()
public reports$: Observable<Report[]>;
@Input()
public filter: ReportsFilter;
public reportItems: ReportTableItem[];
public reportStatuses = ReportStatus;
public ngOnInit() {
this.reports$.subscribe(reports => {
this.reportItems = reports.map(report => new ReportTableItem(report, false));
});
}
public toggleFilesPanel(item: ReportTableItem) {
item.isVisible = !item.isVisible;
}
public filtered(reports: ReportTableItem[]): ReportTableItem[] {
return this.filter && reports
? reports.filter(report => get(report, this.filter.path) === this.filter.value)
: reports;
}
}

View File

@ -0,0 +1,4 @@
export interface ReportsFilter {
path: string;
value: any;
}

View File

@ -0,0 +1,33 @@
div.ui-calendar.search-form {
width: 100%;
margin-bottom: 5px;
}
p-calendar.search-form > span {
width: 100%;
}
p-calendar.search-form input.ui-inputtext {
width: 100%;
height: 34px;
margin: 0;
padding: 6px 12px;
}
p-calendar.has-error input.ui-inputtext {
border-color: #a94442;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
input.has-error {
border-color: #a94442;
}
input.ng-dirty.ng-invalid {
border-color: #a94442;
}
.create-report {
margin-top: 24px;
}

View File

@ -0,0 +1,22 @@
form([formGroup]="form")
.row
.col-xs-12.col-sm-3
.form-group
label ID личности владельца:
input.form-control(formControlName="identityID")
.col-xs-12.col-sm-5
kof-date-range((onSelect)="selectDateRange($event)")
.col-xs-12.col-sm-4
.row
.col-xs-12
button.btn.btn-default.create-report(
(click)="createReports()",
[disabled]="isLoading") Создать
button.btn.btn-default.create-report(
(click)="getReports()",
[disabled]="isLoading") Обновить
.row
.col-xs-12
kof-search-reports-result([reports$]="reports$", [filter]="filter")
div(*ngIf="isLoading")
a.fa.fa-cog.fa-spin.fa-fw.loading_spinner

View File

@ -0,0 +1,92 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder } from '@angular/forms';
import { BehaviorSubject, Subject } from 'rxjs';
import { Report, ReportType } from 'koffing/backend/wapi/model/report';
import { DateRange } from 'koffing/common/date-range/date-range';
import { ReportsFilter } from './wallets-reports-filter';
import { ReportsService } from 'koffing/backend/wapi/reports.service';
@Component({
selector: 'kof-wallets-reports',
templateUrl: 'wallets-reports.component.pug',
styleUrls: ['./wallets-reports.component.less']
})
export class WalletsReportsComponent implements OnInit {
public reports$: Subject<Report[]> = new Subject();
public filter: ReportsFilter;
public isLoading = false;
public reportTypes = ReportType;
public reportType: ReportType;
private dateRange: BehaviorSubject<DateRange> = new BehaviorSubject(null);
private form: FormGroup;
constructor(
private route: ActivatedRoute,
private reportsService: ReportsService,
private fb: FormBuilder
) {
this.form = this.fb.group({ identityID: [''] });
this.form.valueChanges.subscribe(() => this.getReports());
}
public ngOnInit() {
this.route.params.subscribe(params => {
this.reportType = params['type'];
this.filter = {
path: 'report.type',
value: params['type']
};
});
this.dateRange.subscribe(() => this.getReports());
}
public selectDateRange(dateRange: DateRange) {
this.dateRange.next(dateRange);
}
public createReports() {
this.isLoading = true;
this.reportsService
.createReport(
{ identityID: this.form.value.identityID },
{
...this.dateRange.getValue(),
reportType: this.reportType
}
)
.subscribe(
() => {
this.isLoading = false;
this.getReports();
},
e => this.failed(e)
);
}
private getReports() {
const identityID = this.form.value.identityID;
const dateRange = this.dateRange.getValue();
this.reports$.next([]);
if (identityID && dateRange && dateRange.fromTime && dateRange.toTime) {
this.isLoading = true;
const { fromTime, toTime } = dateRange;
this.reportsService
.getReports({ identityID }, { fromTime, toTime, type: this.reportType })
.subscribe(
reports => {
this.isLoading = false;
this.reports$.next(reports);
},
e => this.failed(e)
);
}
}
private failed(e: any) {
console.error(e);
this.isLoading = false;
}
}

View File

@ -0,0 +1,9 @@
.row
.col-xs-12.col-md-10.col-md-offset-1
.x_panel
.x_content
ul.nav.nav-tabs.bar_tabs
li.hand-cursor([routerLink]="['reports', reportTypes.withdrawalRegistry]", [routerLinkActive]="['active']")
a Отчет по выводам
.tab-content
router-outlet

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
import { ReportType } from 'koffing/backend/wapi/model/report';
@Component({
templateUrl: 'wallets-documents.component.pug'
})
export class WalletsDocumentsComponent {
public reportTypes = ReportType;
}

View File

@ -0,0 +1,33 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from 'koffing/common/common.module';
import { BackendModule } from 'koffing/backend/backend.module';
import { WalletsDocumentsComponent } from './wallets-documents.component';
import { SearchReportsResultComponent } from './reports/search-result/search-reports-result.component';
import { ReportFilesComponent } from './reports/search-result/report-files/report-files.component';
import { WalletsReportsComponent } from './reports/wallets-reports.component';
import { ReportsService } from 'koffing/backend/wapi/reports.service';
import { ReportStatusPipe as WalletsReportStatusPipe } from './reports/search-result/report-status.pipe';
@NgModule({
providers: [ReportsService],
imports: [
RouterModule,
BrowserModule,
CommonModule,
BackendModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
WalletsDocumentsComponent,
SearchReportsResultComponent,
ReportFilesComponent,
WalletsReportsComponent,
WalletsReportStatusPipe
]
})
export class WalletsDocumentsModule {}

View File

@ -40,7 +40,7 @@ form {
font-weight: 500;
}
.shop-selector {
@media (max-width: 991px) {
@media (max-width: 600px) {
display: none;
}
}