Skip to content

Commit ad1de16

Browse files
authored
Merge pull request #308 from ioet/204-reports-page
204 reports page
2 parents 652a5f5 + a53932d commit ad1de16

30 files changed

+826
-133
lines changed

src/app/app.module.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
22
import { ToastrModule } from 'ngx-toastr';
3-
import { CommonModule } from '@angular/common';
3+
import {CommonModule, DatePipe} from '@angular/common';
44
import { BrowserModule } from '@angular/platform-browser';
55
import { NgModule } from '@angular/core';
66
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -60,6 +60,11 @@ import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substra
6060
import {TechnologiesComponent} from './modules/shared/components/technologies/technologies.component';
6161
import { TimeEntriesSummaryComponent } from './modules/time-clock/components/time-entries-summary/time-entries-summary.component';
6262
import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
63+
import {InputLabelComponent} from './modules/shared/components/input-label/input-label.component';
64+
import {ReportsComponent} from './modules/reports/pages/reports.component';
65+
import {InputDateComponent} from './modules/shared/components/input-date/input-date.component';
66+
import {TimeRangeFormComponent} from './modules/reports/components/time-range-form/time-range-form.component';
67+
import {TimeEntriesTableComponent} from './modules/reports/components/time-entries-table/time-entries-table.component';
6368

6469
@NgModule({
6570
declarations: [
@@ -97,6 +102,11 @@ import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
97102
TechnologiesComponent,
98103
TimeEntriesSummaryComponent,
99104
TimeDetailsPipe,
105+
InputLabelComponent,
106+
ReportsComponent,
107+
InputDateComponent,
108+
TimeRangeFormComponent,
109+
TimeEntriesTableComponent,
100110
],
101111
imports: [
102112
CommonModule,
@@ -133,6 +143,7 @@ import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
133143
useClass: InjectTokenInterceptor,
134144
multi: true,
135145
},
146+
DatePipe
136147
],
137148
bootstrap: [AppComponent],
138149
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<div class="container-flex m-5">
2+
<table class="table table-sm table-striped mb-0" datatable [dtTrigger]="dtTrigger" [dtOptions]="dtOptions">
3+
<thead class="thead-orange">
4+
<tr class="d-flex">
5+
<th class="col md-col">Date</th>
6+
<th class="col sm-col">Duration</th>
7+
<th class="col md-col">Project</th>
8+
<th class="col md-col">Ticket</th>
9+
<th class="col lg-col" >Description</th>
10+
<th class="col lg-col">Technologies</th>
11+
</tr>
12+
</thead>
13+
<tbody>
14+
<tr
15+
class="d-flex"
16+
*ngFor="let entry of data"
17+
>
18+
<td class="col md-col"> {{ entry.start_date | date: 'dd/MM/yyyy' }} </td>
19+
<td class="col sm-col"> {{ entry.end_date | substractDate: entry.start_date }} </td>
20+
<td class="col md-col"> {{ entry.project_name }} </td>
21+
<td class="col md-col"> {{ entry.uri }} </td>
22+
<td class="col lg-col multiline-col"> {{ entry.description }} </td>
23+
<td class="col lg-col multiline-col"> {{ entry.technologies }} </td>
24+
</tr>
25+
</tbody>
26+
</table>
27+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
.sm-col{
3+
max-width: 6.2em
4+
}
5+
.md-col{
6+
max-width: 8em
7+
}
8+
9+
.lg-col{
10+
min-width: 20em;
11+
}
12+
13+
.multiline-col{
14+
word-break: break-all;
15+
16+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {MockStore, provideMockStore} from '@ngrx/store/testing';
3+
import {EntryState} from '../../../time-clock/store/entry.reducer';
4+
import {TimeEntriesTableComponent} from './time-entries-table.component';
5+
import {entriesForReport} from '../../../time-clock/store/entry.selectors';
6+
7+
describe('Reports Page', () => {
8+
describe('TimeEntriesTableComponent', () => {
9+
let component: TimeEntriesTableComponent;
10+
let fixture: ComponentFixture<TimeEntriesTableComponent>;
11+
let store: MockStore<EntryState>;
12+
let geTimeEntriesSelectorMock;
13+
const timeEntry = {
14+
id: '123',
15+
start_date: new Date(),
16+
end_date: new Date(),
17+
activity_id: '123',
18+
technologies: ['react', 'redux'],
19+
comments: 'any comment',
20+
uri: 'custom uri',
21+
project_id: '123'
22+
};
23+
24+
const state = {
25+
active: timeEntry,
26+
entryList: [timeEntry],
27+
isLoading: false,
28+
message: '',
29+
createError: false,
30+
updateError: false,
31+
timeEntriesSummary: null,
32+
entriesForReport: [timeEntry],
33+
};
34+
35+
beforeEach(async(() => {
36+
TestBed.configureTestingModule({
37+
imports: [],
38+
declarations: [],
39+
providers: [provideMockStore({initialState: state})],
40+
}).compileComponents();
41+
store = TestBed.inject(MockStore);
42+
43+
}));
44+
45+
beforeEach(() => {
46+
fixture = TestBed.createComponent(TimeEntriesTableComponent);
47+
component = fixture.componentInstance;
48+
fixture.detectChanges();
49+
store.setState(state);
50+
geTimeEntriesSelectorMock = store.overrideSelector(entriesForReport, state.entriesForReport);
51+
});
52+
53+
it('component should be created', () => {
54+
expect(component).toBeTruthy();
55+
});
56+
57+
it('on success load time entries, the report should be populated', () => {
58+
component.ngOnInit();
59+
fixture.detectChanges();
60+
61+
expect(component.data).toEqual(state.entriesForReport);
62+
});
63+
64+
it('after the component is initialized it should initialize the table', () => {
65+
spyOn(component.dtTrigger, 'next');
66+
67+
component.ngAfterViewInit();
68+
69+
expect(component.dtTrigger.next).toHaveBeenCalled();
70+
});
71+
72+
afterEach(() => {
73+
fixture.destroy();
74+
});
75+
});
76+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
2+
import {select, Store} from '@ngrx/store';
3+
import {EntryState} from '../../../time-clock/store/entry.reducer';
4+
import {entriesForReport} from '../../../time-clock/store/entry.selectors';
5+
import {Subject} from 'rxjs';
6+
import {DataTableDirective} from 'angular-datatables';
7+
8+
@Component({
9+
selector: 'app-time-entries-table',
10+
templateUrl: './time-entries-table.component.html',
11+
styleUrls: ['./time-entries-table.component.scss']
12+
})
13+
export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit {
14+
15+
data = [];
16+
const;
17+
dtOptions: any = {
18+
scrollY: '600px',
19+
paging: false,
20+
dom: 'Bfrtip',
21+
buttons: [
22+
'colvis',
23+
'print',
24+
'excel'
25+
]
26+
};
27+
28+
dtTrigger: Subject<any> = new Subject();
29+
@ViewChild(DataTableDirective, {static: false})
30+
dtElement: DataTableDirective;
31+
32+
constructor(private store: Store<EntryState>) {
33+
}
34+
35+
ngOnInit(): void {
36+
const dataForReport$ = this.store.pipe(select(entriesForReport));
37+
dataForReport$.subscribe((response) => {
38+
this.data = response;
39+
this.rerenderDataTable();
40+
});
41+
}
42+
43+
ngAfterViewInit(): void {
44+
this.rerenderDataTable();
45+
}
46+
47+
ngOnDestroy(): void {
48+
this.dtTrigger.unsubscribe();
49+
}
50+
51+
private rerenderDataTable(): void {
52+
if (this.dtElement && this.dtElement.dtInstance) {
53+
this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => {
54+
dtInstance.destroy();
55+
this.dtTrigger.next();
56+
});
57+
} else {
58+
this.dtTrigger.next();
59+
}
60+
}
61+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<div class="container-flex">
2+
<form [formGroup]="reportForm" (ngSubmit)="onSubmit()">
3+
<div class="row">
4+
<div class="col">
5+
<div class="input-group">
6+
<app-input-label text="Start Date"></app-input-label>
7+
<app-input-date formControlName="startDate" required="true"></app-input-date>
8+
</div>
9+
</div>
10+
<div class="col">
11+
<div class="input-group">
12+
<app-input-label text="End Date"></app-input-label>
13+
<app-input-date formControlName="endDate" required="true"></app-input-date>
14+
</div>
15+
</div>
16+
</div>
17+
<div class="row">
18+
<div class="col">
19+
<button type="submit" class="btn btn-primary float-right"
20+
[disabled]="!(reportForm.touched && reportForm.valid)">Search
21+
</button>
22+
</div>
23+
</div>
24+
</form>
25+
</div>
26+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {Component} from '@angular/core';
2+
import {FormControl, FormGroup} from '@angular/forms';
3+
import * as entryActions from '../../../time-clock/store/entry.actions';
4+
import {Store} from '@ngrx/store';
5+
import {EntryState} from '../../../time-clock/store/entry.reducer';
6+
7+
@Component({
8+
selector: 'app-time-range-form',
9+
templateUrl: './time-range-form.component.html',
10+
})
11+
export class TimeRangeFormComponent {
12+
public reportForm: FormGroup;
13+
private startDate = new FormControl('');
14+
private endDate = new FormControl('');
15+
16+
constructor(private store: Store<EntryState>) {
17+
this.reportForm = new FormGroup({
18+
startDate: this.startDate,
19+
endDate: this.endDate
20+
});
21+
}
22+
23+
onSubmit() {
24+
this.store.dispatch(new entryActions.LoadEntriesByTimeRange({
25+
start_date: this.startDate.value,
26+
end_date: this.endDate.value
27+
}));
28+
}
29+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {MockStore, provideMockStore} from '@ngrx/store/testing';
3+
import {TimeRangeFormComponent} from './time-range-form.component';
4+
import {EntryState} from '../../../time-clock/store/entry.reducer';
5+
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
6+
import {InputDateComponent} from '../../../shared/components/input-date/input-date.component';
7+
import * as entryActions from '../../../time-clock/store/entry.actions';
8+
9+
describe('Reports Page', () => {
10+
describe('TimeRangeFormComponent', () => {
11+
let component: TimeRangeFormComponent;
12+
let fixture: ComponentFixture<TimeRangeFormComponent>;
13+
let store: MockStore<EntryState>;
14+
const timeEntry = {
15+
id: '123',
16+
start_date: new Date(),
17+
end_date: new Date(),
18+
activity_id: '123',
19+
technologies: ['react', 'redux'],
20+
comments: 'any comment',
21+
uri: 'custom uri',
22+
project_id: '123'
23+
};
24+
25+
const state = {
26+
active: timeEntry,
27+
entryList: [timeEntry],
28+
isLoading: false,
29+
message: '',
30+
createError: false,
31+
updateError: false,
32+
timeEntriesSummary: null,
33+
entriesForReport: [timeEntry],
34+
};
35+
36+
beforeEach(async(() => {
37+
TestBed.configureTestingModule({
38+
imports: [FormsModule, ReactiveFormsModule],
39+
declarations: [TimeRangeFormComponent, InputDateComponent],
40+
providers: [provideMockStore({initialState: state})],
41+
}).compileComponents();
42+
store = TestBed.inject(MockStore);
43+
44+
}));
45+
46+
beforeEach(() => {
47+
fixture = TestBed.createComponent(TimeRangeFormComponent);
48+
component = fixture.componentInstance;
49+
fixture.detectChanges();
50+
});
51+
52+
it('component should be created', () => {
53+
expect(component).toBeTruthy();
54+
});
55+
56+
it('when submitting form a new LoadEntriesByTimeRange action is triggered', () => {
57+
58+
const startDateValue = new Date();
59+
const endDateValue = new Date();
60+
endDateValue.setMonth(1);
61+
spyOn(store, 'dispatch');
62+
component.reportForm.controls.startDate.setValue(startDateValue);
63+
component.reportForm.controls.endDate.setValue(endDateValue);
64+
65+
component.onSubmit();
66+
67+
expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({
68+
start_date: startDateValue,
69+
end_date: endDateValue,
70+
}));
71+
});
72+
73+
afterEach(() => {
74+
fixture.destroy();
75+
});
76+
});
77+
});
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
<div class="text-center mt-5">
2-
<p>reports works!</p>
1+
<div class="w-100">
2+
<app-time-range-form></app-time-range-form>
3+
<app-time-entries-table></app-time-entries-table>
34
</div>

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ describe('ReportsComponent', () => {
2929
expect(component).toBeTruthy();
3030
});
3131

32-
it('should have p tag as "reports works!"', async(() => {
32+
it('should have form and datatable components', async(() => {
3333
// tslint:disable-next-line: no-shadowed-variable
3434
const { fixture } = setup();
35+
3536
fixture.detectChanges();
37+
3638
const compile = fixture.debugElement.nativeElement;
37-
const ptag = compile.querySelector('p');
38-
expect(ptag.textContent).toBe('reports works!');
39+
const div = compile.querySelector('div');
40+
const reportForm = compile.querySelector('app-time-range-form');
41+
const reportDataTable = compile.querySelector('app-time-entries-table');
42+
expect(div).toBeTruthy();
43+
expect(reportForm).toBeTruthy();
44+
expect(reportDataTable).toBeTruthy();
3945
}));
4046
});

0 commit comments

Comments
 (0)