Skip to content

Commit 86bee39

Browse files
New time range picker (#893)
* refactor: TTA-49 change range datepicker * fix: TTA-49 refactor test time range custom and entry effects Co-authored-by: Jimmy Jaramillo <[email protected]>
1 parent 852d566 commit 86bee39

18 files changed

+739
-5
lines changed

src/app/app.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
1313
import { DragDropModule } from '@angular/cdk/drag-drop';
1414
import { MatDatepickerModule } from '@angular/material/datepicker';
1515
import { MatInputModule } from '@angular/material/input';
16+
import { MatIconModule } from '@angular/material/icon';
17+
import { MatCheckboxModule } from '@angular/material/checkbox';
18+
import { MatListModule } from '@angular/material/list';
1619
import { MatMomentDateModule } from '@angular/material-moment-adapter';
1720
import { NgxPaginationModule } from 'ngx-pagination';
1821
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
@@ -91,6 +94,9 @@ import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mo
9194
import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login';
9295
import { GoogleLoginProvider } from 'angularx-social-login';
9396
import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component';
97+
import { TimeRangeCustomComponent } from './modules/reports/components/time-range-custom/time-range-custom.component';
98+
import { TimeRangeHeaderComponent } from './modules/reports/components/time-range-custom/time-range-header/time-range-header.component';
99+
import { TimeRangeOptionsComponent } from './modules/reports/components/time-range-custom/time-range-options/time-range-options.component';
94100

95101
const maskConfig: Partial<IConfig> = {
96102
validation: false,
@@ -147,6 +153,9 @@ const maskConfig: Partial<IConfig> = {
147153
CalendarComponent,
148154
DropdownComponent,
149155
DarkModeComponent,
156+
TimeRangeCustomComponent,
157+
TimeRangeHeaderComponent,
158+
TimeRangeOptionsComponent,
150159
],
151160
imports: [
152161
NgxMaskModule.forRoot(maskConfig),
@@ -166,6 +175,9 @@ const maskConfig: Partial<IConfig> = {
166175
NgxMaterialTimepickerModule,
167176
UiSwitchModule,
168177
DragDropModule,
178+
MatIconModule,
179+
MatCheckboxModule,
180+
MatListModule,
169181
StoreModule.forRoot(reducers, {
170182
metaReducers,
171183
}),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<form (ngSubmit)="onSubmit()" class="date-range-form">
2+
<label class="col-form-label my-1">Select your Date Range:</label>
3+
<mat-form-field appearance="fill">
4+
<mat-label>Enter a date range</mat-label>
5+
<mat-date-range-input [formGroup]="range" [rangePicker]="picker">
6+
<input matStartDate formControlName="start" placeholder="Start date">
7+
<input matEndDate formControlName="end" placeholder="End date">
8+
</mat-date-range-input>
9+
<mat-hint>MM/DD/YYYY – MM/DD/YYYY</mat-hint>
10+
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
11+
<mat-date-range-picker #picker [calendarHeaderComponent]="customHeader"></mat-date-range-picker>
12+
13+
<mat-error *ngIf="range.controls.start.hasError('matStartDateInvalid')">Invalid start date</mat-error>
14+
<mat-error *ngIf="range.controls.end.hasError('matEndDateInvalid')">Invalid end date</mat-error>
15+
</mat-form-field>
16+
<div class="col-12 col-md-2 my-1">
17+
<button type="submit" class="btn btn-primary">Search</button>
18+
</div>
19+
</form>
20+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
:host {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
justify-content: center;
6+
height: 100%;
7+
}
8+
9+
.date-range-form {
10+
display: flex !important;
11+
justify-content: space-between !important;
12+
width: 100% !important;
13+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
3+
import { MockStore, provideMockStore } from '@ngrx/store/testing';
4+
import { TimeRangeCustomComponent } from './time-range-custom.component';
5+
import { ToastrService } from 'ngx-toastr';
6+
import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer';
7+
import * as entryActions from '../../../time-clock/store/entry.actions';
8+
import * as moment from 'moment';
9+
import { SimpleChange } from '@angular/core';
10+
11+
12+
describe('TimeRangeCustomComponent', () => {
13+
let component: TimeRangeCustomComponent;
14+
let fixture: ComponentFixture<TimeRangeCustomComponent>;
15+
let store: MockStore<EntryState>;
16+
const toastrServiceStub = {
17+
error: () => {
18+
return 'test error';
19+
}
20+
};
21+
22+
const timeEntry = {
23+
id: '1',
24+
start_date: new Date(),
25+
end_date: new Date(),
26+
activity_id: '1',
27+
technologies: ['react', 'redux'],
28+
comments: 'any comment',
29+
uri: 'TT-123',
30+
project_id: '1'
31+
};
32+
33+
const state = {
34+
active: timeEntry,
35+
entryList: [timeEntry],
36+
isLoading: false,
37+
message: 'test',
38+
createError: false,
39+
updateError: false,
40+
timeEntriesSummary: null,
41+
entriesForReport: [timeEntry],
42+
};
43+
44+
beforeEach(async () => {
45+
await TestBed.configureTestingModule({
46+
imports: [FormsModule, ReactiveFormsModule],
47+
declarations: [ TimeRangeCustomComponent ],
48+
providers: [
49+
provideMockStore({ initialState: state }),
50+
{ provide: ToastrService, useValue: toastrServiceStub }
51+
],
52+
})
53+
.compileComponents();
54+
store = TestBed.inject(MockStore);
55+
});
56+
57+
beforeEach(() => {
58+
fixture = TestBed.createComponent(TimeRangeCustomComponent);
59+
component = fixture.componentInstance;
60+
fixture.detectChanges();
61+
});
62+
63+
it('should create', () => {
64+
expect(component).toBeTruthy();
65+
});
66+
67+
it('setInitialDataOnScreen on ngOnInit', () => {
68+
spyOn(component, 'setInitialDataOnScreen');
69+
70+
component.ngOnInit();
71+
72+
expect(component.setInitialDataOnScreen).toHaveBeenCalled();
73+
});
74+
75+
it('LoadEntriesByTimeRange action is triggered when start date is before end date', () => {
76+
const end = moment(new Date()).subtract(1, 'days');
77+
const start = moment(new Date());
78+
spyOn(store, 'dispatch');
79+
component.range.controls.start.setValue(end);
80+
component.range.controls.end.setValue(start);
81+
82+
component.onSubmit();
83+
84+
expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({
85+
start_date: end.startOf('day'),
86+
end_date: start.endOf('day')
87+
}));
88+
});
89+
90+
it('shows an error when the end date is before the start date', () => {
91+
spyOn(toastrServiceStub, 'error');
92+
const yesterday = moment(new Date()).subtract(2, 'days');
93+
const today = moment(new Date());
94+
spyOn(store, 'dispatch');
95+
component.range.controls.start.setValue(today);
96+
component.range.controls.end.setValue(yesterday);
97+
98+
component.onSubmit();
99+
100+
expect(toastrServiceStub.error).toHaveBeenCalled();
101+
});
102+
103+
it('setInitialDataOnScreen sets dates in form', () => {
104+
spyOn(component.range.controls.start, 'setValue');
105+
spyOn(component.range.controls.end, 'setValue');
106+
107+
component.setInitialDataOnScreen();
108+
109+
expect(component.range.controls.start.setValue).toHaveBeenCalled();
110+
expect(component.range.controls.end.setValue).toHaveBeenCalled();
111+
112+
});
113+
114+
it('triggers onSubmit to set initial data', () => {
115+
spyOn(component, 'onSubmit');
116+
117+
component.setInitialDataOnScreen();
118+
119+
expect(component.onSubmit).toHaveBeenCalled();
120+
});
121+
122+
it('When the ngOnChanges method is called, the onSubmit method is called', () => {
123+
const userIdCalled = 'test-user-1';
124+
spyOn(component, 'onSubmit');
125+
126+
component.ngOnChanges({userId: new SimpleChange(null, userIdCalled, false)});
127+
128+
expect(component.onSubmit).toHaveBeenCalled();
129+
});
130+
131+
it('When the ngOnChanges method is the first change, the onSubmit method is not called', () => {
132+
const userIdNotCalled = 'test-user-2';
133+
spyOn(component, 'onSubmit');
134+
135+
component.ngOnChanges({userId: new SimpleChange(null, userIdNotCalled, true)});
136+
137+
expect(component.onSubmit).not.toHaveBeenCalled();
138+
});
139+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { formatDate } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
Input,
6+
OnChanges,
7+
OnInit,
8+
SimpleChanges,
9+
} from '@angular/core';
10+
import {FormGroup, FormControl} from '@angular/forms';
11+
import { Store } from '@ngrx/store';
12+
import * as moment from 'moment';
13+
import { ToastrService } from 'ngx-toastr';
14+
import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer';
15+
import { DATE_FORMAT } from 'src/environments/environment';
16+
import * as entryActions from '../../../time-clock/store/entry.actions';
17+
import { TimeRangeHeaderComponent } from './time-range-header/time-range-header.component';
18+
19+
20+
@Component({
21+
selector: 'app-time-range-custom',
22+
templateUrl: './time-range-custom.component.html',
23+
styleUrls: ['./time-range-custom.component.scss'],
24+
changeDetection: ChangeDetectionStrategy.OnPush,
25+
})
26+
export class TimeRangeCustomComponent implements OnInit, OnChanges {
27+
@Input() userId: string;
28+
customHeader = TimeRangeHeaderComponent;
29+
range = new FormGroup({
30+
start: new FormControl(null),
31+
end: new FormControl(null),
32+
});
33+
34+
constructor(private store: Store<EntryState>, private toastrService: ToastrService) {
35+
}
36+
37+
ngOnInit(): void {
38+
this.setInitialDataOnScreen();
39+
}
40+
41+
ngOnChanges(changes: SimpleChanges){
42+
if (!changes.userId.firstChange){
43+
this.onSubmit();
44+
}
45+
}
46+
47+
setInitialDataOnScreen() {
48+
this.range.setValue({
49+
start: formatDate(moment().startOf('isoWeek').format('l'), DATE_FORMAT, 'en'),
50+
end: formatDate(moment().format('l'), DATE_FORMAT, 'en')
51+
});
52+
this.onSubmit();
53+
}
54+
55+
onSubmit() {
56+
const startDate = moment(this.range.getRawValue().start).startOf('day');
57+
const endDate = moment(this.range.getRawValue().end).endOf('day');
58+
if (endDate.isBefore(startDate)) {
59+
this.toastrService.error('The end date should be after the start date');
60+
} else {
61+
this.store.dispatch(new entryActions.LoadEntriesByTimeRange({
62+
start_date: moment(this.range.getRawValue().start).startOf('day'),
63+
end_date: moment(this.range.getRawValue().end).endOf('day'),
64+
}, this.userId));
65+
}
66+
}
67+
68+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<app-time-range-options></app-time-range-options>
2+
<div class="time-range-header">
3+
<button mat-icon-button class="time-range-double-arrow" (click)="previousClicked('year')">
4+
<mat-icon>keyboard_arrow_left</mat-icon>
5+
<mat-icon>keyboard_arrow_left</mat-icon>
6+
</button>
7+
<button mat-icon-button (click)="previousClicked('month')" class="time-range-month">
8+
<mat-icon>keyboard_arrow_left</mat-icon>
9+
</button>
10+
<span class="time-range-header-label">{{periodLabel}}</span>
11+
<button mat-icon-button (click)="nextClicked('month')" class="time-range-month-next">
12+
<mat-icon>keyboard_arrow_right</mat-icon>
13+
</button>
14+
<button mat-icon-button class="time-range-double-arrow time-range-double-arrow-next" (click)="nextClicked('year')">
15+
<mat-icon>keyboard_arrow_right</mat-icon>
16+
<mat-icon>keyboard_arrow_right</mat-icon>
17+
</button>
18+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.time-range-header {
2+
display: flex;
3+
align-items: center;
4+
padding: 0.5em;
5+
}
6+
7+
.time-range-header-label {
8+
flex: 1;
9+
height: 1em;
10+
font-weight: 500;
11+
text-align: center;
12+
}
13+
14+
.time-range-double-arrow .mat-icon {
15+
margin: -22%;
16+
}

0 commit comments

Comments
 (0)