Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { CommonModule } from '@angular/common';
import {CommonModule, DatePipe} from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
Expand Down Expand Up @@ -60,6 +60,11 @@ import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substra
import {TechnologiesComponent} from './modules/shared/components/technologies/technologies.component';
import { TimeEntriesSummaryComponent } from './modules/time-clock/components/time-entries-summary/time-entries-summary.component';
import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
import {InputLabelComponent} from './modules/shared/components/input-label/input-label.component';
import {ReportsComponent} from './modules/reports/pages/reports.component';
import {InputDateComponent} from './modules/shared/components/input-date/input-date.component';
import {TimeRangeFormComponent} from './modules/reports/components/time-range-form/time-range-form.component';
import {TimeEntriesTableComponent} from './modules/reports/components/time-entries-table/time-entries-table.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -97,6 +102,11 @@ import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
TechnologiesComponent,
TimeEntriesSummaryComponent,
TimeDetailsPipe,
InputLabelComponent,
ReportsComponent,
InputDateComponent,
TimeRangeFormComponent,
TimeEntriesTableComponent,
],
imports: [
CommonModule,
Expand Down Expand Up @@ -133,6 +143,7 @@ import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
useClass: InjectTokenInterceptor,
multi: true,
},
DatePipe
],
bootstrap: [AppComponent],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="container-flex m-5">
<table class="table table-sm table-striped mb-0" datatable [dtTrigger]="dtTrigger" [dtOptions]="dtOptions">
<thead class="thead-orange">
<tr class="d-flex">
<th class="col md-col">Date</th>
<th class="col sm-col">Duration</th>
<th class="col md-col">Project</th>
<th class="col md-col">Ticket</th>
<th class="col lg-col" >Description</th>
<th class="col lg-col">Technologies</th>
</tr>
</thead>
<tbody>
<tr
class="d-flex"
*ngFor="let entry of data"
>
<td class="col md-col"> {{ entry.start_date | date: 'dd/MM/yyyy' }} </td>
<td class="col sm-col"> {{ entry.end_date | substractDate: entry.start_date }} </td>
<td class="col md-col"> {{ entry.project_name }} </td>
<td class="col md-col"> {{ entry.uri }} </td>
<td class="col lg-col multiline-col"> {{ entry.description }} </td>
<td class="col lg-col multiline-col"> {{ entry.technologies }} </td>
</tr>
</tbody>
</table>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

.sm-col{
max-width: 6.2em
}
.md-col{
max-width: 8em
}

.lg-col{
min-width: 20em;
}

.multiline-col{
word-break: break-all;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MockStore, provideMockStore} from '@ngrx/store/testing';
import {EntryState} from '../../../time-clock/store/entry.reducer';
import {TimeEntriesTableComponent} from './time-entries-table.component';
import {entriesForReport} from '../../../time-clock/store/entry.selectors';

describe('Reports Page', () => {
describe('TimeEntriesTableComponent', () => {
let component: TimeEntriesTableComponent;
let fixture: ComponentFixture<TimeEntriesTableComponent>;
let store: MockStore<EntryState>;
let geTimeEntriesSelectorMock;
const timeEntry = {
id: '123',
start_date: new Date(),
end_date: new Date(),
activity_id: '123',
technologies: ['react', 'redux'],
comments: 'any comment',
uri: 'custom uri',
project_id: '123'
};

const state = {
active: timeEntry,
entryList: [timeEntry],
isLoading: false,
message: '',
createError: false,
updateError: false,
timeEntriesSummary: null,
entriesForReport: [timeEntry],
};

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
declarations: [],
providers: [provideMockStore({initialState: state})],
}).compileComponents();
store = TestBed.inject(MockStore);

}));

beforeEach(() => {
fixture = TestBed.createComponent(TimeEntriesTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store.setState(state);
geTimeEntriesSelectorMock = store.overrideSelector(entriesForReport, state.entriesForReport);
});

it('component should be created', () => {
expect(component).toBeTruthy();
});

it('on success load time entries, the report should be populated', () => {
component.ngOnInit();
fixture.detectChanges();

expect(component.data).toEqual(state.entriesForReport);
});

it('after the component is initialized it should initialize the table', () => {
spyOn(component.dtTrigger, 'next');

component.ngAfterViewInit();

expect(component.dtTrigger.next).toHaveBeenCalled();
});

afterEach(() => {
fixture.destroy();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {select, Store} from '@ngrx/store';
import {EntryState} from '../../../time-clock/store/entry.reducer';
import {entriesForReport} from '../../../time-clock/store/entry.selectors';
import {Subject} from 'rxjs';
import {DataTableDirective} from 'angular-datatables';

@Component({
selector: 'app-time-entries-table',
templateUrl: './time-entries-table.component.html',
styleUrls: ['./time-entries-table.component.scss']
})
export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit {

data = [];
const;
dtOptions: any = {
scrollY: '600px',
paging: false,
dom: 'Bfrtip',
buttons: [
'colvis',
'print',
'excel'
]
};

dtTrigger: Subject<any> = new Subject();
@ViewChild(DataTableDirective, {static: false})
dtElement: DataTableDirective;

constructor(private store: Store<EntryState>) {
}

ngOnInit(): void {
const dataForReport$ = this.store.pipe(select(entriesForReport));
dataForReport$.subscribe((response) => {
this.data = response;
this.rerenderDataTable();
});
}

ngAfterViewInit(): void {
this.rerenderDataTable();
}

ngOnDestroy(): void {
this.dtTrigger.unsubscribe();
}

private rerenderDataTable(): void {
if (this.dtElement && this.dtElement.dtInstance) {
this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => {
dtInstance.destroy();
this.dtTrigger.next();
});
} else {
this.dtTrigger.next();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div class="container-flex">
<form [formGroup]="reportForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col">
<div class="input-group">
<app-input-label text="Start Date"></app-input-label>
<app-input-date formControlName="startDate" required="true"></app-input-date>
</div>
</div>
<div class="col">
<div class="input-group">
<app-input-label text="End Date"></app-input-label>
<app-input-date formControlName="endDate" required="true"></app-input-date>
</div>
</div>
</div>
<div class="row">
<div class="col">
<button type="submit" class="btn btn-primary float-right"
[disabled]="!(reportForm.touched && reportForm.valid)">Search
</button>
</div>
</div>
</form>
</div>

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Component} from '@angular/core';
import {FormControl, FormGroup} from '@angular/forms';
import * as entryActions from '../../../time-clock/store/entry.actions';
import {Store} from '@ngrx/store';
import {EntryState} from '../../../time-clock/store/entry.reducer';

@Component({
selector: 'app-time-range-form',
templateUrl: './time-range-form.component.html',
})
export class TimeRangeFormComponent {
public reportForm: FormGroup;
private startDate = new FormControl('');
private endDate = new FormControl('');

constructor(private store: Store<EntryState>) {
this.reportForm = new FormGroup({
startDate: this.startDate,
endDate: this.endDate
});
}

onSubmit() {
this.store.dispatch(new entryActions.LoadEntriesByTimeRange({
start_date: this.startDate.value,
end_date: this.endDate.value
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MockStore, provideMockStore} from '@ngrx/store/testing';
import {TimeRangeFormComponent} from './time-range-form.component';
import {EntryState} from '../../../time-clock/store/entry.reducer';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {InputDateComponent} from '../../../shared/components/input-date/input-date.component';
import * as entryActions from '../../../time-clock/store/entry.actions';

describe('Reports Page', () => {
describe('TimeRangeFormComponent', () => {
let component: TimeRangeFormComponent;
let fixture: ComponentFixture<TimeRangeFormComponent>;
let store: MockStore<EntryState>;
const timeEntry = {
id: '123',
start_date: new Date(),
end_date: new Date(),
activity_id: '123',
technologies: ['react', 'redux'],
comments: 'any comment',
uri: 'custom uri',
project_id: '123'
};

const state = {
active: timeEntry,
entryList: [timeEntry],
isLoading: false,
message: '',
createError: false,
updateError: false,
timeEntriesSummary: null,
entriesForReport: [timeEntry],
};

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [TimeRangeFormComponent, InputDateComponent],
providers: [provideMockStore({initialState: state})],
}).compileComponents();
store = TestBed.inject(MockStore);

}));

beforeEach(() => {
fixture = TestBed.createComponent(TimeRangeFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('component should be created', () => {
expect(component).toBeTruthy();
});

it('when submitting form a new LoadEntriesByTimeRange action is triggered', () => {

const startDateValue = new Date();
const endDateValue = new Date();
endDateValue.setMonth(1);
spyOn(store, 'dispatch');
component.reportForm.controls.startDate.setValue(startDateValue);
component.reportForm.controls.endDate.setValue(endDateValue);

component.onSubmit();

expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({
start_date: startDateValue,
end_date: endDateValue,
}));
});

afterEach(() => {
fixture.destroy();
});
});
});
5 changes: 3 additions & 2 deletions src/app/modules/reports/pages/reports.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<div class="text-center mt-5">
<p>reports works!</p>
<div class="w-100">
<app-time-range-form></app-time-range-form>
<app-time-entries-table></app-time-entries-table>
</div>
12 changes: 9 additions & 3 deletions src/app/modules/reports/pages/reports.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ describe('ReportsComponent', () => {
expect(component).toBeTruthy();
});

it('should have p tag as "reports works!"', async(() => {
it('should have form and datatable components', async(() => {
// tslint:disable-next-line: no-shadowed-variable
const { fixture } = setup();

fixture.detectChanges();

const compile = fixture.debugElement.nativeElement;
const ptag = compile.querySelector('p');
expect(ptag.textContent).toBe('reports works!');
const div = compile.querySelector('div');
const reportForm = compile.querySelector('app-time-range-form');
const reportDataTable = compile.querySelector('app-time-entries-table');
expect(div).toBeTruthy();
expect(reportForm).toBeTruthy();
expect(reportDataTable).toBeTruthy();
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<input
[value]="value"
[disabled]="isDisabled"
(input)="onInput($event.target.value)"
type="date"
class="form-control"
aria-label="Small"
aria-describedby="inputGroup-sizing-sm"
required="required"
/>
Loading