Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
fix: #370 select project component has the abilities to switch or upd…
…ate current project
  • Loading branch information
enriquezrene committed Jun 18, 2020
commit 280679d820a1875e158b75403892f9dc18c492f7
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 10 additions & 10 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<IConfig> = {
validation: false,
Expand Down Expand Up @@ -130,8 +130,8 @@ const maskConfig: Partial<IConfig> = {
}),
!environment.production
? StoreDevtoolsModule.instrument({
maxAge: 15, // Retains last 15 states
})
maxAge: 15, // Retains last 15 states
})
: [],
EffectsModule.forRoot([
ProjectEffects,
Expand All @@ -153,4 +153,4 @@ const maskConfig: Partial<IConfig> = {
],
bootstrap: [AppComponent],
})
export class AppModule {}
export class AppModule { }
Original file line number Diff line number Diff line change
@@ -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%;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
<form [formGroup]="projectsForm" (ngSubmit)="clockIn()">
<form [formGroup]="projectsForm">
<div class="input-group input-group-sm mb-3">
<div class="input-group-prepend">
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Project</span>
</div>
<select (change)="clockIn()" formControlName="project_id" class="form-control">
<option value="-1"></option>
<option *ngFor="let project of listProjects" value="{{project.id}}">{{project.customer_name}} - {{project.name}}</option>
</select>
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Project</span>
</div>

<div class="autocomplete">
<ng-autocomplete
formControlName="project_id"
[data]="listProjects"
[searchKeyword]="keyword"
[initialValue]=""
historyIdentifier="projectsSelected"
notFoundText="No projects found"
placeHolder="Enter the project name"
[itemTemplate]="itemTemplate"
(closed)="loadActiveTimeEntry()"
[notFoundTemplate]="notFoundTemplate">
</ng-autocomplete>

<ng-template #itemTemplate let-item>
<div class="container" style="cursor:none">
<div class="left-side">
<span [innerHTML]="item.customer_name"></span> -
<strong><span [innerHTML]="item.name"></span></strong>
</div>
<div class="right-side">
<button *ngIf="showClockIn" class="btn btn-sm btn-primary btn-select" (click)="clockIn(item.id, item.customer_name, item.name)">Clock In</button>
<button *ngIf="!showClockIn" class="btn btn-sm btn-primary btn-select"
(click)="switch(item.id, item.customer_name, item.name)">Switch</button>&nbsp;
<button *ngIf="!showClockIn" class="btn btn-sm btn-warning btn-select" (click)="updateProject(item.id)">Update
project</button>
</div>
</div>
</ng-template>

<ng-template #notFoundTemplate let-notFound>
<div [innerHTML]="notFound"></div>
</ng-template>
</div>
</div>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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'});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<ProjectState>) {
this.projectsForm = this.formBuilder.group({
project_id: '',
});
constructor(private formBuilder: FormBuilder, private store: Store<ProjectState>, private actionsSubject$: ActionsSubject) {
this.projectsForm = this.formBuilder.group({ project_id: null, });
}

ngOnInit(): void {
Expand All @@ -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();
}
}
2 changes: 1 addition & 1 deletion src/app/modules/time-clock/pages/time-clock.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<app-time-entries-summary></app-time-entries-summary>

<div style="width: 60%;">
<div style="width: 70%;">

<div class="row pb-4">
<div class="col-12">
Expand Down
Loading