Skip to content

Commit 3f884a7

Browse files
TT-593 On Reports page, add filter by user name (#839)
* feat: TT-593 add filter for searching a specific user * Created user-search component * Loaded all users to the select * sent the user.id to the api reports * feat: TT-593 added the ngOnChanges event * style: TT-593 added the length of the search button * fix: TT-593 resolved keys * fix: TT-593 added the time-tracker-ui CD-prod file * fix: TT-593 added the time-tracker-ui-cd-prod file * fix: TT-593 fixed the CI-time-tracker-ui.yml file * fix: TT-593 refactor * fix: TT-593 refactor * fix: TT-593 unit test * fix: TT-593 refactor unit tets * fix: TT-593 unit test coverage * fix: TT-593 coverage time-entries-table * fix: TT-593 coverage time-entries-table Co-authored-by: wilc0519 <[email protected]>
1 parent fff4b4f commit 3f884a7

16 files changed

+162
-39
lines changed

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import { NgSelectModule } from '@ng-select/ng-select';
8989
import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mode.component';
9090
import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login';
9191
import { GoogleLoginProvider } from 'angularx-social-login';
92+
import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component';
9293

9394
const maskConfig: Partial<IConfig> = {
9495
validation: false,
@@ -128,6 +129,7 @@ const maskConfig: Partial<IConfig> = {
128129
EntryFieldsComponent,
129130
SubstractDatePipe,
130131
TechnologiesComponent,
132+
SearchUserComponent,
131133
TimeEntriesSummaryComponent,
132134
TimeDetailsPipe,
133135
InputLabelComponent,

src/app/modules/reports/components/time-entries-table/time-entries-table.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<div class="row scroll-table mt-5 ml-0">
2+
<app-search-user [users]="users" (selectedUserId)="user($event)"></app-search-user>
3+
24
<table class="table table-striped mb-0" datatable [dtTrigger]="dtTrigger" [dtOptions]="dtOptions" *ngIf="(reportDataSource$ | async) as dataSource">
35
<thead class="thead-blue">
46
<tr class="d-flex">
@@ -54,4 +56,4 @@
5456
</tr>
5557
</tbody>
5658
</table>
57-
</div>
59+
</div>

src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
22
import { MockStore, provideMockStore } from '@ngrx/store/testing';
3-
import { DataTableDirective } from 'angular-datatables';
3+
import { DataTablesModule } from 'angular-datatables';
4+
import { NgxPaginationModule } from 'ngx-pagination';
45
import { Entry } from 'src/app/modules/shared/models';
56
import { SubstractDatePipe } from 'src/app/modules/shared/pipes/substract-date/substract-date.pipe';
67
import { getReportDataSource } from 'src/app/modules/time-clock/store/entry.selectors';
78
import { EntryState } from '../../../time-clock/store/entry.reducer';
89
import { TimeEntriesTableComponent } from './time-entries-table.component';
10+
import { ActionsSubject } from '@ngrx/store';
11+
import { UserActionTypes } from 'src/app/modules/users/store';
912

1013
describe('Reports Page', () => {
1114
describe('TimeEntriesTableComponent', () => {
@@ -46,25 +49,28 @@ describe('Reports Page', () => {
4649
},
4750
};
4851

52+
const actionSub: ActionsSubject = new ActionsSubject();
53+
4954
beforeEach(
5055
waitForAsync(() => {
5156
TestBed.configureTestingModule({
52-
imports: [],
57+
imports: [NgxPaginationModule, DataTablesModule],
5358
declarations: [TimeEntriesTableComponent, SubstractDatePipe],
54-
providers: [provideMockStore({ initialState: state })],
59+
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
5560
}).compileComponents();
56-
store = TestBed.inject(MockStore);
61+
5762
})
5863
);
5964

6065
beforeEach(
61-
waitForAsync(() => {
66+
() => {
6267
fixture = TestBed.createComponent(TimeEntriesTableComponent);
6368
component = fixture.componentInstance;
69+
store = TestBed.inject(MockStore);
6470
store.setState(state);
6571
getReportDataSourceSelectorMock = store.overrideSelector(getReportDataSource, state.reportDataSource);
6672
fixture.detectChanges();
67-
})
73+
}
6874
);
6975

7076
beforeEach(() => {
@@ -85,7 +91,9 @@ describe('Reports Page', () => {
8591
});
8692

8793
it('after the component is initialized it should initialize the table', () => {
94+
component.dtElement = null;
8895
spyOn(component.dtTrigger, 'next');
96+
8997
component.ngAfterViewInit();
9098

9199
expect(component.dtTrigger.next).toHaveBeenCalled();
@@ -137,14 +145,35 @@ describe('Reports Page', () => {
137145

138146
it('when the rerenderDataTable method is called and dtElement and dtInstance are defined, the destroy and next methods are called ',
139147
() => {
140-
component.dtElement = {
141-
dtInstance: {
142-
then : (dtInstance: DataTables.Api) => { dtInstance.destroy(); }
143-
}
144-
} as unknown as DataTableDirective;
145-
spyOn(component.dtElement.dtInstance, 'then');
148+
spyOn(component.dtTrigger, 'next');
149+
146150
component.ngAfterViewInit();
147-
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
151+
152+
component.dtElement.dtInstance.then( (dtInstance) => {
153+
expect(component.dtTrigger.next).toHaveBeenCalled();
154+
});
155+
});
156+
157+
it(`When the user method is called, the emit method is called`, () => {
158+
const userId = 'abc123';
159+
spyOn(component.selectedUserId, 'emit');
160+
component.user(userId);
161+
expect(component.selectedUserId.emit).toHaveBeenCalled();
162+
163+
});
164+
165+
it('Should populate the users with the payload from the action executed', () => {
166+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
167+
const usersArray = []
168+
const action = {
169+
type: UserActionTypes.LOAD_USERS_SUCCESS,
170+
payload: usersArray
171+
};
172+
173+
actionSubject.next(action);
174+
175+
176+
expect(component.users).toEqual(usersArray);
148177
});
149178

150179
afterEach(() => {

src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { formatDate } from '@angular/common';
2-
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
3-
import { select, Store } from '@ngrx/store';
2+
import { AfterViewInit, Component, EventEmitter, OnDestroy, Output, OnInit, ViewChild } from '@angular/core';
3+
import { select, Store, ActionsSubject } from '@ngrx/store';
44
import { DataTableDirective } from 'angular-datatables';
55
import * as moment from 'moment';
66
import { Observable, Subject, Subscription } from 'rxjs';
7+
import { filter } from 'rxjs/operators';
78
import { Entry } from 'src/app/modules/shared/models';
89
import { DataSource } from 'src/app/modules/shared/models/data-source.model';
910
import { EntryState } from '../../../time-clock/store/entry.reducer';
1011
import { getReportDataSource } from '../../../time-clock/store/entry.selectors';
12+
import { User } from 'src/app/modules/users/models/users';
13+
import { LoadUsers, UserActionTypes } from 'src/app/modules/users/store/user.actions';
1114
import { ParseDateTimeOffset } from '../../../shared/formatters/parse-date-time-offset/parse-date-time-offset';
1215

1316
@Component({
@@ -16,8 +19,11 @@ import { ParseDateTimeOffset } from '../../../shared/formatters/parse-date-time-
1619
styleUrls: ['./time-entries-table.component.scss'],
1720
})
1821
export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit {
22+
@Output() selectedUserId = new EventEmitter<string>();
23+
1924
selectOptionValues = [15, 30, 50, 100, -1];
2025
selectOptionNames = [15, 30, 50, 100, 'All'];
26+
users: User[] = [];
2127
dtOptions: any = {
2228
scrollY: '590px',
2329
dom: '<"d-flex justify-content-between"B<"d-flex"<"mr-5"l>f>>rtip',
@@ -63,15 +69,25 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn
6369
rerenderTableSubscription: Subscription;
6470
dateTimeOffset: ParseDateTimeOffset;
6571

66-
constructor(private store: Store<EntryState>) {
72+
constructor(private store: Store<EntryState>, private actionsSubject$: ActionsSubject, private storeUser: Store<User> ) {
6773
this.reportDataSource$ = this.store.pipe(select(getReportDataSource));
6874
this.dateTimeOffset = new ParseDateTimeOffset();
6975
}
7076

77+
uploadUsers(): void {
78+
this.storeUser.dispatch(new LoadUsers());
79+
this.actionsSubject$
80+
.pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS))
81+
.subscribe((action) => {
82+
this.users = action.payload;
83+
});
84+
}
85+
7186
ngOnInit(): void {
7287
this.rerenderTableSubscription = this.reportDataSource$.subscribe((ds) => {
7388
this.rerenderDataTable();
7489
});
90+
this.uploadUsers();
7591
}
7692

7793
ngAfterViewInit(): void {
@@ -86,11 +102,11 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn
86102
private rerenderDataTable(): void {
87103
if (this.dtElement && this.dtElement.dtInstance) {
88104
this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => {
89-
dtInstance.destroy();
90-
this.dtTrigger.next();
105+
dtInstance.destroy();
106+
this.dtTrigger.next();
91107
});
92108
} else {
93-
this.dtTrigger.next();
109+
this.dtTrigger.next();
94110
}
95111
}
96112

@@ -103,10 +119,15 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn
103119
return regex.test(uri);
104120
}
105121

106-
bodyExportOptions(data, row, column, node){
122+
bodyExportOptions(data, row, column, node) {
107123
const dataFormated = data.toString().replace(/<((.|\n){0,200}?)>/gi, '');
108124
const durationColumnIndex = 3;
109125
return column === durationColumnIndex ? moment.duration(dataFormated).asHours().toFixed(2) : dataFormated;
110126
}
127+
128+
user(userId: string){
129+
this.selectedUserId.emit(userId);
130+
}
131+
111132
}
112133

src/app/modules/reports/components/time-range-form/time-range-form.component.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ToastrService } from 'ngx-toastr';
22
import { formatDate } from '@angular/common';
3-
import { Component, OnInit } from '@angular/core';
3+
import { OnChanges, SimpleChanges, Component, Input, OnInit } from '@angular/core';
44
import { FormControl, FormGroup } from '@angular/forms';
55
import { DATE_FORMAT } from 'src/environments/environment';
66
import * as entryActions from '../../../time-clock/store/entry.actions';
@@ -12,7 +12,10 @@ import * as moment from 'moment';
1212
selector: 'app-time-range-form',
1313
templateUrl: './time-range-form.component.html',
1414
})
15-
export class TimeRangeFormComponent implements OnInit {
15+
export class TimeRangeFormComponent implements OnInit, OnChanges {
16+
17+
@Input() userId: string;
18+
1619
public reportForm: FormGroup;
1720
private startDate = new FormControl('');
1821
private endDate = new FormControl('');
@@ -27,6 +30,12 @@ export class TimeRangeFormComponent implements OnInit {
2730
this.setInitialDataOnScreen();
2831
}
2932

33+
ngOnChanges(changes: SimpleChanges){
34+
if (!changes.userId.firstChange){
35+
this.onSubmit();
36+
}
37+
}
38+
3039
setInitialDataOnScreen() {
3140
this.reportForm.setValue({
3241
startDate: formatDate(moment().startOf('week').format('l'), DATE_FORMAT, 'en'),
@@ -43,7 +52,7 @@ export class TimeRangeFormComponent implements OnInit {
4352
this.store.dispatch(new entryActions.LoadEntriesByTimeRange({
4453
start_date: moment(this.startDate.value).startOf('day'),
4554
end_date: moment(this.endDate.value).endOf('day'),
46-
}));
55+
}, this.userId));
4756
}
4857
}
4958
}

src/app/modules/reports/components/time-range-form/time-range.component.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
77
import { InputDateComponent } from '../../../shared/components/input-date/input-date.component';
88
import * as entryActions from '../../../time-clock/store/entry.actions';
99
import * as moment from 'moment';
10+
import { SimpleChange } from '@angular/core';
1011

1112
describe('Reports Page', () => {
1213
describe('TimeRangeFormComponent', () => {
@@ -114,6 +115,15 @@ describe('Reports Page', () => {
114115
expect(component.onSubmit).toHaveBeenCalled();
115116
});
116117

118+
it('When the ngOnChanges method is called, the onSubmit method is called', () => {
119+
const userId = 'abcd';
120+
spyOn(component, 'onSubmit');
121+
122+
component.ngOnChanges({userId: new SimpleChange(null, userId, false)});
123+
124+
expect(component.onSubmit).toHaveBeenCalled();
125+
});
126+
117127
afterEach(() => {
118128
fixture.destroy();
119129
});
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
<app-time-range-form></app-time-range-form>
2-
<app-time-entries-table></app-time-entries-table>
3-
1+
<app-time-range-form [userId]="userId"></app-time-range-form>
2+
<app-time-entries-table (selectedUserId)="user($event)"></app-time-entries-table>

src/app/modules/reports/pages/reports.component.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,10 @@ describe('ReportsComponent', () => {
3232
expect(reportForm).toBeTruthy();
3333
expect(reportDataTable).toBeTruthy();
3434
}));
35+
36+
it(`Given the id of the user 'abc123' this is assigned to the variable userId`, () => {
37+
const userId = 'abc123';
38+
component.user(userId);
39+
expect(component.userId).toEqual('abc123');
40+
});
3541
});

src/app/modules/reports/pages/reports.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ import { Component } from '@angular/core';
66
styleUrls: ['./reports.component.scss']
77
})
88
export class ReportsComponent {
9+
10+
userId: string;
11+
12+
user(userId: string){
13+
this.userId = userId;
14+
}
915
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="form-group" >
2+
<label>Users: </label>
3+
4+
<ng-select [(ngModel)]="selectedUser" placeholder="Select user" (change)="updateUser()" class="selectUser">
5+
<ng-option *ngFor="let user of users" value={{user.id}}>👤{{user.name}}📨{{ user.email}}</ng-option >
6+
</ng-select>
7+
8+
</div>

0 commit comments

Comments
 (0)