IMP-162: Use local datetime (#169)

This commit is contained in:
Rinat Arsaev 2024-02-06 17:39:40 +07:00 committed by GitHub
parent 881903ccdd
commit 5397e45f40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 203 additions and 340 deletions

View File

@ -1,29 +1,42 @@
<dsh-home *transloco="let t; scope: 'app'; read: 'app'">
<dsh-sections *ngIf="bootstrapped$ | async; else spinner"></dsh-sections>
<ng-template #spinner>
<div fxFlexAlign="center" fxLayout="row">
<dsh-spinner style="margin: auto"></dsh-spinner>
<div
style="
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 48px;
height: 100%;
flex: 1;
"
>
<div>
<dsh-sections *ngIf="bootstrapped$ | async; else spinner"></dsh-sections>
<ng-template #spinner>
<div fxFlexAlign="center" fxLayout="row">
<dsh-spinner style="margin: auto"></dsh-spinner>
</div>
</ng-template>
</div>
</ng-template>
<div *ngIf="isDev" style="display: flex; justify-content: center">
<div
[matMenuTriggerFor]="menu"
class="dsh-body-2"
style="cursor: pointer; display: flex; gap: 8px; align-items: center"
>
<dsh-bi icon="globe" size="sm"></dsh-bi>
<!-- t(language.en) -->
<!-- t(language.ru) -->
{{ t('language.' + languageService.active) }}
</div>
<mat-menu #menu="matMenu">
<button
*ngFor="let language of languageService.list"
mat-menu-item
(click)="languageService.set(language)"
<div *ngIf="isDev" style="display: flex; justify-content: center">
<div
[matMenuTriggerFor]="menu"
class="dsh-body-2"
style="cursor: pointer; display: flex; gap: 8px; align-items: center"
>
{{ t('language.' + language) }}
</button>
</mat-menu>
<dsh-bi icon="globe" size="sm"></dsh-bi>
<!-- t(language.en) -->
<!-- t(language.ru) -->
{{ t('language.' + languageService.active) }}
</div>
<mat-menu #menu="matMenu">
<button
*ngFor="let language of languages"
mat-menu-item
(click)="setLanguage(language)"
>
{{ t('language.' + language) }}
</button>
</mat-menu>
</div>
</div>
</dsh-home>

View File

@ -2,7 +2,7 @@ import { Component, OnInit, isDevMode } from '@angular/core';
import * as sentry from '@sentry/angular-ivy';
import { first } from 'rxjs/operators';
import { LanguageService } from '@dsh/app/language';
import { LanguageService, Language } from '@dsh/app/language';
import { BootstrapService } from './bootstrap.service';
import { ContextOrganizationService } from './shared';
@ -16,6 +16,10 @@ export class AppComponent implements OnInit {
bootstrapped$ = this.bootstrapService.bootstrapped$;
isDev = isDevMode();
get languages() {
return this.languageService.list.filter((l) => l !== this.languageService.active);
}
constructor(
private bootstrapService: BootstrapService,
private contextOrganizationService: ContextOrganizationService,
@ -27,4 +31,9 @@ export class AppComponent implements OnInit {
.pipe(first())
.subscribe(({ party }) => sentry.setUser({ id: party }));
}
async setLanguage(language: Language) {
await this.languageService.set(language);
window.location.reload();
}
}

View File

@ -21,6 +21,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { TranslocoModule, provideTransloco, TRANSLOCO_SCOPE } from '@ngneat/transloco';
import * as sentry from '@sentry/angular-ivy';
import { QUERY_PARAMS_SERIALIZERS } from '@vality/ng-core';
import { FlexLayoutModule } from 'ng-flex-layout';
import { AnapiModule } from '@dsh/app/api/anapi';
@ -29,7 +30,6 @@ import { PaymentsModule } from '@dsh/app/api/payments';
import { UrlShortenerModule } from '@dsh/app/api/url-shortener';
import { WalletModule } from '@dsh/app/api/wallet';
import { ErrorModule } from '@dsh/app/shared/services';
import { QUERY_PARAMS_SERIALIZERS } from '@dsh/app/shared/services/query-params/utils/query-params-serializers';
import { createDateRangeWithPresetSerializer } from '@dsh/components/date-range-filter';
import { SpinnerModule, BootstrapIconModule } from '@dsh/components/indicators';

View File

@ -1,5 +1,8 @@
<div *ngIf="routerNavigationEnd$ | async">
<ng-container *ngTemplateOutlet="(isXSmallSmall$ | async) ? mobile : laptop"> </ng-container>
<div
*ngIf="routerNavigationEnd$ | async"
style="min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column"
>
<ng-container *ngTemplateOutlet="(isXSmallSmall$ | async) ? mobile : laptop"></ng-container>
</div>
<ng-template #content>
@ -13,7 +16,7 @@
</ng-template>
<ng-template #laptop>
<dsh-laptop-grid>
<dsh-laptop-grid style="flex: 1">
<ng-container *ngTemplateOutlet="content"></ng-container>
</dsh-laptop-grid>
</ng-template>

View File

@ -2,11 +2,13 @@ $dsh-container-padding: 16px;
$dsh-container-max-width: 1724px;
:host {
display: block;
display: flex;
padding: $dsh-container-padding;
min-height: calc(100% - $dsh-container-padding * 2);
}
.dsh-laptop-grid {
max-width: $dsh-container-max-width;
margin: 0 auto;
width: 100%;
}

View File

@ -25,6 +25,7 @@
&-content {
padding: 16px;
min-height: 100%;
}
&-toggle-button {

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { QueryParamsService } from '@vality/ng-core';
import { QueryParamsService } from '@dsh/app/shared';
import { ShopCreationService } from '@dsh/app/shared/components/shop-creation';
import { SpinnerType } from '@dsh/components/indicators';

View File

@ -10,38 +10,38 @@
(filterValuesChanged)="updateFilters($event)"
></dsh-analytics-search-filters>
<dsh-payments-amount
[searchParams]="searchParams$ | async"
[searchParams]="accurateSearchParams$ | async"
[spinnerType]="spinnerType"
></dsh-payments-amount>
<dsh-refunds-amount
[searchParams]="searchParams$ | async"
[searchParams]="accurateSearchParams$ | async"
[spinnerType]="spinnerType"
></dsh-refunds-amount>
<dsh-average-payment
[searchParams]="searchParams$ | async"
[searchParams]="accurateSearchParams$ | async"
[spinnerType]="spinnerType"
></dsh-average-payment>
<dsh-payments-count
[searchParams]="searchParams$ | async"
[searchParams]="accurateSearchParams$ | async"
[spinnerType]="spinnerType"
></dsh-payments-count>
<dsh-payment-split-count
[searchParams]="searchParams$ | async"
[searchParams]="barChartSearchParams$ | async"
[spinnerType]="spinnerType"
gdColumn.gt-sm="1/-1"
></dsh-payment-split-count>
<dsh-payment-split-amount
[searchParams]="searchParams$ | async"
[searchParams]="barChartSearchParams$ | async"
[spinnerType]="spinnerType"
gdColumn.gt-sm="1/-1"
></dsh-payment-split-amount>
<dsh-payments-tool-distribution
[searchParams]="searchParams$ | async"
[searchParams]="accurateSearchParams$ | async"
[spinnerType]="spinnerType"
gdColumn.gt-sm="1/3"
></dsh-payments-tool-distribution>
<dsh-payments-error-distribution
[searchParams]="searchParams$ | async"
[searchParams]="accurateSearchParams$ | async"
[spinnerType]="spinnerType"
gdColumn.gt-sm="3/5"
></dsh-payments-error-distribution>

View File

@ -1,14 +1,17 @@
import { Component } from '@angular/core';
import { QueryParamsService } from '@vality/ng-core';
import { combineLatest, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { SpinnerType } from '@dsh/components/indicators';
import { PaymentInstitutionRealmService } from '../services';
import { Filters } from './analytics-search-filters/analytics-search-filters.component';
import { filtersToSearchParams } from './utils/filters-to-search-params';
import {
filtersToSearchParams,
filtersToBarChartSearchParams,
} from './utils/filters-to-search-params';
@Component({
templateUrl: 'analytics.component.html',
@ -18,9 +21,12 @@ export class AnalyticsComponent {
filters$ = new ReplaySubject<Filters>();
searchParams$ = combineLatest([this.filters$, this.realmService.realm$]).pipe(
accurateSearchParams$ = combineLatest([this.filters$, this.realmService.realm$]).pipe(
map(([filters, realm]) => filtersToSearchParams(filters, realm)),
);
barChartSearchParams$ = combineLatest([this.filters$, this.realmService.realm$]).pipe(
map(([filters, realm]) => filtersToBarChartSearchParams(filters, realm)),
);
params$ = this.qp.params$;

View File

@ -1,4 +1,5 @@
import { PaymentInstitution } from '@vality/swag-payments';
import moment from 'moment';
import { Preset } from '@dsh/components/date-range-filter';
@ -11,11 +12,27 @@ export const filtersToSearchParams = (
): SearchParams => {
const { start, end } = dateRange;
return {
fromTime:
dateRange.preset === Preset.Custom
? start.utc().format()
: start.clone().endOf('d').add(1, 'ms').utc().format(),
toTime: end.utc().format(),
fromTime: start.clone().utc().format(),
toTime: end.clone().utc().format(),
realm,
...otherParams,
};
};
export const filtersToBarChartSearchParams = (
{ dateRange, ...otherParams }: Filters,
realm: PaymentInstitution.RealmEnum,
): SearchParams => {
const { start, end } = dateRange;
return {
fromTime:
dateRange.preset === Preset.Last24hour
? start.clone().startOf('hour').utc().format()
: start.clone().startOf('day').utc().format(),
toTime:
dateRange.preset === Preset.Last24hour
? moment().endOf('hour').utc().format()
: end.clone().endOf('day').utc().format(),
realm,
...otherParams,
};

View File

@ -8,7 +8,7 @@ export const getOffsets = (fromTime: string, toTime: string, splitUnit: SplitUni
current = moment(fromTime).subtract(moment(fromTime).date() - 1, 'd');
break;
case 'week':
current = moment(fromTime).weekday(1);
current = moment(fromTime).startOf('isoWeek');
break;
default:
current = moment(fromTime);

View File

@ -2,13 +2,12 @@ import { Breakpoints } from '@angular/cdk/layout';
import { Component } from '@angular/core';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { DialogService } from '@vality/ng-core';
import { DialogService, QueryParamsService } from '@vality/ng-core';
import { ApiKeyStatus, ApiKey } from '@vality/swag-api-keys-v2';
import { map } from 'rxjs/operators';
import { ApiKeysDictionaryService } from '@dsh/app/api/api-keys';
import { mapToTimestamp } from '@dsh/app/custom-operators';
import { QueryParamsService } from '@dsh/app/shared';
import {
ExpandedFragment,
Column,

View File

@ -1,10 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { NotifyLogService } from '@vality/ng-core';
import { NotifyLogService, QueryParamsService } from '@vality/ng-core';
import { take } from 'rxjs/operators';
import { QueryParamsService } from '@dsh/app/shared/services/query-params/query-params.service';
import { SpinnerType } from '@dsh/components/indicators';
import { RealmMixService, PaymentInstitutionRealmService } from '../../services';
@ -61,8 +60,8 @@ export class InvoicesComponent implements OnInit {
const { dateRange, ...otherFilters } = p;
this.realmMixService.mix({
...otherFilters,
fromTime: dateRange.start.utc().format(),
toTime: dateRange.end.utc().format(),
fromTime: dateRange.start.clone().utc().format(),
toTime: dateRange.end.clone().utc().format(),
realm: null,
});
}

View File

@ -1,10 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { QueryParamsService } from '@vality/ng-core';
import { PaymentSearchResult } from '@vality/swag-anapi-v2';
import { Observable } from 'rxjs';
import { QueryParamsService } from '@dsh/app/shared/services';
import { RealmMixService } from '../../services';
import { PaymentInstitutionRealmService } from '../../services/payment-institution-realm.service';
@ -69,8 +68,8 @@ export class PaymentsComponent implements OnInit {
this.realmMixService.mix({
...otherFilters,
...paymentMethod,
fromTime: dateRange.start.utc().format(),
toTime: dateRange.end.utc().format(),
fromTime: dateRange.start.clone().utc().format(),
toTime: dateRange.end.clone().utc().format(),
realm: null,
});
}

View File

@ -1,9 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { NotifyLogService } from '@vality/ng-core';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { NotifyLogService, QueryParamsService } from '@vality/ng-core';
import { RealmMixService, RealmShopsService } from '../../services';
@ -58,8 +56,8 @@ export class RefundsComponent implements OnInit {
const { dateRange, ...params } = p;
this.realmMixinService.mix({
realm: null,
fromTime: dateRange.start.utc().format(),
toTime: dateRange.end.utc().format(),
fromTime: dateRange.start.clone().utc().format(),
toTime: dateRange.end.clone().utc().format(),
...params,
});
}

View File

@ -2,12 +2,10 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { NotifyLogService } from '@vality/ng-core';
import { NotifyLogService, QueryParamsService } from '@vality/ng-core';
import { Subject } from 'rxjs';
import { filter, first, switchMap, switchMapTo } from 'rxjs/operators';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { RealmMixService, PaymentInstitutionRealmService, RealmShopsService } from '../services';
import { CreatePayoutDialogComponent } from './create-payout/create-payout-dialog.component';
@ -90,8 +88,8 @@ export class PayoutsComponent implements OnInit {
void this.qp.set(p);
const { dateRange, ...otherParams } = p;
this.realmMixService.mix({
fromTime: dateRange.start.utc().format(),
toTime: dateRange.end.utc().format(),
fromTime: dateRange.start.clone().utc().format(),
toTime: dateRange.end.clone().utc().format(),
realm: null,
...otherParams,
});

View File

@ -3,11 +3,10 @@ import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { QueryParamsService } from '@vality/ng-core';
import { Subject, combineLatest } from 'rxjs';
import { filter, first, switchMap } from 'rxjs/operators';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { RealmMixService, PaymentInstitutionRealmService } from '../services';
import { CreateReportDialogComponent } from './create-report/create-report-dialog.component';
@ -82,8 +81,8 @@ export class ReportsComponent implements OnInit {
const { dateRange, ...params } = p;
this.realmMixinService.mix({
...params,
fromTime: dateRange.start.utc().format(),
toTime: dateRange.end.utc().format(),
fromTime: dateRange.start.clone().utc().format(),
toTime: dateRange.end.clone().utc().format(),
realm: null,
});
}

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { QueryParamsService } from '@vality/ng-core';
import { ErrorService } from '@dsh/app/shared';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { DepositsFilters } from './deposits-filters/types/deposits-filters';
import { DepositsExpandedIdManagerService } from './services/deposits-expanded-id-manager/deposits-expanded-id-manager.service';

View File

@ -12,8 +12,8 @@ export const filtersToSearchParams = ({
walletID,
identityID,
}: DepositsFilters): Omit<ListDepositsRequestParams, 'xRequestID' | 'limit'> => ({
createdAtFrom: dateRange.start.utc().format(),
createdAtTo: dateRange.end.utc().format(),
createdAtFrom: dateRange.start.clone().utc().format(),
createdAtTo: dateRange.end.clone().utc().format(),
walletID,
identityID,
sourceID,

View File

@ -21,6 +21,9 @@ export class FetchReportsService extends FetchSuperclass<
protected fetch(
params: Omit<GetReportsRequestParams, 'xRequestID' | 'xRequestDeadline'>,
): Observable<FetchResult<Report, string>> {
if (!params?.identityID) {
return of({ result: [] });
}
return this.reportsService.getReports(params).pipe(
map((result) => ({ result })),
catchError((err) => {

View File

@ -4,6 +4,7 @@ import { NonNullableFormBuilder } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { TranslocoService } from '@ngneat/transloco';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { QueryParamsService } from '@vality/ng-core';
import { Report } from '@vality/swag-wallet';
import isEqual from 'lodash-es/isEqual';
import moment from 'moment';
@ -11,7 +12,6 @@ import { startWith, distinctUntilChanged, filter, first, map } from 'rxjs/operat
import { WalletDictionaryService, IdentitiesService } from '@dsh/app/api/wallet';
import { mapToTimestamp } from '@dsh/app/custom-operators';
import { QueryParamsService } from '@dsh/app/shared';
import { Column, ExpandedFragment } from '@dsh/app/shared/components/accordion-table';
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
import { StatusColor } from '@dsh/app/theme-manager';
@ -122,12 +122,9 @@ export class ReportsComponent implements OnInit {
load() {
const { dateRange, identityID } = this.form.value;
if (!identityID) {
return;
}
this.fetchReportsService.load({
fromTime: dateRange.start.utc().format(),
toTime: dateRange.end.utc().format(),
fromTime: dateRange.start.clone().utc().format(),
toTime: dateRange.end.clone().utc().format(),
identityID,
type: 'withdrawalRegistry',
});

View File

@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { TranslocoService } from '@ngneat/transloco';
import { NotifyLogService } from '@vality/ng-core';
import { NotifyLogService, QueryParamsService } from '@vality/ng-core';
import { shareReplayRefCount } from '@dsh/app/custom-operators';
import { QueryParamsService } from '@dsh/app/shared/services/query-params';
import { FetchWithdrawalsService, WithdrawalsExpandedIdManager } from './services';
import { WithdrawalsFilters } from './withdrawals-filters';
@ -42,8 +41,8 @@ export class WithdrawalsComponent implements OnInit {
void this.qp.set(filters);
this.fetchWithdrawalsService.search({
...filters,
createdAtFrom: filters.dateRange.start.utc().format(),
createdAtTo: filters.dateRange.end.utc().format(),
createdAtFrom: filters.dateRange.start.clone().utc().format(),
createdAtTo: filters.dateRange.end.clone().utc().format(),
});
}

View File

@ -9,7 +9,7 @@
*ngFor="let column of columns"
[fxFlex]="column.width ?? true"
[fxHide]="isHided(column.hide) | async"
>{{ column.label | vPossiblyAsync }}
>{{ column.label | async }}
</dsh-row-header-label>
</dsh-row>
<dsh-accordion
@ -39,7 +39,7 @@
</ng-template>
<ng-template ngSwitchCase="tag">
<dsh-status
*ngIf="column.typeParameters.label | vPossiblyAsync as tagLabel"
*ngIf="column.typeParameters.label | async as tagLabel"
[color]="column.typeParameters.color[value]"
>{{ tagLabel[value] }}</dsh-status
>
@ -57,7 +57,7 @@
>
<div fxLayout fxLayoutAlign="space-between" fxLayoutGap="24px">
<div *ngFor="let header of contentHeader">
{{ header.label(item) | vPossiblyAsync }}
{{ header.label(item) | async }}
</div>
</div>
</dsh-accordion-item-content-header>

View File

@ -1,7 +1,7 @@
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { Component, Input, Output, EventEmitter, TemplateRef, ContentChild } from '@angular/core';
import { PossiblyAsync } from '@vality/ng-core';
import { of } from 'rxjs';
import { of, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { StatusColor } from '@dsh/app/theme-manager';
@ -24,12 +24,12 @@ export interface Column<T extends object> {
type?: 'daterange' | 'datetime' | 'tag';
typeParameters?: {
color: Record<PropertyKey, StatusColor>;
label: PossiblyAsync<Record<PropertyKey, string>>;
label: Observable<Record<PropertyKey, string>>;
};
}
export interface ContentHeader<T extends object> {
label: (row: T) => PossiblyAsync<unknown>;
label: (row: T) => Observable<unknown>;
}
@Component({

View File

@ -29,8 +29,8 @@ export class DaterangeManagerService {
serializeDateRange({ begin, end }: Daterange): DaterangeParams {
return {
begin: begin.utc().format(),
end: end.utc().format(),
begin: begin.clone().utc().format(),
end: end.clone().utc().format(),
};
}

View File

@ -6,7 +6,6 @@ export * from './notification';
export * from './error';
export * from './keycloak-token-info';
export * from './sections-links';
export * from './query-params';
export * from './id-generator';
export * from './partial-fetcher';
export * from './context-organization';

View File

@ -1,2 +0,0 @@
export * from './query-params.service';
export * from './utils/query-params-serializers';

View File

@ -1,78 +0,0 @@
import { Inject, Injectable } 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, publishReplay, refCount, startWith } from 'rxjs/operators';
import { isEmptyValue } from '@dsh/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),
publishReplay(1),
refCount(),
);
get params(): Params {
return this.deserialize(this.route.snapshot.queryParams);
}
constructor(
private router: Router,
private route: ActivatedRoute,
@Inject(QUERY_PARAMS_SERIALIZERS) private serializers: Serializer[] = [],
) {}
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);
}
}

View File

@ -1,6 +0,0 @@
export type Serializer<T = unknown> = {
id: string;
serialize: (v: T) => string;
deserialize: (v: string) => T;
recognize: (v: T) => boolean;
};

View File

@ -1,10 +0,0 @@
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;
}

View File

@ -1,7 +0,0 @@
import { InjectionToken } from '@angular/core';
import { Serializer } from '@dsh/app/shared/services/query-params/types/serializer';
export const QUERY_PARAMS_SERIALIZERS = new InjectionToken<Serializer[]>(
'query params serializers',
);

View File

@ -1,6 +0,0 @@
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);
}

View File

@ -445,7 +445,7 @@
"title": "Documents"
},
"searchFilters": {
"dateRangeDescription": "Reporting period"
"dateRangeDescription": "Report creation period"
}
},
"shops": {

View File

@ -54,7 +54,7 @@
"to": "End of the period"
},
"createdAt": "Created at",
"dateRangeDescription": "Reporting period",
"dateRangeDescription": "Report creation period",
"errors": {
"downloadReportError": "Failed to load documents",
"identityNotSpecified": "Wallet holder not specified"

View File

@ -4,6 +4,7 @@
[activeLabel]="activeLabel$ | async"
[content]="content"
[label]="t('label')"
[title]="isActive ? (activeLabel$ | async) : ''"
(clear)="clear()"
(save)="save()"
>
@ -22,7 +23,7 @@
<mat-calendar
*ngIf="step === stepEnum.Calendar"
[maxDate]="maxDate"
[selected]="value.dateRange"
[selected]="selectedCalendar$ | async"
(selectedChange)="selectedChange($event)"
></mat-calendar>
</ng-template>

View File

@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, Injector, Input } from '@angular/co
import { DateRange as MatDateRange } from '@angular/material/datepicker';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy } from '@ngneat/until-destroy';
import { createControlProviders } from '@vality/ng-core';
import { createControlProviders, getValueChanges } from '@vality/ng-core';
import { Moment } from 'moment';
import { switchMap, map } from 'rxjs/operators';
import { switchMap, map, shareReplay } from 'rxjs/operators';
import { FilterSuperclass } from '@dsh/components/filter';
@ -59,6 +59,16 @@ export class DateRangeFilterComponent extends FilterSuperclass<
presetLabels$ = this.transloco
.selectTranslation('core-components')
.pipe(map(() => this.getPresetLabels()));
selectedCalendar$ = getValueChanges(this.control).pipe(
map(
({ dateRange }) =>
new MatDateRange(
dateRange.start?.clone?.()?.local?.(),
dateRange.end?.clone?.()?.local?.(),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
protected get empty(): InnerDateRange {
return { dateRange: new MatDateRange<Moment>(null, null) };
@ -91,8 +101,8 @@ export class DateRangeFilterComponent extends FilterSuperclass<
}
this.value = {
dateRange: new MatDateRange(
newStart?.local()?.startOf('day')?.utc(true),
newEnd?.local()?.endOf('day')?.utc(true),
newStart?.clone()?.startOf('day'),
newEnd?.clone()?.endOf('day'),
),
preset: Preset.Custom,
};
@ -108,9 +118,17 @@ export class DateRangeFilterComponent extends FilterSuperclass<
}
save(value = this.control.value): void {
if (!value.dateRange.start || !value.dateRange.end) {
const { start, end } = value.dateRange;
if (!start && !end) {
this.clear();
value = this.control.value;
} else if (!start || !end) {
const date = start || end;
value = {
dateRange: new MatDateRange(date.clone().startOf('day'), date.clone().endOf('day')),
preset: Preset.Custom,
};
this.control.setValue(value);
}
this.step = Step.Presets;
this.set(value);
@ -128,17 +146,16 @@ export class DateRangeFilterComponent extends FilterSuperclass<
}
protected outerToInnerValue(dateRange: Partial<DateRangeWithPreset>): InnerDateRange {
if (dateRange?.preset && dateRange.preset !== Preset.Custom) {
const { start, end } = createDateRangeByPreset(dateRange.preset);
return { dateRange: new MatDateRange(start, end), preset: dateRange.preset };
}
if (!dateRange?.start || !dateRange?.end) {
return this.empty;
}
return {
dateRange: new MatDateRange(dateRange.start, dateRange.end),
preset: Preset.Custom,
};
const { start, end } =
dateRange?.preset && dateRange.preset !== Preset.Custom
? createDateRangeByPreset(dateRange.preset)
: dateRange ?? {};
return start && end
? {
dateRange: new MatDateRange(start, end),
preset: dateRange?.preset ?? Preset.Custom,
}
: this.empty;
}
private getPresetLabels(): Record<Preset, string> {

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslocoModule } from '@ngneat/transloco';
import { FlexModule } from 'ng-flex-layout';
@ -19,6 +20,7 @@ import { DateRangeFilterComponent } from './date-range-filter.component';
MatListModule,
DaterangeModule,
FlexModule,
MatTooltipModule,
],
declarations: [DateRangeFilterComponent],
exports: [DateRangeFilterComponent],

View File

@ -1,33 +1,9 @@
import { DateRange } from '@angular/material/datepicker';
import moment, { Moment } from 'moment';
import { isDay, isMonth, isYear } from './get-date-range-type';
export const isCurrentYear = (dateRange: DateRange<Moment>): boolean =>
isYear(dateRange) && moment().isSame(dateRange.start, 'year');
export const isCurrentMonth = (dateRange: DateRange<Moment>): boolean =>
isMonth(dateRange) && dateRange.start.isSame(moment(), 'month');
moment().isSame(dateRange.start, 'year') && moment().isSame(dateRange.end, 'year');
export const isCurrentWeek = ({ start, end }: DateRange<Moment>): boolean =>
start.isSame(moment().startOf('week'), 'day') && end.isSame(moment().endOf('week'), 'day');
export const isToday = (dateRange: DateRange<Moment>): boolean =>
isDay(dateRange) && dateRange.start.isSame(moment(), 'day');
export enum DateRangeCurrentType {
CurrentYear,
CurrentMonth,
CurrentWeek,
Today,
}
export const getDateRangeCurrentType = (dateRange: DateRange<Moment>): DateRangeCurrentType => {
if (isToday(dateRange)) {
return DateRangeCurrentType.Today;
} else if (isCurrentWeek(dateRange)) {
return DateRangeCurrentType.CurrentWeek;
} else if (isCurrentMonth(dateRange)) {
return DateRangeCurrentType.CurrentMonth;
} else if (isCurrentYear(dateRange)) {
return DateRangeCurrentType.CurrentYear;
}
return null;
};
dateRange.start.isSame(moment(), 'day') && dateRange.end.isSame(moment(), 'day');

View File

@ -4,29 +4,6 @@ import { Moment } from 'moment';
export const isYearsRange = ({ start, end }: DateRange<Moment>): boolean =>
start.isSame(start.clone().startOf('year'), 'day') &&
end.isSame(end.clone().endOf('year'), 'day');
export const isYear = (dateRange: DateRange<Moment>): boolean =>
isYearsRange(dateRange) && dateRange.start.isSame(dateRange.end, 'year');
export const isMonthsRange = ({ start, end }: DateRange<Moment>): boolean =>
start.isSame(start.clone().startOf('month'), 'day') &&
end.isSame(end.clone().endOf('month'), 'day');
export const isMonth = (dateRange: DateRange<Moment>): boolean =>
isMonthsRange(dateRange) && dateRange.start.isSame(dateRange.end, 'month');
export const isDay = ({ start, end }: DateRange<Moment>): boolean => start.isSame(end, 'days');
export enum DateRangeType {
Years,
Year,
Months,
Month,
Days,
Day,
}
export const getDateRangeType = (dateRange: DateRange<Moment>): DateRangeType => {
if (isYearsRange(dateRange)) {
return isYear(dateRange) ? DateRangeType.Year : DateRangeType.Years;
} else if (isMonthsRange(dateRange)) {
return isMonth(dateRange) ? DateRangeType.Month : DateRangeType.Months;
}
return isDay(dateRange) ? DateRangeType.Day : DateRangeType.Days;
};

View File

@ -4,9 +4,8 @@ import { Moment } from 'moment';
import { DateRangeTranslations } from '../types/translations';
import { isCurrentWeek, isToday } from './get-date-range-current-type';
import { isMonthsRange, isYearsRange } from './get-date-range-type';
import { isYearsRange } from './get-date-range-type';
import { getLocalizedDayRange } from './get-localized-day-range';
import { getLocalizedMonthRange } from './get-localized-month-range';
import { getLocalizedYearRange } from './get-localized-year-range';
export function getLocalizedDateRange(
@ -16,11 +15,8 @@ export function getLocalizedDateRange(
): string {
if (!dateRange.start && !dateRange.end) {
return null;
}
if (isYearsRange(dateRange)) {
} else if (isYearsRange(dateRange)) {
return getLocalizedYearRange(dateRange, t);
} else if (isMonthsRange(dateRange)) {
return getLocalizedMonthRange(dateRange, t, locale);
} else if (isCurrentWeek(dateRange)) {
return t.currentWeek;
} else if (isToday(dateRange)) {

View File

@ -11,7 +11,7 @@ export function getLocalizedDate(
): string {
return formatDate(
date.toDate(),
[d && 'd', m && standalone ? 'LLLL' : 'MMMM', y && 'y'].filter((v) => v).join(' '),
[d && 'd', m && (standalone ? 'LLLL' : 'MMMM'), y && 'y'].filter((v) => v).join(' '),
locale,
);
}

View File

@ -1,10 +1,14 @@
import { DateRange } from '@angular/material/datepicker';
import { Moment } from 'moment';
import { capitalizeFirstLetter } from './capitilize-first-letter';
import { isCurrentYear } from './get-date-range-current-type';
import { isMonthsRange as getIsMonthsRange } from './get-date-range-type';
import { getLocalizedDate } from './get-localized-date';
/**
* Январь
* Январь 2020
* 2 января
* 2 января 2020
*
@ -17,18 +21,22 @@ export function getLocalizedDayRange(
locale: string,
): string {
const { start, end } = dateRange;
const startStr = getLocalizedDate(
start,
{ d: true, m: !start.isSame(end, 'month'), y: !start.isSame(end, 'year') },
locale,
);
const isMonthsRange = getIsMonthsRange(dateRange);
const endStr = getLocalizedDate(
end,
{ d: true, m: true, y: !isCurrentYear(dateRange) },
{ d: !isMonthsRange, m: true, y: !isCurrentYear(dateRange) },
locale,
isMonthsRange,
);
const isSameYear = start.isSame(end, 'year');
const isSameYearMonth = isSameYear && start.isSame(end, 'month');
if (isSameYearMonth && (start.isSame(end, 'day') || isMonthsRange)) {
return capitalizeFirstLetter(endStr);
}
const startStr = getLocalizedDate(
start,
{ d: !isMonthsRange, m: !isSameYearMonth, y: !isSameYear },
locale,
);
if (start.isSame(end, 'day')) {
return endStr;
}
return `${start.date() === 2 ? t.fromStartWith2 : t.from} ${startStr} ${t.to} ${endStr}`;
}

View File

@ -1,28 +0,0 @@
import { DateRange } from '@angular/material/datepicker';
import { Moment } from 'moment';
import { capitalizeFirstLetter } from './capitilize-first-letter';
import { isCurrentYear } from './get-date-range-current-type';
import { isMonth } from './get-date-range-type';
import { getLocalizedDate } from './get-localized-date';
/**
* Январь
* Январь 2020
*
* С января по март
* С января 2019 по март 2019 / С декабря 2019 по март 2020
*/
export function getLocalizedMonthRange(
dateRange: DateRange<Moment>,
t: Record<'from' | 'to', string>,
locale: string,
): string {
const currentYear = isCurrentYear(dateRange);
const startStr = getLocalizedDate(dateRange.start, { m: true, y: !currentYear }, locale);
const endStr = getLocalizedDate(dateRange.end, { m: true, y: !currentYear }, locale, true);
if (isMonth(dateRange)) {
return capitalizeFirstLetter(endStr);
}
return `${t.from} ${startStr} ${t.to} ${endStr}`;
}

View File

@ -1,8 +1,7 @@
import { Serializer } from '@vality/ng-core';
import isNil from 'lodash-es/isNil';
import moment from 'moment';
import { Serializer } from '@dsh/app/shared/services/query-params/types/serializer';
import { DateRangeWithPreset } from '../types/date-range-with-preset';
import { Preset } from '../types/preset';
@ -11,13 +10,14 @@ import { createDateRangeWithPreset } from './create-date-range-with-preset';
export function createDateRangeWithPresetSerializer(id = 'dr'): Serializer<DateRangeWithPreset> {
return {
id,
serialize: (dateRange) =>
dateRange.preset && dateRange.preset !== Preset.Custom
serialize: (dateRange) => {
return dateRange.preset && dateRange.preset !== Preset.Custom
? dateRange.preset
: [
dateRange.start ? dateRange.start.utc().format() : '',
dateRange.end ? dateRange.end.utc().format() : '',
].join(','),
dateRange.start ? dateRange.start.clone().utc().format() : '',
dateRange.end ? dateRange.end.clone().utc().format() : '',
].join(',');
},
deserialize: (str) => {
if (Object.values(Preset).includes(str as Preset)) {
return createDateRangeWithPreset(str as Preset);

View File

@ -15,21 +15,3 @@ export const isMonth = ({ begin, end }: Daterange) =>
isMonthsRange({ begin, end }) && begin.isSame(end, 'month');
export const isDay = ({ begin, end }: Daterange) => begin.isSame(end, 'days');
export enum DaterangeType {
Years = 'years',
Year = 'year',
Months = 'months',
Month = 'month',
Days = 'days',
Day = 'day',
}
export const daterangeType = (daterange: Daterange): DaterangeType => {
if (isYearsRange(daterange)) {
return isYear(daterange) ? DaterangeType.Year : DaterangeType.Years;
} else if (isMonthsRange(daterange)) {
return isMonth(daterange) ? DaterangeType.Month : DaterangeType.Months;
}
return isDay(daterange) ? DaterangeType.Day : DaterangeType.Days;
};