diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.html b/src/app/modules/shared/components/details-fields/details-fields.component.html index ba4039782..c57e4d882 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.html +++ b/src/app/modules/shared/components/details-fields/details-fields.component.html @@ -1,5 +1,9 @@
+ +
+
Project
@@ -11,13 +15,14 @@ formControlName="project_id" > - +
- Activity + Activity
- Date + Date
- Time in + Time in
-
- Time out +
+ Time out
- +
diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts b/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts index b19cdd322..b307d5632 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts +++ b/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts @@ -2,7 +2,7 @@ import { TechnologiesComponent } from './../technologies/technologies.component' import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { formatDate } from '@angular/common'; +import { DatePipe, formatDate } from '@angular/common'; import { TechnologyState } from '../../store/technology.reducers'; import { allTechnologies } from '../../store/technology.selectors'; @@ -22,21 +22,23 @@ describe('DetailsFieldsComponent', () => { let mockProjectsSelector; let mockEntriesUpdateErrorSelector; let mockEntriesCreateErrorSelector; + let entryToEdit; + let formValues; const state = { projects: { - projects: [{ id: 'id', name: 'name', project_type_id: '' }], - customerProjects: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }], + projects: [{id: 'id', name: 'name', project_type_id: ''}], + customerProjects: [{id: 'id', name: 'name', description: 'description', project_type_id: '123'}], isLoading: false, message: '', projectToEdit: undefined, }, technologies: { - technologyList: { items: [{ name: 'java' }] }, + technologyList: {items: [{name: 'java'}]}, isLoading: false, }, activities: { - data: [{ id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc' }], + data: [{id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc'}], isLoading: false, message: 'Data fetch successfully!', activityIdToEdit: '', @@ -61,7 +63,7 @@ describe('DetailsFieldsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [DetailsFieldsComponent, TechnologiesComponent], - providers: [provideMockStore({ initialState: state })], + providers: [provideMockStore({initialState: state})], imports: [FormsModule, ReactiveFormsModule], }).compileComponents(); store = TestBed.inject(MockStore); @@ -74,28 +76,17 @@ describe('DetailsFieldsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DetailsFieldsComponent); component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should emit ngOnChange without data', () => { - component.entryToEdit = null; - component.ngOnChanges(); - expect(component.entryForm.value).toEqual(initialData); - }); - - it('should emit ngOnChange with new data', () => { - const entryToEdit = { + entryToEdit = { project_id: '', activity_id: '', uri: 'ticketUri', start_date: null, end_date: null, description: '', + technologies: [], + id: 'xyz' }; - const formValue = { + formValues = { project_id: '', activity_id: '', uri: 'ticketUri', @@ -105,9 +96,24 @@ describe('DetailsFieldsComponent', () => { description: '', technology: '', }; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit ngOnChange without data', () => { + component.entryToEdit = null; + component.ngOnChanges(); + expect(component.entryForm.value).toEqual(initialData); + }); + + it('should emit ngOnChange with new data', () => { component.entryToEdit = entryToEdit; + component.ngOnChanges(); - expect(component.entryForm.value).toEqual(formValue); + + expect(component.entryForm.value).toEqual(formValues); }); it('should emit ngOnChange with new data', () => { @@ -170,4 +176,79 @@ describe('DetailsFieldsComponent', () => { }; expect(component.saveEntry.emit).toHaveBeenCalledWith(data); }); + + it('when the current entry is not running, then the end hour input should be rendered', () => { + component.isEntryRunning = false; + fixture.detectChanges(); + + const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour'); + expect(endHourInput).toBeDefined(); + }); + + it('when the current entry is running, then the end hour input should not be rendered', () => { + component.isEntryRunning = true; + fixture.detectChanges(); + + const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour'); + expect(endHourInput).toBeNull(); + }); + + it('when creating a new entry, then the new entry should be marked as not running', () => { + component.entryToEdit = null; + + expect(component.isEntryRunning).toBeFalse(); + }); + + it('when editing entry that is currently running, then the entry should be marked as running', () => { + component.entryToEdit = {...entryToEdit, running: true}; + + fixture.componentInstance.ngOnChanges(); + + expect(component.isEntryRunning).toBeTrue(); + }); + + it('when editing entry that already finished, then the entry should not be marked as running', () => { + component.entryToEdit = {...entryToEdit, running: false}; + + fixture.componentInstance.ngOnChanges(); + + expect(component.isEntryRunning).toBeFalse(); + }); + + it('when editing entry that already finished, then the entry should not be marked as running', () => { + component.entryToEdit = {...entryToEdit, running: false}; + + fixture.componentInstance.ngOnChanges(); + + expect(component.isEntryRunning).toBeFalse(); + }); + + it('when submitting a entry that is currently running, the end date should not be sent ', () => { + component.isEntryRunning = true; + spyOn(component.saveEntry, 'emit'); + + component.entryForm.setValue({...formValues, entry_date: '2020-06-11'}); + component.onSubmit(); + const data = { + project_id: '', + activity_id: '', + technologies: [], + description: '', + start_date: '2020-06-11T00:00', + uri: 'ticketUri', + }; + expect(component.saveEntry.emit).toHaveBeenCalledWith(data); + }); + + it('when disabling current entry is running, then the end hour should be set to the current time', () => { + const datePipe: DatePipe = new DatePipe('en'); + const currentTime = datePipe.transform(new Date(), 'HH:mm'); + + const checkIsEntryRunning: Element = fixture.debugElement.nativeElement.querySelector('#isEntryRunning'); + checkIsEntryRunning.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + const endHourInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_hour'); + expect(endHourInput.value).toEqual(currentTime); + }); }); diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.ts b/src/app/modules/shared/components/details-fields/details-fields.component.ts index a141a9de0..623b0611c 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.ts +++ b/src/app/modules/shared/components/details-fields/details-fields.component.ts @@ -1,26 +1,18 @@ -import { - Component, - OnChanges, - OnInit, - Input, - Output, - EventEmitter, - ViewChild, - ElementRef, -} from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { Store, select } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { formatDate } from '@angular/common'; -import { Project, Activity } from '../../models'; +import { Activity, Entry, Project } from '../../models'; import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; import { TechnologyState } from '../../store/technology.reducers'; -import { LoadActivities, ActivityState, allActivities } from '../../../activities-management/store'; +import { ActivityState, allActivities, LoadActivities } from '../../../activities-management/store'; import { getProjects } from '../../../customer-management/components/projects/components/store/project.selectors'; import * as projectActions from '../../../customer-management/components/projects/components/store/project.actions'; import { EntryState } from '../../../time-clock/store/entry.reducer'; import * as entryActions from '../../../time-clock/store/entry.actions'; -import { getUpdateError, getCreateError } from 'src/app/modules/time-clock/store/entry.selectors'; +import { getCreateError, getUpdateError } from 'src/app/modules/time-clock/store/entry.selectors'; + type Merged = TechnologyState & ProjectState & ActivityState & EntryState; @Component({ @@ -29,7 +21,7 @@ type Merged = TechnologyState & ProjectState & ActivityState & EntryState; styleUrls: ['./details-fields.component.scss'], }) export class DetailsFieldsComponent implements OnChanges, OnInit { - @Input() entryToEdit; + @Input() entryToEdit: Entry; @Input() formType: string; @Output() saveEntry = new EventEmitter(); @ViewChild('closeModal') closeModal: ElementRef; @@ -38,8 +30,7 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { isLoading = false; listProjects: Project[] = []; activities: Activity[] = []; - keyword = 'name'; - showlist: boolean; + isEntryRunning = false; constructor(private formBuilder: FormBuilder, private store: Store) { this.entryForm = this.formBuilder.group({ @@ -84,6 +75,7 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { } ngOnChanges(): void { + this.isEntryRunning = this.entryToEdit ? this.entryToEdit.running : false; if (this.entryToEdit) { this.selectedTechnologies = this.entryToEdit.technologies; this.entryForm.setValue({ @@ -156,6 +148,16 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { end_date: `${entryDate}T${this.entryForm.value.end_hour.trim()}`, uri: this.entryForm.value.uri, }; + if (this.isEntryRunning) { + delete entry.end_date; + } this.saveEntry.emit(entry); } + + onIsRunningChange(event: any) { + this.isEntryRunning = event.currentTarget.checked; + if (!this.isEntryRunning) { + this.entryForm.patchValue({end_hour: formatDate(new Date(), 'HH:mm', 'en')}); + } + } } diff --git a/src/app/modules/shared/models/entry.model.ts b/src/app/modules/shared/models/entry.model.ts index ce821a200..31ea9c987 100644 --- a/src/app/modules/shared/models/entry.model.ts +++ b/src/app/modules/shared/models/entry.model.ts @@ -1,4 +1,5 @@ export interface Entry { + running?: boolean; id: string; start_date: Date; end_date: Date; @@ -8,6 +9,7 @@ export interface Entry { uri?: string; project_id?: string; owner_email?: string; + description?: string; } export interface NewEntry { diff --git a/src/app/modules/time-clock/pipes/time-details.pipe.spec.ts b/src/app/modules/time-clock/pipes/time-details.pipe.spec.ts index f31691631..6cfd93ef4 100644 --- a/src/app/modules/time-clock/pipes/time-details.pipe.spec.ts +++ b/src/app/modules/time-clock/pipes/time-details.pipe.spec.ts @@ -1,4 +1,3 @@ -import { TimeDetails } from './../models/time.entry.summary'; import { TimeDetailsPipe } from './time-details.pipe'; describe('TimeDetailsPipe', () => { diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts index 4423d3d2a..aae5ae452 100644 --- a/src/app/modules/time-clock/store/entry.effects.ts +++ b/src/app/modules/time-clock/store/entry.effects.ts @@ -1,8 +1,8 @@ -import { INFO_SAVED_SUCCESSFULLY, INFO_DELETE_SUCCESSFULLY } from './../../shared/messages'; +import { INFO_DELETE_SUCCESSFULLY, INFO_SAVED_SUCCESSFULLY } from './../../shared/messages'; import { Injectable } from '@angular/core'; -import { ofType, Actions, Effect } from '@ngrx/effects'; +import { Actions, Effect, ofType } from '@ngrx/effects'; import { Action } from '@ngrx/store'; -import { of, Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { ToastrService } from 'ngx-toastr'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { EntryService } from '../services/entry.service'; @@ -10,7 +10,8 @@ import * as actions from './entry.actions'; @Injectable() export class EntryEffects { - constructor(private actions$: Actions, private entryService: EntryService, private toastrService: ToastrService) { } + constructor(private actions$: Actions, private entryService: EntryService, private toastrService: ToastrService) { + } @Effect() loadEntriesSummary$: Observable = this.actions$.pipe( @@ -45,7 +46,7 @@ export class EntryEffects { } else { const endDate = new Date(activeEntry.start_date); endDate.setHours(23, 59, 59); - return new actions.UpdateActiveEntry({ id: activeEntry.id, end_date: endDate.toISOString() }); + return new actions.UpdateActiveEntry({id: activeEntry.id, end_date: endDate.toISOString()}); } } }), @@ -118,9 +119,7 @@ export class EntryEffects { mergeMap((entry) => this.entryService.updateActiveEntry(entry).pipe( map((entryResponse) => { - if (entryResponse.end_date && entry.start_date === null) { - this.toastrService.success(INFO_SAVED_SUCCESSFULLY); - } + this.toastrService.success(INFO_SAVED_SUCCESSFULLY); return new actions.UpdateActiveEntrySuccess(entryResponse); }), catchError((error) => { diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index 3513e46cd..62c09e3d8 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -249,4 +249,17 @@ describe('TimeEntriesComponent', () => { expect(component.doSave).toHaveBeenCalledWith(entryToSave); })); + + it('when saving time entries, the time entries should be queried', () => { + const currentMonth = new Date().getMonth() + 1; + const entryToSave = { + project_id: 'project-id' + }; + component.activeTimeEntry = null; + spyOn(store, 'dispatch'); + + component.saveEntry(entryToSave); + + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntries(currentMonth)); + }); }); 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 84012aa36..2628d24e8 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -65,6 +65,7 @@ export class TimeEntriesComponent implements OnInit { } else { this.store.dispatch(new entryActions.CreateEntry(entry)); } + this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1)); } removeEntry(entryId: string) {