FE-1011: Create range-datepicker (#169)

This commit is contained in:
Rinat Arsaev 2020-03-10 17:38:21 +03:00 committed by GitHub
parent f8f7257aad
commit 7e84c5269f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 711 additions and 182 deletions

54
package-lock.json generated
View File

@ -6525,8 +6525,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -6550,15 +6549,13 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -6575,22 +6572,19 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -6721,8 +6715,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -6736,7 +6729,6 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -6753,7 +6745,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -6762,15 +6753,13 @@
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -6791,7 +6780,6 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -6880,8 +6868,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -6895,7 +6882,6 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -6991,8 +6977,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -7034,7 +7019,6 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -7056,7 +7040,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -7105,15 +7088,13 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true,
"optional": true
"dev": true
}
}
},
@ -11270,6 +11251,14 @@
}
}
},
"saturn-datepicker": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/saturn-datepicker/-/saturn-datepicker-8.0.1.tgz",
"integrity": "sha512-kUmf8xg5oeGAZtgwqfkNY8Dro6EzX0zGIWnAcQ9cfQ+w3xBq4TbjdBUWuEA19rJlvLLP/uH3PAoXG/SHVLYncQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"saucelabs": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz",
@ -12977,6 +12966,11 @@
"object.getownpropertydescriptors": "^2.0.3"
}
},
"utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg=="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -62,8 +62,10 @@
"lodash.template": "^4.4.0",
"moment": "^2.24.0",
"rxjs": "~6.5.4",
"saturn-datepicker": "8.0.1",
"ts-keycode-enum": "^1.0.6",
"tslib": "^1.9.0",
"utility-types": "^3.10.0",
"uuid": "^3.3.3",
"zone.js": "~0.9.1"
},

View File

@ -340,6 +340,11 @@ export class ButtonToggleComponent extends _MatButtonToggleMixinBase implements
}
}
@HostBinding('attr.disabled')
get attrDisabled() {
return this.disabled ? 'disabled' : null;
}
@HostBinding('class.dsh-button-toggle-disabled')
@Input()
get disabled(): boolean {

View File

@ -1,8 +1,9 @@
import { NgModule } from '@angular/core';
import { FormatInputModule } from './format-input';
import { RangeDatepickerModule } from './range-datepicker';
const EXPORTED_DECLARATIONS = [FormatInputModule];
const EXPORTED_DECLARATIONS = [FormatInputModule, RangeDatepickerModule];
@NgModule({
imports: EXPORTED_DECLARATIONS,

View File

@ -16,5 +16,5 @@ export const binConfig: FormatInputConfig = {
mask: binMask,
placeholder: '0000 00',
postfix: '** **** ****',
getValue: (v: string) => (v ? v.replace(' ', '') : '')
toPublicValue: (v: string) => (v ? v.replace(' ', '') : '')
};

View File

@ -7,5 +7,6 @@ export interface FormatInputConfig {
size?: number;
prefix?: string;
postfix?: string;
getValue?: (value: any) => any;
toPublicValue?: (value: string) => string;
toInternalValue?: (value: string) => string;
}

View File

@ -16,7 +16,8 @@ export class FormatInputComponent extends CustomFormControl {
prefix = '';
postfix = '';
size: string = null;
getValue: (v: any) => any;
toInternalValue: (v: any) => any;
toPublicValue: (v: any) => any;
private _format: Type;
@Input()
@ -28,9 +29,9 @@ export class FormatInputComponent extends CustomFormControl {
return this._format;
}
setType(type: Type) {
private setType(type: Type) {
const c = configs[type];
const { placeholder, prefix, postfix, size, mask, getValue } = c;
const { placeholder, prefix, postfix, size, mask, toInternalValue, toPublicValue } = c;
const sizeFromPlaceholder = c.sizeFromPlaceholder === undefined ? true : c.sizeFromPlaceholder;
const estimatedSize = sizeFromPlaceholder && !size && placeholder ? placeholder.length : size;
@ -39,12 +40,15 @@ export class FormatInputComponent extends CustomFormControl {
this.prefix = this.prepareText(prefix);
this.postfix = this.prepareText(postfix);
this.mask = mask;
if (getValue) {
this.getValue = getValue;
if (toInternalValue) {
this.toInternalValue = toInternalValue;
}
if (toPublicValue) {
this.toPublicValue = toPublicValue;
}
}
prepareText(str: string): string {
private prepareText(str: string): string {
return (typeof str === 'string' ? str.replace(/ /g, '\xa0') : str) || '';
}
}

View File

@ -2,4 +2,5 @@ export * from './form-controls.module';
export * from './masks';
export * from './validators';
export * from './format-input';
export * from './range-datepicker';
export * from './utils';

View File

@ -0,0 +1,38 @@
@import '~@angular/material/theming';
@mixin dsh-range-datepicker-theme($theme) {
$gray: map-get($theme, gray);
$bg: map-get($gray, 200);
$foreground: map-get($theme, foreground);
$text: map-get($foreground, text);
.dsh-range-datepicker {
&-button {
border-color: mat-color($foreground, divider, 0.12);
color: $text;
&:enabled {
&:hover,
&:active {
background-color: $bg;
}
}
&:disabled {
color: map-get($foreground, disabled-button);
}
}
}
}
@mixin dsh-range-datepicker-typography($config) {
.dsh-range-datepicker {
&-button {
font: {
family: mat-font-family($config, button);
size: mat-font-size($config, button);
weight: mat-font-weight($config, button);
}
}
}
}

View File

@ -0,0 +1,2 @@
export * from './range-datepicker.module';
export * from './range-datepicker.component';

View File

@ -0,0 +1,103 @@
import { Pipe, PipeTransform, Inject, LOCALE_ID } from '@angular/core';
import { formatDate } from '@angular/common';
import { Moment } from 'moment';
import { TranslocoService } from '@ngneat/transloco';
import moment from 'moment';
@Pipe({ name: 'rangeDate' })
export class RangeDatePipe implements PipeTransform {
constructor(@Inject(LOCALE_ID) private locale: string, private transloco: TranslocoService) {}
transform({ begin, end }: { begin: Moment; end: Moment }): string {
if (begin.isSame(begin.clone().startOf('year'), 'day') && end.isSame(end.clone().endOf('year'), 'day')) {
return this.toYearStr(begin, end);
}
if (begin.isSame(begin.clone().startOf('month'), 'day') && end.isSame(end.clone().endOf('month'), 'day')) {
return this.toMonthStr(begin, end);
}
if (begin.isSame(moment().startOf('week'), 'day') && end.isSame(moment().endOf('week'), 'day')) {
return this.rangeDateTranslate('currentWeek');
}
return this.toDateStr(begin, end);
}
/**
* 2020 год
* С 2019 по 2020 год
*/
private toYearStr(begin: Moment, end: Moment) {
const endStr = `${end.year()} ${this.rangeDateTranslate('year')}`;
if (begin.isSame(end, 'year')) {
return endStr;
}
const fromStr = this.rangeDateTranslate('from');
const toStr = this.rangeDateTranslate('to');
return `${fromStr} ${begin.year()} ${toStr} ${endStr}`;
}
/**
* Январь
* Январь 2020
*
* С января по март
* С января 2019 по март 2019 / С декабря 2019 по март 2020
*/
private toMonthStr(begin: Moment, end: Moment) {
const fromStr = this.rangeDateTranslate('from');
const toStr = this.rangeDateTranslate('to');
const currentYear = this.isCurrentYear(begin, end);
const beginStr = this.formatDate(begin, false, true, !currentYear);
const endStr = this.formatStandaloneDate(end, false, true, !currentYear);
if (begin.isSame(end, 'month')) {
return this.capitalizeFirstLetter(endStr);
}
return `${fromStr} ${beginStr} ${toStr} ${endStr}`;
}
/**
* 2 января
* 2 января 2020
*
* Со 2 по 8 марта / Со 2 января по 8 марта
* Со 2 по 8 марта 2019 / Со 2 января 2019 по 8 марта 2020
*/
private toDateStr(begin: Moment, end: Moment) {
const fromByDayStr = this.rangeDateTranslate(begin.date() === 2 ? 'fromStartWith2' : 'from');
const toStr = this.rangeDateTranslate('to');
const beginStr = this.formatDate(begin, true, !begin.isSame(end, 'month'), !begin.isSame(end, 'year'));
const endStr = this.formatDate(end, true, true, !this.isCurrentYear(begin, end));
if (begin.isSame(end, 'day')) {
return endStr;
}
return `${fromByDayStr} ${beginStr} ${toStr} ${endStr}`;
}
private isCurrentYear(begin: Moment, end: Moment) {
return moment().isSame(begin, 'year') && moment().isSame(end, 'year');
}
private capitalizeFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
private rangeDateTranslate(key: string) {
return this.transloco.translate(`rangeDate.${key}`, null, 'range-datepicker|scoped');
}
private formatDate(date: Moment, d: boolean = false, m: boolean = false, y: boolean = false) {
return formatDate(date.toDate(), [d && 'd', m && 'MMMM', y && 'y'].filter(v => v).join(' '), this.locale);
}
private formatStandaloneDate(date: Moment, d: boolean = false, m: boolean = false, y: boolean = false) {
return formatDate(date.toDate(), [d && 'd', m && 'LLLL', y && 'y'].filter(v => v).join(' '), this.locale);
}
}

View File

@ -0,0 +1,52 @@
<ng-container *transloco="let t; scope: 'range-datepicker'; read: 'rangeDatepicker'">
<div fxLayout class="dsh-range-datepicker">
<button (click)="back()" class="dsh-range-datepicker-button dsh-range-datepicker-back" [disabled]="isMinDate">
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
<button
[matMenuTriggerFor]="menu"
fxFlex
class="dsh-range-datepicker-button dsh-range-datepicker-input"
title="{{ value?.begin | date: 'shortDate' }} - {{ value?.end | date: 'shortDate' }}"
>
<div class="dsh-range-datepicker-input-content">
<ng-container *ngIf="publicValue?.begin && publicValue?.end; else placeholder" [ngSwitch]="period">
{{ publicValue | rangeDate }}
</ng-container>
<ng-template #placeholder>
{{ t.selectPeriod }}
</ng-template>
<input
#input
matInput
[formControl]="formControl"
[satDatepicker]="picker"
[min]="minDate"
[max]="maxDate"
/>
</div>
</button>
<button
(click)="forward()"
class="dsh-range-datepicker-button dsh-range-datepicker-forward"
[disabled]="isMaxDate"
>
<mat-icon>keyboard_arrow_right</mat-icon>
</button>
</div>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="selectPeriod('week')">{{ t.select.currentWeek }}</button>
<button mat-menu-item (click)="selectPeriod('month')">{{ current | date: 'LLLL' | titlecase }}</button>
<button mat-menu-item (click)="selectPeriod('3month')">{{ t.select.threeMonths }}</button>
<button mat-menu-item (click)="selectPeriod('year')">
{{ t.select.year | translocoParams: { year: current | date: 'y' } }}
</button>
<mat-divider class="dsh-range-datepicker-menu-divider"></mat-divider>
<button mat-menu-item (click)="picker.open(); selectPeriod()">
{{ t.select.period }}
</button>
</mat-menu>
<sat-datepicker #picker [rangeMode]="true"></sat-datepicker>
</ng-container>

View File

@ -0,0 +1,69 @@
$height: 50px;
$border-radius: 4px;
:host {
min-width: 0;
}
.dsh-range-datepicker {
&-button {
white-space: nowrap;
transition: background-color 0.25s ease-in-out;
background-color: transparent;
display: block;
outline: none;
cursor: pointer;
border: 1px solid;
padding: 0 10px;
margin: 0;
line-height: $height;
}
&-back,
&-forward {
width: $height;
& > * {
vertical-align: middle;
}
}
&-back {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
&-forward {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
&-input {
position: relative;
border-left: none;
border-right: none;
overflow: hidden;
input {
position: absolute;
left: 0;
top: 0;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
border: none;
visibility: hidden;
}
}
&-input-content {
overflow: hidden;
text-overflow: ellipsis;
}
&-menu-divider {
width: calc(100% + 20px);
transform: translate(-10px, -5px);
}
}

View File

@ -0,0 +1,199 @@
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { MatFormFieldControl } from '@angular/material';
import moment, { Moment } from 'moment';
import { SatDatepickerRangeValue } from 'saturn-datepicker';
import { SetIntersection } from 'utility-types';
import { map } from 'rxjs/operators';
import { CustomFormControl } from '../utils';
type InternalRange = SatDatepickerRangeValue<Date>;
export type Range = SatDatepickerRangeValue<Moment>;
type MomentPeriod = SetIntersection<moment.unitOfTime.StartOf, 'week' | 'month' | 'year'>;
type Period = MomentPeriod | '3month';
@Component({
selector: 'dsh-range-datepicker',
templateUrl: 'range-datepicker.component.html',
styleUrls: ['range-datepicker.component.scss'],
providers: [{ provide: MatFormFieldControl, useExisting: RangeDatepickerComponent }]
})
export class RangeDatepickerComponent extends CustomFormControl<InternalRange, Range> {
minDate = moment()
.subtract(15, 'year')
.startOf('year')
.toDate();
@Input()
set min(min: Moment) {
this.minDate = min.toDate();
}
get min() {
return moment(this.minDate);
}
maxDate = moment()
.endOf('day')
.toDate();
@Input()
set max(max: Moment) {
this.maxDate = max.toDate();
}
get max() {
return moment(this.maxDate);
}
@ViewChild('input', { static: false })
set input(input: ElementRef<HTMLInputElement>) {
if (input && input.nativeElement) {
this.setInputElement(input.nativeElement);
}
}
current = moment();
period: Period = null;
formControlSubscription = this.formControl.valueChanges.pipe(map(this.toPublicValue.bind(this))).subscribe(() => {
if (!this.period) {
this.period = this.takeUnitOfTime();
}
});
get isMaxDate() {
return this.publicValue.end.isSameOrAfter(this.max, 'day');
}
get isMinDate() {
return this.publicValue.begin.isSameOrBefore(this.min, 'day');
}
toPublicValue({ begin, end }: InternalRange): Range {
return { begin: moment(begin), end: moment(end) };
}
toInternalValue({ begin, end }: Range): InternalRange {
return { begin: begin.toDate(), end: end.toDate() };
}
back() {
const { begin, end } = this.publicValue;
switch (this.period) {
case 'year': {
const newBegin = begin.clone().subtract(1, 'year');
this.changeRange(newBegin, newBegin.clone().endOf('year'));
return;
}
case '3month': {
const newBegin = begin.clone().subtract(3, 'month');
this.changeRange(
newBegin,
newBegin
.clone()
.add(2, 'month')
.endOf('month')
);
return;
}
case 'month': {
const newBegin = begin.clone().subtract(1, 'month');
this.changeRange(newBegin, newBegin.clone().endOf('month'));
return;
}
case 'week': {
const newBegin = begin.clone().subtract(1, 'week');
this.changeRange(newBegin, newBegin.clone().endOf('week'));
return;
}
default:
const diff = end.diff(begin);
this.changeRange(begin.subtract(diff).subtract(1, 'day'), end.subtract(diff).subtract(1, 'day'));
}
}
forward() {
const { begin, end } = this.publicValue;
switch (this.period) {
case 'year': {
const newBegin = begin.clone().add(1, 'year');
this.changeRange(newBegin, newBegin.clone().endOf('year'));
return;
}
case '3month': {
const newBegin = begin.clone().add(3, 'month');
this.changeRange(
newBegin,
newBegin
.clone()
.add(2, 'month')
.endOf('month')
);
return;
}
case 'month': {
const newBegin = begin.clone().add(1, 'month');
this.changeRange(newBegin, newBegin.clone().endOf('month'));
return;
}
case 'week': {
const newBegin = begin.clone().add(1, 'week');
this.changeRange(newBegin, newBegin.clone().endOf('week'));
return;
}
default:
const diff = end.diff(begin, 'day');
this.changeRange(begin.clone().add(diff + 1, 'day'), end.clone().add(diff + 1, 'day'));
}
}
selectPeriod(period: Period = null) {
this.period = period;
switch (period) {
case 'year':
this.changeRange(moment().startOf('year'), moment().endOf('year'));
break;
case '3month':
this.changeRange(
moment()
.subtract(2, 'month')
.startOf('month'),
moment().endOf('month')
);
break;
case 'month':
this.changeRange(moment().startOf('month'), moment().endOf('month'));
break;
case 'week':
this.changeRange(moment().startOf('week'), moment().endOf('week'));
break;
}
}
private checkIsUnitOfTime(unitOfTime: MomentPeriod, countOfUnits = 1): boolean {
const { begin, end } = this.publicValue;
const beginOfUnit = begin.clone().startOf(unitOfTime);
const expectedEndOfPeriodByBegin = beginOfUnit
.clone()
.add(countOfUnits - 1, unitOfTime)
.endOf(unitOfTime);
return begin.isSame(beginOfUnit, 'day') && end.isSame(expectedEndOfPeriodByBegin, 'day');
}
private takeUnitOfTime(): Period {
if (this.checkIsUnitOfTime('year')) {
return 'year';
}
if (this.checkIsUnitOfTime('month', 3)) {
return '3month';
}
if (this.checkIsUnitOfTime('month')) {
return 'month';
}
if (this.checkIsUnitOfTime('week')) {
return 'week';
}
return null;
}
private changeRange(begin: Moment, end: Moment) {
this.publicValue = { begin, end };
}
}

View File

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core';
import { SatDatepickerModule, SatNativeDateModule } from 'saturn-datepicker';
import { MatNativeDateModule, MatMenuModule, MatIconModule, MatInputModule, MatDividerModule } from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { TranslocoModule } from '@ngneat/transloco';
import { RangeDatepickerComponent } from './range-datepicker.component';
import { ButtonToggleModule } from '../../button-toggle';
import { RangeDatePipe } from './range-date.pipe';
@NgModule({
imports: [
SatDatepickerModule,
SatNativeDateModule,
MatNativeDateModule,
MatMenuModule,
MatIconModule,
ButtonToggleModule,
FlexLayoutModule,
MatInputModule,
ReactiveFormsModule,
CommonModule,
TranslocoModule,
MatDividerModule
],
declarations: [RangeDatepickerComponent, RangeDatePipe],
exports: [RangeDatepickerComponent]
})
export class RangeDatepickerModule {}

View File

@ -13,7 +13,8 @@ import {
Optional,
Self,
HostListener,
OnChanges
OnChanges,
SimpleChanges
} from '@angular/core';
import uuid from 'uuid';
import { AutofillMonitor } from '@angular/cdk/text-field';
@ -21,8 +22,8 @@ import { Platform } from '@angular/cdk/platform';
import { InputMixinBase } from './input-base';
export class CustomFormControl<T extends any = string> extends InputMixinBase
implements AfterViewInit, ControlValueAccessor, MatFormFieldControl<T>, OnDestroy, DoCheck, OnChanges {
export class CustomFormControl<I extends any = any, P extends any = I> extends InputMixinBase
implements AfterViewInit, ControlValueAccessor, MatFormFieldControl<I>, OnDestroy, DoCheck, OnChanges {
/** The aria-describedby attribute on the input for improved a11y. */
@HostBinding('attr.aria-describedby') _ariaDescribedby: string;
@ -32,6 +33,7 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
autofilled = false;
protected _disabled = false;
@Input()
get disabled(): boolean {
if (this.ngControl && this.ngControl.disabled !== null) {
@ -49,8 +51,8 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
this.stateChanges.next();
}
}
protected _disabled = false;
protected _id: string;
@HostBinding('attr.id')
@Input()
get id(): string {
@ -59,11 +61,11 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
set id(value: string) {
this._id = value || `custom-input-${uuid()}`;
}
protected _id: string;
@Input()
placeholder: string;
protected _required = false;
@Input()
get required(): boolean {
return this._required;
@ -71,21 +73,27 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
set required(value: boolean) {
this._required = coerceBooleanProperty(value);
}
protected _required = false;
protected type = 'text';
@Input()
get value(): T {
get value() {
return this.formControl.value;
}
set value(value: T) {
set value(value: I) {
this.formControl.setValue(value);
this.stateChanges.next();
}
get publicValue() {
return this.toPublicValue(this.value);
}
set publicValue(value: P) {
this.value = this.toInternalValue(value);
}
get details() {
return this.getDetails(this.value);
return this.getDetails(this.publicValue);
}
@HostBinding('class.floating')
@ -93,16 +101,13 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
return this.focused || !this.empty;
}
_inputRef = new ElementRef<HTMLInputElement>(null);
get inputRef() {
this._inputRef.nativeElement = this.elementRef.nativeElement.querySelector('input');
return this._inputRef;
}
inputRef = new ElementRef<HTMLInputElement>(null);
get empty(): boolean {
return !this.formControl.value;
}
private _focused = false;
get focused(): boolean {
return this._focused;
}
@ -113,8 +118,8 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
formControl = new FormControl();
autocompleteOrigin: MatAutocompleteOrigin;
monitorsRegistered = false;
private _focused = false;
private _onTouched: () => void;
constructor(
@ -136,7 +141,7 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
}
}
ngOnChanges() {
ngOnChanges(_changes?: SimpleChanges) {
this.stateChanges.next();
}
@ -150,15 +155,7 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
}
ngAfterViewInit(): void {
if (this.platform.isBrowser) {
this.autofillMonitor.monitor(this.inputRef).subscribe(event => {
this.autofilled = event.isAutofilled;
this.stateChanges.next();
});
}
this.focusMonitor.monitor(this.elementRef.nativeElement, true).subscribe(focusOrigin => {
this.focused = !!focusOrigin;
});
this.setInputElement();
}
ngOnDestroy() {
@ -180,14 +177,29 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
this._onTouched();
}
registerOnChange(onChange: (value: T) => void): void {
this.formControl.valueChanges.subscribe(v => onChange(this.getValue(v)));
registerOnChange(onChange: (value: P) => void): void {
this.formControl.valueChanges.subscribe(v => onChange(this.toPublicValue(v)));
}
registerOnTouched(onTouched: () => void): void {
this._onTouched = onTouched;
}
private registerMonitors() {
if (!this.monitorsRegistered && this.inputRef.nativeElement) {
this.monitorsRegistered = true;
if (this.platform.isBrowser) {
this.autofillMonitor.monitor(this.inputRef).subscribe(event => {
this.autofilled = event.isAutofilled;
this.stateChanges.next();
});
}
this.focusMonitor.monitor(this.elementRef.nativeElement, true).subscribe(focusOrigin => {
this.focused = !!focusOrigin;
});
}
}
setDescribedByIds(ids: string[]): void {
this._ariaDescribedby = ids.join(' ');
}
@ -202,15 +214,24 @@ export class CustomFormControl<T extends any = string> extends InputMixinBase
this.disabled = shouldDisable;
}
writeValue(value: string): void {
this.formControl.setValue(value, { emitEvent: false });
setInputElement(input: HTMLInputElement = this.elementRef.nativeElement.querySelector('input')) {
this.inputRef.nativeElement = input;
this.registerMonitors();
}
getDetails(value: T) {
return value;
writeValue(value: P): void {
this.formControl.setValue(this.toInternalValue(value), { emitEvent: false });
}
getValue(value = this.value): T {
return value;
protected getDetails(value: P): string {
return value as any;
}
protected toInternalValue(value: P): I {
return value as any;
}
protected toPublicValue(value: I): P {
return value as any;
}
}

View File

@ -6,22 +6,34 @@
}"
(dshResized)="cardHeight = $event.height"
>
<div (dshResized)="setBaseContentHeight($event.height)" class="dsh-float-panel-base">
<div fxLayout fxLayout.lt-md="column" [fxLayoutGap]="layoutGap">
<div fxFlex.gt-sm>
<ng-content></ng-content>
<div class="dsh-float-panel-base" (dshResized)="setBaseContentHeight($event.height)">
<div fxLayout="column" [fxLayoutGap]="layoutGap">
<div fxLayout fxLayout.lt-md="column" [fxLayoutGap]="layoutGap">
<div fxFlex.gt-sm>
<ng-content></ng-content>
</div>
<div
*ngIf="!expanded && floatPanelMore"
class="dsh-float-panel-actions"
fxLayout
fxLayoutAlign="center center"
>
<button dsh-icon-button (click)="expandToggle()" fxHide.lt-md>
<mat-icon svgIcon="keyboard_arrow_down"></mat-icon>
</button>
<button dsh-button (click)="expandToggle()" fxFlex fxHide.gt-sm>
{{ t.showMore }}
</button>
</div>
</div>
<div *ngIf="!expanded" class="dsh-float-panel-actions" fxLayout fxLayoutAlign="center center">
<button dsh-icon-button (click)="expandToggle()" fxHide.lt-md>
<mat-icon svgIcon="keyboard_arrow_down"></mat-icon>
</button>
<button dsh-button (click)="expandToggle()" fxFlex fxHide.gt-sm>
{{ t.showMore }}
</button>
<div fxLayout fxLayoutAlign="center" *ngIf="!floatPanelMore && floatPanelActions">
<div>
<ng-container *ngTemplateOutlet="floatPanelActions?.templateRef"></ng-container>
</div>
</div>
</div>
</div>
<div class="dsh-float-panel-more" [@expand]="expandTrigger" *ngIf="expanded">
<div class="dsh-float-panel-more" [@expand]="expandTrigger" *ngIf="expanded && floatPanelMore">
<div (dshResized)="setMoreContentHeight($event.height)">
<div fxLayout="column" [fxLayoutGap]="layoutGap">
<div class="dsh-float-panel-more-content">

View File

@ -29,6 +29,7 @@ import { LastUpdatedModule } from '../operations/last-updated/last-updated.modul
import { SpinnerModule } from '../../../spinner';
import { ActionsComponent } from './table/actions';
import { CreateReportDialogComponent } from './create-report-dialog';
import { FormControlsModule } from '../../../form-controls';
@NgModule({
imports: [
@ -54,7 +55,8 @@ import { CreateReportDialogComponent } from './create-report-dialog';
SpinnerModule,
MatDialogModule,
MatSnackBarModule,
MatMenuModule
MatMenuModule,
FormControlsModule
],
declarations: [
ReportsComponent,

View File

@ -1,10 +1,8 @@
import { Moment } from 'moment';
import { Report } from '../../../../api-codegen/anapi';
import { Range } from '../../../../form-controls';
export interface FormParams {
fromTime: Moment;
toTime: Moment;
date: Range;
reportType: Report.ReportTypeEnum;
shopID?: string;
}

View File

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

View File

@ -1,65 +1,34 @@
<dsh-float-panel *transloco="let r; scope: 'reports'; read: 'reports'" [formGroup]="form" [(expanded)]="expanded">
<ng-container *transloco="let t">
<div
fxLayout
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutAlign="space-between center"
fxLayoutAlign.lt-md="space-between stretch"
>
<dsh-daterange-selector
fxFlex.gt-sm
[value]="form.value"
(selectDaterange)="selectDaterange($event)"
(selectMore)="expanded = true"
></dsh-daterange-selector>
<dsh-justify-wrapper fxFlex.gt-sm fxLayout fxLayout.lt-md="column" fxLayoutGap="20px">
<mat-form-field fxFlex>
<mat-label>{{ r.filter.shopID }}</mat-label>
<mat-select formControlName="shopID">
<mat-option>
{{ t.any }}
</mat-option>
<mat-option *ngFor="let shopInfo of shopsInfo$ | async" [value]="shopInfo.shopID">
{{ shopInfo.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ r.filter.type }}</mat-label>
<mat-select formControlName="reportType">
<mat-option>
{{ t.any }}
</mat-option>
<mat-option *ngFor="let type of reportTypes" [value]="type">
{{ r.type[type] }}
</mat-option>
</mat-select>
</mat-form-field>
</dsh-justify-wrapper>
</div>
<dsh-float-panel-more>
<div fxLayout fxLayout.lt-md="column" fxLayoutGap.gt-sm="20px">
<div fxFlex.gt-sm fxLayout fxLayout.xs="column" fxLayoutGap.gt-xs="20px">
<mat-form-field fxFlex>
<mat-label>{{ t.period.fromTime }}</mat-label>
<input required matInput formControlName="fromTime" [matDatepicker]="fromTime" />
<mat-datepicker-toggle matSuffix [for]="fromTime"></mat-datepicker-toggle>
<mat-datepicker #fromTime></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ t.period.toTime }}</mat-label>
<input required matInput formControlName="toTime" [matDatepicker]="toTime" />
<mat-datepicker-toggle matSuffix [for]="toTime"></mat-datepicker-toggle>
<mat-datepicker #toTime></mat-datepicker>
</mat-form-field>
</div>
</div>
</dsh-float-panel-more>
<dsh-float-panel-actions>
<button dsh-button (click)="reset()">
{{ t.resetSearchParams }}
</button>
</dsh-float-panel-actions>
</ng-container>
<dsh-float-panel *transloco="let r; scope: 'reports'; read: 'reports'" [formGroup]="form">
<dsh-justify-wrapper
fxLayout
fxLayoutGap="20px"
fxLayoutAlign="space-between center"
fxLayout.lt-md="column"
fxLayoutAlign.lt-md="space-between stretch"
*transloco="let t"
>
<dsh-range-datepicker formControlName="date" fxFlex></dsh-range-datepicker>
<mat-form-field fxFlex>
<mat-label>{{ r.filter.shopID }}</mat-label>
<mat-select formControlName="shopID">
<mat-option>
{{ t.any }}
</mat-option>
<mat-option *ngFor="let shopInfo of shopsInfo$ | async" [value]="shopInfo.shopID">
{{ shopInfo.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ r.filter.type }}</mat-label>
<mat-select formControlName="reportType">
<mat-option>
{{ t.any }}
</mat-option>
<mat-option *ngFor="let type of reportTypes" [value]="type">
{{ r.type[type] }}
</mat-option>
</mat-select>
</mat-form-field>
</dsh-justify-wrapper>
</dsh-float-panel>

View File

@ -1,7 +1,6 @@
import { Component } from '@angular/core';
import { Report } from '../../../../api-codegen/anapi/swagger-codegen';
import { SearchFormValue } from '../../operations/search-form-value';
import { SearchFormService } from './search-form.service';
import { ReportsService } from '../reports.service';
@ -13,16 +12,8 @@ import { ReportsService } from '../reports.service';
export class SearchFormComponent {
form = this.searchFormService.form;
reset = this.searchFormService.reset;
shopsInfo$ = this.reportsService.shopsInfo$;
reportTypes = Object.values(Report.ReportTypeEnum);
expanded = false;
constructor(private searchFormService: SearchFormService, private reportsService: ReportsService) {}
selectDaterange(v: SearchFormValue) {
this.form.patchValue(v);
}
}

View File

@ -8,19 +8,20 @@ import { toSearchParams } from './to-search-params';
import { FormParams } from './form-params';
import { toFormValue } from './to-form-value';
import { toQueryParams } from './to-query-params';
import { QueryParams } from './query-params';
@Injectable()
export class SearchFormService {
static defaultParams: FormParams = {
fromTime: moment()
.subtract(1, 'month')
.startOf('day'),
toTime: moment().endOf('day'),
defaultParams: FormParams = {
shopID: null,
reportType: null
reportType: null,
date: {
begin: moment().startOf('month'),
end: moment().endOf('month')
}
};
form = this.fb.group(SearchFormService.defaultParams);
form = this.fb.group(this.defaultParams);
constructor(
private fb: FormBuilder,
@ -36,7 +37,7 @@ export class SearchFormService {
}
reset() {
this.form.setValue(SearchFormService.defaultParams);
this.form.setValue(this.defaultParams);
}
private init() {
@ -46,8 +47,8 @@ export class SearchFormService {
}
private syncQueryParams() {
const queryParams = this.route.snapshot.queryParams as Record<keyof FormParams, string>;
const formValue = toFormValue(queryParams, SearchFormService.defaultParams);
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));

View File

@ -2,16 +2,19 @@ import moment from 'moment';
import { FormParams } from './form-params';
import { Report } from '../../../../api-codegen/anapi/swagger-codegen';
import { QueryParams } from './query-params';
export function toFormValue(
{ fromTime, toTime, reportType, ...params }: Record<keyof FormParams, string>,
{ fromTime, toTime, reportType, ...params }: QueryParams,
defaultParams: FormParams
): FormParams {
return {
...defaultParams,
...params,
fromTime: fromTime ? moment(fromTime) : defaultParams.fromTime,
toTime: toTime ? moment(toTime) : defaultParams.toTime,
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,9 +1,10 @@
import { FormParams } from './form-params';
import { QueryParams } from './query-params';
export function toQueryParams({ fromTime, toTime, ...params }: FormParams): Partial<Record<keyof FormParams, string>> {
export function toQueryParams({ date, ...params }: FormParams): QueryParams {
return {
...params,
fromTime: fromTime.utc().format(),
toTime: toTime.utc().format()
fromTime: date.begin.utc().format(),
toTime: date.end.utc().format()
};
}

View File

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

View File

@ -3,7 +3,7 @@
<mat-icon svgIcon="more_vert"></mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>
<button mat-menu-item (click)="goToReportDetails(report.id)">
{{ t.details }}
</button>
</mat-menu>

View File

@ -0,0 +1,16 @@
{
"selectPeriod": "Выберите период",
"select": {
"currentWeek": "Текущая неделя",
"threeMonths": "3 месяца",
"year": "{{year}} год",
"period": "Указать период..."
},
"rangeDate": {
"from": "С",
"fromStartWith2": "Со",
"to": "по",
"currentWeek": "Текущая неделя",
"year": "год"
}
}

View File

@ -1,4 +1,5 @@
@import '~@angular/material/theming';
@import '~saturn-datepicker/bundle.css';
@import './utils/typography';
@import '../app/container/container-theme';
@import '../app/dropdown/dropdown-theme';
@ -27,6 +28,7 @@
@import '../app/dadata/dadata-theme';
@import '../app/sections/payment-details/payment-details-theme';
@import '../app/layout/panel/panel-theme';
@import '../app/form-controls/range-datepicker/range-datepicker-theme';
@mixin dsh-overrides() {
@include body-override();
@ -53,6 +55,7 @@
@include dsh-dadata-autocomplete-typography($config);
@include mat-radio-button-override-typography($config);
@include dsh-payment-details-typography($config);
@include dsh-range-datepicker-typography($config);
@include dsh-panel-typography($config);
}

View File

@ -32,6 +32,7 @@
@import '../../app/sections/invoice-details/payments/payments-theme';
@import '../../app/sections/payment-section/payouts/payout-panel/payout-panel-theme';
@import '../../app/layout/panel/panel-theme';
@import '../../app/form-controls/range-datepicker/range-datepicker-theme';
@mixin dsh-theme($theme) {
body.#{map-get($theme, name)} {
@ -68,6 +69,7 @@
@include dsh-details-secondary-title-theme($theme);
@include dsh-file-uploader-theme($theme);
@include dsh-panel-theme($theme);
@include dsh-range-datepicker-theme($theme);
@include dsh-payout-panel-theme($theme);
}
}