diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts index b17db891e..3ea1d1ae4 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts @@ -1,4 +1,4 @@ -import { LoadActiveEntry, EntryActionTypes } from './../../store/entry.actions'; +import { LoadActiveEntry, EntryActionTypes, UpdateEntry } from './../../store/entry.actions'; import { ActivityManagementActionTypes } from './../../../activities-management/store/activity-management.actions'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {MockStore, provideMockStore} from '@ngrx/store/testing'; @@ -27,6 +27,11 @@ describe('EntryFieldsComponent', () => { error: (message?: string, title?: string, override?: Partial) => { }, warning: (message?: string, title?: string, override?: Partial) => { } }; + const lastDate = moment().format('YYYY-MM-DD'); + const startHourTest = moment().subtract(5, 'hours').format('HH:mm:ss'); + const endHourTest = moment().subtract(3, 'hours').format('HH:mm:ss'); + const lastStartHourEntryEntered = new Date(`${lastDate}T${startHourTest.trim()}`).toISOString(); + const lastEndHourEntryEntered = new Date(`${lastDate}T${endHourTest.trim()}`).toISOString(); const state = { projects: { @@ -57,6 +62,28 @@ describe('EntryFieldsComponent', () => { }, entryList: [], message: '', + timeEntriesDataSource: { data: [ + { + activity_id: 'xyz', + activity_name: 'abc', + id: 'id-15', + project_id: 'project-id-15', + description: 'description for an entry', + uri: 'abc', + start_date : moment().toISOString(), + end_date : moment().toISOString(), + }, + { + activity_id: 'xyz', + activity_name: 'abc', + id: 'id-15', + project_id: 'project-id-15', + description: 'description for an entry', + uri: 'abc', + start_date : lastStartHourEntryEntered, + end_date : lastEndHourEntryEntered, + } + ]} }, }; @@ -134,6 +161,37 @@ describe('EntryFieldsComponent', () => { expect(toastrServiceStub.error).toHaveBeenCalled(); }); + it('Displays an error message when the active entry has start_time before the start_time of another entry', () => { + component.newData = entry; + component.activeEntry = entry ; + component.setDataToUpdate(entry); + spyOn(toastrServiceStub, 'error'); + + const hourInThePast = moment().subtract(6, 'hour').format('HH:mm:ss'); + component.entryForm.patchValue({ start_hour : hourInThePast}); + component.onUpdateStartHour(); + + expect(toastrServiceStub.error).toHaveBeenCalled(); + }); + + it('should reset to current start_date when start_date has an error', () => { + component.newData = entry; + component.activeEntry = entry ; + component.setDataToUpdate(entry); + + const updatedTime = moment().subtract(6, 'hours').format('HH:mm:ss'); + component.entryForm.patchValue({ start_hour : updatedTime}); + + spyOn(component.entryForm, 'patchValue'); + component.onUpdateStartHour(); + + expect(component.entryForm.patchValue).toHaveBeenCalledWith( + { + start_hour: component.newData.start_hour + } + ); + }); + it('If start hour is in the future, reset to initial start_date in form', () => { component.newData = entry; component.activeEntry = entry ; @@ -155,12 +213,40 @@ describe('EntryFieldsComponent', () => { it('when a start hour is updated, then dispatch UpdateActiveEntry', () => { component.activeEntry = entry ; component.setDataToUpdate(entry); + const updatedTime = moment().format('HH:mm:ss'); + component.entryForm.patchValue({ start_hour : updatedTime}); spyOn(store, 'dispatch'); component.onUpdateStartHour(); expect(store.dispatch).toHaveBeenCalled(); }); + it('When start_time is updated, component.last_entry is equal to time entry in the position 1', async(() => { + component.activeEntry = entry ; + component.setDataToUpdate(entry); + const updatedTime = moment().format('HH:mm:ss'); + + component.entryForm.patchValue({ start_hour : updatedTime}); + component.onUpdateStartHour(); + + expect(component.lastEntry).toBe(state.entries.timeEntriesDataSource.data[1]); + })); + + it('When start_time is updated for a time entry. UpdateEntry and UpdateEntryRuning actions are dispatched', () => { + component.activeEntry = entry ; + component.setDataToUpdate(entry); + const lastEntry = state.entries.timeEntriesDataSource.data[1]; + const updatedTime = moment().subtract(4, 'hours').format('HH:mm:ss'); + const lastStartHourEntryEnteredTest = new Date(`${lastDate}T${updatedTime.trim()}`).toISOString(); + component.entryForm.patchValue({ start_hour : updatedTime}); + spyOn(store, 'dispatch'); + + component.onUpdateStartHour(); + + expect(store.dispatch).toHaveBeenCalledTimes(2); + expect(store.dispatch).toHaveBeenCalledWith(new UpdateEntry({id: lastEntry.id, end_date: lastStartHourEntryEnteredTest})); + }); + it('when a technology is added, then dispatch UpdateActiveEntry', () => { const addedTechnologies = ['react']; spyOn(store, 'dispatch'); 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 5f52bc794..4f4dae138 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 @@ -3,18 +3,18 @@ import { EntryActionTypes, LoadActiveEntry } from './../../store/entry.actions'; import { filter } from 'rxjs/operators'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { Store, ActionsSubject } from '@ngrx/store'; - +import { Store, ActionsSubject, select } 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, LoadActivities } from '../../../activities-management/store'; import * as entryActions from '../../store/entry.actions'; - +import { get } from 'lodash'; import * as moment from 'moment'; import { ToastrService } from 'ngx-toastr'; import { formatDate } from '@angular/common'; +import { getTimeEntriesDataSource } from '../../store/entry.selectors'; type Merged = TechnologyState & ProjectState & ActivityState; @@ -29,6 +29,7 @@ export class EntryFieldsComponent implements OnInit { activities: Activity[] = []; activeEntry; newData; + lastEntry; constructor( private formBuilder: FormBuilder, @@ -47,7 +48,7 @@ export class EntryFieldsComponent implements OnInit { ngOnInit(): void { this.store.dispatch(new LoadActivities()); - + this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1)); this.actionsSubject$ .pipe(filter((action: any) => action.type === ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS)) .subscribe((action) => { @@ -57,11 +58,13 @@ export class EntryFieldsComponent implements OnInit { this.actionsSubject$ .pipe( - filter((action: any) => ( - action.type === EntryActionTypes.CREATE_ENTRY_SUCCESS || - action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS - )) - ).subscribe((action) => { + filter( + (action: any) => + action.type === EntryActionTypes.CREATE_ENTRY_SUCCESS || + action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS + ) + ) + .subscribe((action) => { if (!action.payload.end_date) { this.store.dispatch(new LoadActiveEntry()); this.store.dispatch(new entryActions.LoadEntriesSummary()); @@ -118,6 +121,7 @@ export class EntryFieldsComponent implements OnInit { } onUpdateStartHour() { + this.getLastEntry(); const startDate = formatDate(this.activeEntry.start_date, 'yyyy-MM-dd', 'en'); const newHourEntered = new Date(`${startDate}T${this.entryForm.value.start_hour.trim()}`).toISOString(); const isEntryDateInTheFuture = moment(newHourEntered).isAfter(moment()); @@ -126,10 +130,34 @@ export class EntryFieldsComponent implements OnInit { this.entryForm.patchValue({ start_hour: this.newData.start_hour }); return; } + + const lastEntryStartDate = get(this.lastEntry, 'start_date', moment().subtract(1, 'hours')); + const isEntryDateInLastStartDate = moment(newHourEntered).isSameOrBefore(lastEntryStartDate); + if (isEntryDateInLastStartDate) { + this.toastrService.error('There is another time entry registered in this time range'); + this.entryForm.patchValue({ start_hour: this.newData.start_hour }); + return; + } this.entryForm.patchValue({ start_date: newHourEntered }); + this.dispatchEntries(newHourEntered); + } + + private dispatchEntries(newHourEntered) { + const lastEntryEndDate = get(this.lastEntry, 'end_date', moment().subtract(1, 'hours')); + const isInLastEntry = moment(newHourEntered).isBefore(lastEntryEndDate); + if (isInLastEntry) { + this.store.dispatch(new entryActions.UpdateEntry({ id: this.lastEntry.id, end_date: newHourEntered })); + } this.store.dispatch(new entryActions.UpdateEntryRunning({ ...this.newData, ...this.entryForm.value })); } + private getLastEntry() { + const lastEntry$ = this.store.pipe(select(getTimeEntriesDataSource)); + lastEntry$.subscribe((entry) => { + this.lastEntry = entry.data[1]; + }); + } + onTechnologyAdded($event: string[]) { this.store.dispatch(new entryActions.UpdateEntryRunning({ ...this.newData, technologies: $event })); } diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts index bd3ef5c49..3aeb4cdb3 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts @@ -105,6 +105,7 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { } else { this.store.dispatch(new entryActions.SwitchTimeEntry(this.activeEntry.id, selectedProject)); this.projectsForm.setValue( { project_id: `${customerName} - ${name}`, } ); + this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1)); } } diff --git a/src/app/modules/users/store/user.actions.ts b/src/app/modules/users/store/user.actions.ts index cd4654a1e..e6419423f 100644 --- a/src/app/modules/users/store/user.actions.ts +++ b/src/app/modules/users/store/user.actions.ts @@ -5,6 +5,7 @@ export enum UserActionTypes { LOAD_USERS = '[User] LOAD_USERS', LOAD_USERS_SUCCESS = '[User] LOAD_USERS_SUCCESS', LOAD_USERS_FAIL = '[User] LOAD_USERS_FAIL', + DEFAULT_USER = '[USER] DEFAULT_USER', } export class LoadUsers implements Action { @@ -21,4 +22,8 @@ export class LoadUsersFail implements Action { constructor(public error: string) {} } -export type UserActions = LoadUsers | LoadUsersSuccess | LoadUsersFail; +export class DefaultUser implements Action { + public readonly type = UserActionTypes.DEFAULT_USER; +} + +export type UserActions = LoadUsers | LoadUsersSuccess | LoadUsersFail | DefaultUser; diff --git a/src/app/modules/users/store/user.reducer.spec.ts b/src/app/modules/users/store/user.reducer.spec.ts index 8fb02f4f7..ccb8b69f8 100644 --- a/src/app/modules/users/store/user.reducer.spec.ts +++ b/src/app/modules/users/store/user.reducer.spec.ts @@ -27,4 +27,10 @@ describe('userReducer', () => { expect(state.isLoading).toEqual(false); expect(state.data.length).toBe(0); }); + + it('on Default, ', () => { + const action = new actions.DefaultUser(); + const state = userReducer(initialState, action); + expect(state).toEqual(initialState); + }); });