Skip to content

Commit

Permalink
refactor: added schedule filter to dateTimeField()
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Dec 3, 2022
1 parent ab0e381 commit 0e37643
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DateScheduleDayCode } from './../../../../../../../../../packages/date/src/lib/date/date.schedule';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { Component } from '@angular/core';
import { addressField, addressListField, cityField, countryField, emailField, phoneField, nameField, phoneAndLabelSectionField, wrappedPhoneAndLabelField, repeatArrayField, stateField, textAreaField, textField, zipCodeField, phoneListField, dateTimeField, DbxDateTimeFieldTimeMode, toggleField, checkboxField, numberField, latLngTextField, DbxDateTimeValueMode, dateRangeField, dollarAmountField } from '@dereekb/dbx-form';
import { addDays } from 'date-fns';
import { addressField, addressListField, cityField, countryField, emailField, phoneField, nameField, phoneAndLabelSectionField, wrappedPhoneAndLabelField, repeatArrayField, stateField, textAreaField, textField, zipCodeField, phoneListField, dateTimeField, DbxDateTimeFieldTimeMode, toggleField, checkboxField, numberField, latLngTextField, DbxDateTimeValueMode, dateRangeField, dollarAmountField, DateTimePickerConfiguration } from '@dereekb/dbx-form';
import { addDays, startOfDay } from 'date-fns';
import { addSuffixFunction, randomBoolean } from '@dereekb/util';
import { of } from 'rxjs';

@Component({
templateUrl: './value.component.html'
Expand Down Expand Up @@ -46,10 +48,62 @@ export class DocFormValueComponent {
dateTimeField({ key: 'timeOptional', timeMode: DbxDateTimeFieldTimeMode.OPTIONAL, description: 'This date field is for picking a day, with an optional time.' }),
dateTimeField({ key: 'dayOnly', timeMode: DbxDateTimeFieldTimeMode.NONE, description: 'This date field is for picking a day only.' }),
dateTimeField({ key: 'dayOnlyAsString', timeMode: DbxDateTimeFieldTimeMode.NONE, valueMode: DbxDateTimeValueMode.DAY_STRING, description: 'This date field is for picking a day only and as an ISO8601DayString.' }),
dateTimeField({ key: 'timeOnly', timeOnly: true, description: 'This date field is for picking a time only. The date hint is also hidden.', hideDateHint: true })
dateTimeField({ key: 'timeOnly', timeOnly: true, description: 'This date field is for picking a time only. The date hint is also hidden.', hideDateHint: true }),
dateTimeField({
key: 'dateWithASchedule',
required: true,
description: 'This date is limited to specific days specified by a schedule of M/W/F and the next 7 days from today. A minimum of today and a maximum of 14 days from now.',
getConfigObs: () => {
const config: DateTimePickerConfiguration = {
limits: {
min: startOfDay(new Date()),
max: addDays(new Date(), 14)
},
schedule: {
w: `${DateScheduleDayCode.MONDAY}${DateScheduleDayCode.WEDNESDAY}${DateScheduleDayCode.FRIDAY}`,
d: [0, 1, 2, 3, 4, 5, 6] // next 7 days
}
};

return of(config);
}
})
];

readonly dateRangeFields: FormlyFieldConfig[] = [dateRangeField({})];
readonly dateRangeFields: FormlyFieldConfig[] = [
dateRangeField({}),
dateRangeField({
start: {
key: 'startLimited',
description: 'Must start on a M/T and no later than 14 days ago',
getConfigObs: () => {
const config: DateTimePickerConfiguration = {
limits: {
min: addDays(startOfDay(new Date()), -14)
},
schedule: {
w: `${DateScheduleDayCode.MONDAY}${DateScheduleDayCode.TUESDAY}`
}
};

return of(config);
}
},
end: {
key: 'endLimited',
description: 'Must end on a W/T/F',
getConfigObs: () => {
const config: DateTimePickerConfiguration = {
schedule: {
w: `${DateScheduleDayCode.WEDNESDAY}${DateScheduleDayCode.THURSDAY}${DateScheduleDayCode.FRIDAY}`
}
};

return of(config);
}
}
})
];

readonly addressFields: FormlyFieldConfig[] = [
//
Expand Down
14 changes: 14 additions & 0 deletions packages/date/src/lib/date/date.schedule.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ describe('dateScheduleDateFilter()', () => {
const start = new Date('2022-01-02T00:00:00Z'); // Sunday

describe('function', () => {
describe('included', () => {
const dayIndexes = [0, 1, 2, 3];
const schedule: DateScheduleDateFilterConfig = { start: systemBaseDateToNormalDate(start), w: '0', d: dayIndexes };
const firstFourDays = dateScheduleDateFilter(schedule);

it('should allow the included days (indexes)', () => {
const maxIndex = 5;
const dateBlocks: DateBlockIndex[] = range(0, maxIndex);
const results = dateBlocks.filter(firstFourDays);

expect(results.length).toBe(dayIndexes.length);
});
});

describe('schedule', () => {
describe('weekdays and weekends', () => {
const schedule: DateScheduleDateFilterConfig = { start: systemBaseDateToNormalDate(start), w: '89' };
Expand Down
2 changes: 1 addition & 1 deletion packages/date/src/lib/date/date.schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export function dateScheduleDateFilter(config: DateScheduleDateFilterConfig): Da
i = input;
day = dayForIndex(i);
} else {
i = differenceInDays(input, firstDateDay);
i = differenceInDays(input, firstDate);
day = dayOfWeek(input);
}

Expand Down
1 change: 1 addition & 0 deletions packages/date/src/lib/date/date.time.limit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Minutes, Hours, Days, LogicalDate, DATE_NOW_VALUE, Maybe } from '@dereekb/util';
import { addMinutes, isBefore, min as minDate, max as maxDate } from 'date-fns';
import { daysToMinutes, isAfter, roundDownToMinute, takeNextUpcomingTime } from './date';
import { DateSchedule } from './date.schedule';

export interface LimitDateTimeConfig {
/**
Expand Down
68 changes: 60 additions & 8 deletions packages/date/src/lib/date/date.time.minute.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { dateFromLogicalDate, Minutes } from '@dereekb/util';
import { dateFromLogicalDate, Minutes, DecisionFunction } from '@dereekb/util';
import { addMinutes, isAfter, set, isBefore } from 'date-fns';
import { roundDownToMinute } from './date';
import { roundDateTimeDownToSteps, StepRoundDateTimeDown } from './date.round';
import { DateSchedule, dateScheduleDateFilter, DateScheduleDateFilter, DateScheduleDateFilterConfig } from './date.schedule';
import { LimitDateTimeConfig, LimitDateTimeInstance } from './date.time.limit';

export interface DateTimeMinuteConfig extends LimitDateTimeConfig {
/**
* Default date to consider.
*/
date?: Date;

/**
* Number of minutes each "step" is.
*/
step?: Minutes;

/**
* Additional behavior
*/
Expand All @@ -25,6 +28,10 @@ export interface DateTimeMinuteConfig extends LimitDateTimeConfig {
*/
capToMaxLimit?: boolean;
};
/**
* Schedule to filter the days to.
*/
schedule?: DateScheduleDateFilterConfig;
}

/**
Expand Down Expand Up @@ -55,6 +62,11 @@ export interface DateTimeMinuteDateStatus {
* If the date is in the past.
*/
inPast: boolean;

/**
* If the date is on a schedule day.
*/
isInSchedule: boolean;
}

export interface RoundDateTimeMinute extends StepRoundDateTimeDown {
Expand All @@ -64,17 +76,19 @@ export interface RoundDateTimeMinute extends StepRoundDateTimeDown {
/**
* Instance for working with a single date/time.
*
* Can step the date forward/backwards, and validate
* Can step the date forward/backwards, and validate.
*/
export class DateTimeMinuteInstance {
private _date: Date;
private _step: Minutes;
private _limit: LimitDateTimeInstance;
private _dateFilter: DateScheduleDateFilter;

constructor(readonly config: DateTimeMinuteConfig = {}) {
this._date = config.date ?? new Date();
constructor(readonly config: DateTimeMinuteConfig = {}, dateOverride?: Date | null) {
this._date = (dateOverride == undefined ? config.date : dateOverride) || new Date();
this._step = config.step ?? 1;
this._limit = new LimitDateTimeInstance(config);
this._dateFilter = config.schedule ? dateScheduleDateFilter(config.schedule) : () => true;
}

get date(): Date {
Expand All @@ -93,16 +107,39 @@ export class DateTimeMinuteInstance {
this._step = step;
}

/**
* Returns true if the input is within the range and in the schedule.
*
* @param date
* @returns
*/
isInValidRange(date?: Date): boolean {
const result = this.getStatus(date);
return result.isAfterMinimum && result.isBeforeMaximum && result.isInSchedule;
}

/**
* Returns true if the status is completely valid.
*
* @param date
* @returns
*/
isValid(date?: Date): boolean {
const result = this.getStatus(date);
return result.isAfterMinimum && result.isBeforeMaximum && result.inFuture && result.inFutureMinutes && result.inPast && result.isInSchedule;
}

getStatus(date = this.date): DateTimeMinuteDateStatus {
let isBeforeMaximum = true;
let isAfterMinimum = true;
let inFuture = true;
let inFutureMinutes = true;
let inPast = true;
let isInSchedule = true;

const { limits = {} } = this._limit.config;
const { minimumMinutesIntoFuture } = this._limit;
const now = set(new Date(), { seconds: 0, milliseconds: 0 });
const now = roundDownToMinute(new Date());

// Min/Future
if (limits.min) {
Expand All @@ -125,12 +162,16 @@ export class DateTimeMinuteInstance {
inPast = isBefore(date, now);
}

// Schedule
isInSchedule = this._dateFilter(date);

return {
isBeforeMaximum,
isAfterMinimum,
inFuture,
inFutureMinutes,
inPast
inPast,
isInSchedule
};
}

Expand Down Expand Up @@ -179,3 +220,14 @@ export class DateTimeMinuteInstance {
return date;
}
}

/**
* Creates a DecisionFunction for the input Date value.
*
* @param config
* @returns
*/
export function dateTimeMinuteDecisionFunction(config: DateTimeMinuteConfig): DecisionFunction<Date> {
const instance = new DateTimeMinuteInstance(config, null);
return (date: Date) => instance.isValid(date);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
</button>
<mat-form-field class="dbx-datetime-row-field">
<mat-label>{{ dateLabel }}</mat-label>
<input #dateInput matInput [min]="dateInputMin$ | async" [max]="dateInputMax$ | async" [matDatepicker]="picker" (dateChange)="datePicked($event)" [value]="dateValue$ | async" (keydown)="keydownOnDateInput($event)" />
<input #dateInput matInput [min]="dateInputMin$ | async" [max]="dateInputMax$ | async" [matDatepicker]="picker" [matDatepickerFilter]="(pickerFilter$ | async) || defaultPickerFilter" (dateChange)="datePicked($event)" [value]="dateValue$ | async" (keydown)="keydownOnDateInput($event)" />
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LogicalDateStringCode, Maybe, ReadableTimeString, ArrayOrValue, ISO8601DateString, asArray, filterMaybeValues, dateFromLogicalDate } from '@dereekb/util';
import { DateTimeMinuteConfig, DateTimeMinuteInstance, formatToISO8601DayString, guessCurrentTimezone, readableTimeStringToDate, toLocalReadableTimeString, toReadableTimeString, utcDayForDate, formatToISO8601DateString, toJsDate, parseISO8601DayStringToDate, safeToJsDate, findMinDate, findMaxDate } from '@dereekb/date';
import { LogicalDateStringCode, Maybe, ReadableTimeString, ArrayOrValue, ISO8601DateString, asArray, filterMaybeValues, dateFromLogicalDate, DecisionFunction } from '@dereekb/util';
import { DateTimeMinuteConfig, DateTimeMinuteInstance, formatToISO8601DayString, guessCurrentTimezone, readableTimeStringToDate, toLocalReadableTimeString, toReadableTimeString, utcDayForDate, formatToISO8601DateString, toJsDate, parseISO8601DayStringToDate, safeToJsDate, findMinDate, findMaxDate, dateTimeMinuteDecisionFunction } from '@dereekb/date';
import { switchMap, shareReplay, map, startWith, tap, first, distinctUntilChanged, debounceTime, throttleTime, BehaviorSubject, Observable, combineLatest, Subject, merge, interval, of } from 'rxjs';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormControl, Validators, FormGroup } from '@angular/forms';
Expand Down Expand Up @@ -160,17 +160,20 @@ export interface DbxDateTimeFieldSyncParsedField extends Pick<DbxDateTimeFieldSy
control: AbstractControl<Maybe<Date | ISO8601DateString>>;
}

export function syncConfigValueObs(parseConfigsObs: Observable<DbxDateTimeFieldSyncParsedField[]>, type: DbxDateTimeFieldSyncType): Observable<Maybe<Date>> {
export function syncConfigValueObs(parseConfigsObs: Observable<DbxDateTimeFieldSyncParsedField[]>, type: DbxDateTimeFieldSyncType): Observable<Date | null> {
return parseConfigsObs.pipe(
switchMap((x) => {
const config = x.find((y) => y.syncType === type);
let result: Observable<Maybe<Date>>;
let result: Observable<Date | null>;

if (config) {
const { control } = config;
result = control.valueChanges.pipe(startWith(control.value), map(safeToJsDate));
result = control.valueChanges.pipe(
startWith(control.value),
map((x) => safeToJsDate(x) ?? null)
);
} else {
result = of(undefined);
result = of(null);
}

return result;
Expand Down Expand Up @@ -286,8 +289,8 @@ export class DbxDateTimeFieldComponent extends FieldType<FieldTypeConfig<DbxDate

readonly date$ = this.dateInputCtrl.valueChanges.pipe(startWith(this.dateInputCtrl.value), filterMaybe(), shareReplay(1));

readonly dateValue$ = merge(this.date$, this.value$.pipe(skipFirstMaybe())).pipe(
map((x: Maybe<Date>) => (x ? startOfDay(x) : x)),
readonly dateValue$: Observable<Date | null> = merge(this.date$, this.value$.pipe(skipFirstMaybe())).pipe(
map((x: Maybe<Date>) => (x ? startOfDay(x) : null)),
distinctUntilChanged((a, b) => a != null && b != null && isSameDay(a, b)),
shareReplay(1)
);
Expand Down Expand Up @@ -328,13 +331,13 @@ export class DbxDateTimeFieldComponent extends FieldType<FieldTypeConfig<DbxDate
shareReplay(1)
);

readonly syncConfigBeforeValue$: Observable<Maybe<Date>> = syncConfigValueObs(this.parsedSyncConfigs$, 'before');
readonly syncConfigAfterValue$: Observable<Maybe<Date>> = syncConfigValueObs(this.parsedSyncConfigs$, 'after');
readonly syncConfigBeforeValue$: Observable<Date | null> = syncConfigValueObs(this.parsedSyncConfigs$, 'before');
readonly syncConfigAfterValue$: Observable<Date | null> = syncConfigValueObs(this.parsedSyncConfigs$, 'after');

// TODO: Get min/max using the DateTimePickerConfiguration too

readonly dateInputMin$: Observable<Maybe<Date>> = this.syncConfigBeforeValue$;
readonly dateInputMax$: Observable<Maybe<Date>> = this.syncConfigAfterValue$;
readonly dateInputMin$: Observable<Date | null> = this.syncConfigBeforeValue$;
readonly dateInputMax$: Observable<Date | null> = this.syncConfigAfterValue$;

readonly rawDateTime$: Observable<Maybe<Date>> = combineLatest([this.dateValue$, this.timeInput$.pipe(startWith(null)), this.fullDay$]).pipe(
map(([date, timeString, fullDay]) => {
Expand Down Expand Up @@ -389,6 +392,21 @@ export class DbxDateTimeFieldComponent extends FieldType<FieldTypeConfig<DbxDate
shareReplay(1)
);

readonly pickerFilter$: Observable<DecisionFunction<Date | null>> = this.config$.pipe(
distinctUntilChanged(),
map((x) => {
if (x) {
const filter = dateTimeMinuteDecisionFunction(x);
return (x: Date | null) => (x != null ? filter(x) : true);
} else {
return () => true;
}
}),
shareReplay(1)
);

readonly defaultPickerFilter: DecisionFunction<Date | null> = () => true;

readonly timeOutput$: Observable<Maybe<Date>> = combineLatest([this.rawDateTime$, this._offset, this.config$]).pipe(
throttleTime(40, undefined, { leading: false, trailing: true }),
distinctUntilChanged((current, next) => current[0] === next[0] && next[1] === 0),
Expand Down

0 comments on commit 0e37643

Please sign in to comment.