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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StopTimeEntryRunning } from './../../store/entry.actions';
import { SwitchTimeEntry } from './../../store/entry.actions';
import { FormBuilder } from '@angular/forms';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
Expand Down Expand Up @@ -67,7 +67,7 @@ describe('ProjectListHoverComponent', () => {
expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(CreateEntry));
});

it('dispatchs a UpdateEntryRunning action on updateProject', () => {
it('dispatch a UpdateEntryRunning action on updateProject', () => {
component.activeEntry = { id: '123' };
spyOn(store, 'dispatch');

Expand All @@ -76,15 +76,13 @@ describe('ProjectListHoverComponent', () => {
expect(store.dispatch).toHaveBeenCalledWith(new UpdateEntryRunning({ id: component.activeEntry.id, project_id: 1 }));
});

it('stop activeEntry and clockIn on swith', () => {
spyOn(component, 'clockIn');
it('dispatch a SwitchTimeEntry action', () => {
spyOn(store, 'dispatch');
component.activeEntry = { id: '123' };

component.switch(1, 'customer', 'project');

expect(store.dispatch).toHaveBeenCalledWith(new StopTimeEntryRunning(component.activeEntry.id));
expect(component.clockIn).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new SwitchTimeEntry(component.activeEntry.id, 1));
});

it('calls unsubscribe on ngDestroy', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy {
}

switch(selectedProject, customerName, name) {
this.store.dispatch(new entryActions.StopTimeEntryRunning(this.activeEntry.id));
this.clockIn(selectedProject, customerName, name);
this.store.dispatch(new entryActions.LoadActiveEntry());
this.store.dispatch(new entryActions.SwitchTimeEntry(this.activeEntry.id, selectedProject));
this.projectsForm.setValue( { project_id: `${customerName} - ${name}`, } );
}

ngOnDestroy(): void {
Expand Down
4 changes: 2 additions & 2 deletions src/app/modules/time-clock/services/entry.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ describe('EntryService', () => {

it('loads summary with get /summary?time_offset=<time-offset>', () => {
service.summary().subscribe();
const time_offset = new Date().getTimezoneOffset();
const loadEntryRequest = httpMock.expectOne(`${service.baseUrl}/summary?time_offset=${time_offset}`);
const timeOffset = new Date().getTimezoneOffset();
const loadEntryRequest = httpMock.expectOne(`${service.baseUrl}/summary?time_offset=${timeOffset}`);
expect(loadEntryRequest.request.method).toBe('GET');
});

Expand Down
4 changes: 2 additions & 2 deletions src/app/modules/time-clock/services/entry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export class EntryService {
}

summary(): Observable<TimeEntriesSummary> {
const time_offset = new Date().getTimezoneOffset();
const summaryUrl = `${this.baseUrl}/summary?time_offset=${time_offset}`;
const timeOffset = new Date().getTimezoneOffset();
const summaryUrl = `${this.baseUrl}/summary?time_offset=${timeOffset}`;
return this.http.get<TimeEntriesSummary>(summaryUrl);
}

Expand Down
16 changes: 12 additions & 4 deletions src/app/modules/time-clock/store/entry.actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { TimeEntriesTimeRange } from '../models/time-entries-time-range';

describe('Actions for Entries', () => {


it('SwitchTimeEntry type is EntryActionTypes.SWITCH_TIME_ENTRY', () => {
const action = new actions.SwitchTimeEntry('entry-id', 'project-id');
expect(action.type).toEqual(actions.EntryActionTypes.SWITCH_TIME_ENTRY);
});

it('SwitchTimeEntryFail type is EntryActionTypes.SWITCH_TIME_ENTRY_FAIL', () => {
const action = new actions.SwitchTimeEntryFail('error msg');
expect(action.type).toEqual(actions.EntryActionTypes.SWITCH_TIME_ENTRY_FAIL);
});

it('LoadEntriesSummaryFail type is EntryActionTypes.LOAD_ENTRIES_SUMMARY_FAIL', () => {
const action = new actions.LoadEntriesSummaryFail();
expect(action.type).toEqual(actions.EntryActionTypes.LOAD_ENTRIES_SUMMARY_FAIL);
Expand Down Expand Up @@ -35,10 +46,7 @@ describe('Actions for Entries', () => {
});

it('CreateEntrySuccess type is EntryActionTypes.CREATE_ENTRY_SUCCESS', () => {
const createEntrySuccess = new actions.CreateEntrySuccess({
project_id: '1',
start_date: '2020-04-21T19:51:36.559000+00:00',
});
const createEntrySuccess = new actions.CreateEntrySuccess(null);
expect(createEntrySuccess.type).toEqual(actions.EntryActionTypes.CREATE_ENTRY_SUCCESS);
});

Expand Down
18 changes: 16 additions & 2 deletions src/app/modules/time-clock/store/entry.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ export enum EntryActionTypes {
LOAD_ENTRIES_BY_TIME_RANGE = '[Entry] LOAD_ENTRIES_BY_TIME_RANGE',
LOAD_ENTRIES_BY_TIME_RANGE_SUCCESS = '[Entry] LOAD_ENTRIES_BY_TIME_RANGE_SUCCESS',
LOAD_ENTRIES_BY_TIME_RANGE_FAIL = '[Entry] LOAD_ENTRIES_BY_TIME_RANGE_FAIL',
SWITCH_TIME_ENTRY = '[Entry] SWITCH_TIME_ENTRY',
SWITCH_TIME_ENTRY_FAIL = '[Entry] SWITCH_TIME_ENTRY_FAIL',
}

export class SwitchTimeEntry implements Action {
public readonly type = EntryActionTypes.SWITCH_TIME_ENTRY;
constructor(readonly idEntrySwitching: string, readonly idProjectSwitching) {}
}

export class SwitchTimeEntryFail implements Action {
public readonly type = EntryActionTypes.SWITCH_TIME_ENTRY_FAIL;
constructor(readonly error: string) {}
}

export class LoadEntriesSummary implements Action {
Expand Down Expand Up @@ -87,7 +99,7 @@ export class CreateEntry implements Action {
export class CreateEntrySuccess implements Action {
public readonly type = EntryActionTypes.CREATE_ENTRY_SUCCESS;

constructor(public payload: NewEntry) {}
constructor(public payload: Entry) {}
}

export class CreateEntryFail implements Action {
Expand Down Expand Up @@ -208,4 +220,6 @@ export type EntryActions =
| DefaultEntry
| LoadEntriesByTimeRange
| LoadEntriesByTimeRangeSuccess
| LoadEntriesByTimeRangeFail;
| LoadEntriesByTimeRangeFail
| SwitchTimeEntry
| SwitchTimeEntryFail;
13 changes: 12 additions & 1 deletion src/app/modules/time-clock/store/entry.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ToastrModule, ToastrService } from 'ngx-toastr';
import { Action } from '@ngrx/store';
import { DatePipe } from '@angular/common';
import { EntryActionTypes } from './entry.actions';
import { EntryActionTypes, SwitchTimeEntry } from './entry.actions';
import { EntryService } from '../services/entry.service';
import { TimeEntriesTimeRange } from '../models/time-entries-time-range';
import * as moment from 'moment';
Expand Down Expand Up @@ -38,6 +38,17 @@ describe('TimeEntryActionEffects', () => {
expect(effects).toBeTruthy();
});

it('stop the active entry and return a CreateEntryAction', async () => {
actions$ = of(new SwitchTimeEntry('entry-id', 'project-id'));
const serviceSpy = spyOn(service, 'stopEntryRunning');
serviceSpy.and.returnValue(of({}));

effects.switchEntryRunning$.subscribe(action => {
expect(service.stopEntryRunning).toHaveBeenCalledWith('entry-id');
expect(action.type).toBe(EntryActionTypes.CREATE_ENTRY);
});
});

it('returns an action with type LOAD_ENTRIES_SUMMARY_SUCCESS when the service returns a value', () => {
actions$ = of({type: EntryActionTypes.LOAD_ENTRIES_SUMMARY});
const serviceSpy = spyOn(service, 'summary');
Expand Down
20 changes: 18 additions & 2 deletions src/app/modules/time-clock/store/entry.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
import { EntryService } from '../services/entry.service';
import * as actions from './entry.actions';

Expand All @@ -13,6 +13,22 @@ export class EntryEffects {
constructor(private actions$: Actions, private entryService: EntryService, private toastrService: ToastrService) {
}

@Effect()
switchEntryRunning$: Observable<Action> = this.actions$.pipe(
ofType(actions.EntryActionTypes.SWITCH_TIME_ENTRY),
switchMap((action: actions.SwitchTimeEntry) =>
this.entryService.stopEntryRunning(action.idEntrySwitching).pipe(
map(() => {
return new actions.CreateEntry({ project_id: action.idProjectSwitching, start_date: new Date().toISOString() });
}),
catchError((error) => {
this.toastrService.warning(error.error.message);
return of(new actions.StopTimeEntryRunningFail(error.error.message));
})
)
)
);

@Effect()
loadEntriesSummary$: Observable<Action> = this.actions$.pipe(
ofType(actions.EntryActionTypes.LOAD_ENTRIES_SUMMARY),
Expand Down Expand Up @@ -46,7 +62,7 @@ export class EntryEffects {
} else {
const endDate = new Date(activeEntry.start_date);
endDate.setHours(23, 59, 59);
return new actions.UpdateEntry({id: activeEntry.id, end_date: endDate.toISOString()});
return new actions.UpdateEntry({ id: activeEntry.id, end_date: endDate.toISOString() });
}
}
}),
Expand Down
30 changes: 26 additions & 4 deletions src/app/modules/time-clock/store/entry.reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,34 @@ describe('entryReducer', () => {
expect(state.isLoading).toEqual(true);
});

it('on CreateEntrySuccess, message is updated', () => {
const entryToCreate: NewEntry = {project_id: '1', start_date: '2020-04-21T19:51:36.559000+00:00'};
const action = new actions.CreateEntrySuccess(entryToCreate);
it('on CreateEntrySuccess, if end_date is null then it is the active entry', () => {
const entryCreated: Entry = { ... entry };
entryCreated.end_date = null;
const action = new actions.CreateEntrySuccess(entryCreated);

const state = entryReducer(initialState, action);

expect(state.active).toBe(entryCreated);
});

it('on CreateEntrySuccess, if end_date is undefined then it is the active entry', () => {
const entryCreated: Entry = { ... entry };
entryCreated.end_date = undefined;
const action = new actions.CreateEntrySuccess(entryCreated);

const state = entryReducer(initialState, action);

expect(state.active).toBe(entryCreated);
});

it('on CreateEntrySuccess, if end_date has a value then it the active field is not updated', () => {
const activeEntry: Entry = { ... entry };
initialState.active = activeEntry;
const action = new actions.CreateEntrySuccess(entry);

const state = entryReducer(initialState, action);

expect(state.message).toEqual('You clocked-in successfully');
expect(state.active).toBe(activeEntry);
});

it('on CreateEntryFail, entryList equal []', () => {
Expand Down
23 changes: 16 additions & 7 deletions src/app/modules/time-clock/store/entry.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,22 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi
case EntryActionTypes.CREATE_ENTRY_SUCCESS: {
const entryList = [action.payload, ...state.entryList];
entryList.sort((a, b) => new Date(b.start_date).getTime() - new Date(a.start_date).getTime());
return {
...state,
entryList,
isLoading: false,
createError: false,
message: 'You clocked-in successfully',
};
if (action.payload.end_date === null || action.payload.end_date === undefined) {
return {
...state,
active: action.payload,
entryList,
isLoading: false,
createError: false,
};
} else {
return {
...state,
entryList,
isLoading: false,
createError: false,
};
}
}

case EntryActionTypes.CREATE_ENTRY_FAIL: {
Expand Down