From 280679d820a1875e158b75403892f9dc18c492f7 Mon Sep 17 00:00:00 2001 From: Rene Enriquez Date: Thu, 18 Jun 2020 11:22:59 -0500 Subject: [PATCH] fix: #370 select project component has the abilities to switch or update current project --- package-lock.json | 3 +- package.json | 1 - src/app/app.module.ts | 20 ++--- .../activity-list.component.scss | 2 +- .../project-list-hover.component.html | 45 +++++++++-- .../project-list-hover.component.scss | 25 ++++++ .../project-list-hover.component.spec.ts | 72 ++++++++++++----- .../project-list-hover.component.ts | 80 +++++++++++++------ .../pages/time-clock.component.html | 2 +- .../time-clock/store/entry.effects.spec.ts | 6 +- 10 files changed, 188 insertions(+), 68 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f5282ea9..ab78a2e0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3943,7 +3943,8 @@ "angular-ng-autocomplete": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/angular-ng-autocomplete/-/angular-ng-autocomplete-2.0.1.tgz", - "integrity": "sha512-IlKrUeMM6V0/ipnlXF5WluiWmav7eiIlZWtEoch6eXUGylYCHGNdwRO4Kb2snMQuY/6Kv6tDbRFT51Tihd3JwQ==" + "integrity": "sha512-IlKrUeMM6V0/ipnlXF5WluiWmav7eiIlZWtEoch6eXUGylYCHGNdwRO4Kb2snMQuY/6Kv6tDbRFT51Tihd3JwQ==", + "dev": true }, "ansi-colors": { "version": "3.2.4", diff --git a/package.json b/package.json index 496e7c14e..012979665 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@ngrx/store-devtools": "^9.0.0", "@types/datatables.net-buttons": "^1.4.3", "angular-datatables": "^9.0.2", - "angular-ng-autocomplete": "^2.0.1", "bootstrap": "^4.4.1", "datatables.net": "^1.10.21", "datatables.net-buttons": "^1.6.2", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d15d3cb06..bfa231bf6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ import { NgxMaskModule, IConfig } from 'ngx-mask'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ToastrModule } from 'ngx-toastr'; -import {CommonModule, DatePipe} from '@angular/common'; +import { CommonModule, DatePipe } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -58,14 +58,14 @@ import { CustomerEffects } from './modules/customer-management/store/customer-ma import { EntryEffects } from './modules/time-clock/store/entry.effects'; import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor'; import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe'; -import {TechnologiesComponent} from './modules/shared/components/technologies/technologies.component'; +import { TechnologiesComponent } from './modules/shared/components/technologies/technologies.component'; import { TimeEntriesSummaryComponent } from './modules/time-clock/components/time-entries-summary/time-entries-summary.component'; import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe'; -import {InputLabelComponent} from './modules/shared/components/input-label/input-label.component'; -import {ReportsComponent} from './modules/reports/pages/reports.component'; -import {InputDateComponent} from './modules/shared/components/input-date/input-date.component'; -import {TimeRangeFormComponent} from './modules/reports/components/time-range-form/time-range-form.component'; -import {TimeEntriesTableComponent} from './modules/reports/components/time-entries-table/time-entries-table.component'; +import { InputLabelComponent } from './modules/shared/components/input-label/input-label.component'; +import { ReportsComponent } from './modules/reports/pages/reports.component'; +import { InputDateComponent } from './modules/shared/components/input-date/input-date.component'; +import { TimeRangeFormComponent } from './modules/reports/components/time-range-form/time-range-form.component'; +import { TimeEntriesTableComponent } from './modules/reports/components/time-entries-table/time-entries-table.component'; const maskConfig: Partial = { validation: false, @@ -130,8 +130,8 @@ const maskConfig: Partial = { }), !environment.production ? StoreDevtoolsModule.instrument({ - maxAge: 15, // Retains last 15 states - }) + maxAge: 15, // Retains last 15 states + }) : [], EffectsModule.forRoot([ ProjectEffects, @@ -153,4 +153,4 @@ const maskConfig: Partial = { ], bootstrap: [AppComponent], }) -export class AppModule {} +export class AppModule { } diff --git a/src/app/modules/activities-management/components/activity-list/activity-list.component.scss b/src/app/modules/activities-management/components/activity-list/activity-list.component.scss index d42df4936..4325e5166 100644 --- a/src/app/modules/activities-management/components/activity-list/activity-list.component.scss +++ b/src/app/modules/activities-management/components/activity-list/activity-list.component.scss @@ -1,5 +1,5 @@ @import '../../../../../styles/colors.scss'; .activity-list { - max-height: 400px; overflow: auto; display:inline-block; width: 60%; + overflow: auto; display:inline-block; width: 60%; } 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 421bde069..5030515ae 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,11 +1,42 @@ -
+
- Project -
- + Project +
+ +
+ + + + +
+
+ - + +
+
+ +   + +
+
+
+ + +
+
+
diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.scss b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.scss index 61fd4da8d..91819c1e2 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.scss +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.scss @@ -5,3 +5,28 @@ background-color: $primary; color: white; } + +.btn-select { + padding: 2px 10px; + font-size: x-small; +} + +.container { + font-size: small; + width: 100%; + min-height: 30px; + position: relative; +} + +.autocomplete { + width: 80% +} + +.left-side { + position: absolute; +} + +.right-side { + position: absolute; + right: 15px; +} 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 1624e316c..95ddbea2d 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 @@ -1,14 +1,16 @@ -import {FormBuilder} from '@angular/forms'; -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {provideMockStore, MockStore} from '@ngrx/store/testing'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; - -import {ProjectListHoverComponent} from './project-list-hover.component'; -import {ProjectState} from '../../../customer-management/components/projects/components/store/project.reducer'; -import {getCustomerProjects} from '../../../customer-management/components/projects/components/store/project.selectors'; -import {FilterProjectPipe} from '../../../shared/pipes'; -import {CreateEntry, UpdateEntryRunning} from '../../store/entry.actions'; -import {AutocompleteLibModule} from 'angular-ng-autocomplete'; +import { StopTimeEntryRunning } from './../../store/entry.actions'; +import { FormBuilder } from '@angular/forms'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +import { ProjectListHoverComponent } from './project-list-hover.component'; +import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; +import { getCustomerProjects } from '../../../customer-management/components/projects/components/store/project.selectors'; +import { FilterProjectPipe } from '../../../shared/pipes'; +import { CreateEntry, UpdateEntryRunning } from '../../store/entry.actions'; +import { AutocompleteLibModule } from 'angular-ng-autocomplete'; +import { Subscription } from 'rxjs'; describe('ProjectListHoverComponent', () => { let component: ProjectListHoverComponent; @@ -19,7 +21,7 @@ describe('ProjectListHoverComponent', () => { const state = { projects: { projects: [], - customerProjects: [{id: 'id', name: 'name', description: 'description', project_type_id: '123'}], + customerProjects: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }], isLoading: false, message: '', projectToEdit: undefined, @@ -39,7 +41,7 @@ describe('ProjectListHoverComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ProjectListHoverComponent, FilterProjectPipe], - providers: [FormBuilder, provideMockStore({initialState: state})], + providers: [FormBuilder, provideMockStore({ initialState: state })], imports: [HttpClientTestingModule, AutocompleteLibModule], }).compileComponents(); store = TestBed.inject(MockStore); @@ -56,22 +58,52 @@ describe('ProjectListHoverComponent', () => { expect(component).toBeTruthy(); }); - it('dispatchs a CreateEntry action when activeEntry is null', () => { + it('dispatchs a CreateEntry action on clockIn', () => { component.activeEntry = null; spyOn(store, 'dispatch'); - component.clockIn(); + component.clockIn(1, 'customer', 'project'); expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(CreateEntry)); }); - it('dispatchs a UpdateEntryRunning action when activeEntry is not null', () => { - const entry = {id: '123', project_id: 'p1', start_date: new Date().toISOString()}; - component.activeEntry = entry; + it('dispatchs a UpdateEntryRunning action on updateProject', () => { + component.activeEntry = { id: '123' }; spyOn(store, 'dispatch'); - component.clockIn(); + component.updateProject(1); + + expect(store.dispatch).toHaveBeenCalledWith(new UpdateEntryRunning({ id: component.activeEntry.id, project_id: 1 })); + }); + + it('stop activeEntry and clockIn on swith', () => { + spyOn(component, 'clockIn'); + 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(); + }); + + it('calls unsubscribe on ngDestroy', () => { + component.updateEntrySubscription = new Subscription(); + spyOn(component.updateEntrySubscription, 'unsubscribe'); + + component.ngOnDestroy(); + + expect(component.updateEntrySubscription.unsubscribe).toHaveBeenCalled(); + }); + + it('sets customer name and project name on setSelectedProject', () => { + spyOn(component.projectsForm, 'setValue'); + component.activeEntry = { project_id : 'p1'}; + component.listProjects = [{ id: 'p1', customer_name: 'customer', name: 'xyz' }]; + + component.setSelectedProject(); - expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(UpdateEntryRunning)); + expect(component.projectsForm.setValue) + .toHaveBeenCalledWith({ project_id: 'customer - xyz'}); }); }); 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 763a87edc..f7620edc4 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 @@ -1,7 +1,10 @@ +import { EntryActionTypes } from './../../store/entry.actions'; +import { filter } from 'rxjs/operators'; import { FormGroup, FormBuilder } from '@angular/forms'; import { getProjects } from './../../../customer-management/components/projects/components/store/project.selectors'; -import { Component, OnInit } from '@angular/core'; -import { Store, select } from '@ngrx/store'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Store, select, ActionsSubject } from '@ngrx/store'; +import { Subscription } from 'rxjs'; import { getActiveTimeEntry } from './../../store/entry.selectors'; import { Project } from 'src/app/modules/shared/models'; @@ -14,16 +17,17 @@ import * as entryActions from '../../store/entry.actions'; templateUrl: './project-list-hover.component.html', styleUrls: ['./project-list-hover.component.scss'], }) -export class ProjectListHoverComponent implements OnInit { +export class ProjectListHoverComponent implements OnInit, OnDestroy { + keyword = 'name'; listProjects: Project[] = []; activeEntry; projectsForm: FormGroup; + showClockIn: boolean; + updateEntrySubscription: Subscription; - constructor(private formBuilder: FormBuilder, private store: Store) { - this.projectsForm = this.formBuilder.group({ - project_id: '', - }); + constructor(private formBuilder: FormBuilder, private store: Store, private actionsSubject$: ActionsSubject) { + this.projectsForm = this.formBuilder.group({ project_id: null, }); } ngOnInit(): void { @@ -33,35 +37,63 @@ export class ProjectListHoverComponent implements OnInit { this.listProjects = projects; this.loadActiveTimeEntry(); }); + + this.updateEntrySubscription = this.actionsSubject$.pipe( + filter((action: any) => ( + action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS + ) + ) + ).subscribe((action) => { + this.activeEntry = action.payload; + this.setSelectedProject(); + }); + } - private loadActiveTimeEntry() { + loadActiveTimeEntry() { this.store.dispatch(new entryActions.LoadActiveEntry()); const activeEntry$ = this.store.pipe(select(getActiveTimeEntry)); activeEntry$.subscribe((activeEntry) => { this.activeEntry = activeEntry; if (activeEntry) { - this.projectsForm.setValue({ - project_id: activeEntry.project_id, - }); + this.showClockIn = false; + this.setSelectedProject(); } else { - this.projectsForm.setValue({ - project_id: '-1', - }); + this.showClockIn = true; + this.projectsForm.setValue({ project_id: null }); + } + }); + } + + setSelectedProject() { + this.listProjects.forEach( (project) => { + if (project.id === this.activeEntry.project_id) { + this.projectsForm.setValue( + { project_id: `${project.customer_name} - ${project.name}`, } + ); } }); } - clockIn() { - const selectedProject = this.projectsForm.get('project_id').value; - if (this.activeEntry) { - const entry = { id: this.activeEntry.id, project_id: selectedProject }; - this.store.dispatch(new entryActions.UpdateEntryRunning(entry)); - } else { - const newEntry = { project_id: selectedProject, start_date: new Date().toISOString() }; - this.store.dispatch(new entryActions.CreateEntry(newEntry)); - } - this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1 )); + clockIn(selectedProject, customerName, name) { + const entry = { project_id: selectedProject, start_date: new Date().toISOString() }; + this.store.dispatch(new entryActions.CreateEntry(entry)); + this.projectsForm.setValue( { project_id: `${customerName} - ${name}`, } ); } + updateProject(selectedProject) { + const entry = { id: this.activeEntry.id, project_id: selectedProject }; + this.store.dispatch(new entryActions.UpdateEntryRunning(entry)); + this.store.dispatch(new entryActions.LoadActiveEntry()); + } + + switch(selectedProject, customerName, name) { + this.store.dispatch(new entryActions.StopTimeEntryRunning(this.activeEntry.id)); + this.clockIn(selectedProject, customerName, name); + this.store.dispatch(new entryActions.LoadActiveEntry()); + } + + ngOnDestroy(): void { + this.updateEntrySubscription.unsubscribe(); + } } 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 1eb7f2e00..808d733dc 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.html +++ b/src/app/modules/time-clock/pages/time-clock.component.html @@ -1,6 +1,6 @@ -
+
diff --git a/src/app/modules/time-clock/store/entry.effects.spec.ts b/src/app/modules/time-clock/store/entry.effects.spec.ts index 65dfea5a5..185694c37 100644 --- a/src/app/modules/time-clock/store/entry.effects.spec.ts +++ b/src/app/modules/time-clock/store/entry.effects.spec.ts @@ -82,7 +82,7 @@ describe('TimeEntryActionEffects', () => { }); it('returns a LOAD_ACTIVE_ENTRY_SUCCESS when the entry that is running it is in the same day', async () => { - const activeEntry = {start_date: new Date()}; + const activeEntry = {id: '123', start_date: new Date()}; actions$ = of({type: EntryActionTypes.LOAD_ACTIVE_ENTRY, activeEntry}); const serviceSpy = spyOn(service, 'loadActiveEntry'); serviceSpy.and.returnValue(of(activeEntry)); @@ -92,10 +92,10 @@ describe('TimeEntryActionEffects', () => { }); }); - it('returns a LOAD_ACTIVE_ENTRY_SUCCESS when the entry that is running it is in the same day', async () => { + it('returns a UPDATE_ENTRY when the entry that is running it is in the past', async () => { const startDateInPast = new Date(); startDateInPast.setDate(startDateInPast.getDate() - 5); - const activeEntry = {start_date: startDateInPast}; + const activeEntry = {id: '123', start_date: startDateInPast}; actions$ = of({type: EntryActionTypes.LOAD_ACTIVE_ENTRY, activeEntry}); const serviceSpy = spyOn(service, 'loadActiveEntry'); serviceSpy.and.returnValue(of(activeEntry));