Skip to content

Commit 6cd51af

Browse files
scastillo-jpAngeluz-07
authored andcommitted
feat: #571 technology report
1 parent 0601f5b commit 6cd51af

15 files changed

+454
-1
lines changed

src/app/app-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { HomeComponent } from './modules/home/home.component';
1111
import { LoginComponent } from './modules/login/login.component';
1212
import { CustomerComponent } from './modules/customer-management/pages/customer.component';
1313
import { UsersComponent } from './modules/users/pages/users.component';
14+
import { TechnologyReportComponent } from './modules/technology-report/pages/technology-report.component';
1415

1516
const routes: Routes = [
1617
{
@@ -24,6 +25,7 @@ const routes: Routes = [
2425
{ path: 'activities-management', component: ActivitiesManagementComponent },
2526
{ path: 'customers-management', canActivate: [AdminGuard], component: CustomerComponent },
2627
{ path: 'users', canActivate: [AdminGuard], component: UsersComponent },
28+
{ path: 'technology-report', canActivate: [AdminGuard], component: TechnologyReportComponent},
2729
{ path: '', pathMatch: 'full', redirectTo: 'time-clock' },
2830
],
2931
},

src/app/app.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ import { LoadingBarComponent } from './modules/shared/components/loading-bar/loa
7373
import { UsersComponent } from './modules/users/pages/users.component';
7474
import { UsersListComponent } from './modules/users/components/users-list/users-list.component';
7575
import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker';
76+
// tslint:disable-next-line: max-line-length
77+
import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component';
78+
import { TechnologyReportComponent } from './modules/technology-report/pages/technology-report.component';
7679

7780
const maskConfig: Partial<IConfig> = {
7881
validation: false,
@@ -122,7 +125,9 @@ const maskConfig: Partial<IConfig> = {
122125
DialogComponent,
123126
LoadingBarComponent,
124127
UsersComponent,
125-
UsersListComponent
128+
UsersListComponent,
129+
TechnologyReportComponent,
130+
TechnologyReportTableComponent
126131
],
127132
imports: [
128133
NgxMaskModule.forRoot(maskConfig),

src/app/modules/shared/components/sidebar/sidebar.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class SidebarComponent implements OnInit {
4646
{route: '/activities-management', icon: 'fas fa-file-alt', text: 'Activities', active: false},
4747
{route: '/customers-management', icon: 'fas fa-user', text: 'Customers', active: false},
4848
{route: '/users', icon: 'fas fa-user', text: 'Users', active: false},
49+
{route: '/technology-report', icon: 'fas fa-user', text: 'Technology Report', active: false},
4950
];
5051
} else {
5152
this.itemsSidebar = [

src/app/modules/technology-report/components/.gitkeep

Whitespace-only changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<div class="row mt-5">
2+
<div class="col">
3+
<table
4+
class="table table-sm table-striped"
5+
datatable
6+
[dtTrigger]="dtTrigger"
7+
[dtOptions]="dtOptions"
8+
*ngIf="(reportDataSource$ | async) as dataSource"
9+
>
10+
<thead class="thead-blue">
11+
<tr class="d-flex">
12+
<th class="col md-col">User email</th>
13+
<th class="col lg-col">Technology</th>
14+
<th class="col x-sm-col" title="Duration (hours)">Time spend</th>
15+
</tr>
16+
</thead>
17+
<app-loading-bar
18+
*ngIf="dataSource.isLoading"
19+
></app-loading-bar>
20+
<tbody *ngIf="!dataSource.isLoading">
21+
<tr
22+
class="d-flex"
23+
*ngFor="let entry of dataSource.data"
24+
>
25+
<td class="col md-col">{{ entry.owner_email }}</td>
26+
<td class="col lg-col">{{ entry.technologies }}</td>
27+
<td class="col sm-col">
28+
{{ entry.start_date | date: 'MM/dd/yyyy' }}
29+
</td>
30+
</tr>
31+
</tbody>
32+
</table>
33+
</div>
34+
</div>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.col{
2+
white-space: nowrap;
3+
overflow: hidden;
4+
text-overflow: ellipsis;
5+
font-size: small;
6+
}
7+
.x-sm-col{
8+
width: 5em;
9+
max-width: 7em;
10+
}
11+
12+
.sm-col{
13+
width: 6em;
14+
max-width: 8em;
15+
}
16+
17+
.md-col{
18+
width: 9em;
19+
}
20+
21+
.lg-col{
22+
width: 12em;
23+
overflow: hidden;
24+
white-space: normal;
25+
}
26+
.hidden-col{
27+
display: none;
28+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
// import { MockStore, provideMockStore } from '@ngrx/store/testing';
3+
// import { Entry } from 'src/app/modules/shared/models';
4+
// import { SubstractDatePipe } from 'src/app/modules/shared/pipes/substract-date/substract-date.pipe';
5+
// import { getReportDataSource } from 'src/app/modules/time-clock/store/entry.selectors';
6+
// import { EntryState } from '../../../time-clock/store/entry.reducer';
7+
// import { TimeEntriesTableComponent } from './time-entries-table.component';
8+
9+
// describe('Reports Page', () => {
10+
// describe('TimeEntriesTableComponent', () => {
11+
// let component: TimeEntriesTableComponent;
12+
// let fixture: ComponentFixture<TimeEntriesTableComponent>;
13+
// let store: MockStore<EntryState>;
14+
// let getReportDataSourceSelectorMock;
15+
// const timeEntry: Entry = {
16+
// id: '123',
17+
// start_date: new Date(),
18+
// end_date: new Date(),
19+
// activity_id: '123',
20+
// technologies: ['react', 'redux'],
21+
// description: 'any comment',
22+
// uri: 'custom uri',
23+
// project_id: '123',
24+
// project_name: 'Time-Tracker'
25+
// };
26+
27+
// const state: EntryState = {
28+
// active: timeEntry,
29+
// isLoading: false,
30+
// message: '',
31+
// createError: false,
32+
// updateError: false,
33+
// timeEntriesSummary: null,
34+
// timeEntriesDataSource: {
35+
// data: [timeEntry],
36+
// isLoading: false
37+
// },
38+
// reportDataSource: {
39+
// data: [timeEntry],
40+
// isLoading: false
41+
// }
42+
// };
43+
44+
// beforeEach(async(() => {
45+
// TestBed.configureTestingModule({
46+
// imports: [],
47+
// declarations: [TimeEntriesTableComponent, SubstractDatePipe],
48+
// providers: [provideMockStore({ initialState: state })],
49+
// }).compileComponents();
50+
// store = TestBed.inject(MockStore);
51+
52+
// }));
53+
54+
// beforeEach(async(() => {
55+
// fixture = TestBed.createComponent(TimeEntriesTableComponent);
56+
// component = fixture.componentInstance;
57+
// store.setState(state);
58+
// getReportDataSourceSelectorMock = store.overrideSelector(getReportDataSource, state.reportDataSource);
59+
// fixture.detectChanges();
60+
// }));
61+
62+
// it('component should be created', async () => {
63+
// expect(component).toBeTruthy();
64+
// });
65+
66+
// it('on success load time entries, the report should be populated', () => {
67+
// component.reportDataSource$.subscribe(ds => {
68+
// expect(ds.data).toEqual(state.reportDataSource.data);
69+
// });
70+
// });
71+
72+
// it('after the component is initialized it should initialize the table', () => {
73+
// spyOn(component.dtTrigger, 'next');
74+
// component.ngAfterViewInit();
75+
76+
// expect(component.dtTrigger.next).toHaveBeenCalled();
77+
// });
78+
79+
// afterEach(() => {
80+
// fixture.destroy();
81+
// });
82+
// });
83+
// });
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { formatDate } from '@angular/common';
2+
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
3+
import { select, Store } from '@ngrx/store';
4+
import { DataTableDirective } from 'angular-datatables';
5+
import * as moment from 'moment';
6+
import { Observable, Subject } from 'rxjs';
7+
import { Entry } from 'src/app/modules/shared/models';
8+
import { DataSource } from 'src/app/modules/shared/models/data-source.model';
9+
10+
import { EntryState } from '../../../time-clock/store/entry.reducer';
11+
import { getReportDataSource } from '../../../time-clock/store/entry.selectors';
12+
13+
@Component({
14+
selector: 'app-technology-report-table',
15+
templateUrl: './technology-report-table.component.html',
16+
styleUrls: ['./technology-report-table.component.scss']
17+
})
18+
export class TechnologyReportTableComponent implements OnInit, OnDestroy, AfterViewInit {
19+
dtOptions: any = {
20+
scrollY: '600px',
21+
paging: false,
22+
dom: 'Bfrtip',
23+
buttons: [
24+
{
25+
extend: 'colvis',
26+
columns: ':not(.hidden-col)',
27+
28+
},
29+
'print',
30+
{
31+
extend: 'excel',
32+
exportOptions: {
33+
format: {
34+
body: (data, row, column, node) => {
35+
return column === 3 ?
36+
moment.duration(data).asHours().toFixed(4).slice(0, -1) :
37+
data;
38+
}
39+
}
40+
},
41+
text: 'Excel',
42+
filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`
43+
},
44+
{
45+
extend: 'csv',
46+
exportOptions: {
47+
format: {
48+
body: (data, row, column, node) => {
49+
return column === 3 ?
50+
moment.duration(data).asHours().toFixed(4).slice(0, -1) :
51+
data;
52+
}
53+
}
54+
},
55+
text: 'CSV',
56+
filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`
57+
}
58+
]
59+
};
60+
dtTrigger: Subject<any> = new Subject();
61+
@ViewChild(DataTableDirective, { static: false })
62+
dtElement: DataTableDirective;
63+
isLoading$: Observable<boolean>;
64+
reportDataSource$: Observable<DataSource<Entry>>;
65+
66+
constructor(private store: Store<EntryState>) {
67+
this.reportDataSource$ = this.store.pipe(select(getReportDataSource));
68+
}
69+
70+
ngOnInit(): void {
71+
this.reportDataSource$.subscribe((ds) => {
72+
this.rerenderDataTable();
73+
});
74+
}
75+
76+
ngAfterViewInit(): void {
77+
this.rerenderDataTable();
78+
}
79+
80+
ngOnDestroy(): void {
81+
this.dtTrigger.unsubscribe();
82+
}
83+
84+
private rerenderDataTable(): void {
85+
if (this.dtElement && this.dtElement.dtInstance) {
86+
this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => {
87+
dtInstance.destroy();
88+
this.dtTrigger.next();
89+
});
90+
} else {
91+
this.dtTrigger.next();
92+
}
93+
}
94+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<form [formGroup]="reportForm" (ngSubmit)="onSubmit()">
2+
3+
<div class="form-group row">
4+
<label class="col-12 col-md-2 col-form-label my-1">Start date:</label>
5+
<div class="col-12 col-sm-6 col-md-3 my-1">
6+
<app-input-date
7+
formControlName="startDate"
8+
id="startDate"
9+
required="true"
10+
></app-input-date>
11+
</div>
12+
13+
<label class="col-12 col-md-2 col-form-label my-1">End date:</label>
14+
<div class="col-12 col-sm-6 col-md-3 my-1">
15+
<app-input-date
16+
formControlName="endDate"
17+
id="endDate"
18+
required="true"
19+
></app-input-date>
20+
</div>
21+
22+
<div class="col-12 col-md-2 my-1">
23+
<button type="submit" class="btn btn-primary">Search</button>
24+
</div>
25+
</div>
26+
</form>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ToastrService } from 'ngx-toastr';
2+
import { formatDate } from '@angular/common';
3+
import { Component, OnInit } from '@angular/core';
4+
import { FormControl, FormGroup } from '@angular/forms';
5+
import * as entryActions from '../../../time-clock/store/entry.actions';
6+
import {Store} from '@ngrx/store';
7+
import {EntryState} from '../../../time-clock/store/entry.reducer';
8+
import * as moment from 'moment';
9+
10+
@Component({
11+
selector: 'app-time-range-form',
12+
templateUrl: './time-range-form.component.html',
13+
})
14+
export class TimeRangeFormComponent implements OnInit {
15+
public reportForm: FormGroup;
16+
private startDate = new FormControl('');
17+
private endDate = new FormControl('');
18+
19+
constructor(private store: Store<EntryState>, private toastrService: ToastrService) {
20+
this.reportForm = new FormGroup({
21+
startDate: this.startDate,
22+
endDate: this.endDate
23+
});
24+
}
25+
ngOnInit(): void {
26+
this.setInitialDataOnScreen();
27+
}
28+
29+
setInitialDataOnScreen() {
30+
this.reportForm.setValue({
31+
startDate: formatDate(moment().startOf('week').toString(), 'yyyy-MM-dd', 'en'),
32+
endDate: formatDate(moment().endOf('week').toString(), 'yyyy-MM-dd', 'en')
33+
});
34+
this.onSubmit();
35+
}
36+
37+
onSubmit() {
38+
const endDate = moment(this.endDate.value).endOf('day');
39+
const startDate = moment(this.startDate.value).startOf('day');
40+
if (endDate.isBefore(startDate)) {
41+
this.toastrService.error('The end date should be after the start date');
42+
} else {
43+
this.store.dispatch(new entryActions.LoadEntriesByTimeRange({
44+
start_date: moment(this.startDate.value).startOf('day'),
45+
end_date: moment(this.endDate.value).endOf('day'),
46+
}));
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)