diff --git a/package-lock.json b/package-lock.json index 01177620..bc49aba3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5e581a98..80114f3a 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/app/button-toggle/button-toggle.component.ts b/src/app/button-toggle/button-toggle.component.ts index 8d8affb6..a547cca3 100644 --- a/src/app/button-toggle/button-toggle.component.ts +++ b/src/app/button-toggle/button-toggle.component.ts @@ -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 { diff --git a/src/app/form-controls/form-controls.module.ts b/src/app/form-controls/form-controls.module.ts index 8bbc2781..dbc99cba 100644 --- a/src/app/form-controls/form-controls.module.ts +++ b/src/app/form-controls/form-controls.module.ts @@ -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, diff --git a/src/app/form-controls/format-input/configs/bank-card/bin.ts b/src/app/form-controls/format-input/configs/bank-card/bin.ts index 13222f39..a0bfc9b7 100644 --- a/src/app/form-controls/format-input/configs/bank-card/bin.ts +++ b/src/app/form-controls/format-input/configs/bank-card/bin.ts @@ -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(' ', '') : '') }; diff --git a/src/app/form-controls/format-input/configs/format-input-config.ts b/src/app/form-controls/format-input/configs/format-input-config.ts index f6accf28..e47c15bd 100644 --- a/src/app/form-controls/format-input/configs/format-input-config.ts +++ b/src/app/form-controls/format-input/configs/format-input-config.ts @@ -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; } diff --git a/src/app/form-controls/format-input/format-input.component.ts b/src/app/form-controls/format-input/format-input.component.ts index 9b01f95c..ed06f1da 100644 --- a/src/app/form-controls/format-input/format-input.component.ts +++ b/src/app/form-controls/format-input/format-input.component.ts @@ -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) || ''; } } diff --git a/src/app/form-controls/index.ts b/src/app/form-controls/index.ts index 03842596..7e09157f 100644 --- a/src/app/form-controls/index.ts +++ b/src/app/form-controls/index.ts @@ -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'; diff --git a/src/app/form-controls/range-datepicker/_range-datepicker-theme.scss b/src/app/form-controls/range-datepicker/_range-datepicker-theme.scss new file mode 100644 index 00000000..4ebc4277 --- /dev/null +++ b/src/app/form-controls/range-datepicker/_range-datepicker-theme.scss @@ -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); + } + } + } +} diff --git a/src/app/form-controls/range-datepicker/index.ts b/src/app/form-controls/range-datepicker/index.ts new file mode 100644 index 00000000..cb056a76 --- /dev/null +++ b/src/app/form-controls/range-datepicker/index.ts @@ -0,0 +1,2 @@ +export * from './range-datepicker.module'; +export * from './range-datepicker.component'; diff --git a/src/app/form-controls/range-datepicker/range-date.pipe.ts b/src/app/form-controls/range-datepicker/range-date.pipe.ts new file mode 100644 index 00000000..d6b954f0 --- /dev/null +++ b/src/app/form-controls/range-datepicker/range-date.pipe.ts @@ -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); + } +} diff --git a/src/app/form-controls/range-datepicker/range-datepicker.component.html b/src/app/form-controls/range-datepicker/range-datepicker.component.html new file mode 100644 index 00000000..5838e779 --- /dev/null +++ b/src/app/form-controls/range-datepicker/range-datepicker.component.html @@ -0,0 +1,52 @@ + +
+ + + +
+ + + + + + + + + + + +
diff --git a/src/app/form-controls/range-datepicker/range-datepicker.component.scss b/src/app/form-controls/range-datepicker/range-datepicker.component.scss new file mode 100644 index 00000000..941faa0e --- /dev/null +++ b/src/app/form-controls/range-datepicker/range-datepicker.component.scss @@ -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); + } +} diff --git a/src/app/form-controls/range-datepicker/range-datepicker.component.ts b/src/app/form-controls/range-datepicker/range-datepicker.component.ts new file mode 100644 index 00000000..d309ce16 --- /dev/null +++ b/src/app/form-controls/range-datepicker/range-datepicker.component.ts @@ -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; +export type Range = SatDatepickerRangeValue; + +type MomentPeriod = SetIntersection; +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 { + 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) { + 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 }; + } +} diff --git a/src/app/form-controls/range-datepicker/range-datepicker.module.ts b/src/app/form-controls/range-datepicker/range-datepicker.module.ts new file mode 100644 index 00000000..6bef6db6 --- /dev/null +++ b/src/app/form-controls/range-datepicker/range-datepicker.module.ts @@ -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 {} diff --git a/src/app/form-controls/utils/custom-form-control.ts b/src/app/form-controls/utils/custom-form-control.ts index 9b650cb8..bdf62d26 100644 --- a/src/app/form-controls/utils/custom-form-control.ts +++ b/src/app/form-controls/utils/custom-form-control.ts @@ -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 extends InputMixinBase - implements AfterViewInit, ControlValueAccessor, MatFormFieldControl, OnDestroy, DoCheck, OnChanges { +export class CustomFormControl extends InputMixinBase + implements AfterViewInit, ControlValueAccessor, MatFormFieldControl, 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 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 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 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 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 extends InputMixinBase return this.focused || !this.empty; } - _inputRef = new ElementRef(null); - get inputRef() { - this._inputRef.nativeElement = this.elementRef.nativeElement.querySelector('input'); - return this._inputRef; - } + inputRef = new ElementRef(null); get empty(): boolean { return !this.formControl.value; } + private _focused = false; get focused(): boolean { return this._focused; } @@ -113,8 +118,8 @@ export class CustomFormControl extends InputMixinBase formControl = new FormControl(); autocompleteOrigin: MatAutocompleteOrigin; + monitorsRegistered = false; - private _focused = false; private _onTouched: () => void; constructor( @@ -136,7 +141,7 @@ export class CustomFormControl extends InputMixinBase } } - ngOnChanges() { + ngOnChanges(_changes?: SimpleChanges) { this.stateChanges.next(); } @@ -150,15 +155,7 @@ export class CustomFormControl 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 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 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; } } diff --git a/src/app/layout/float-panel/float-panel.component.html b/src/app/layout/float-panel/float-panel.component.html index 4575101d..9d5839d7 100644 --- a/src/app/layout/float-panel/float-panel.component.html +++ b/src/app/layout/float-panel/float-panel.component.html @@ -6,22 +6,34 @@ }" (dshResized)="cardHeight = $event.height" > -
-
-
- +
+
+
+
+ +
+
+ + +
-
- - +
+
+ +
-
+
diff --git a/src/app/sections/payment-section/reports/reports.module.ts b/src/app/sections/payment-section/reports/reports.module.ts index b08c344c..f6203644 100644 --- a/src/app/sections/payment-section/reports/reports.module.ts +++ b/src/app/sections/payment-section/reports/reports.module.ts @@ -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, diff --git a/src/app/sections/payment-section/reports/search-form/form-params.ts b/src/app/sections/payment-section/reports/search-form/form-params.ts index 4e1ded53..a8649d67 100644 --- a/src/app/sections/payment-section/reports/search-form/form-params.ts +++ b/src/app/sections/payment-section/reports/search-form/form-params.ts @@ -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; } diff --git a/src/app/sections/payment-section/reports/search-form/query-params.ts b/src/app/sections/payment-section/reports/search-form/query-params.ts new file mode 100644 index 00000000..777a1af7 --- /dev/null +++ b/src/app/sections/payment-section/reports/search-form/query-params.ts @@ -0,0 +1,8 @@ +import { Report } from '../../../../api-codegen/anapi'; + +export interface QueryParams { + fromTime: string; + toTime: string; + reportType: Report.ReportTypeEnum; + shopID?: string; +} diff --git a/src/app/sections/payment-section/reports/search-form/search-form.component.html b/src/app/sections/payment-section/reports/search-form/search-form.component.html index 5f7183c6..5e470edb 100644 --- a/src/app/sections/payment-section/reports/search-form/search-form.component.html +++ b/src/app/sections/payment-section/reports/search-form/search-form.component.html @@ -1,65 +1,34 @@ - - -
- - - - {{ r.filter.shopID }} - - - {{ t.any }} - - - {{ shopInfo.name }} - - - - - {{ r.filter.type }} - - - {{ t.any }} - - - {{ r.type[type] }} - - - - -
- -
-
- - {{ t.period.fromTime }} - - - - - - {{ t.period.toTime }} - - - - -
-
-
- - - -
+ + + + + {{ r.filter.shopID }} + + + {{ t.any }} + + + {{ shopInfo.name }} + + + + + {{ r.filter.type }} + + + {{ t.any }} + + + {{ r.type[type] }} + + + + diff --git a/src/app/sections/payment-section/reports/search-form/search-form.component.ts b/src/app/sections/payment-section/reports/search-form/search-form.component.ts index cdccf640..56c09d2d 100644 --- a/src/app/sections/payment-section/reports/search-form/search-form.component.ts +++ b/src/app/sections/payment-section/reports/search-form/search-form.component.ts @@ -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); - } } diff --git a/src/app/sections/payment-section/reports/search-form/search-form.service.ts b/src/app/sections/payment-section/reports/search-form/search-form.service.ts index ed404887..962c37cd 100644 --- a/src/app/sections/payment-section/reports/search-form/search-form.service.ts +++ b/src/app/sections/payment-section/reports/search-form/search-form.service.ts @@ -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; - 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)); diff --git a/src/app/sections/payment-section/reports/search-form/to-form-value.ts b/src/app/sections/payment-section/reports/search-form/to-form-value.ts index b9566b13..78b5f5f5 100644 --- a/src/app/sections/payment-section/reports/search-form/to-form-value.ts +++ b/src/app/sections/payment-section/reports/search-form/to-form-value.ts @@ -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, + { 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 }; } diff --git a/src/app/sections/payment-section/reports/search-form/to-query-params.ts b/src/app/sections/payment-section/reports/search-form/to-query-params.ts index e5e3e150..ece7e624 100644 --- a/src/app/sections/payment-section/reports/search-form/to-query-params.ts +++ b/src/app/sections/payment-section/reports/search-form/to-query-params.ts @@ -1,9 +1,10 @@ import { FormParams } from './form-params'; +import { QueryParams } from './query-params'; -export function toQueryParams({ fromTime, toTime, ...params }: FormParams): Partial> { +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() }; } diff --git a/src/app/sections/payment-section/reports/search-form/to-search-params.ts b/src/app/sections/payment-section/reports/search-form/to-search-params.ts index c5428b74..0d56fe3e 100644 --- a/src/app/sections/payment-section/reports/search-form/to-search-params.ts +++ b/src/app/sections/payment-section/reports/search-form/to-search-params.ts @@ -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() }; } diff --git a/src/app/sections/payment-section/reports/table/actions/actions.component.html b/src/app/sections/payment-section/reports/table/actions/actions.component.html index 48029df8..6a7891be 100644 --- a/src/app/sections/payment-section/reports/table/actions/actions.component.html +++ b/src/app/sections/payment-section/reports/table/actions/actions.component.html @@ -3,7 +3,7 @@ - diff --git a/src/assets/i18n/range-datepicker/ru.json b/src/assets/i18n/range-datepicker/ru.json new file mode 100644 index 00000000..fa5ea218 --- /dev/null +++ b/src/assets/i18n/range-datepicker/ru.json @@ -0,0 +1,16 @@ +{ + "selectPeriod": "Выберите период", + "select": { + "currentWeek": "Текущая неделя", + "threeMonths": "3 месяца", + "year": "{{year}} год", + "period": "Указать период..." + }, + "rangeDate": { + "from": "С", + "fromStartWith2": "Со", + "to": "по", + "currentWeek": "Текущая неделя", + "year": "год" + } +} diff --git a/src/styles/core.scss b/src/styles/core.scss index edeb379a..73328a32 100644 --- a/src/styles/core.scss +++ b/src/styles/core.scss @@ -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); } diff --git a/src/styles/themes/_theme.scss b/src/styles/themes/_theme.scss index 6a148b20..5c70eee6 100644 --- a/src/styles/themes/_theme.scss +++ b/src/styles/themes/_theme.scss @@ -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); } }