diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e226c5626..03dba42aa 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -13,6 +13,11 @@ 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 { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatListModule } from '@angular/material/list'; +import { MatNativeDateModule } from '@angular/material/core'; import { MatMomentDateModule } from '@angular/material-moment-adapter'; import { NgxPaginationModule } from 'ngx-pagination'; import { AutocompleteLibModule } from 'angular-ng-autocomplete'; @@ -90,6 +95,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 { TimeRangePanelComponent } from './modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component'; const maskConfig: Partial = { validation: false, @@ -146,6 +154,9 @@ const maskConfig: Partial = { CalendarComponent, DropdownComponent, DarkModeComponent, + TimeRangeCustomComponent, + TimeRangeHeaderComponent, + TimeRangePanelComponent, ], imports: [ NgxMaskModule.forRoot(maskConfig), @@ -165,6 +176,11 @@ const maskConfig: Partial = { NgxMaterialTimepickerModule, UiSwitchModule, DragDropModule, + MatIconModule, + MatCardModule, + MatCheckboxModule, + MatListModule, + MatNativeDateModule, 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..a6822d831 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.html @@ -0,0 +1,18 @@ +
+ + + Enter a date range + + + + + + + +
+ +
+
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..6787b15fc --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.scss @@ -0,0 +1,14 @@ +:host { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + } + + .date-range-form{ + display: flex; + justify-content: space-between; + width: 100%; + } + \ No newline at end of file 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..1da23f8ad --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts @@ -0,0 +1,138 @@ +import { SimpleChange } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import * as moment from 'moment'; +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 { TimeRangeCustomComponent } from './time-range-custom.component'; + + +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..15b5a4014 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts @@ -0,0 +1,61 @@ +import { OnChanges, SimpleChanges, Component, OnInit, Input } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { TimeRangeHeaderComponent } from './time-range-header/time-range-header.component'; +import { DATE_FORMAT } from 'src/environments/environment'; +import { formatDate } from '@angular/common'; +import { Store } from '@ngrx/store'; +import { EntryState } from '../../../time-clock/store/entry.reducer'; +import { ToastrService } from 'ngx-toastr'; +import * as entryActions from '../../../time-clock/store/entry.actions'; +import * as moment from 'moment'; + + +@Component({ + selector: 'app-time-range-custom', + templateUrl: './time-range-custom.component.html', + styleUrls: ['./time-range-custom.component.scss'] +}) +export class TimeRangeCustomComponent implements OnInit, OnChanges { + @Input() userId: string; + + readonly TimeRangeHeader = TimeRangeHeaderComponent; + + range = new FormGroup({ + start: new FormControl(), + end: new FormControl(), + }); + + 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..c20e5df8a --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.html @@ -0,0 +1,29 @@ + + +
+ + + + {{ 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..d436547e0 --- /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..a6a595715 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.spec.ts @@ -0,0 +1,98 @@ +import { ComponentFixture, fakeAsync, TestBed, tick } 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 call method changeDate with nextClicked', () => { + spyOn(component, 'changeDate').withArgs('month', 1); + component.nextClicked('month'); + expect(component.changeDate).toHaveBeenCalledWith('month', 1); + }); + + it('should call method changeDate with previousClicked', () => { + spyOn(component, 'changeDate').withArgs('year', -1); + component.previousClicked('year'); + expect(component.changeDate).toHaveBeenCalledWith('year', -1); + }); + + it('should change the activeDate with month on calendar', () => { + const makeDateMonth = new Date(); + makeDateMonth.setMonth(makeDateMonth.getMonth() - 1); + component.changeDate('month', -1); + expect(component.calendar.activeDate.toDateString()).toEqual(makeDateMonth.toDateString()); + }); + + it('should change the activeDate with year on calendar', () => { + component.calendar.activeDate = new Date(); + fixture.detectChanges(); + const makeDateYear = new Date(); + makeDateYear.setFullYear(makeDateYear.getFullYear() - 1); + component.changeDate('year', -1); + 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..065c5eef3 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-header/time-range-header.component.ts @@ -0,0 +1,63 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + OnDestroy, +} from '@angular/core'; +import { + DateAdapter, + MatDateFormats, + MAT_DATE_FORMATS, +} from '@angular/material/core'; +import { MatCalendar } from '@angular/material/datepicker'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + + +@Component({ + // selector: 'app-time-range-header', + templateUrl: './time-range-header.component.html', + styleUrls: ['./time-range-header.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) + +export class TimeRangeHeaderComponent implements OnDestroy { + private readonly destroy$ = new Subject(); + + constructor( + public calendar: MatCalendar, + public dateAdapter: DateAdapter, + @Inject(MAT_DATE_FORMATS) private dateFormats: MatDateFormats, + cdr: ChangeDetectorRef + ) { + calendar.stateChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(() => cdr.markForCheck()); + } + + get periodLabel(): string { + return this.dateAdapter + .format(this.calendar.activeDate, this.dateFormats.display.monthYearLabel) + .toLocaleUpperCase(); + } + + previousClicked(mode: 'month' | 'year'): void { + this.changeDate(mode, -1); + } + + nextClicked(mode: 'month' | 'year'): void { + this.changeDate(mode, 1); + } + + changeDate(mode: 'month' | 'year', amount: -1 | 1): void { + this.calendar.activeDate = + mode === 'month' + ? this.dateAdapter.addCalendarMonths(this.calendar.activeDate, amount) + : this.dateAdapter.addCalendarYears(this.calendar.activeDate, amount); + } + + ngOnDestroy(): void { + this.destroy$.next(); + } +} diff --git a/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.html b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.html new file mode 100644 index 000000000..8827d4b83 --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.html @@ -0,0 +1,12 @@ +
+ + + custom + + + + + {{item}} + + +
\ No newline at end of file diff --git a/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.scss b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.scss new file mode 100644 index 000000000..720ab4a6a --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.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-panel/time-range-panel.component.spec.ts b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.spec.ts new file mode 100644 index 000000000..634fee56a --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.spec.ts @@ -0,0 +1,135 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatCardModule } from '@angular/material/card'; +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 { By } from '@angular/platform-browser'; +import { ToastrService } from 'ngx-toastr'; +import { TimeRangePanelComponent } from './time-range-panel.component'; + + +describe('TimeRangePanelComponent', () => { + let component: TimeRangePanelComponent; + let fixture: ComponentFixture>; + const valueFunction = () => { + return ''; + }; + const toastrServiceStub = { + error: () => { + return 'error'; + } + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatNativeDateModule, MatDialogModule, MatCardModule, MatListModule], + declarations: [ TimeRangePanelComponent ], + providers: [ + { provide: MatDateRangePicker, useValue: {select: valueFunction, close: valueFunction} }, + { provide: ToastrService, useValue: toastrServiceStub }], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeRangePanelComponent); + 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, 'getValidDateOrNull').and.returnValue(null); + component.getToday(); + expect(toastrServiceStub.error).toHaveBeenCalled(); + }); + +}); diff --git a/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.ts b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.ts new file mode 100644 index 000000000..4d851976c --- /dev/null +++ b/src/app/modules/reports/components/time-range-custom/time-range-panel/time-range-panel.component.ts @@ -0,0 +1,117 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Output } 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-panel', + templateUrl: './time-range-panel.component.html', + styleUrls: ['./time-range-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimeRangePanelComponent { + readonly 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: D, end: D] { + 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: D): [start: D, end: D] { + 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: D): [start: D, end: D] { + 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(): D { + const today = this.dateAdapter.getValidDateOrNull(new Date()); + 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..65f015257 100644 --- a/src/app/modules/reports/pages/reports.component.html +++ b/src/app/modules/reports/pages/reports.component.html @@ -1,2 +1,3 @@ - + + \ 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/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_ = {