Skip to content

Commit ac69654

Browse files
refactor: TTA-49 change range datepicker
1 parent 358e576 commit ac69654

18 files changed

+737
-3
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';
@@ -90,6 +93,9 @@ import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mo
9093
import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login';
9194
import { GoogleLoginProvider } from 'angularx-social-login';
9295
import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component';
96+
import { TimeRangeCustomComponent } from './modules/reports/components/time-range-custom/time-range-custom.component';
97+
import { TimeRangeHeaderComponent } from './modules/reports/components/time-range-custom/time-range-header/time-range-header.component';
98+
import { TimeRangeOptionsComponent } from './modules/reports/components/time-range-custom/time-range-options/time-range-options.component';
9399

94100
const maskConfig: Partial<IConfig> = {
95101
validation: false,
@@ -146,6 +152,9 @@ const maskConfig: Partial<IConfig> = {
146152
CalendarComponent,
147153
DropdownComponent,
148154
DarkModeComponent,
155+
TimeRangeCustomComponent,
156+
TimeRangeHeaderComponent,
157+
TimeRangeOptionsComponent,
149158
],
150159
imports: [
151160
NgxMaskModule.forRoot(maskConfig),
@@ -165,6 +174,9 @@ const maskConfig: Partial<IConfig> = {
165174
NgxMaterialTimepickerModule,
166175
UiSwitchModule,
167176
DragDropModule,
177+
MatIconModule,
178+
MatCheckboxModule,
179+
MatListModule,
168180
StoreModule.forRoot(reducers, {
169181
metaReducers,
170182
}),
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: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
};
19+
20+
const timeEntry = {
21+
id: '1',
22+
start_date: new Date(),
23+
end_date: new Date(),
24+
activity_id: '1',
25+
technologies: ['react', 'redux'],
26+
comments: 'any comment',
27+
uri: 'TT-123',
28+
project_id: '1'
29+
};
30+
31+
const state = {
32+
active: timeEntry,
33+
entryList: [timeEntry],
34+
isLoading: false,
35+
message: 'test',
36+
createError: false,
37+
updateError: false,
38+
timeEntriesSummary: null,
39+
entriesForReport: [timeEntry],
40+
};
41+
42+
beforeEach(async () => {
43+
await TestBed.configureTestingModule({
44+
imports: [FormsModule, ReactiveFormsModule],
45+
declarations: [ TimeRangeCustomComponent ],
46+
providers: [
47+
provideMockStore({ initialState: state }),
48+
{ provide: ToastrService, useValue: toastrServiceStub }
49+
],
50+
})
51+
.compileComponents();
52+
store = TestBed.inject(MockStore);
53+
});
54+
55+
beforeEach(() => {
56+
fixture = TestBed.createComponent(TimeRangeCustomComponent);
57+
component = fixture.componentInstance;
58+
fixture.detectChanges();
59+
});
60+
61+
it('should create', () => {
62+
expect(component).toBeTruthy();
63+
});
64+
65+
it('setInitialDataOnScreen on ngOnInit', () => {
66+
spyOn(component, 'setInitialDataOnScreen');
67+
68+
component.ngOnInit();
69+
70+
expect(component.setInitialDataOnScreen).toHaveBeenCalled();
71+
});
72+
73+
it('LoadEntriesByTimeRange action is triggered when start date is before end date', () => {
74+
const end = moment(new Date()).subtract(1, 'days');
75+
const start = moment(new Date());
76+
spyOn(store, 'dispatch');
77+
component.range.controls.start.setValue(end);
78+
component.range.controls.end.setValue(start);
79+
80+
component.onSubmit();
81+
82+
expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({
83+
start_date: end.startOf('day'),
84+
end_date: start.endOf('day')
85+
}));
86+
});
87+
88+
it('shows an error when the end date is before the start date', () => {
89+
spyOn(toastrServiceStub, 'error');
90+
const yesterday = moment(new Date()).subtract(2, 'days');
91+
const today = moment(new Date());
92+
spyOn(store, 'dispatch');
93+
component.range.controls.start.setValue(today);
94+
component.range.controls.end.setValue(yesterday);
95+
96+
component.onSubmit();
97+
98+
expect(toastrServiceStub.error).toHaveBeenCalled();
99+
});
100+
101+
it('setInitialDataOnScreen sets dates in form', () => {
102+
spyOn(component.range.controls.start, 'setValue');
103+
spyOn(component.range.controls.end, 'setValue');
104+
105+
component.setInitialDataOnScreen();
106+
107+
expect(component.range.controls.start.setValue).toHaveBeenCalled();
108+
expect(component.range.controls.end.setValue).toHaveBeenCalled();
109+
110+
});
111+
112+
it('triggers onSubmit to set initial data', () => {
113+
spyOn(component, 'onSubmit');
114+
115+
component.setInitialDataOnScreen();
116+
117+
expect(component.onSubmit).toHaveBeenCalled();
118+
});
119+
120+
it('When the ngOnChanges method is called, the onSubmit method is called', () => {
121+
const userIdCalled = 'test-user-1';
122+
spyOn(component, 'onSubmit');
123+
124+
component.ngOnChanges({userId: new SimpleChange(null, userIdCalled, false)});
125+
126+
expect(component.onSubmit).toHaveBeenCalled();
127+
});
128+
129+
it('When the ngOnChanges method is the first change, the onSubmit method is not called', () => {
130+
const userIdNotCalled = 'test-user-2';
131+
spyOn(component, 'onSubmit');
132+
133+
component.ngOnChanges({userId: new SimpleChange(null, userIdNotCalled, true)});
134+
135+
expect(component.onSubmit).not.toHaveBeenCalled();
136+
});
137+
});
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)