diff --git a/package-lock.json b/package-lock.json index 8eab38262..e0567aaab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18537,8 +18537,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "resolved": "", "dev": true, "optional": true, "requires": { @@ -18713,8 +18712,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "resolved": "", "dev": true, "optional": true } @@ -19528,8 +19526,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "resolved": "", "dev": true, "optional": true, "requires": { @@ -19704,8 +19701,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "resolved": "", "dev": true, "optional": true } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 89313ee17..b23f7a9d6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -29,6 +29,7 @@ import { ActivityListComponent } from './modules/activities-management/component import { CreateActivityComponent } from './modules/activities-management/components/create-activity/create-activity.component'; import { FilterProjectPipe } from './modules/shared/pipes/filter-project/filter-project.pipe'; import { SearchComponent } from './modules/shared/components/search/search.component'; +import { EntryFieldsComponent } from './modules/time-clock/components/entry-fields/entry-fields.component'; import { HomeComponent } from './modules/home/home.component'; import { LoginComponent } from './modules/login/login.component'; import { ActivityEffects } from './modules/activities-management/store/activity-management.effects'; @@ -86,6 +87,7 @@ import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.tok ProjectListComponent, ProjectTypeListComponent, CreateProjectTypeComponent, + EntryFieldsComponent, ], imports: [ CommonModule, diff --git a/src/app/modules/shared/models/entry.model.ts b/src/app/modules/shared/models/entry.model.ts index 85785b380..bea6b229e 100644 --- a/src/app/modules/shared/models/entry.model.ts +++ b/src/app/modules/shared/models/entry.model.ts @@ -6,10 +6,13 @@ export interface Entry { activity: string; technologies: string[]; comments?: string; - ticket?: string; + uri?: string; } export interface NewEntry { project_id: string; - start_date: string; + start_date?: string; + description?: string; + technologies?: string[]; + uri?: string; } diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html new file mode 100644 index 000000000..be279282a --- /dev/null +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.html @@ -0,0 +1,58 @@ +
+
+
+ Activity +
+ +
+
+
+ Ticket URI +
+ +
+ +
+
+ Technology +
+ +
+ +
LOADING...
+
+
+ {{ item.name }} +
+
+
+
+ {{ technology }} + +
+
+ +
+ + +
+
diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.scss b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.scss new file mode 100644 index 000000000..c26089d15 --- /dev/null +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.scss @@ -0,0 +1,91 @@ +@import '../../../../../styles/colors.scss'; + +@mixin tagTechnology() { + background-color: $modal-button-secondary; + color: #ffffff; + + &:hover { + opacity: 0.8; + } +} + +.span-width { + width: 6rem; + background-image: $background-pantone; + color: white; +} + +.hidden { + display: none; +} + +.save-button-style { + background-color: $modal-button-primary; + color: white; +} + +.close-button-style { + background-color: $modal-button-secondary; + color: white; +} + +.technology-content { + box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08); + margin: 0 0 2rem 6rem; + max-height: 7.5rem; + overflow-y: auto; + + .technology-list { + cursor: pointer; + font-size: 0.8rem; + margin-bottom: 0.1rem; + padding: 0.2rem 0.5rem; + + &:hover { + opacity: 0.7; + } + } + + .active { + background-color: #efefef; + } +} + +.tags-content { + margin: 2rem 0; + + div { + @include tagTechnology(); + + border-radius: 0.2rem; + box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.75); + font-size: 0.8rem; + padding: 0.1rem 1rem 0.2rem 1.5rem; + position: relative; + margin: 0 0.5rem 0.5rem 0; + + i { + cursor: pointer; + } + } +} + +.ng-autocomplete { + width: 100%; +} + +.autocomplete::ng-deep .autocomplete-container { + border: 1px solid #ced4da; + border-radius: 0 0.25rem 0.25rem 0; + box-shadow: none; + height: 2rem; + + .input-container { + height: 100%; + + input { + border-radius: 0.25rem; + height: 100%; + } + } +} 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 new file mode 100644 index 000000000..77fc9b534 --- /dev/null +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts @@ -0,0 +1,188 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TechnologyState } from '../../../shared/store/technology.reducers'; +import { allTechnologies } from '../../../shared/store/technology.selectors'; +import { EntryFieldsComponent } from './entry-fields.component'; +import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; +import { allProjects } from '../../../customer-management/components/projects/components/store/project.selectors'; +import { allEntries } from '../../store/entry.selectors'; +import * as actions from '../../../shared/store/technology.actions'; +import * as entryActions from '../../store/entry.actions'; + +describe('EntryFieldsComponent', () => { + type Merged = TechnologyState & ProjectState; + let component: EntryFieldsComponent; + let fixture: ComponentFixture; + let store: MockStore; + let mockTechnologySelector; + let mockProjectsSelector; + let mockEntrySelector; + let length; + + const state = { + projects: { + projectList: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }], + isLoading: false, + message: '', + projectToEdit: undefined, + }, + technologies: { + technologyList: { items: [{ name: 'java' }] }, + isLoading: false, + }, + activities: { + data: [{ id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'Training 2' }], + isLoading: false, + message: 'Data fetch successfully!', + activityIdToEdit: '', + }, + entries: { + active: { + id: 'id-15', + project_id: 'project-id-15', + description: 'description for active entry', + technologies: ['java', 'typescript'], + uri: 'abc', + }, + entryList: [], + isLoading: false, + message: '', + }, + }; + + const entry = { + id: 'id-15', + project_id: 'project-id-15', + description: 'description for active entry', + uri: 'abc', + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [EntryFieldsComponent], + providers: [provideMockStore({ initialState: state })], + imports: [FormsModule, ReactiveFormsModule], + }).compileComponents(); + store = TestBed.inject(MockStore); + mockTechnologySelector = store.overrideSelector(allTechnologies, state.technologies); + mockProjectsSelector = store.overrideSelector(allProjects, state.projects); + mockEntrySelector = store.overrideSelector(allEntries, state.entries); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EntryFieldsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set data in entryForm', () => { + const entryDataForm = { + description: 'description for active entry', + technologies: null, + uri: 'abc', + }; + + spyOn(component.entryForm, 'patchValue'); + + component.setDataToUpdate(entry); + + expect(component.entryForm.patchValue).toHaveBeenCalledTimes(1); + expect(component.entryForm.patchValue).toHaveBeenCalledWith( + { description: entryDataForm.description, uri: entryDataForm.uri, }); + expect(component.selectedTechnology).toEqual([]); + }); + + it('should dispatch FindTechnology action #getTechnologies', () => { + const value = 'java'; + spyOn(store, 'dispatch'); + length = value.length; + component.getTechnologies(value); + + expect(component.showlist).toBe(true); + expect(store.dispatch).toHaveBeenCalledWith(new actions.FindTechnology(value)); + }); + + it('should NOT dispatch FindTechnology action #getTechnologies', () => { + const value = 'j'; + spyOn(store, 'dispatch'); + length = value.length; + component.getTechnologies(value); + + expect(store.dispatch).not.toHaveBeenCalledWith(new actions.FindTechnology(value)); + }); + + it('should add a new tag #setTechnology', () => { + spyOn(store, 'dispatch'); + const name = 'ngrx'; + component.selectedTechnology = ['java', 'javascript']; + component.selectedTechnology.indexOf(name); + length = component.selectedTechnology.length; + + const newEntry = { + id: 'id-15', + project_id: 'project-id-15', + uri: 'abc', + }; + + component.setTechnology(name); + expect(component.selectedTechnology.length).toBe(3); + expect(store.dispatch).toHaveBeenCalledWith( + new entryActions.UpdateActiveEntry({ ...newEntry, technologies: component.selectedTechnology }) + ); + }); + + it('should NOT add a new tag #setTechnology', () => { + const name = 'ngrx'; + component.selectedTechnology = [ + 'java', + 'javascript', + 'angular', + 'angular-ui', + 'typescript', + 'scss', + 'bootstrap', + 'jasmine', + 'karme', + 'github', + ]; + component.selectedTechnology.indexOf(name); + length = component.selectedTechnology.length; + component.setTechnology(name); + expect(component.selectedTechnology.length).toBe(10); + }); + + it('should call the removeTag function #setTechnology', () => { + const name = 'java'; + component.selectedTechnology = ['java', 'javascript']; + const index = component.selectedTechnology.indexOf(name); + spyOn(component, 'removeTag'); + component.setTechnology(name); + expect(component.removeTag).toHaveBeenCalledWith(index); + }); + + it('should dispatch UpdateActiveEntry action #removeTag function', () => { + spyOn(store, 'dispatch'); + const index = 1; + const newEntry = { + id: 'id-15', + project_id: 'project-id-15', + uri: 'abc', + }; + component.selectedTechnology = ['java', 'angular']; + const technologies = component.selectedTechnology.filter((item) => item !== component.selectedTechnology[index]); + component.removeTag(index); + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.UpdateActiveEntry({ ...newEntry, technologies })); + }); + + it('should dispatch UpdateActiveEntry action #onSubmit', () => { + spyOn(store, 'dispatch'); + component.onSubmit(); + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.UpdateActiveEntry(entry)); + }); +}); 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 new file mode 100644 index 000000000..bdd9f4bf1 --- /dev/null +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts @@ -0,0 +1,114 @@ +import { Component, OnInit, ViewChild, ElementRef, Renderer2 } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store, select } from '@ngrx/store'; + +import { Technology, Activity, NewEntry } from '../../../shared/models'; +import { allTechnologies } from '../../../shared/store/technology.selectors'; +import * as actions from '../../../shared/store/technology.actions'; + +import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; +import { TechnologyState } from '../../../shared/store/technology.reducers'; +import { LoadActivities, ActivityState, allActivities } from '../../../activities-management/store'; + +import { allEntries } from '../../store/entry.selectors'; +import * as entryActions from '../../store/entry.actions'; + +type Merged = TechnologyState & ProjectState & ActivityState; + +@Component({ + selector: 'app-entry-fields', + templateUrl: './entry-fields.component.html', + styleUrls: ['./entry-fields.component.scss'], +}) +export class EntryFieldsComponent implements OnInit { + @ViewChild('list') list: ElementRef; + entryForm: FormGroup; + technology: Technology; + selectedTechnology: string[] = []; + isLoading = false; + activities: Activity[] = []; + keyword = 'name'; + showlist: boolean; + activeEntry; + newData; + + constructor(private formBuilder: FormBuilder, private store: Store, private renderer: Renderer2) { + this.renderer.listen('window', 'click', (e: Event) => { + if (this.showlist && !this.list.nativeElement.contains(e.target)) { + this.showlist = false; + } + }); + this.entryForm = this.formBuilder.group({ + description: '', + uri: '', + }); + } + + ngOnInit(): void { + const technologies$ = this.store.pipe(select(allTechnologies)); + technologies$.subscribe((response) => { + this.isLoading = response.isLoading; + this.technology = response.technologyList; + }); + + this.store.dispatch(new LoadActivities()); + const activities$ = this.store.pipe(select(allActivities)); + activities$.subscribe((response) => { + this.activities = response; + }); + + const activeEntry$ = this.store.pipe(select(allEntries)); + activeEntry$.subscribe((response) => { + this.activeEntry = response.active; + this.setDataToUpdate(this.activeEntry); + this.newData = { + id: this.activeEntry.id, + project_id: this.activeEntry.project_id, + uri: this.activeEntry.uri, + }; + }); + } + + setDataToUpdate(entryData: NewEntry) { + if (entryData) { + this.entryForm.patchValue({ + description: entryData.description, + uri: entryData.uri, + }); + if (entryData.technologies) { + this.selectedTechnology = entryData.technologies; + } else { + this.selectedTechnology = []; + } + } + } + + getTechnologies(value) { + if (value.length >= 2) { + this.showlist = true; + this.store.dispatch(new actions.FindTechnology(value)); + } + } + + setTechnology(name: string) { + const index = this.selectedTechnology.indexOf(name); + + if (index > -1) { + this.removeTag(index); + } else if (this.selectedTechnology.length < 10) { + this.selectedTechnology = [...this.selectedTechnology, name]; + this.store.dispatch( + new entryActions.UpdateActiveEntry({ ...this.newData, technologies: this.selectedTechnology }) + ); + } + } + + removeTag(index: number) { + const technologies = this.selectedTechnology.filter((item) => item !== this.selectedTechnology[index]); + this.store.dispatch(new entryActions.UpdateActiveEntry({ ...this.newData, technologies })); + } + + onSubmit() { + this.store.dispatch(new entryActions.UpdateActiveEntry({ ...this.newData, ...this.entryForm.value })); + } +} diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.html b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.html index f3b0e0da6..72cb6c151 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.html +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.html @@ -1,13 +1,26 @@
+ + + +
{ let mockProjectsSelector; const state = { - projectList: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }], - isLoading: false, - message: '', - projectToEdit: undefined, + projects: { + projectList: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }], + isLoading: false, + message: '', + projectToEdit: undefined, + }, + entries: { + active: { + project_id: '2b87372b-3d0d-4dc0-832b-ae5863cd39e5', + start_date: '2020-04-23T16:11:06.455000+00:00', + technologies: ['java', 'typescript'], + }, + entryList: [], + isLoading: false, + message: '', + }, }; beforeEach(async(() => { @@ -29,7 +41,7 @@ describe('ProjectListHoverComponent', () => { imports: [HttpClientTestingModule], }).compileComponents(); store = TestBed.inject(MockStore); - mockProjectsSelector = store.overrideSelector(allProjects, state); + mockProjectsSelector = store.overrideSelector(allProjects, state.projects); })); beforeEach(() => { 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 ced1dd7a9..bd70f87e4 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 @@ -6,6 +6,8 @@ import { ProjectState } from '../../../customer-management/components/projects/c import * as actions from '../../../customer-management/components/projects/components/store/project.actions'; import * as entryActions from '../../store/entry.actions'; +import { selectActiveEntry } from '../../store/entry.selectors'; + @Component({ selector: 'app-project-list-hover', templateUrl: './project-list-hover.component.html', @@ -20,6 +22,7 @@ export class ProjectListHoverComponent implements OnInit { filterProjects = ''; showButton = ''; keyword = 'name'; + nameActiveProject: string; constructor(private store: Store) {} @@ -31,12 +34,21 @@ export class ProjectListHoverComponent implements OnInit { this.isLoading = response.isLoading; this.listProjects = response.projectList; }); + + this.store.dispatch(new entryActions.LoadActiveEntry()); + const activeEntry$ = this.store.pipe(select(selectActiveEntry)); + + activeEntry$.subscribe((response) => { + if (response) { + this.nameActiveProject = response.name; + this.showFields.emit(true); + } + }); } 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/pages/time-clock.component.html b/src/app/modules/time-clock/pages/time-clock.component.html index 17df8f99e..4e8a461a0 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.html +++ b/src/app/modules/time-clock/pages/time-clock.component.html @@ -35,9 +35,10 @@

14:00

-
- -
+
+ + +

diff --git a/src/app/modules/time-clock/pages/time-clock.component.spec.ts b/src/app/modules/time-clock/pages/time-clock.component.spec.ts index 458c92926..50e9ae77e 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.spec.ts +++ b/src/app/modules/time-clock/pages/time-clock.component.spec.ts @@ -9,7 +9,7 @@ import { ProjectState } from '../../customer-management/components/projects/comp import { ProjectListHoverComponent } from '../components'; import { ProjectService } from '../../customer-management/components/projects/components/services/project.service'; import { FilterProjectPipe } from '../../shared/pipes'; -import {AzureAdB2CService} from '../../login/services/azure.ad.b2c.service'; +import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; describe('TimeClockComponent', () => { let component: TimeClockComponent; @@ -28,6 +28,16 @@ describe('TimeClockComponent', () => { isLoading: false, message: 'message', }, + entries: { + active: { + project_id: '2b87372b-3d0d-4dc0-832b-ae5863cd39e5', + start_date: '2020-04-23T16:11:06.455000+00:00', + technologies: ['java', 'typescript'], + }, + entryList: [], + isLoading: false, + message: '', + }, }; beforeEach(async(() => { diff --git a/src/app/modules/time-clock/services/entry.service.ts b/src/app/modules/time-clock/services/entry.service.ts index ca3e2de9e..c4d2b7ea2 100644 --- a/src/app/modules/time-clock/services/entry.service.ts +++ b/src/app/modules/time-clock/services/entry.service.ts @@ -12,7 +12,16 @@ export class EntryService { constructor(private http: HttpClient) {} + loadActiveEntry(): Observable { + return this.http.get(`${this.baseUrl}/running`); + } + createEntry(entryData): Observable { return this.http.post(this.baseUrl, entryData); } + + updateActiveEntry(entryData): Observable { + const { id } = entryData; + return this.http.put(`${this.baseUrl}/${id}`, entryData); + } } diff --git a/src/app/modules/time-clock/store/entry.actions.spec.ts b/src/app/modules/time-clock/store/entry.actions.spec.ts index eaf1d45b3..b2c3380a8 100644 --- a/src/app/modules/time-clock/store/entry.actions.spec.ts +++ b/src/app/modules/time-clock/store/entry.actions.spec.ts @@ -1,6 +1,16 @@ import * as actions from './entry.actions'; describe('Actions for Entries', () => { + it('LoadActiveEntrySuccess type is EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS', () => { + const loadActiveEntrySuccess = new actions.LoadActiveEntrySuccess([]); + expect(loadActiveEntrySuccess.type).toEqual(actions.EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS); + }); + + it('LoadActiveEntryFail type is EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL', () => { + const loadActiveEntryFail = new actions.LoadActiveEntryFail('error'); + expect(loadActiveEntryFail.type).toEqual(actions.EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL); + }); + it('CreateEntrySuccess type is EntryActionTypes.CREATE_ENTRY_SUCCESS', () => { const createEntrySuccess = new actions.CreateEntrySuccess({ project_id: '1', @@ -13,4 +23,17 @@ describe('Actions for Entries', () => { const createEntryFail = new actions.CreateEntryFail('error'); expect(createEntryFail.type).toEqual(actions.EntryActionTypes.CREATE_ENTRY_FAIL); }); + + it('UpdateActiveEntrySuccess type is EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS', () => { + const updateActiveEntrySuccess = new actions.UpdateActiveEntrySuccess({ + project_id: '1', + description: 'It is good for learning', + }); + expect(updateActiveEntrySuccess.type).toEqual(actions.EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS); + }); + + it('UpdateActiveEntryFail type is EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL', () => { + const updateActiveEntryFail = new actions.UpdateActiveEntryFail('error'); + expect(updateActiveEntryFail.type).toEqual(actions.EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL); + }); }); diff --git a/src/app/modules/time-clock/store/entry.actions.ts b/src/app/modules/time-clock/store/entry.actions.ts index 5d39c38da..34df7e70a 100644 --- a/src/app/modules/time-clock/store/entry.actions.ts +++ b/src/app/modules/time-clock/store/entry.actions.ts @@ -2,9 +2,30 @@ import { Action } from '@ngrx/store'; import { NewEntry } from '../../shared/models'; export enum EntryActionTypes { + LOAD_ACTIVE_ENTRY = '[Entry] LOAD_ACTIVE_ENTRY', + LOAD_ACTIVE_ENTRY_SUCCESS = '[Entry] LOAD_ACTIVE_ENTRY_SUCCESS', + LOAD_ACTIVE_ENTRY_FAIL = '[Entry] LOAD_ACTIVE_ENTRY_FAIL', CREATE_ENTRY = '[Entry] CREATE_ENTRY', CREATE_ENTRY_SUCCESS = '[Entry] CREATE_ENTRY_SUCCESS', CREATE_ENTRY_FAIL = '[Entry] CREATE_ENTRY_FAIL', + UDPATE_ACTIVE_ENTRY = '[Entry] UDPATE_ACTIVE_ENTRY', + UDPATE_ACTIVE_ENTRY_SUCCESS = '[Entry] UDPATE_ACTIVE_ENTRY_SUCCESS', + UDPATE_ACTIVE_ENTRY_FAIL = '[Entry] UDPATE_ACTIVE_ENTRY_FAIL', +} + +export class LoadActiveEntry implements Action { + public readonly type = EntryActionTypes.LOAD_ACTIVE_ENTRY; +} + +export class LoadActiveEntrySuccess implements Action { + readonly type = EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS; + constructor(readonly payload: NewEntry[]) {} +} + +export class LoadActiveEntryFail implements Action { + public readonly type = EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL; + + constructor(public error: string) {} } export class CreateEntry implements Action { @@ -25,4 +46,31 @@ export class CreateEntryFail implements Action { constructor(public error: string) {} } -export type EntryActions = CreateEntry | CreateEntrySuccess | CreateEntryFail; +export class UpdateActiveEntry implements Action { + public readonly type = EntryActionTypes.UDPATE_ACTIVE_ENTRY; + + constructor(public payload: NewEntry) {} +} + +export class UpdateActiveEntrySuccess implements Action { + public readonly type = EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS; + + constructor(public payload: NewEntry) {} +} + +export class UpdateActiveEntryFail implements Action { + public readonly type = EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL; + + constructor(public error: string) {} +} + +export type EntryActions = + | LoadActiveEntry + | LoadActiveEntrySuccess + | LoadActiveEntryFail + | CreateEntry + | CreateEntrySuccess + | CreateEntryFail + | UpdateActiveEntry + | UpdateActiveEntrySuccess + | UpdateActiveEntryFail; diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts index aa6d305e2..c6763fb4c 100644 --- a/src/app/modules/time-clock/store/entry.effects.ts +++ b/src/app/modules/time-clock/store/entry.effects.ts @@ -10,6 +10,19 @@ import * as actions from './entry.actions'; export class EntryEffects { constructor(private actions$: Actions, private entryService: EntryService) {} + @Effect() + loadActiveEntry$: Observable = this.actions$.pipe( + ofType(actions.EntryActionTypes.LOAD_ACTIVE_ENTRY), + mergeMap(() => + this.entryService.loadActiveEntry().pipe( + map((activeEntry) => { + return new actions.LoadActiveEntrySuccess(activeEntry); + }), + catchError((error) => of(new actions.LoadActiveEntryFail(error))) + ) + ) + ); + @Effect() createEntry$: Observable = this.actions$.pipe( ofType(actions.EntryActionTypes.CREATE_ENTRY), @@ -23,4 +36,18 @@ export class EntryEffects { ) ) ); + + @Effect() + updateActiveEntry$: Observable = this.actions$.pipe( + ofType(actions.EntryActionTypes.UDPATE_ACTIVE_ENTRY), + map((action: actions.UpdateActiveEntry) => action.payload), + mergeMap((project) => + this.entryService.updateActiveEntry(project).pipe( + map((projectData) => { + return new actions.UpdateActiveEntrySuccess(projectData); + }), + catchError((error) => of(new actions.UpdateActiveEntryFail(error))) + ) + ) + ); } diff --git a/src/app/modules/time-clock/store/entry.reducer.spec.ts b/src/app/modules/time-clock/store/entry.reducer.spec.ts index c93a8b994..726b0b989 100644 --- a/src/app/modules/time-clock/store/entry.reducer.spec.ts +++ b/src/app/modules/time-clock/store/entry.reducer.spec.ts @@ -3,7 +3,29 @@ import * as actions from './entry.actions'; import { entryReducer, EntryState } from './entry.reducer'; describe('entryReducer', () => { - const initialState: EntryState = { entryList: [], isLoading: false, message: '' }; + const initialState: EntryState = { active: null, entryList: [], isLoading: false, message: '' }; + const newEntry: NewEntry = { project_id: '112', description: 'aaa', technologies: ['angular', 'typescript'] }; + + it('on LoadActiveEntry, isLoading is true', () => { + const action = new actions.LoadActiveEntry(); + const state = entryReducer(initialState, action); + expect(state.isLoading).toEqual(true); + }); + + it('on LoadActiveEntrySuccess, activeEntryFound are saved in the store', () => { + const activeEntryFound: NewEntry[] = [ + { project_id: '123', description: 'description', technologies: ['angular', 'javascript'] }, + ]; + const action = new actions.LoadActiveEntrySuccess(activeEntryFound); + const state = entryReducer(initialState, action); + expect(state.active).toEqual(activeEntryFound); + }); + + it('on LoadActiveEntryFail, active tobe null', () => { + const action = new actions.LoadActiveEntryFail('error'); + const state = entryReducer(initialState, action); + expect(state.active).toBe(null); + }); it('on CreateEntry, isLoading is true', () => { const entry: NewEntry = { project_id: '1', start_date: '2020-04-21T19:51:36.559000+00:00' }; @@ -29,4 +51,33 @@ describe('entryReducer', () => { expect(state.entryList).toEqual([]); expect(state.isLoading).toEqual(false); }); + + it('on UpdateActiveEntry, isLoading is true', () => { + const action = new actions.UpdateActiveEntry(newEntry); + const state = entryReducer(initialState, action); + + expect(state.isLoading).toEqual(true); + }); + + it('on UpdateActiveEntrySuccess, active is saved in the store', () => { + const currentState: EntryState = { + active: newEntry, + entryList: [], + isLoading: false, + message: '', + }; + const action = new actions.UpdateActiveEntrySuccess(newEntry); + const state = entryReducer(currentState, action); + + expect(state.active).toEqual(newEntry); + expect(state.isLoading).toEqual(false); + }); + + it('on UpdateActiveEntryFail, active to be null', () => { + const action = new actions.UpdateActiveEntryFail('error'); + const state = entryReducer(initialState, action); + + expect(state.active).toBe(null); + 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 index 0dd910a90..67e5b2aa6 100644 --- a/src/app/modules/time-clock/store/entry.reducer.ts +++ b/src/app/modules/time-clock/store/entry.reducer.ts @@ -1,13 +1,15 @@ import { EntryActions, EntryActionTypes } from './entry.actions'; -import { Entry } from '../../shared/models'; +import { Entry, NewEntry } from '../../shared/models'; export interface EntryState { + active: NewEntry; entryList: Entry[]; isLoading: boolean; message: string; } export const initialState = { + active: null, entryList: [], isLoading: false, message: '', @@ -15,6 +17,28 @@ export const initialState = { export const entryReducer = (state: EntryState = initialState, action: EntryActions) => { switch (action.type) { + case EntryActionTypes.LOAD_ACTIVE_ENTRY: { + return { + ...state, + isLoading: true, + }; + } + case EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS: + return { + ...state, + active: action.payload, + isLoading: false, + }; + + case EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL: { + return { + ...state, + active: null, + isLoading: false, + message: 'Something went wrong fetching active entry!', + }; + } + case EntryActionTypes.CREATE_ENTRY: { return { ...state, @@ -33,12 +57,38 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi case EntryActionTypes.CREATE_ENTRY_FAIL: { return { + ...state, entryList: [], isLoading: false, message: action.error, }; } + case EntryActionTypes.UDPATE_ACTIVE_ENTRY: { + return { + ...state, + isLoading: true, + }; + } + + case EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS: { + const activeEntry = { ...state.active, ...action.payload }; + + return { + ...state, + active: activeEntry, + isLoading: false, + }; + } + + case EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL: { + return { + ...state, + active: null, + isLoading: false, + }; + } + default: return state; } diff --git a/src/app/modules/time-clock/store/entry.selectors.ts b/src/app/modules/time-clock/store/entry.selectors.ts new file mode 100644 index 000000000..3ac5beec5 --- /dev/null +++ b/src/app/modules/time-clock/store/entry.selectors.ts @@ -0,0 +1,18 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; + +import { EntryState } from './entry.reducer'; + +const getEntryState = createFeatureSelector('entries'); + +export const allEntries = createSelector(getEntryState, (state: EntryState) => { + return state; +}); + +export const selectProjects = (state) => state.projects.projectList; +export const selectEntries = (state) => state.entries.active; + +export const selectActiveEntry = createSelector(selectProjects, selectEntries, (selectedProject, selectedEntry) => { + if (selectedProject && selectedEntry) { + return selectedProject.find((project) => project.id === selectedEntry.project_id); + } +});