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
fix: #146 Create new time entry
  • Loading branch information
jorgecod committed Apr 21, 2020
commit 6f07cc7460b9b9c3490dbde6a40f00fa220e83b7
22 changes: 16 additions & 6 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ProjectTypeListComponent } from './modules/customer-management/componen
// tslint:disable-next-line: max-line-length
import { CreateProjectTypeComponent } from './modules/customer-management/components/projects-type/components/create-project-type/create-project-type.component';
import { CustomerEffects } from './modules/customer-management/store/customer-management.effects';
import { EntryEffects } from './modules/time-clock/store/entry.effects';
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';

@NgModule({
Expand Down Expand Up @@ -103,13 +104,22 @@ import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.tok
maxAge: 15, // Retains last 15 states
})
: [],
EffectsModule.forRoot([ProjectEffects, ActivityEffects, CustomerEffects, TechnologyEffects, ProjectTypeEffects]),
EffectsModule.forRoot([
ProjectEffects,
ActivityEffects,
CustomerEffects,
TechnologyEffects,
ProjectTypeEffects,
EntryEffects,
]),
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: InjectTokenInterceptor,
multi: true,
},
],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: InjectTokenInterceptor,
multi: true,
}],
bootstrap: [AppComponent],
})
export class AppModule {}
5 changes: 5 additions & 0 deletions src/app/modules/shared/models/entry.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ export interface Entry {
comments?: string;
ticket?: string;
}

export interface NewEntry {
project_id: string;
start_date: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ProjectListHoverComponent } from './project-list-hover.component';
import { ProjectState } from '../../../project-management/store/project.reducer';
import { allProjects } from '../../../project-management/store/project.selectors';
import { FilterProjectPipe } from '../../../shared/pipes';
import { NewEntry } from '../../../shared/models';
import * as action from '../../store/entry.actions';

describe('ProjectListHoverComponent', () => {
let component: ProjectListHoverComponent;
Expand Down Expand Up @@ -38,9 +40,16 @@ describe('ProjectListHoverComponent', () => {
expect(component).toBeTruthy();
});

it('should set selectedId with Id', () => {
it('should set selectedId with Id and dispatch CreateEntry action', () => {
spyOn(store, 'dispatch');
const id = 'P1';
const entryData: NewEntry = {
project_id: id,
start_date: new Date().toISOString(),
};
component.clockIn(id);

expect(store.dispatch).toHaveBeenCalledWith(new action.CreateEntry(entryData));
expect(component.selectedId).toBe(id);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Project } from 'src/app/modules/shared/models';
import { allProjects } from '../../../project-management/store/project.selectors';
import { ProjectState } from '../../../project-management/store/project.reducer';
import * as actions from '../../../project-management/store/project.actions';
import * as entryActions from '../../store/entry.actions';

@Component({
selector: 'app-project-list-hover',
Expand Down Expand Up @@ -33,6 +34,8 @@ export class ProjectListHoverComponent implements OnInit {
}

clockIn(id: string) {
const newEntry = { project_id: id, start_date: new Date().toISOString() };
this.store.dispatch(new entryActions.CreateEntry(newEntry));
this.selectedId = id;
this.showFields.emit(true);
}
Expand Down
Empty file.
42 changes: 42 additions & 0 deletions src/app/modules/time-clock/services/entry.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed, inject } from '@angular/core/testing';

import { EntryService } from './entry.service';
import { NewEntry } from '../../shared/models';

describe('EntryService', () => {
let service: EntryService;
let httpMock: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
service = TestBed.inject(EntryService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

it('services are ready to be used', inject(
[HttpClientTestingModule, EntryService],
(httpClient: HttpClientTestingModule, entryService: EntryService) => {
expect(entryService).toBeTruthy();
expect(httpClient).toBeTruthy();
}
));

it('create entry using POST from baseUrl', () => {
const entry: NewEntry[] = [{ project_id: '1', start_date: new Date().toISOString() }];

service.baseUrl = 'time-entries';

service.createEntry(entry).subscribe((response) => {
expect(response.length).toBe(1);
});

const createEntryRequest = httpMock.expectOne(service.baseUrl);
expect(createEntryRequest.request.method).toBe('POST');
createEntryRequest.flush(entry);
});
});
18 changes: 18 additions & 0 deletions src/app/modules/time-clock/services/entry.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';
import { environment } from './../../../../environments/environment';

@Injectable({
providedIn: 'root',
})
export class EntryService {
baseUrl = `${environment.timeTrackerApiUrl}/time-entries`;

constructor(private http: HttpClient) {}

createEntry(entryData): Observable<any> {
return this.http.post(this.baseUrl, entryData);
}
}
Empty file.
16 changes: 16 additions & 0 deletions src/app/modules/time-clock/store/entry.actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as actions from './entry.actions';

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',
});
expect(createEntrySuccess.type).toEqual(actions.EntryActionTypes.CREATE_ENTRY_SUCCESS);
});

it('CreateEntryFail type is EntryActionTypes.CREATE_ENTRY_FAIL', () => {
const createEntryFail = new actions.CreateEntryFail('error');
expect(createEntryFail.type).toEqual(actions.EntryActionTypes.CREATE_ENTRY_FAIL);
});
});
28 changes: 28 additions & 0 deletions src/app/modules/time-clock/store/entry.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Action } from '@ngrx/store';
import { NewEntry } from '../../shared/models';

export enum EntryActionTypes {
CREATE_ENTRY = '[Entry] CREATE_ENTRY',
CREATE_ENTRY_SUCCESS = '[Entry] CREATE_ENTRY_SUCCESS',
CREATE_ENTRY_FAIL = '[Entry] CREATE_ENTRY_FAIL',
}

export class CreateEntry implements Action {
public readonly type = EntryActionTypes.CREATE_ENTRY;

constructor(public payload: NewEntry) {}
}

export class CreateEntrySuccess implements Action {
public readonly type = EntryActionTypes.CREATE_ENTRY_SUCCESS;

constructor(public payload: NewEntry) {}
}

export class CreateEntryFail implements Action {
public readonly type = EntryActionTypes.CREATE_ENTRY_FAIL;

constructor(public error: string) {}
}

export type EntryActions = CreateEntry | CreateEntrySuccess | CreateEntryFail;
26 changes: 26 additions & 0 deletions src/app/modules/time-clock/store/entry.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { ofType, Actions, Effect } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { of, Observable } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { EntryService } from '../services/entry.service';
import * as actions from './entry.actions';

@Injectable()
export class EntryEffects {
constructor(private actions$: Actions, private entryService: EntryService) {}

@Effect()
createEntry$: Observable<Action> = this.actions$.pipe(
ofType(actions.EntryActionTypes.CREATE_ENTRY),
map((action: actions.CreateEntry) => action.payload),
mergeMap((entry) =>
this.entryService.createEntry(entry).pipe(
map((entryData) => {
return new actions.CreateEntrySuccess(entryData);
}),
catchError((error) => of(new actions.CreateEntryFail(error.error.message)))
)
)
);
}
32 changes: 32 additions & 0 deletions src/app/modules/time-clock/store/entry.reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NewEntry } from './../../shared/models';
import * as actions from './entry.actions';
import { entryReducer, EntryState } from './entry.reducer';

describe('entryReducer', () => {
const initialState: EntryState = { entryList: [], isLoading: false, message: '' };

it('on CreateEntry, isLoading is true', () => {
const entry: NewEntry = { project_id: '1', start_date: '2020-04-21T19:51:36.559000+00:00' };
const action = new actions.CreateEntry(entry);
const state = entryReducer(initialState, action);

expect(state.isLoading).toEqual(true);
});

it('on CreateEntrySuccess, entry is saved in the store', () => {
const entry: NewEntry = { project_id: '1', start_date: '2020-04-21T19:51:36.559000+00:00' };
const action = new actions.CreateEntrySuccess(entry);
const state = entryReducer(initialState, action);

expect(state.entryList).toEqual([entry]);
expect(state.isLoading).toEqual(false);
});

it('on CreateEntryFail, entryList equal []', () => {
const action = new actions.CreateEntryFail('error');
const state = entryReducer(initialState, action);

expect(state.entryList).toEqual([]);
expect(state.isLoading).toEqual(false);
});
});
45 changes: 45 additions & 0 deletions src/app/modules/time-clock/store/entry.reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { EntryActions, EntryActionTypes } from './entry.actions';
import { Entry } from '../../shared/models';

export interface EntryState {
entryList: Entry[];
isLoading: boolean;
message: string;
}

export const initialState = {
entryList: [],
isLoading: false,
message: '',
};

export const entryReducer = (state: EntryState = initialState, action: EntryActions) => {
switch (action.type) {
case EntryActionTypes.CREATE_ENTRY: {
return {
...state,
isLoading: true,
};
}

case EntryActionTypes.CREATE_ENTRY_SUCCESS: {
return {
...state,
entryList: [...state.entryList, action.payload],
isLoading: false,
message: 'Entry Created',
};
}

case EntryActionTypes.CREATE_ENTRY_FAIL: {
return {
entryList: [],
isLoading: false,
message: action.error,
};
}

default:
return state;
}
};
3 changes: 3 additions & 0 deletions src/app/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { activityManagementReducer } from '../modules/activities-management/stor
import { technologyReducer } from '../modules/shared/store/technology.reducers';
import { customerManagementReducer } from '../modules/customer-management/store/customer-management.reducers';
import { projectTypeReducer } from '../modules/customer-management/components/projects-type/store/project-type.reducers';
import { entryReducer } from '../modules/time-clock/store/entry.reducer';
import { environment } from '../../environments/environment';

export interface State {
Expand All @@ -12,6 +13,7 @@ export interface State {
technologies;
customers;
projectType;
entries;
}

export const reducers: ActionReducerMap<State> = {
Expand All @@ -20,6 +22,7 @@ export const reducers: ActionReducerMap<State> = {
customers: customerManagementReducer,
technologies: technologyReducer,
projectType: projectTypeReducer,
entries: entryReducer,
};

export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];