diff --git a/src/app/app.module.ts b/src/app/app.module.ts index aacf419a2..30cd0a596 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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({ @@ -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 {} diff --git a/src/app/modules/shared/models/entry.model.ts b/src/app/modules/shared/models/entry.model.ts index 74c642fec..85785b380 100644 --- a/src/app/modules/shared/models/entry.model.ts +++ b/src/app/modules/shared/models/entry.model.ts @@ -8,3 +8,8 @@ export interface Entry { comments?: string; ticket?: string; } + +export interface NewEntry { + project_id: string; + start_date: string; +} diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts index 67da84a30..a9093e64a 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts @@ -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; @@ -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); }); 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 f097e42b3..2ccfda345 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 @@ -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', @@ -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); } diff --git a/src/app/modules/time-clock/services/.gitkeep b/src/app/modules/time-clock/services/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/modules/time-clock/services/entry.service.spec.ts b/src/app/modules/time-clock/services/entry.service.spec.ts new file mode 100644 index 000000000..9081a8bdc --- /dev/null +++ b/src/app/modules/time-clock/services/entry.service.spec.ts @@ -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); + }); +}); diff --git a/src/app/modules/time-clock/services/entry.service.ts b/src/app/modules/time-clock/services/entry.service.ts new file mode 100644 index 000000000..ca3e2de9e --- /dev/null +++ b/src/app/modules/time-clock/services/entry.service.ts @@ -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 { + return this.http.post(this.baseUrl, entryData); + } +} diff --git a/src/app/modules/time-clock/store/.gitkeep b/src/app/modules/time-clock/store/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/modules/time-clock/store/entry.actions.spec.ts b/src/app/modules/time-clock/store/entry.actions.spec.ts new file mode 100644 index 000000000..eaf1d45b3 --- /dev/null +++ b/src/app/modules/time-clock/store/entry.actions.spec.ts @@ -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); + }); +}); diff --git a/src/app/modules/time-clock/store/entry.actions.ts b/src/app/modules/time-clock/store/entry.actions.ts new file mode 100644 index 000000000..5d39c38da --- /dev/null +++ b/src/app/modules/time-clock/store/entry.actions.ts @@ -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; diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts new file mode 100644 index 000000000..aa6d305e2 --- /dev/null +++ b/src/app/modules/time-clock/store/entry.effects.ts @@ -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 = 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))) + ) + ) + ); +} diff --git a/src/app/modules/time-clock/store/entry.reducer.spec.ts b/src/app/modules/time-clock/store/entry.reducer.spec.ts new file mode 100644 index 000000000..c93a8b994 --- /dev/null +++ b/src/app/modules/time-clock/store/entry.reducer.spec.ts @@ -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); + }); +}); diff --git a/src/app/modules/time-clock/store/entry.reducer.ts b/src/app/modules/time-clock/store/entry.reducer.ts new file mode 100644 index 000000000..0dd910a90 --- /dev/null +++ b/src/app/modules/time-clock/store/entry.reducer.ts @@ -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; + } +}; diff --git a/src/app/reducers/index.ts b/src/app/reducers/index.ts index 080e05509..06baa53b2 100644 --- a/src/app/reducers/index.ts +++ b/src/app/reducers/index.ts @@ -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 { @@ -12,6 +13,7 @@ export interface State { technologies; customers; projectType; + entries; } export const reducers: ActionReducerMap = { @@ -20,6 +22,7 @@ export const reducers: ActionReducerMap = { customers: customerManagementReducer, technologies: technologyReducer, projectType: projectTypeReducer, + entries: entryReducer, }; export const metaReducers: MetaReducer[] = !environment.production ? [] : [];