diff --git a/package-lock.json b/package-lock.json index ea9d58f98..30a17ef4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.9.1", + "version": "1.9.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f7a5dc4aa..0d4e51438 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.9.1", + "version": "1.9.5", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts index 43c903778..9a6c7dd28 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts @@ -21,7 +21,16 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn buttons: [ 'colvis', 'print', - 'excel' + { + extend: 'excel', + text: 'Excel', + filename: `time-entries-${ new Date() }` + }, + { + extend: 'csv', + text: 'CSV', + filename: `time-entries-${ new Date() }` + } ] }; diff --git a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts index a9ec16b14..b2db647e2 100644 --- a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts +++ b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts @@ -15,6 +15,15 @@ describe('SubstractDatePipe', () => { expect(diff).toBe('02:20'); }); + it('returns the date diff using hh:mm:ss for a diff < 1 min when displaySeconds is true', () => { + const fromDate = new Date('2011-04-11T10:22:40Z'); + const substractDate = new Date('2011-04-11T10:20:30Z'); + + const diff = new SubstractDatePipe().transform(fromDate, substractDate, true); + + expect(diff).toBe('00:02:10'); + }); + it('returns the date diff including seconds if difference is less than a minute', () => { const fromDate = new Date('2011-04-11T10:20:40Z'); const substractDate = new Date('2011-04-11T10:20:30Z'); diff --git a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts index 8791d3741..6ca03db08 100644 --- a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts +++ b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts @@ -6,7 +6,7 @@ import * as moment from 'moment'; }) export class SubstractDatePipe implements PipeTransform { - transform(fromDate: Date, substractDate: Date): string { + transform(fromDate: Date, substractDate: Date, displaySeconds: boolean = false): string { if (fromDate === null || substractDate === null ) { return '--:--'; @@ -16,7 +16,7 @@ export class SubstractDatePipe implements PipeTransform { let endDate = moment(fromDate, 'YYYY-MM-DD HH:mm:ss'); let duration: moment.Duration = moment.duration(endDate.diff(startDate)); - if (duration.asSeconds() > 60) { + if (duration.asSeconds() > 60 && !displaySeconds) { endDate = endDate.add(1, 'minute').startOf('minute'); duration = moment.duration(endDate.diff(startDate)); return `${ this.formatTime(duration.hours())}:${this.formatTime(duration.minutes()) }`; diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html index cfa427c84..272e9d57e 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html @@ -3,24 +3,39 @@
Activity
- - +
Ticket URI
- +
- + -
Description @@ -31,8 +46,8 @@ formControlName="description" class="form-control" id="NotesTextarea" - rows="2"> + rows="2" + >
- diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts index 0b1e2f556..b119c4a59 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts @@ -1,12 +1,12 @@ -import {getActiveTimeEntry} from './../../store/entry.selectors'; -import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup} from '@angular/forms'; -import {select, Store} from '@ngrx/store'; +import { getActiveTimeEntry } from './../../store/entry.selectors'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { select, Store } from '@ngrx/store'; -import {Activity, NewEntry} from '../../../shared/models'; -import {ProjectState} from '../../../customer-management/components/projects/components/store/project.reducer'; -import {TechnologyState} from '../../../shared/store/technology.reducers'; -import {ActivityState, allActivities, LoadActivities} from '../../../activities-management/store'; +import { Activity, NewEntry } from '../../../shared/models'; +import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; +import { TechnologyState } from '../../../shared/store/technology.reducers'; +import { ActivityState, allActivities, LoadActivities } from '../../../activities-management/store'; import * as entryActions from '../../store/entry.actions'; @@ -18,7 +18,6 @@ type Merged = TechnologyState & ProjectState & ActivityState; styleUrls: ['./entry-fields.component.scss'], }) export class EntryFieldsComponent implements OnInit { - entryForm: FormGroup; selectedTechnologies: string[] = []; activities: Activity[] = []; @@ -29,7 +28,7 @@ export class EntryFieldsComponent implements OnInit { this.entryForm = this.formBuilder.group({ description: '', uri: '', - activity_id: '-1' + activity_id: '-1', }); } @@ -58,6 +57,10 @@ export class EntryFieldsComponent implements OnInit { }); } + get activity_id() { + return this.entryForm.get('activity_id'); + } + setDataToUpdate(entryData: NewEntry) { if (entryData) { this.entryForm.patchValue({ @@ -73,17 +76,19 @@ export class EntryFieldsComponent implements OnInit { } } + entryFormIsValidate() { + return this.entryForm.valid; + } + onSubmit() { - this.store.dispatch(new entryActions.UpdateEntryRunning({...this.newData, ...this.entryForm.value})); + this.store.dispatch(new entryActions.UpdateEntryRunning({ ...this.newData, ...this.entryForm.value })); } onTechnologyAdded($event: string[]) { - this.store.dispatch(new entryActions.UpdateEntryRunning({...this.newData, technologies: $event}) - ); + this.store.dispatch(new entryActions.UpdateEntryRunning({ ...this.newData, technologies: $event })); } onTechnologyRemoved($event: string[]) { - this.store.dispatch(new entryActions.UpdateEntryRunning({...this.newData, technologies: $event})); + this.store.dispatch(new entryActions.UpdateEntryRunning({ ...this.newData, technologies: $event })); } - } diff --git a/src/app/modules/time-clock/components/time-entries-summary/time-entries-summary.component.ts b/src/app/modules/time-clock/components/time-entries-summary/time-entries-summary.component.ts index d8e56404e..9150d0128 100644 --- a/src/app/modules/time-clock/components/time-entries-summary/time-entries-summary.component.ts +++ b/src/app/modules/time-clock/components/time-entries-summary/time-entries-summary.component.ts @@ -91,7 +91,7 @@ export class TimeEntriesSummaryComponent implements OnInit, OnDestroy { this.timeInterval = interval(1000).pipe( takeUntil(this.destroyed$) ).subscribe(() => { - this.currentWorkingTime = new SubstractDatePipe().transform(new Date(), new Date(entry.start_date)); + this.currentWorkingTime = new SubstractDatePipe().transform(new Date(), new Date(entry.start_date), true); }); } } diff --git a/src/app/modules/time-clock/pages/time-clock.component.spec.ts b/src/app/modules/time-clock/pages/time-clock.component.spec.ts index b77631a4c..218d1eea1 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.spec.ts +++ b/src/app/modules/time-clock/pages/time-clock.component.spec.ts @@ -1,5 +1,5 @@ import { FormBuilder } from '@angular/forms'; -import { StopTimeEntryRunning } from './../store/entry.actions'; +import { StopTimeEntryRunning, EntryActionTypes, LoadEntriesSummary } from './../store/entry.actions'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; @@ -8,12 +8,22 @@ import { ProjectState } from '../../customer-management/components/projects/comp import { ProjectListHoverComponent } from '../components'; import { FilterProjectPipe } from '../../shared/pipes'; import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; +import { ActionsSubject } from '@ngrx/store'; +import { EntryFieldsComponent } from '../components/entry-fields/entry-fields.component'; +import { ToastrService } from 'ngx-toastr'; describe('TimeClockComponent', () => { let component: TimeClockComponent; let fixture: ComponentFixture; let store: MockStore; let azureAdB2CService: AzureAdB2CService; + const actionSub: ActionsSubject = new ActionsSubject(); + + let injectedToastrService; + const toastrService = { + error: () => {}, + }; + const state = { projects: { projects: [{ id: 'id', name: 'name', project_type_id: '' }], @@ -41,11 +51,13 @@ describe('TimeClockComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - declarations: [TimeClockComponent, ProjectListHoverComponent, FilterProjectPipe], + declarations: [TimeClockComponent, ProjectListHoverComponent, FilterProjectPipe, EntryFieldsComponent], providers: [ FormBuilder, AzureAdB2CService, provideMockStore({ initialState: state }), + { provide: ActionsSubject, useValue: actionSub }, + { provide: ToastrService, useValue: toastrService }, ], }).compileComponents(); store = TestBed.inject(MockStore); @@ -56,12 +68,41 @@ describe('TimeClockComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); azureAdB2CService = TestBed.inject(AzureAdB2CService); + injectedToastrService = TestBed.inject(ToastrService); }); it('should be created', () => { expect(component).toBeTruthy(); }); + it('on STOP_TIME_ENTRY_RUNNING_SUCCESS summaries are reloaded', () => { + const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject; + const action = { + type: EntryActionTypes.STOP_TIME_ENTRY_RUNNING_SUCCESS + }; + spyOn(store, 'dispatch'); + + actionSubject.next(action); + + expect(store.dispatch).toHaveBeenCalledWith(new LoadEntriesSummary()); + }); + + it('register reloadSummaries on ngOnInit', () => { + spyOn(component, 'reloadSummariesOnClockOut'); + + component.ngOnInit(); + + expect(component.reloadSummariesOnClockOut).toHaveBeenCalled(); + }); + + it('unsubscribe clockOutSubscription onDestroy', () => { + spyOn(component.clockOutSubscription, 'unsubscribe'); + + component.ngOnDestroy(); + + expect(component.clockOutSubscription.unsubscribe).toHaveBeenCalled(); + }); + it('onInit checks if isLogin and gets the userName', () => { spyOn(azureAdB2CService, 'isLogin').and.returnValue(true); spyOn(azureAdB2CService, 'getName').and.returnValue('Name'); @@ -78,11 +119,27 @@ describe('TimeClockComponent', () => { expect(azureAdB2CService.getName).toHaveBeenCalledTimes(0); }); - it('clockOut dispatch a StopTimeEntryRunning action', () => { + it('stopEntry dispatch a StopTimeEntryRunning action', () => { spyOn(store, 'dispatch'); + component.stopEntry(); + expect(store.dispatch).toHaveBeenCalledWith(new StopTimeEntryRunning('id')); + }); + + it('clockOut dispatch a StopTimeEntryRunning action', () => { + spyOn(store, 'dispatch'); + spyOn(component.entryFieldsComponent, 'entryFormIsValidate').and.returnValue(true); component.clockOut(); expect(store.dispatch).toHaveBeenCalledWith(new StopTimeEntryRunning('id')); }); + + it('clockOut set error Activity is required', () => { + spyOn(store, 'dispatch'); + spyOn(injectedToastrService, 'error'); + spyOn(component.entryFieldsComponent, 'entryFormIsValidate').and.returnValue(false); + component.clockOut(); + + expect(injectedToastrService.error).toHaveBeenCalled(); + }); }); diff --git a/src/app/modules/time-clock/pages/time-clock.component.ts b/src/app/modules/time-clock/pages/time-clock.component.ts index bf98bfee4..8d1bf8f50 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.ts +++ b/src/app/modules/time-clock/pages/time-clock.component.ts @@ -1,23 +1,36 @@ +import { filter } from 'rxjs/operators'; import { getActiveTimeEntry } from './../store/entry.selectors'; -import { StopTimeEntryRunning } from './../store/entry.actions'; +import { StopTimeEntryRunning, EntryActionTypes, LoadEntriesSummary } from './../store/entry.actions'; import { Entry } from './../../shared/models/entry.model'; -import { Store, select } from '@ngrx/store'; -import { Component, OnInit } from '@angular/core'; +import { Store, select, ActionsSubject } from '@ngrx/store'; +import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; import { Subscription } from 'rxjs'; - +import { EntryFieldsComponent } from '../components/entry-fields/entry-fields.component'; +import { ToastrService } from 'ngx-toastr'; @Component({ selector: 'app-time-clock', templateUrl: './time-clock.component.html', styleUrls: ['./time-clock.component.scss'], }) -export class TimeClockComponent implements OnInit { +export class TimeClockComponent implements OnInit, OnDestroy { + @ViewChild(EntryFieldsComponent) + entryFieldsComponent: EntryFieldsComponent; username: string; areFieldsVisible = false; activeTimeEntry: Entry; - actionsSubscription: Subscription; + clockOutSubscription: Subscription; + + + constructor( + private azureAdB2CService: AzureAdB2CService, + private store: Store, + private toastrService: ToastrService, + private actionsSubject$: ActionsSubject + ) {} - constructor(private azureAdB2CService: AzureAdB2CService, private store: Store) { + ngOnDestroy(): void { + this.clockOutSubscription.unsubscribe(); } ngOnInit() { @@ -30,10 +43,33 @@ export class TimeClockComponent implements OnInit { this.areFieldsVisible = false; } }); + + this.reloadSummariesOnClockOut(); + } - clockOut() { + reloadSummariesOnClockOut() { + this.clockOutSubscription = this.actionsSubject$.pipe( + filter((action) => ( + action.type === EntryActionTypes.STOP_TIME_ENTRY_RUNNING_SUCCESS + ) + ) + ).subscribe( (action) => { + this.store.dispatch(new LoadEntriesSummary()); + }); + } + + stopEntry() { this.store.dispatch(new StopTimeEntryRunning(this.activeTimeEntry.id)); this.areFieldsVisible = false; } + + clockOut() { + if (this.entryFieldsComponent.entryFormIsValidate()) { + this.stopEntry(); + } else { + this.entryFieldsComponent.entryForm.get('activity_id').markAsTouched(); + this.toastrService.error('Activity is required'); + } + } } diff --git a/src/app/modules/time-entries/pages/time-entries.component.ts b/src/app/modules/time-entries/pages/time-entries.component.ts index 22c717f67..620bdea2b 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -62,17 +62,31 @@ export class TimeEntriesComponent implements OnInit { } doSave(event: SaveEntryEvent) { - event.entry.start_date = new Date(event.entry.start_date).toISOString(); - if (event.entry.end_date !== null && event.entry.end_date !== undefined) { - event.entry.end_date = new Date(event.entry.end_date).toISOString(); - } + const endDateIsDefined = event.entry.end_date !== null && event.entry.end_date !== undefined; if (this.entryId) { + const startDateChanged = this.entry.start_date !== event.entry.start_date; + if (startDateChanged) { + event.entry.start_date = this.adjustDateSecs(event.entry.start_date, 1); + } + + if (endDateIsDefined) { + const endDateChanged = this.entry.end_date !== event.entry.end_date; + if (endDateChanged) { + event.entry.end_date = this.adjustDateSecs(event.entry.end_date, 0); + } + } + event.entry.id = this.entryId; this.store.dispatch(new entryActions.UpdateEntry(event.entry)); if (event.shouldRestartEntry) { this.store.dispatch(new entryActions.RestartEntry(event.entry)); } } else { + event.entry.start_date = this.adjustDateSecs(event.entry.start_date, 1); + if (endDateIsDefined) { + event.entry.end_date = this.adjustDateSecs(event.entry.end_date, 0); + } + this.store.dispatch(new entryActions.CreateEntry(event.entry)); } } @@ -97,7 +111,13 @@ export class TimeEntriesComponent implements OnInit { openModal(item: any) { this.idToDelete = item.id; - this.message = `Are you sure you want to delete ${item.activity_name}`; + this.message = `Are you sure you want to delete ${item.activity_name}?`; this.showModal = true; } + + adjustDateSecs(date: string, sec: number): string { + const newDate = new Date(date); + newDate.setSeconds(sec, 0); + return newDate.toISOString(); + } }