Skip to content

Commit 80f5d2c

Browse files
author
Juan Gabriel Guzman
committed
feat: #204 Adding report page
1 parent 7747b0e commit 80f5d2c

19 files changed

+603
-14
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" style="max-width: 8em">Date</th>
6+
<th class="col" style="max-width: 6.2em">Duration</th>
7+
<th class="col">Project</th>
8+
<th class="col">Ticket</th>
9+
<th class="col" style="min-width: 20em">Description</th>
10+
<th class="col" style="min-width: 20em">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" style="max-width: 8em"> {{ entry.start_date | date: 'dd/MM/yyyy' }} </td>
19+
<td class="col" style="max-width: 6.2em"> {{ entry.end_date | substractDate: entry.start_date }} </td>
20+
<td class="col"> {{ entry.project_name }} </td>
21+
<td class="col"> {{ entry.uri }} </td>
22+
<td class="col" style="min-width: 20em"> {{ entry.description }} </td>
23+
<td class="col" style="word-break: break-all; min-width: 20em"> {{ entry.technologies }} </td>
24+
</tr>
25+
</tbody>
26+
</table>
27+
</div>
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 {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+
fit('after the component is initialized it should initialize the table', () => {
65+
spyOn(component.dtTrigger, 'next');
66+
67+
component.ngAfterViewInit();
68+
// fixture.detectChanges();
69+
70+
expect(component.dtTrigger.next).toHaveBeenCalled();
71+
});
72+
73+
afterEach(() => {
74+
fixture.destroy();
75+
});
76+
});
77+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
})
12+
export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit {
13+
14+
data = [];
15+
const;
16+
dtOptions: any = {
17+
scrollY: '790px',
18+
paging: false,
19+
dom: 'Bfrtip',
20+
buttons: [
21+
'colvis',
22+
'print',
23+
'excel'
24+
]
25+
};
26+
27+
dtTrigger: Subject<any> = new Subject();
28+
@ViewChild(DataTableDirective, {static: false})
29+
dtElement: DataTableDirective;
30+
31+
constructor(private store: Store<EntryState>) {
32+
}
33+
34+
ngOnInit(): void {
35+
const dataForReport$ = this.store.pipe(select(entriesForReport));
36+
dataForReport$.subscribe((response) => {
37+
this.data = response;
38+
this.rerenderDataTable();
39+
});
40+
}
41+
42+
ngAfterViewInit(): void {
43+
this.rerenderDataTable();
44+
}
45+
46+
ngOnDestroy(): void {
47+
this.dtTrigger.unsubscribe();
48+
}
49+
50+
private rerenderDataTable(): void {
51+
console.log('1', this.dtElement);
52+
console.log('1', this.dtElement);
53+
if (this.dtElement && this.dtElement.dtInstance) {
54+
console.log('2');
55+
this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => {
56+
console.log('3');
57+
58+
dtInstance.destroy();
59+
this.dtTrigger.next();
60+
});
61+
} else {
62+
console.log('4');
63+
this.dtTrigger.next();
64+
}
65+
}
66+
}
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
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<input
2+
[value]="value"
3+
[disabled]="isDisabled"
4+
(input)="onInput($event.target.value)"
5+
type="date"
6+
class="form-control"
7+
aria-label="Small"
8+
aria-describedby="inputGroup-sizing-sm"
9+
required="required"
10+
/>

0 commit comments

Comments
 (0)