From ac69654882b59488c81447b9cf8892af8f8e814c Mon Sep 17 00:00:00 2001 From: Jimmy Jaramillo Date: Fri, 24 Jun 2022 03:59:16 -0500 Subject: [PATCH 1/2] refactor: TTA-49 change range datepicker --- src/app/app.module.ts | 12 ++ .../time-range-custom.component.html | 20 +++ .../time-range-custom.component.scss | 13 ++ .../time-range-custom.component.spec.ts | 137 ++++++++++++++++++ .../time-range-custom.component.ts | 68 +++++++++ .../time-range-header.component.html | 18 +++ .../time-range-header.component.scss | 16 ++ .../time-range-header.component.spec.ts | 105 ++++++++++++++ .../time-range-header.component.ts | 53 +++++++ .../time-range-options.component.html | 12 ++ .../time-range-options.component.scss | 27 ++++ .../time-range-options.component.spec.ts | 133 +++++++++++++++++ .../time-range-options.component.ts | 116 +++++++++++++++ .../reports/pages/reports.component.html | 2 +- .../reports/pages/reports.component.spec.ts | 2 +- .../time-clock/store/entry.effects.spec.ts | 4 +- src/index.html | 1 + webpack.config.js | 1 + 18 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-custom.component.html create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-custom.component.scss create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.html create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.scss create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.spec.ts create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.ts create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.html create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.scss create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.spec.ts create mode 100644 src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e226c5626..a38f55215 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -13,6 +13,9 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatListModule } from '@angular/material/list'; import { MatMomentDateModule } from '@angular/material-moment-adapter'; import { NgxPaginationModule } from 'ngx-pagination'; import { AutocompleteLibModule } from 'angular-ng-autocomplete'; @@ -90,6 +93,9 @@ import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mo import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login'; import { GoogleLoginProvider } from 'angularx-social-login'; import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component'; +import { TimeRangeCustomComponent } from './modules/reports/components/time-range-custom/time-range-custom.component'; +import { TimeRangeHeaderComponent } from './modules/reports/components/time-range-custom/time-range-header/time-range-header.component'; +import { TimeRangeOptionsComponent } from './modules/reports/components/time-range-custom/time-range-options/time-range-options.component'; const maskConfig: Partial = { validation: false, @@ -146,6 +152,9 @@ const maskConfig: Partial = { CalendarComponent, DropdownComponent, DarkModeComponent, + TimeRangeCustomComponent, + TimeRangeHeaderComponent, + TimeRangeOptionsComponent, ], imports: [ NgxMaskModule.forRoot(maskConfig), @@ -165,6 +174,9 @@ const maskConfig: Partial = { NgxMaterialTimepickerModule, UiSwitchModule, DragDropModule, + MatIconModule, + MatCheckboxModule, + MatListModule, StoreModule.forRoot(reducers, { metaReducers, }), diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.html b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.html new file mode 100644 index 000000000..b680520de --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.html @@ -0,0 +1,20 @@ +
+ + + Enter a date range + + + + + MM/DD/YYYY – MM/DD/YYYY + + + + Invalid start date + Invalid end date + +
+ +
+
+ diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.scss b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.scss new file mode 100644 index 000000000..5159b0883 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.scss @@ -0,0 +1,13 @@ +:host { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + } + + .date-range-form { + display: flex !important; + justify-content: space-between !important; + width: 100% !important; + } diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts new file mode 100644 index 000000000..91091d0d3 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts @@ -0,0 +1,137 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { TimeRangeCustomComponent } from './time-range-custom.component'; +import { ToastrService } from 'ngx-toastr'; +import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer'; +import * as entryActions from '../../../time-clock/store/entry.actions'; +import * as moment from 'moment'; +import { SimpleChange } from '@angular/core'; + + +describe('TimeRangeCustomComponent', () => { + let component: TimeRangeCustomComponent; + let fixture: ComponentFixture; + let store: MockStore; + const toastrServiceStub = { + error: () => {} + }; + + const timeEntry = { + id: '1', + start_date: new Date(), + end_date: new Date(), + activity_id: '1', + technologies: ['react', 'redux'], + comments: 'any comment', + uri: 'TT-123', + project_id: '1' + }; + + const state = { + active: timeEntry, + entryList: [timeEntry], + isLoading: false, + message: 'test', + createError: false, + updateError: false, + timeEntriesSummary: null, + entriesForReport: [timeEntry], + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormsModule, ReactiveFormsModule], + declarations: [ TimeRangeCustomComponent ], + providers: [ + provideMockStore({ initialState: state }), + { provide: ToastrService, useValue: toastrServiceStub } + ], + }) + .compileComponents(); + store = TestBed.inject(MockStore); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeRangeCustomComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('setInitialDataOnScreen on ngOnInit', () => { + spyOn(component, 'setInitialDataOnScreen'); + + component.ngOnInit(); + + expect(component.setInitialDataOnScreen).toHaveBeenCalled(); + }); + + it('LoadEntriesByTimeRange action is triggered when start date is before end date', () => { + const end = moment(new Date()).subtract(1, 'days'); + const start = moment(new Date()); + spyOn(store, 'dispatch'); + component.range.controls.start.setValue(end); + component.range.controls.end.setValue(start); + + component.onSubmit(); + + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({ + start_date: end.startOf('day'), + end_date: start.endOf('day') + })); + }); + + it('shows an error when the end date is before the start date', () => { + spyOn(toastrServiceStub, 'error'); + const yesterday = moment(new Date()).subtract(2, 'days'); + const today = moment(new Date()); + spyOn(store, 'dispatch'); + component.range.controls.start.setValue(today); + component.range.controls.end.setValue(yesterday); + + component.onSubmit(); + + expect(toastrServiceStub.error).toHaveBeenCalled(); + }); + + it('setInitialDataOnScreen sets dates in form', () => { + spyOn(component.range.controls.start, 'setValue'); + spyOn(component.range.controls.end, 'setValue'); + + component.setInitialDataOnScreen(); + + expect(component.range.controls.start.setValue).toHaveBeenCalled(); + expect(component.range.controls.end.setValue).toHaveBeenCalled(); + + }); + + it('triggers onSubmit to set initial data', () => { + spyOn(component, 'onSubmit'); + + component.setInitialDataOnScreen(); + + expect(component.onSubmit).toHaveBeenCalled(); + }); + + it('When the ngOnChanges method is called, the onSubmit method is called', () => { + const userIdCalled = 'test-user-1'; + spyOn(component, 'onSubmit'); + + component.ngOnChanges({userId: new SimpleChange(null, userIdCalled, false)}); + + expect(component.onSubmit).toHaveBeenCalled(); + }); + + it('When the ngOnChanges method is the first change, the onSubmit method is not called', () => { + const userIdNotCalled = 'test-user-2'; + spyOn(component, 'onSubmit'); + + component.ngOnChanges({userId: new SimpleChange(null, userIdNotCalled, true)}); + + expect(component.onSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts new file mode 100644 index 000000000..8bb76cdb8 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts @@ -0,0 +1,68 @@ +import { formatDate } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {FormGroup, FormControl} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import * as moment from 'moment'; +import { ToastrService } from 'ngx-toastr'; +import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer'; +import { DATE_FORMAT } from 'src/environments/environment'; +import * as entryActions from '../../../time-clock/store/entry.actions'; +import { TimeRangeHeaderComponent } from './time-range-header/time-range-header.component'; + + +@Component({ + selector: 'app-time-range-custom', + templateUrl: './time-range-custom.component.html', + styleUrls: ['./time-range-custom.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimeRangeCustomComponent implements OnInit, OnChanges { + @Input() userId: string; + customHeader = TimeRangeHeaderComponent; + range = new FormGroup({ + start: new FormControl(null), + end: new FormControl(null), + }); + + constructor(private store: Store, private toastrService: ToastrService) { + } + + ngOnInit(): void { + this.setInitialDataOnScreen(); + } + + ngOnChanges(changes: SimpleChanges){ + if (!changes.userId.firstChange){ + this.onSubmit(); + } + } + + setInitialDataOnScreen() { + this.range.setValue({ + start: formatDate(moment().startOf('isoWeek').format('l'), DATE_FORMAT, 'en'), + end: formatDate(moment().format('l'), DATE_FORMAT, 'en') + }); + this.onSubmit(); + } + + onSubmit() { + const startDate = moment(this.range.getRawValue().start).startOf('day'); + const endDate = moment(this.range.getRawValue().end).endOf('day'); + if (endDate.isBefore(startDate)) { + this.toastrService.error('The end date should be after the start date'); + } else { + this.store.dispatch(new entryActions.LoadEntriesByTimeRange({ + start_date: moment(this.range.getRawValue().start).startOf('day'), + end_date: moment(this.range.getRawValue().end).endOf('day'), + }, this.userId)); + } + } + +} diff --git a/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.html b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.html new file mode 100644 index 000000000..707d5328e --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.html @@ -0,0 +1,18 @@ + +
+ + + {{periodLabel}} + + +
diff --git a/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.scss b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.scss new file mode 100644 index 000000000..ab4206816 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.scss @@ -0,0 +1,16 @@ +.time-range-header { + display: flex; + align-items: center; + padding: 0.5em; +} + +.time-range-header-label { + flex: 1; + height: 1em; + font-weight: 500; + text-align: center; +} + +.time-range-double-arrow .mat-icon { + margin: -22%; +} diff --git a/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.spec.ts b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.spec.ts new file mode 100644 index 000000000..274d39751 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.spec.ts @@ -0,0 +1,105 @@ +import { ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing'; +import { MatNativeDateModule } from '@angular/material/core'; +import { MatCalendar, MatDateRangePicker } from '@angular/material/datepicker'; +import { By } from '@angular/platform-browser'; +import { of } from 'rxjs'; +import { TimeRangeHeaderComponent } from './time-range-header.component'; + + +describe('TimeRangeHeaderComponent', () => { + let component: TimeRangeHeaderComponent; + let fixture: ComponentFixture>; + const value = { + stateChanges: { + pipe: () => { + return of({sucess: 'test'}); + } + }, + activeDate: new Date() + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatNativeDateModule], + declarations: [ TimeRangeHeaderComponent ], + providers: [{ provide: MatCalendar, useValue: value }, { provide: MatDateRangePicker, useValue: {} }] , + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeRangeHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should click previous year, previous month, next year and next month button with fakeAsync', fakeAsync(() => { + + const buttonsAll = [ + { + method: 'previousClicked', + values: [ + {style: '.time-range-double-arrow', call: 'year'}, + {style: '.time-range-month', call: 'month'} + ] + }, + { + method: 'nextClicked', + values: [ + {style: '.time-range-month-next', call: 'month'}, + {style: '.time-range-double-arrow-next', call: 'year'} + ] + }]; + buttonsAll.forEach((button: any) => { + spyOn(component, button.method); + button.values.forEach((val: any) => { + const buttonElement = fixture.debugElement.query(By.css(val.style)); + buttonElement.triggerEventHandler('click', null); + tick(); + expect(component[button.method]).toHaveBeenCalledWith(val.call); + }); + }); + })); + + it('should change the year with previousClicked method', () => { + component.calendar.activeDate = new Date(); + fixture.detectChanges(); + const makeDateYear = new Date(); + makeDateYear.setFullYear(makeDateYear.getFullYear() - 1); + component.previousClicked('year'); + expect(component.calendar.activeDate.toDateString()).toEqual(makeDateYear.toDateString()); + }); + + it('should change the month with previousClicked method', () => { + component.calendar.activeDate = new Date(); + fixture.detectChanges(); + const makeDateYear = new Date(); + makeDateYear.setMonth(makeDateYear.getMonth() - 1); + component.previousClicked('month'); + expect(component.calendar.activeDate.toDateString()).toEqual(makeDateYear.toDateString()); + }); + + + it('should change the year with nextClicked method', () => { + component.calendar.activeDate = new Date(); + fixture.detectChanges(); + const makeDateYear = new Date(); + makeDateYear.setFullYear(makeDateYear.getFullYear() + 1); + component.nextClicked('year'); + expect(component.calendar.activeDate.toDateString()).toEqual(makeDateYear.toDateString()); + }); + + it('should change the month with nextClicked method', () => { + component.calendar.activeDate = new Date(); + fixture.detectChanges(); + const makeDateYear = new Date(); + makeDateYear.setMonth(makeDateYear.getMonth() + 1); + component.nextClicked('month'); + expect(component.calendar.activeDate.toDateString()).toEqual(makeDateYear.toDateString()); + }); + +}); diff --git a/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.ts b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.ts new file mode 100644 index 000000000..37611051b --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.ts @@ -0,0 +1,53 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + OnDestroy +} from '@angular/core'; +import {MatCalendar} from '@angular/material/datepicker'; +import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; + + +@Component({ + // selector: 'app-time-range-header', + styleUrls: ['./time-range-header.component.scss'], + templateUrl: './time-range-header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimeRangeHeaderComponent implements OnDestroy { + private destroyed = new Subject(); + + constructor( + public calendar: MatCalendar, public dateAdapter: DateAdapter, + @Inject(MAT_DATE_FORMATS) private dateFormats: MatDateFormats, cdr: ChangeDetectorRef) { + calendar.stateChanges + .pipe(takeUntil(this.destroyed)) + .subscribe(() => cdr.markForCheck()); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } + + get periodLabel() { + return this.dateAdapter + .format(this.calendar.activeDate, this.dateFormats.display.monthYearLabel) + .toLocaleUpperCase(); + } + + previousClicked(mode: 'month' | 'year') { + this.calendar.activeDate = mode === 'month' ? + this.dateAdapter.addCalendarMonths(this.calendar.activeDate, -1) : + this.dateAdapter.addCalendarYears(this.calendar.activeDate, -1); + } + + nextClicked(mode: 'month' | 'year') { + this.calendar.activeDate = mode === 'month' ? + this.dateAdapter.addCalendarMonths(this.calendar.activeDate, 1) : + this.dateAdapter.addCalendarYears(this.calendar.activeDate, 1); + } +} diff --git a/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.html b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.html new file mode 100644 index 000000000..b3082762c --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.html @@ -0,0 +1,12 @@ +
+ + + custom + + + + + {{item}} + + +
diff --git a/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.scss b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.scss new file mode 100644 index 000000000..76c75c9fb --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.scss @@ -0,0 +1,27 @@ +$width: 128px; + +:host { + position: absolute; + width: $width; + left: -$width; + } + + :host(.touch-ui) { + position: relative; + left: 0; + display: flex; + width: 100%; + } + +.list-time-range{ + background-color: white; + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; +} + +.custom-items-time-range{ + padding-top: 0 !important; +} + +.custom-mat-list-option{ + height: 35px !important; +} diff --git a/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.spec.ts b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.spec.ts new file mode 100644 index 000000000..33a442cf2 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.spec.ts @@ -0,0 +1,133 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatNativeDateModule } from '@angular/material/core'; +import { MatDateRangePicker } from '@angular/material/datepicker'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatListModule } from '@angular/material/list'; +import { ToastrService } from 'ngx-toastr'; +import { TimeRangeOptionsComponent } from './time-range-options.component'; + + +describe('TimeRangeOptionsComponent', () => { + let component: TimeRangeOptionsComponent; + let fixture: ComponentFixture>; + const valueFunction = () => { + return ''; + }; + const toastrServiceStub = { + error: () => { + return 'error'; + } + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatNativeDateModule, MatDialogModule, MatListModule], + declarations: [ TimeRangeOptionsComponent ], + providers: [ + { provide: MatDateRangePicker, useValue: {select: valueFunction, close: valueFunction} }, + { provide: ToastrService, useValue: toastrServiceStub }], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeRangeOptionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call resetTimeRange method and clean time range input ', () => { + component.resetTimeRange(); + expect(component.picker.startAt).toEqual(undefined); + }); + + it('should click selectRange button and call calculateDateRange method', () => { + spyOn(component, 'calculateDateRange').and.returnValues(['', '']); + component.selectRange('today'); + expect(component.calculateDateRange).toHaveBeenCalled(); + }); + + it('should return current date when is called calculateDateRange method', () => { + const [start , end] = component.calculateDateRange('today'); + expect(new Date(start).toDateString()).toEqual(new Date().toDateString()); + expect(new Date(end).toDateString()).toEqual(new Date().toDateString()); + }); + + it('should return last 7 days when is called calculateDateRange method', () => { + const [start , end] = component.calculateDateRange('last 7 days'); + const lastDate = new Date(); + lastDate.setDate(new Date().getDate() - 6); + expect(new Date(start).toDateString()).toEqual(lastDate.toDateString()); + expect(new Date(end).toDateString()).toEqual(new Date().toDateString()); + }); + + it('should call calculateMonth and calculateWeek method when is called calculateDateRange method', () => { + + const dataAll = [ + {method: 'calculateWeek', options: ['this week', 'last week']}, + {method: 'calculateMonth', options: ['this month', 'last month']}]; + + dataAll.forEach((val: any) => { + spyOn(component, val.method); + val.options.forEach((option: any) => { + component.calculateDateRange(option); + expect(component[val.method]).toHaveBeenCalled(); + }); + }); + }); + + it('should return time range last year or this year when is called calculateDateRange method', () => { + const currentYear = new Date().getFullYear(); + const dataAll = [ + {range: 'this year', values: + {start: {year: currentYear, month: 0, day: 1}, + end: {year: currentYear, month: 11, day: 31} + } + }, + {range: 'last year', + values: + {start: {year: currentYear - 1, month: 0, day: 1}, + end: {year: currentYear - 1, month: 11, day: 31} + } + }, + ]; + dataAll.forEach((val: any) => { + const [start, end] = component.calculateDateRange(val.range); + expect(new Date(start).toDateString()).toEqual( + new Date(val.values.start.year, val.values.start.month, val.values.start.day).toDateString()); + expect(new Date(end).toDateString()).toEqual( + new Date(val.values.end.year, val.values.end.month, val.values.end.day).toDateString()); + }); + }); + + it('should return a time range month when is called calculateMonth method', () => { + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth(); + const currentDays = new Date(currentYear, currentMonth + 1, 0).getDate(); + const [start, end] = component.calculateMonth(new Date()); + expect(new Date(start).toDateString()).toEqual(new Date(currentYear, currentMonth, 1).toDateString()); + expect(new Date(end).toDateString()).toEqual(new Date(currentYear, currentMonth , currentDays).toDateString()); + }); + + it('should return a time range week when is called calculateWeek method', () => { + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth(); + const firstDay = new Date().getDate() - new Date().getDay() + 1; + const lastDay = firstDay + 6; + const [start, end] = component.calculateWeek(new Date()); + expect(new Date(start).toDateString()).toEqual(new Date(currentYear, currentMonth, firstDay).toDateString()); + expect(new Date(end).toDateString()).toEqual(new Date(currentYear, currentMonth , lastDay).toDateString()); + }); + + it('shows an error when the date created is null from date adapter', () => { + spyOn(toastrServiceStub, 'error'); + spyOn(component.dateAdapter, 'today').and.returnValue(null); + component.getToday(); + expect(toastrServiceStub.error).toHaveBeenCalled(); + }); + +}); diff --git a/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.ts b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.ts new file mode 100644 index 000000000..01dfb39a7 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-options/time-range-options.component.ts @@ -0,0 +1,116 @@ +import { Component, HostBinding, ChangeDetectionStrategy } from '@angular/core'; +import { DateAdapter } from '@angular/material/core'; +import { MatDateRangePicker } from '@angular/material/datepicker'; +import { ToastrService } from 'ngx-toastr'; + + +const customPresets = [ + 'today', + 'last 7 days', + 'this week', + 'this month', + 'this year', + 'last week', + 'last month', + 'last year', +] as const; + +type CustomPreset = typeof customPresets[number]; +@Component({ + selector: 'app-time-range-options', + templateUrl: './time-range-options.component.html', + styleUrls: ['./time-range-options.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimeRangeOptionsComponent { + + customPresets = customPresets; + @HostBinding('class.touch-ui') + readonly isTouchUi = this.picker.touchUi; + constructor( + public dateAdapter: DateAdapter, + public picker: MatDateRangePicker, + private toastrService: ToastrService + ) { + this.dateAdapter.getFirstDayOfWeek = () => 1; + } + + selectRange(rangeName: CustomPreset): void { + const [start, end] = this.calculateDateRange(rangeName); + this.picker.select(start); + this.picker.select(end); + this.picker.close(); + } + + calculateDateRange(rangeName: CustomPreset): [start: Date, end: Date] { + const today = this.getToday(); + const year = this.dateAdapter.getYear(today); + + switch (rangeName) { + case 'today': + return [today, today]; + case 'last 7 days': { + const start = this.dateAdapter.addCalendarDays(today, -6); + return [start, today]; + } + case 'this week': { + return this.calculateWeek(today); + } + case 'this month': { + return this.calculateMonth(today); + } + case 'this year': { + const start = this.dateAdapter.createDate(year, 0, 1); + const end = this.dateAdapter.createDate(year, 11, 31); + return [start, end]; + } + case 'last week': { + const thisDayLastWeek = this.dateAdapter.addCalendarDays(today, -7); + return this.calculateWeek(thisDayLastWeek); + } + case 'last month': { + const thisDayLastMonth = this.dateAdapter.addCalendarMonths(today, -1); + return this.calculateMonth(thisDayLastMonth); + } + case 'last year': { + const start = this.dateAdapter.createDate(year - 1, 0, 1); + const end = this.dateAdapter.createDate(year - 1, 11, 31); + return [start, end]; + } + } + } + + calculateMonth(forDay: Date): [start: Date, end: Date] { + const year = this.dateAdapter.getYear(forDay); + const month = this.dateAdapter.getMonth(forDay); + const start = this.dateAdapter.createDate(year, month, 1); + const end = this.dateAdapter.addCalendarDays( + start, + this.dateAdapter.getNumDaysInMonth(forDay) - 1 + ); + return [start, end]; + } + + calculateWeek(forDay: Date): [start: Date, end: Date] { + const deltaStart = + this.dateAdapter.getFirstDayOfWeek() - + this.dateAdapter.getDayOfWeek(forDay); + const start = this.dateAdapter.addCalendarDays(forDay, deltaStart); + const end = this.dateAdapter.addCalendarDays(start, 6); + return [start, end]; + } + + getToday(): Date { + const today = this.dateAdapter.today(); + if (today === null) { + this.toastrService.error('The end date should be after the start date'); + } + return today; + } + + resetTimeRange() { + this.picker.select(undefined); + this.picker.select(undefined); + } + +} diff --git a/src/app/modules/reports/pages/reports.component.html b/src/app/modules/reports/pages/reports.component.html index 1b8fde066..a9b65ded9 100644 --- a/src/app/modules/reports/pages/reports.component.html +++ b/src/app/modules/reports/pages/reports.component.html @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/src/app/modules/reports/pages/reports.component.spec.ts b/src/app/modules/reports/pages/reports.component.spec.ts index b749a03f3..958791abd 100644 --- a/src/app/modules/reports/pages/reports.component.spec.ts +++ b/src/app/modules/reports/pages/reports.component.spec.ts @@ -27,7 +27,7 @@ describe('ReportsComponent', () => { fixture.detectChanges(); const compile = fixture.debugElement.nativeElement; - const reportForm = compile.querySelector('app-time-range-form'); + const reportForm = compile.querySelector('app-time-range-custom'); const reportDataTable = compile.querySelector('app-time-entries-table'); expect(reportForm).toBeTruthy(); expect(reportDataTable).toBeTruthy(); diff --git a/src/app/modules/time-clock/store/entry.effects.spec.ts b/src/app/modules/time-clock/store/entry.effects.spec.ts index 27ec9b2d6..295ff75fd 100644 --- a/src/app/modules/time-clock/store/entry.effects.spec.ts +++ b/src/app/modules/time-clock/store/entry.effects.spec.ts @@ -379,7 +379,9 @@ describe('TimeEntryActionEffects', () => { }); it('should update current entry when UPDATE_CURRENT_OR_LAST_ENTRY is executed', async () => { - const lastEntry: Entry = { project_id: 'p-id', start_date: new Date(), id: 'id', end_date: endDateTest}; + const makeDateYear = new Date(); + makeDateYear.setMonth(makeDateYear.getMonth() - 1); + const lastEntry: Entry = { project_id: 'p-id', start_date: new Date(), id: 'id', end_date: makeDateYear}; actions$ = of({ type: EntryActionTypes.UPDATE_CURRENT_OR_LAST_ENTRY, payload: entryUpdate }); spyOn(service, 'loadEntries').and.returnValue(of([lastEntry, lastEntry])); spyOn(toastrService, 'success'); diff --git a/src/index.html b/src/index.html index 553c99bcd..dcda207fb 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,7 @@ + diff --git a/webpack.config.js b/webpack.config.js index 818dc9370..bcc92d0b8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,7 @@ const webpack = require('webpack') const { addTailwindPlugin } = require("@ngneat/tailwind"); const tailwindConfig = require("./tailwind.config.js"); +require('dotenv').config(); module.exports = (config) => { const config_ = { From 6f99b39aa4a526b8a2c3619e02e5c89b6727fe7c Mon Sep 17 00:00:00 2001 From: Jimmy Jaramillo Date: Fri, 24 Jun 2022 10:57:27 -0500 Subject: [PATCH 2/2] fix: TTA-49 refactor test time range custom and entry effects --- .../time-range-custom/time-range-custom.component.spec.ts | 4 +++- src/app/modules/time-clock/store/entry.effects.spec.ts | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts index 91091d0d3..3fad6305f 100644 --- a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts @@ -14,7 +14,9 @@ describe('TimeRangeCustomComponent', () => { let fixture: ComponentFixture; let store: MockStore; const toastrServiceStub = { - error: () => {} + error: () => { + return 'test error'; + } }; const timeEntry = { diff --git a/src/app/modules/time-clock/store/entry.effects.spec.ts b/src/app/modules/time-clock/store/entry.effects.spec.ts index 295ff75fd..562f68269 100644 --- a/src/app/modules/time-clock/store/entry.effects.spec.ts +++ b/src/app/modules/time-clock/store/entry.effects.spec.ts @@ -23,9 +23,7 @@ describe('TimeEntryActionEffects', () => { const entry: Entry = { project_id: 'p-id', start_date: new Date(), id: 'id' }; const dateTest = moment().format('YYYY-MM-DD'); - const endHourTest = moment().subtract(5, 'hours').format('HH:mm:ss'); const startHourTest = moment().subtract(3, 'hours').format('HH:mm:ss'); - const endDateTest = new Date(`${dateTest}T${endHourTest.trim()}`); const startDateTest = new Date(`${dateTest}T${startHourTest.trim()}`); const entryUpdate = {