Integrate daterange filter into reports module (#273)

This commit is contained in:
Ildar Galeev 2020-08-17 18:21:56 +03:00 committed by GitHub
parent 8cf96c561f
commit 90a29f1b7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 430 additions and 352 deletions

View File

@ -24,16 +24,15 @@ export class ReportsService {
return this.reportsService.getReport(genXRequestID(), reportID);
}
searchReports({ fromTime, toTime, reportTypes, shopIDs, continuationToken }: SearchReportsReq) {
console.warn('Skip types, return after backend fix', reportTypes);
searchReports({ fromTime, toTime, reportTypes, continuationToken }: SearchReportsReq) {
return this.reportsService.searchReports(
genXRequestID(),
toDateLike(fromTime),
toDateLike(toTime),
['paymentRegistry', 'provisionOfService'],
reportTypes,
undefined,
undefined,
undefined,
shopIDs,
continuationToken
);
}

View File

@ -1 +0,0 @@
export * from './filter-shops';

View File

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

View File

@ -19,7 +19,6 @@ import { LayoutModule } from '@dsh/components/layout';
import { StateNavModule } from '@dsh/components/navigation';
import { TableModule } from '@dsh/components/table';
import { FilterShopsModule } from '../../../../components';
import { LanguageModule } from '../../../../language';
import { ToMajorModule } from '../../../../to-major';
import { ShopSelectorModule } from '../../../shop-selector';
@ -53,7 +52,6 @@ import { TableComponent } from './table';
RangeDatepickerModule,
EmptySearchResultModule,
ShopSelectorModule,
FilterShopsModule,
FiltersModule,
],
declarations: [PaymentsComponent, SearchFormComponent, PaymentStatusColorPipe, TableComponent],

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, of, Subject } from 'rxjs';
import { first, map, pluck, shareReplay, switchMap } from 'rxjs/operators';
import { map, pluck, shareReplay, switchMap, take } from 'rxjs/operators';
@Injectable()
export class ExpandedIdManager<T extends { id: string | number }> {
@ -10,7 +10,7 @@ export class ExpandedIdManager<T extends { id: string | number }> {
data$: Observable<T[]> = of([]);
expandedId$: Observable<number> = this.route.fragment.pipe(
first(),
take(1),
switchMap((fragment) => this.data$.pipe(map((d) => d.findIndex(({ id }) => id + '' === fragment)))),
shareReplay(1)
);

View File

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { ReportsService as ReportsApiService } from '../../../api';
import { Report } from '../../../api-codegen/anapi';
import { booleanDebounceTime } from '../../../custom-operators';
import { PartialFetcher } from '../../partial-fetcher';
import { mapToTimestamp } from '../operations/operators';
import { SearchFiltersParams } from './reports-search-filters';
@Injectable()
export class FetchReportsService extends PartialFetcher<Report, SearchFiltersParams> {
isLoading$: Observable<boolean> = this.doAction$.pipe(booleanDebounceTime(), shareReplay(1));
lastUpdated$: Observable<string> = this.searchResult$.pipe(mapToTimestamp, shareReplay(1));
constructor(private reportsService: ReportsApiService) {
super();
}
protected fetch(params: SearchFiltersParams, continuationToken: string) {
const reportTypes = ['paymentRegistry', 'provisionOfService'] as Report.ReportTypeEnum[];
return this.reportsService.searchReports({ ...params, reportTypes, continuationToken });
}
}

View File

@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { ExpandedIdManager } from '@dsh/app/shared/services';
import { Report } from '../../../api-codegen/anapi';
import { FetchReportsService } from './fetch-reports.service';
@Injectable()
export class PayoutsExpandedIdManager extends ExpandedIdManager<Report> {
constructor(
protected route: ActivatedRoute,
protected router: Router,
private fetchReportsService: FetchReportsService
) {
super(route, router);
}
protected get dataSet$(): Observable<Report[]> {
return this.fetchReportsService.searchResult$;
}
}

View File

@ -26,7 +26,7 @@ export class ReportFilesComponent implements OnInit {
ngOnInit() {
this.reportFilesService.errorOccurred$.subscribe(() =>
this.snackBar.open(this.transloco.translate('commonError', null, 'reports|scoped'), 'OK', {
this.snackBar.open(this.transloco.translate('errors.downloadReportError', null, 'reports|scoped'), 'OK', {
duration: 2000,
})
);

View File

@ -20,7 +20,9 @@
>{{ report.fromTime | date: 'dd MMMM yyyy, HH:mm' }} -
{{ report.toTime | date: 'dd MMMM yyyy, HH:mm' }}</dsh-details-item
>
<dsh-shop-details-item *ngIf="report?.shopID" [shopID]="report?.shopID"></dsh-shop-details-item>
<dsh-details-item *ngIf="report?.shopID" [title]="t.shop">
{{ report?.shopID | shopDetails }}
</dsh-details-item>
<mat-divider></mat-divider>
<dsh-report-files [reportID]="report.id" [files]="report.files"></dsh-report-files>
</div>

View File

@ -6,13 +6,13 @@ import { MatIconModule } from '@angular/material/icon';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoModule } from '@ngneat/transloco';
import { ApiModelRefsModule } from '@dsh/app/shared/pipes';
import { ButtonModule } from '@dsh/components/buttons';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
import { ReportsModule } from '../../../../api';
import { ReportFilesModule } from '../report-files';
import { ShopDetailsItemModule } from '../shop-details-item';
import { ReportDetailsComponent } from './report-details';
import { ReportRowComponent } from './report-row';
import { ReportRowHeaderComponent } from './report-row-header';
@ -32,9 +32,9 @@ import { ReportsListComponent } from './reports-list.component';
CommonModule,
MatSnackBarModule,
MatDividerModule,
ShopDetailsItemModule,
ReportFilesModule,
IndicatorsModule,
ApiModelRefsModule,
],
declarations: [
ReportsListComponent,

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { QueryParamsStore } from '@dsh/app/shared/services';
import { SearchFiltersParams } from './reports-search-filters';
@Injectable()
export class ReportsSearchFiltersStore extends QueryParamsStore<SearchFiltersParams> {
constructor(protected route: ActivatedRoute, protected router: Router) {
super(router, route);
}
mapToData(queryParams: Params): SearchFiltersParams {
return queryParams as SearchFiltersParams;
}
mapToParams(data: SearchFiltersParams): Params {
return data;
}
}

View File

@ -0,0 +1,8 @@
import { Daterange } from '@dsh/pipes/daterange';
import { SearchFiltersParams } from './search-filters-params';
export const daterangeToSearchFilterParams = ({ begin, end }: Daterange): SearchFiltersParams => ({
fromTime: begin.utc().format(),
toTime: end.utc().format(),
});

View File

@ -0,0 +1,8 @@
import moment from 'moment';
import { Daterange } from '@dsh/pipes/daterange';
export const getDefaultDaterange = (): Daterange => ({
begin: moment().startOf('M'),
end: moment().endOf('M'),
});

View File

@ -0,0 +1,3 @@
export * from './reports-search-filters.module';
export * from './reports-search-filters.component';
export * from './search-filters-params';

View File

@ -0,0 +1,12 @@
<div
*transloco="let t; scope: 'reports'; read: 'reports.searchFilters'"
fxLayout="row"
fxLayoutGap="16px"
fxLayoutAlign="center center"
>
<div class="dsh-body-1">{{ t.dateRangeDescription }}:</div>
<dsh-daterange-filter
[selected]="daterange"
(selectedChange)="daterangeSelectionChange($event)"
></dsh-daterange-filter>
</div>

View File

@ -0,0 +1,44 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { Daterange } from '@dsh/pipes/daterange';
import { daterangeToSearchFilterParams } from './daterange-to-search-filter-params';
import { getDefaultDaterange } from './get-default-daterange';
import { searchFilterParamsToDaterange } from './search-filter-params-to-daterange';
import { SearchFiltersParams } from './search-filters-params';
@Component({
selector: 'dsh-reports-search-filters',
templateUrl: 'reports-search-filters.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReportsSearchFiltersComponent implements OnChanges {
@Input() initParams: SearchFiltersParams;
@Output() searchParamsChanges: EventEmitter<SearchFiltersParams> = new EventEmitter();
daterange: Daterange;
ngOnChanges({ initParams }: SimpleChanges) {
if (initParams && initParams.firstChange && initParams.currentValue) {
const v = initParams.currentValue;
this.daterange = !(v.fromTime || v.toTime) ? getDefaultDaterange() : searchFilterParamsToDaterange(v);
this.daterangeSelectionChange(this.daterange);
}
}
daterangeSelectionChange(v: Daterange | null) {
const daterange = v === null ? getDefaultDaterange() : v;
if (v === null) {
this.daterange = daterange;
}
this.searchParamsChanges.emit(daterangeToSearchFilterParams(daterange));
}
}

View File

@ -0,0 +1,15 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslocoModule } from '@ngneat/transloco';
import { FiltersModule } from '@dsh/components/filters';
import { ReportsSearchFiltersComponent } from './reports-search-filters.component';
@NgModule({
imports: [CommonModule, TranslocoModule, FiltersModule, FlexLayoutModule],
declarations: [ReportsSearchFiltersComponent],
exports: [ReportsSearchFiltersComponent],
})
export class ReportsSearchFiltersModule {}

View File

@ -0,0 +1,10 @@
import moment from 'moment';
import { Daterange } from '@dsh/pipes/daterange';
import { SearchFiltersParams } from './search-filters-params';
export const searchFilterParamsToDaterange = ({ fromTime, toTime }: SearchFiltersParams): Daterange => ({
begin: moment(fromTime),
end: moment(toTime),
});

View File

@ -0,0 +1,4 @@
export interface SearchFiltersParams {
fromTime: string;
toTime: string;
}

View File

@ -1,16 +1,14 @@
<div class="dsh-reports" *transloco="let r; scope: 'reports'; read: 'reports'" fxLayout="column" fxLayoutGap="32px">
<h1 class="dsh-display-1">{{ r.title }}</h1>
<div fxLayout="row" fxLayoutGap="24px">
<div fxFlex class="dsh-body-1">
{{ r.description }}
</div>
<div>
<button dsh-button color="accent" (click)="create()">
{{ r.create.button }}
</button>
</div>
<div fxLayout="row" fxLayoutAlign="space-between">
<dsh-reports-search-filters
[initParams]="initSearchParams$ | async"
(searchParamsChanges)="searchParamsChanges($event)"
></dsh-reports-search-filters>
<button dsh-button color="accent" (click)="create()">
{{ r.create.button }}
</button>
</div>
<dsh-reports-search-form></dsh-reports-search-form>
<dsh-reports-list
[expandedId]="expandedId$ | async"
(expandedIdChange)="expandedIdChange($event)"
@ -18,7 +16,9 @@
[lastUpdated]="lastUpdated$ | async"
(refreshData)="refresh()"
></dsh-reports-list>
<dsh-empty-search-result *ngIf="(reports$ | async)?.length === 0"></dsh-empty-search-result>
<dsh-empty-search-result
*ngIf="!(fetchErrors$ | async) && (reports$ | async)?.length === 0"
></dsh-empty-search-result>
<dsh-spinner *ngIf="isLoading$ | async" fxLayoutAlign="center"></dsh-spinner>
</div>
<dsh-scroll-up></dsh-scroll-up>

View File

@ -1,9 +1,4 @@
.dsh-reports {
max-width: 744px;
margin: auto;
&-empty {
padding-top: 30px;
text-align: center;
}
}

View File

@ -1,49 +1,67 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router';
import { TranslocoService } from '@ngneat/transloco';
import { filter, shareReplay } from 'rxjs/operators';
import { filter, pluck, take } from 'rxjs/operators';
import { Report } from '../../../api-codegen/anapi';
import { booleanDebounceTime } from '../../../custom-operators';
import { mapToTimestamp } from '../operations/operators';
import { ShopService } from '../../../api';
import { filterShopsByEnv, mapToShopInfo } from '../operations/operators';
import { CreateReportDialogComponent } from './create-report-dialog';
import { ExpandedIdManager } from './expanded-id-manager.service';
import { ReportsService } from './reports.service';
import { FetchReportsService } from './fetch-reports.service';
import { PayoutsExpandedIdManager } from './payouts-expanded-id-manager.service';
import { SearchFiltersParams } from './reports-search-filters';
import { ReportsSearchFiltersStore } from './reports-search-filters-store.service';
@Component({
selector: 'dsh-reports',
templateUrl: 'reports.component.html',
styleUrls: ['reports.component.scss'],
providers: [ReportsService, ExpandedIdManager],
providers: [FetchReportsService, ReportsSearchFiltersStore, PayoutsExpandedIdManager],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReportsComponent {
reports$ = this.reportsService.searchResult$;
isLoading$ = this.reportsService.doAction$.pipe(booleanDebounceTime(), shareReplay(1));
lastUpdated$ = this.reportsService.searchResult$.pipe(mapToTimestamp, shareReplay(1));
expandedId$ = this.expandedIdManager.expandedId$;
export class ReportsComponent implements OnInit {
reports$ = this.fetchReportsService.searchResult$;
isLoading$ = this.fetchReportsService.isLoading$;
lastUpdated$ = this.fetchReportsService.lastUpdated$;
expandedId$ = this.payoutsExpandedIdManager.expandedId$;
initSearchParams$ = this.reportsSearchFiltersStore.data$.pipe(take(1));
fetchErrors$ = this.fetchReportsService.errors$;
private shopsInfo$ = this.route.params.pipe(
pluck('envID'),
filterShopsByEnv(this.shopService.shops$),
mapToShopInfo
);
constructor(
private reportsService: ReportsService,
private fetchReportsService: FetchReportsService,
private payoutsExpandedIdManager: PayoutsExpandedIdManager,
private reportsSearchFiltersStore: ReportsSearchFiltersStore,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private transloco: TranslocoService,
private expandedIdManager: ExpandedIdManager<Report>
) {
this.expandedIdManager.data$ = this.reports$;
private shopService: ShopService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.fetchReportsService.errors$.subscribe(() =>
this.snackBar.open(this.transloco.translate('errors.fetchError', null, 'reports|scoped'), 'OK')
);
}
searchParamsChanges(p: SearchFiltersParams) {
this.fetchReportsService.search(p);
this.reportsSearchFiltersStore.preserve(p);
}
expandedIdChange(id: number) {
this.expandedIdManager.expandedIdChange(id);
}
fetchMore() {
this.reportsService.fetchMore();
this.payoutsExpandedIdManager.expandedIdChange(id);
}
refresh() {
this.reportsService.refresh();
this.fetchReportsService.refresh();
}
create() {
@ -52,7 +70,7 @@ export class ReportsComponent {
width: '560px',
disableClose: true,
data: {
shopsInfo$: this.reportsService.shopsInfo$,
shopsInfo$: this.shopsInfo$,
},
})
.afterClosed()

View File

@ -23,9 +23,8 @@ import { ReportsModule as ReportsApiModule } from '../../../api';
import { CreateReportDialogComponent } from './create-report-dialog';
import { ReportsListModule } from './reports-list';
import { ReportsRoutingModule } from './reports-routing.module';
import { ReportsSearchFiltersModule } from './reports-search-filters';
import { ReportsComponent } from './reports.component';
import { SearchFormComponent } from './search-form';
import { ShopDetailsItemModule } from './shop-details-item';
@NgModule({
imports: [
@ -49,10 +48,10 @@ import { ShopDetailsItemModule } from './shop-details-item';
EmptySearchResultModule,
MatDividerModule,
ReportsListModule,
ShopDetailsItemModule,
ScrollUpModule,
ReportsSearchFiltersModule,
],
declarations: [ReportsComponent, SearchFormComponent, CreateReportDialogComponent],
declarations: [ReportsComponent, CreateReportDialogComponent],
exports: [ReportsComponent],
entryComponents: [CreateReportDialogComponent],
})

View File

@ -1,25 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { pluck } from 'rxjs/operators';
import { ReportsService as ReportsApiService, ShopService } from '../../../api';
import { Report } from '../../../api-codegen/anapi';
import { PartialFetcher } from '../../partial-fetcher';
import { filterShopsByEnv, mapToShopInfo } from '../operations/operators';
import { SearchParams } from './search-params';
@Injectable()
export class ReportsService extends PartialFetcher<Report, SearchParams> {
shopsInfo$ = this.route.params.pipe(pluck('envID'), filterShopsByEnv(this.shopService.shops$), mapToShopInfo);
constructor(
private reportsService: ReportsApiService,
private route: ActivatedRoute,
private shopService: ShopService
) {
super();
}
protected fetch(params: SearchParams, continuationToken: string) {
return this.reportsService.searchReports({ ...params, continuationToken });
}
}

View File

@ -1,9 +0,0 @@
import { Range } from '@dsh/components/form-controls';
import { Report } from '../../../../api-codegen/anapi';
export interface FormParams {
date: Range;
reportType: Report.ReportTypeEnum;
shopIDs?: string[];
}

View File

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

View File

@ -1,8 +0,0 @@
import { Report } from '../../../../api-codegen/anapi';
export interface QueryParams {
fromTime: string;
toTime: string;
reportType: Report.ReportTypeEnum;
shopIDs?: string[];
}

View File

@ -1,32 +0,0 @@
<dsh-float-panel *transloco="let r; scope: 'reports'; read: 'reports'">
<form *transloco="let c" [formGroup]="form">
<dsh-justify-wrapper
fxLayout
fxLayoutGap="20px"
fxLayoutAlign="space-between center"
fxLayout.lt-md="column"
fxLayoutAlign.lt-md="space-between stretch"
>
<dsh-range-datepicker formControlName="date" fxFlex></dsh-range-datepicker>
<dsh-shop-selector formControlName="shopIDs" fxFlex></dsh-shop-selector>
</dsh-justify-wrapper>
<dsh-float-panel-actions>
<button dsh-button (click)="reset()">
{{ c.resetSearchParams }}
</button>
</dsh-float-panel-actions>
<dsh-float-panel-more>
<mat-form-field fxFlexFill>
<mat-label>{{ r.filter.type }}</mat-label>
<mat-select formControlName="reportType">
<mat-option>
{{ c.any }}
</mat-option>
<mat-option *ngFor="let type of reportTypes" [value]="type">
{{ r.type[type] }}
</mat-option>
</mat-select>
</mat-form-field>
</dsh-float-panel-more>
</form>
</dsh-float-panel>

View File

@ -1,17 +0,0 @@
import { Component } from '@angular/core';
import { Report } from '../../../../api-codegen/anapi/swagger-codegen';
import { SearchFormService } from './search-form.service';
@Component({
selector: 'dsh-reports-search-form',
templateUrl: 'search-form.component.html',
providers: [SearchFormService],
})
export class SearchFormComponent {
form = this.searchFormService.form;
reset = this.searchFormService.reset;
reportTypes = Object.values(Report.ReportTypeEnum);
constructor(private searchFormService: SearchFormService) {}
}

View File

@ -1,70 +0,0 @@
import { Injectable } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { filter, pluck, take } from 'rxjs/operators';
import { RouteEnv } from '../../../route-env';
import { ReportsService } from '../reports.service';
import { FormParams } from './form-params';
import { QueryParams } from './query-params';
import { toFormValue } from './to-form-value';
import { toQueryParams } from './to-query-params';
import { toSearchParams } from './to-search-params';
@Injectable()
export class SearchFormService {
defaultParams: FormParams = {
shopIDs: [],
reportType: null,
date: {
begin: moment().startOf('month'),
end: moment().endOf('month'),
},
};
form = this.fb.group(this.defaultParams);
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private reportsService: ReportsService
) {
this.init();
}
search(value = this.form.value) {
this.reportsService.search(toSearchParams(value));
}
reset() {
this.form.setValue(this.defaultParams);
}
private init() {
this.route.params
.pipe(
pluck('envID'),
take(1),
filter((e) => e === RouteEnv.test)
)
.subscribe(() => this.form.controls.shopIDs.disable());
this.syncQueryParams();
this.search();
this.form.valueChanges.subscribe((v) => this.search(v));
}
private syncQueryParams() {
const queryParams = this.route.snapshot.queryParams as QueryParams;
const formValue = toFormValue(queryParams, this.defaultParams);
this.form.setValue(formValue);
this.setQueryParams(formValue);
this.form.valueChanges.subscribe((v) => this.setQueryParams(v));
}
private setQueryParams(formValue: FormParams) {
const queryParams = toQueryParams(formValue);
this.router.navigate([location.pathname], { queryParams, preserveFragment: true });
}
}

View File

@ -1,21 +0,0 @@
import moment from 'moment';
import { Report } from '../../../../api-codegen/anapi/swagger-codegen';
import { FormParams } from './form-params';
import { QueryParams } from './query-params';
export function toFormValue(
{ fromTime, toTime, reportType, shopIDs, ...params }: QueryParams,
defaultParams: FormParams
): FormParams {
return {
...defaultParams,
...params,
shopIDs: shopIDs ? (Array.isArray(shopIDs) ? shopIDs : [shopIDs]) : null,
date: {
begin: fromTime ? moment(fromTime) : defaultParams.date.begin,
end: toTime ? moment(toTime) : defaultParams.date.end,
},
reportType: reportType ? (reportType as Report.ReportTypeEnum) : defaultParams.reportType,
};
}

View File

@ -1,11 +0,0 @@
import { FormParams } from './form-params';
import { QueryParams } from './query-params';
export function toQueryParams({ date, shopIDs, ...params }: FormParams): QueryParams {
return {
...params,
shopIDs: shopIDs?.length ? shopIDs : null,
fromTime: date.begin.utc().format(),
toTime: date.end.utc().format(),
};
}

View File

@ -1,13 +0,0 @@
import { Report } from '../../../../api-codegen/anapi';
import { SearchParams } from '../search-params';
import { FormParams } from './form-params';
export function toSearchParams({ reportType, date, shopIDs, ...params }: FormParams): SearchParams {
return {
...params,
shopIDs: shopIDs?.length ? shopIDs : null,
reportTypes: reportType ? [reportType] : Object.values(Report.ReportTypeEnum),
fromTime: date.begin.utc().format(),
toTime: date.end.utc().format(),
};
}

View File

@ -1,8 +0,0 @@
import { Report } from '../../../api-codegen/anapi';
export interface SearchParams {
fromTime: string;
toTime: string;
reportTypes: Report.ReportTypeEnum[];
shopIDs?: string[];
}

View File

@ -1,2 +0,0 @@
export * from './shop-details-item.module';
export * from './shop-details-item.component';

View File

@ -1 +0,0 @@
<dsh-details-item *transloco="let t" [title]="t.shop">{{ shopName$ | async }}</dsh-details-item>

View File

@ -1,20 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ShopDetailsItemService } from './shop-details-item.service';
@Component({
selector: 'dsh-shop-details-item',
templateUrl: 'shop-details-item.component.html',
providers: [ShopDetailsItemService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopDetailsItemComponent {
@Input()
set shopID(id: string) {
this.shopDetailsItemService.setShopID(id);
}
shopName$ = this.shopDetailsItemService.shopName$;
constructor(private shopDetailsItemService: ShopDetailsItemService) {}
}

View File

@ -1,14 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslocoModule } from '@ngneat/transloco';
import { LayoutModule } from '@dsh/components/layout';
import { ShopDetailsItemComponent } from './shop-details-item.component';
@NgModule({
imports: [CommonModule, TranslocoModule, LayoutModule],
declarations: [ShopDetailsItemComponent],
exports: [ShopDetailsItemComponent],
})
export class ShopDetailsItemModule {}

View File

@ -1,25 +0,0 @@
import { Injectable } from '@angular/core';
import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs';
import { distinctUntilChanged, map, pluck, shareReplay } from 'rxjs/operators';
import { ShopService } from '../../../../api';
@Injectable()
export class ShopDetailsItemService {
private shopID$: Subject<string> = new ReplaySubject(1);
shopName$: Observable<string> = combineLatest([
this.shopID$.pipe(distinctUntilChanged()),
this.shopService.shops$,
]).pipe(
map(([shopID, shops]) => shops.find((s) => s.id === shopID)),
pluck('details', 'name'),
shareReplay(1)
);
constructor(private shopService: ShopService) {}
setShopID(shopID: string) {
this.shopID$.next(shopID);
}
}

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Shop } from '../../../api-codegen/capi';
import { Shop } from '../../../../api-codegen/capi';
@Component({
selector: 'dsh-filter-shops',

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { FilterShopsModule } from './filter-shops';
const EXPORTED_MODULES = [FilterShopsModule];
@NgModule({
imports: EXPORTED_MODULES,
exports: EXPORTED_MODULES,
})
export class FiltersModule {}

View File

@ -0,0 +1,2 @@
export * from './filters.module';
export * from './filter-shops';

View File

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

3
src/app/shared/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './components';
export * from './services';
export * from './pipes';

View File

@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { ShopDetailsPipe } from './shop-details.pipe';
@NgModule({
declarations: [ShopDetailsPipe],
exports: [ShopDetailsPipe],
})
export class ApiModelRefsModule {}

View File

@ -0,0 +1 @@
export * from './api-model-refs.module';

View File

@ -0,0 +1,37 @@
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { distinctUntilChanged, map, pluck, takeUntil } from 'rxjs/operators';
import { ShopService } from '../../../api';
@Pipe({
name: 'shopDetails',
pure: false,
})
export class ShopDetailsPipe implements PipeTransform, OnDestroy {
private shopName$: BehaviorSubject<string> = new BehaviorSubject('');
private shopIDChange$: Subject<string> = new Subject();
private destroy$: Subject<void> = new Subject();
constructor(private shopService: ShopService, private ref: ChangeDetectorRef) {
combineLatest([this.shopService.shops$, this.shopIDChange$.pipe(distinctUntilChanged())])
.pipe(
takeUntil(this.destroy$),
map(([shops, shopID]) => shops.find((s) => s.id === shopID)),
pluck('details', 'name')
)
.subscribe((v) => {
this.shopName$.next(v);
this.ref.markForCheck();
});
}
transform(shopID: string): string {
this.shopIDChange$.next(shopID);
return this.shopName$.value;
}
ngOnDestroy(): void {
this.destroy$.next();
}
}

View File

@ -0,0 +1 @@
export * from './api-model-refs';

View File

@ -0,0 +1,40 @@
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { map, pluck, shareReplay, switchMap, take } from 'rxjs/operators';
export type ExpandedID = number;
export type DataSetItemID = { id: string | number };
const dataIdToFragment = <T extends DataSetItemID>(id: T['id']): string => (!!id ? id + '' : '');
const byFragment = (fragment: string) => ({ id }: DataSetItemID) => id + '' === fragment;
const findExpandedId = <T extends DataSetItemID>(fragment: string) => (d: T[]) => d.findIndex(byFragment(fragment));
export abstract class ExpandedIdManager<T extends DataSetItemID> {
private expandedIdChange$: Subject<ExpandedID> = new Subject();
expandedId$: Observable<ExpandedID> = this.route.fragment.pipe(
take(1),
switchMap((fragment) => this.dataSet$.pipe(map(findExpandedId(fragment)))),
shareReplay(1)
);
constructor(protected route: ActivatedRoute, protected router: Router) {
this.expandedIdChange$
.pipe(
switchMap((expandedId) => this.dataSet$.pipe(pluck(expandedId, 'id'))),
map(dataIdToFragment)
)
.subscribe((fragment) => this.router.navigate([], { fragment, preserveQueryParams: true }));
}
expandedIdChange(id: ExpandedID | null) {
if (id === null) {
return;
}
this.expandedIdChange$.next(id);
}
protected abstract get dataSet$(): Observable<T[]>;
}

View File

@ -0,0 +1,2 @@
export * from './query-params-store';
export * from './expanded-id-manager';

View File

@ -0,0 +1,22 @@
import { ActivatedRoute, Params, Router } from '@angular/router';
import isEqual from 'lodash.isequal';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
export abstract class QueryParamsStore<D> {
data$: Observable<D> = this.route.queryParams.pipe(
distinctUntilChanged(isEqual),
map((p) => this.mapToData(p)),
shareReplay(1)
);
constructor(protected router: Router, protected route: ActivatedRoute) {}
abstract mapToData(queryParams: Params): D;
abstract mapToParams(data: D): Params;
preserve(data: D) {
this.router.navigate([], { queryParams: this.mapToParams(data), preserveFragment: true });
}
}

View File

@ -12,7 +12,8 @@
"status": "Статус",
"type": "Тип формирования",
"createdAt": "Дата создания",
"period": "Период отчетности"
"period": "Период отчетности",
"shop": "Магазин"
},
"status": {
"created": "Сформирован",
@ -23,20 +24,24 @@
"downloadAll": "Загрузить все"
},
"errors": {
"commonError": "Не удалось загрузить документы"
"downloadReportError": "Не удалось загрузить документы",
"fetchError": "Не удалось получить отчеты"
},
"type": {
"provisionOfService": "Автоматический",
"paymentRegistry": "Реестр операций",
"paymentRegistryByPayout": "Реестр операций выплаты"
},
"searchFilters": {
"dateRangeDescription": "Период создания отчетов"
},
"filter": {
"shopID": "Магазин",
"type": "Тип формирования"
},
"create": {
"button": "Сформировать отчет",
"button": "Создать отчет",
"success": "Отчет формируется",
"dialog": {
"title": "Параметры формирования отчета",

View File

@ -190,6 +190,5 @@
"risk_score_is_too_high": "Операция с высоким риском"
},
"timeout": "Превышен лимит ожидания"
},
"shop": "Магазин"
}
}

View File

@ -0,0 +1,7 @@
import { NativeDateAdapter } from '@angular/material/core';
export class DateAdapter extends NativeDateAdapter {
getFirstDayOfWeek(): number {
return 1;
}
}

View File

@ -65,6 +65,7 @@ export class DaterangeFilterComponent implements OnChanges {
this.endCalendar.activeDate = end.toDate();
}
});
this.savedSelected$.subscribe();
}
ngOnChanges({ selected }: ComponentChanges<DaterangeFilterComponent>): void {

View File

@ -1,12 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { DateAdapter } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { TranslocoModule } from '@ngneat/transloco';
import { DaterangeModule } from '@dsh/pipes/daterange';
import { FilterModule } from '../filter';
import { DateAdapter as CustomDateAdapter } from './date-adapter';
import { DaterangeFilterMenuComponent } from './daterange-filter-selector';
import { DaterangeFilterComponent } from './daterange-filter.component';
@ -16,5 +18,6 @@ const EXPORTED_DECLARATIONS = [DaterangeFilterComponent, DaterangeFilterMenuComp
imports: [CommonModule, FlexLayoutModule, FilterModule, TranslocoModule, DaterangeModule, MatDatepickerModule],
declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS,
providers: [{ provide: DateAdapter, useClass: CustomDateAdapter }],
})
export class DaterangeFilterModule {}

View File

@ -1,6 +1,9 @@
<dsh-filter-button [disabled]="disabled" [active]="active" [dshDropdownTriggerFor]="dropdown">
{{ title }}
</dsh-filter-button>
<div class="button-wrapper" [ngClass]="{ 'button-wrapper-disabled': disabled }">
<div class="dropdown-trigger" [dshDropdownTriggerFor]="dropdown"></div>
<dsh-filter-button [disabled]="disabled" [active]="active">
{{ title }}
</dsh-filter-button>
</div>
<dsh-dropdown #dropdown="dshDropdown" hasArrow="false" position="left" offset="16px 0 0" (closed)="closed.emit()">
<ng-template>

View File

@ -0,0 +1,17 @@
// Hack, the title won't always update with the dshDropdownTriggerFor
.button-wrapper {
position: relative;
cursor: pointer;
&-disabled {
cursor: default;
}
.dropdown-trigger {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}

View File

@ -7,6 +7,7 @@ import { coerceBoolean } from '../../../utils';
@Component({
selector: 'dsh-filter',
templateUrl: 'filter.component.html',
styleUrls: ['filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilterComponent {

View File

@ -1,23 +1,29 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ChangeDetectorRef, Pipe, PipeTransform } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Daterange } from './daterange';
import { DaterangeService } from './daterange.service';
@Pipe({ name: 'daterange' })
@Pipe({ name: 'daterange', pure: false })
export class DaterangePipe implements PipeTransform {
private daterange$ = new BehaviorSubject<Partial<Daterange>>({});
private result = '';
constructor(daterangeService: DaterangeService) {
constructor(daterangeService: DaterangeService, private ref: ChangeDetectorRef) {
this.daterange$
.pipe(switchMap((daterange) => daterangeService.switchToDaterangeStr(daterange)))
.subscribe((r) => (this.result = r));
.pipe(
switchMap((daterange) => daterangeService.switchToDaterangeStr(daterange)),
distinctUntilChanged()
)
.subscribe((r) => {
this.result = r;
this.ref.markForCheck();
});
}
transform(daterange: Partial<Daterange>): string {
this.daterange$.next(daterange || {});
this.daterange$.next(daterange);
return this.result;
}
}

View File

@ -16,6 +16,7 @@ export class DaterangeService {
constructor(@Inject(LOCALE_ID) private locale: string, private transloco: TranslocoService) {}
switchToDaterangeStr(daterange: Partial<Daterange>): Observable<string> {
daterange = { begin: daterange?.begin?.local(), end: daterange?.end?.local() };
if (!isDaterange(daterange)) {
return of('');
} else if (isYearsRange(daterange)) {

View File

@ -19,7 +19,8 @@
"noUnusedParameters": true,
"paths": {
"@dsh/components/*": ["src/components/*"],
"@dsh/pipes/*": ["src/pipes/*"]
"@dsh/pipes/*": ["src/pipes/*"],
"@dsh/app/shared/*": ["src/app/shared"]
}
},
"angularCompilerOptions": {