Skip to content

Commit f835f19

Browse files
authored
Merge pull request #398 from ioet/370-select-project-component
fix: #370 select project component has the abilities to switch or upd…
2 parents c1ee894 + f271f64 commit f835f19

File tree

10 files changed

+188
-68
lines changed

10 files changed

+188
-68
lines changed

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"@ngrx/store-devtools": "^9.0.0",
2626
"@types/datatables.net-buttons": "^1.4.3",
2727
"angular-datatables": "^9.0.2",
28-
"angular-ng-autocomplete": "^2.0.1",
2928
"bootstrap": "^4.4.1",
3029
"datatables.net": "^1.10.21",
3130
"datatables.net-buttons": "^1.6.2",

src/app/app.module.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NgxMaskModule, IConfig } from 'ngx-mask';
22
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
33
import { ToastrModule } from 'ngx-toastr';
4-
import {CommonModule, DatePipe} from '@angular/common';
4+
import { CommonModule, DatePipe } from '@angular/common';
55
import { BrowserModule } from '@angular/platform-browser';
66
import { NgModule } from '@angular/core';
77
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -58,14 +58,14 @@ import { CustomerEffects } from './modules/customer-management/store/customer-ma
5858
import { EntryEffects } from './modules/time-clock/store/entry.effects';
5959
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';
6060
import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe';
61-
import {TechnologiesComponent} from './modules/shared/components/technologies/technologies.component';
61+
import { TechnologiesComponent } from './modules/shared/components/technologies/technologies.component';
6262
import { TimeEntriesSummaryComponent } from './modules/time-clock/components/time-entries-summary/time-entries-summary.component';
6363
import { TimeDetailsPipe } from './modules/time-clock/pipes/time-details.pipe';
64-
import {InputLabelComponent} from './modules/shared/components/input-label/input-label.component';
65-
import {ReportsComponent} from './modules/reports/pages/reports.component';
66-
import {InputDateComponent} from './modules/shared/components/input-date/input-date.component';
67-
import {TimeRangeFormComponent} from './modules/reports/components/time-range-form/time-range-form.component';
68-
import {TimeEntriesTableComponent} from './modules/reports/components/time-entries-table/time-entries-table.component';
64+
import { InputLabelComponent } from './modules/shared/components/input-label/input-label.component';
65+
import { ReportsComponent } from './modules/reports/pages/reports.component';
66+
import { InputDateComponent } from './modules/shared/components/input-date/input-date.component';
67+
import { TimeRangeFormComponent } from './modules/reports/components/time-range-form/time-range-form.component';
68+
import { TimeEntriesTableComponent } from './modules/reports/components/time-entries-table/time-entries-table.component';
6969
import { DialogComponent } from './modules/shared/components/dialog/dialog.component';
7070

7171
const maskConfig: Partial<IConfig> = {
@@ -132,8 +132,8 @@ const maskConfig: Partial<IConfig> = {
132132
}),
133133
!environment.production
134134
? StoreDevtoolsModule.instrument({
135-
maxAge: 15, // Retains last 15 states
136-
})
135+
maxAge: 15, // Retains last 15 states
136+
})
137137
: [],
138138
EffectsModule.forRoot([
139139
ProjectEffects,
@@ -155,4 +155,4 @@ const maskConfig: Partial<IConfig> = {
155155
],
156156
bootstrap: [AppComponent],
157157
})
158-
export class AppModule {}
158+
export class AppModule { }
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@import '../../../../../styles/colors.scss';
22

33
.activity-list {
4-
max-height: 400px; overflow: auto; display:inline-block; width: 60%;
4+
overflow: auto; display:inline-block; width: 60%;
55
}
Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
1-
<form [formGroup]="projectsForm" (ngSubmit)="clockIn()">
1+
<form [formGroup]="projectsForm">
22
<div class="input-group input-group-sm mb-3">
33
<div class="input-group-prepend">
4-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Project</span>
5-
</div>
6-
<select (change)="clockIn()" formControlName="project_id" class="form-control">
7-
<option value="-1"></option>
8-
<option *ngFor="let project of listProjects" value="{{project.id}}">{{project.customer_name}} - {{project.name}}</option>
9-
</select>
4+
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Project</span>
5+
</div>
6+
7+
<div class="autocomplete">
8+
<ng-autocomplete
9+
formControlName="project_id"
10+
[data]="listProjects"
11+
[searchKeyword]="keyword"
12+
[initialValue]=""
13+
historyIdentifier="projectsSelected"
14+
notFoundText="No projects found"
15+
placeHolder="Enter the project name"
16+
[itemTemplate]="itemTemplate"
17+
(closed)="loadActiveTimeEntry()"
18+
[notFoundTemplate]="notFoundTemplate">
19+
</ng-autocomplete>
20+
21+
<ng-template #itemTemplate let-item>
22+
<div class="container" style="cursor:none">
23+
<div class="left-side">
24+
<span [innerHTML]="item.customer_name"></span> -
25+
<strong><span [innerHTML]="item.name"></span></strong>
26+
</div>
27+
<div class="right-side">
28+
<button *ngIf="showClockIn" class="btn btn-sm btn-primary btn-select" (click)="clockIn(item.id, item.customer_name, item.name)">Clock In</button>
29+
<button *ngIf="!showClockIn" class="btn btn-sm btn-primary btn-select"
30+
(click)="switch(item.id, item.customer_name, item.name)">Switch</button>&nbsp;
31+
<button *ngIf="!showClockIn" class="btn btn-sm btn-warning btn-select" (click)="updateProject(item.id)">Update
32+
project</button>
33+
</div>
34+
</div>
35+
</ng-template>
36+
37+
<ng-template #notFoundTemplate let-notFound>
38+
<div [innerHTML]="notFound"></div>
39+
</ng-template>
40+
</div>
1041
</div>
1142
</form>

src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.scss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,28 @@
55
background-color: $primary;
66
color: white;
77
}
8+
9+
.btn-select {
10+
padding: 2px 10px;
11+
font-size: x-small;
12+
}
13+
14+
.container {
15+
font-size: small;
16+
width: 100%;
17+
min-height: 30px;
18+
position: relative;
19+
}
20+
21+
.autocomplete {
22+
width: 80%
23+
}
24+
25+
.left-side {
26+
position: absolute;
27+
}
28+
29+
.right-side {
30+
position: absolute;
31+
right: 15px;
32+
}

src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import {FormBuilder} from '@angular/forms';
2-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
3-
import {provideMockStore, MockStore} from '@ngrx/store/testing';
4-
import {HttpClientTestingModule} from '@angular/common/http/testing';
5-
6-
import {ProjectListHoverComponent} from './project-list-hover.component';
7-
import {ProjectState} from '../../../customer-management/components/projects/components/store/project.reducer';
8-
import {getCustomerProjects} from '../../../customer-management/components/projects/components/store/project.selectors';
9-
import {FilterProjectPipe} from '../../../shared/pipes';
10-
import {CreateEntry, UpdateEntryRunning} from '../../store/entry.actions';
11-
import {AutocompleteLibModule} from 'angular-ng-autocomplete';
1+
import { StopTimeEntryRunning } from './../../store/entry.actions';
2+
import { FormBuilder } from '@angular/forms';
3+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
4+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
5+
import { HttpClientTestingModule } from '@angular/common/http/testing';
6+
7+
import { ProjectListHoverComponent } from './project-list-hover.component';
8+
import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer';
9+
import { getCustomerProjects } from '../../../customer-management/components/projects/components/store/project.selectors';
10+
import { FilterProjectPipe } from '../../../shared/pipes';
11+
import { CreateEntry, UpdateEntryRunning } from '../../store/entry.actions';
12+
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
13+
import { Subscription } from 'rxjs';
1214

1315
describe('ProjectListHoverComponent', () => {
1416
let component: ProjectListHoverComponent;
@@ -19,7 +21,7 @@ describe('ProjectListHoverComponent', () => {
1921
const state = {
2022
projects: {
2123
projects: [],
22-
customerProjects: [{id: 'id', name: 'name', description: 'description', project_type_id: '123'}],
24+
customerProjects: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }],
2325
isLoading: false,
2426
message: '',
2527
projectToEdit: undefined,
@@ -39,7 +41,7 @@ describe('ProjectListHoverComponent', () => {
3941
beforeEach(async(() => {
4042
TestBed.configureTestingModule({
4143
declarations: [ProjectListHoverComponent, FilterProjectPipe],
42-
providers: [FormBuilder, provideMockStore({initialState: state})],
44+
providers: [FormBuilder, provideMockStore({ initialState: state })],
4345
imports: [HttpClientTestingModule, AutocompleteLibModule],
4446
}).compileComponents();
4547
store = TestBed.inject(MockStore);
@@ -56,22 +58,52 @@ describe('ProjectListHoverComponent', () => {
5658
expect(component).toBeTruthy();
5759
});
5860

59-
it('dispatchs a CreateEntry action when activeEntry is null', () => {
61+
it('dispatchs a CreateEntry action on clockIn', () => {
6062
component.activeEntry = null;
6163
spyOn(store, 'dispatch');
6264

63-
component.clockIn();
65+
component.clockIn(1, 'customer', 'project');
6466

6567
expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(CreateEntry));
6668
});
6769

68-
it('dispatchs a UpdateEntryRunning action when activeEntry is not null', () => {
69-
const entry = {id: '123', project_id: 'p1', start_date: new Date().toISOString()};
70-
component.activeEntry = entry;
70+
it('dispatchs a UpdateEntryRunning action on updateProject', () => {
71+
component.activeEntry = { id: '123' };
7172
spyOn(store, 'dispatch');
7273

73-
component.clockIn();
74+
component.updateProject(1);
75+
76+
expect(store.dispatch).toHaveBeenCalledWith(new UpdateEntryRunning({ id: component.activeEntry.id, project_id: 1 }));
77+
});
78+
79+
it('stop activeEntry and clockIn on swith', () => {
80+
spyOn(component, 'clockIn');
81+
spyOn(store, 'dispatch');
82+
component.activeEntry = { id: '123' };
83+
84+
component.switch(1, 'customer', 'project');
85+
86+
expect(store.dispatch).toHaveBeenCalledWith(new StopTimeEntryRunning(component.activeEntry.id));
87+
expect(component.clockIn).toHaveBeenCalled();
88+
});
89+
90+
it('calls unsubscribe on ngDestroy', () => {
91+
component.updateEntrySubscription = new Subscription();
92+
spyOn(component.updateEntrySubscription, 'unsubscribe');
93+
94+
component.ngOnDestroy();
95+
96+
expect(component.updateEntrySubscription.unsubscribe).toHaveBeenCalled();
97+
});
98+
99+
it('sets customer name and project name on setSelectedProject', () => {
100+
spyOn(component.projectsForm, 'setValue');
101+
component.activeEntry = { project_id : 'p1'};
102+
component.listProjects = [{ id: 'p1', customer_name: 'customer', name: 'xyz' }];
103+
104+
component.setSelectedProject();
74105

75-
expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(UpdateEntryRunning));
106+
expect(component.projectsForm.setValue)
107+
.toHaveBeenCalledWith({ project_id: 'customer - xyz'});
76108
});
77109
});
Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { EntryActionTypes } from './../../store/entry.actions';
2+
import { filter } from 'rxjs/operators';
13
import { FormGroup, FormBuilder } from '@angular/forms';
24
import { getProjects } from './../../../customer-management/components/projects/components/store/project.selectors';
3-
import { Component, OnInit } from '@angular/core';
4-
import { Store, select } from '@ngrx/store';
5+
import { Component, OnInit, OnDestroy } from '@angular/core';
6+
import { Store, select, ActionsSubject } from '@ngrx/store';
7+
import { Subscription } from 'rxjs';
58

69
import { getActiveTimeEntry } from './../../store/entry.selectors';
710
import { Project } from 'src/app/modules/shared/models';
@@ -14,16 +17,17 @@ import * as entryActions from '../../store/entry.actions';
1417
templateUrl: './project-list-hover.component.html',
1518
styleUrls: ['./project-list-hover.component.scss'],
1619
})
17-
export class ProjectListHoverComponent implements OnInit {
20+
export class ProjectListHoverComponent implements OnInit, OnDestroy {
1821

22+
keyword = 'name';
1923
listProjects: Project[] = [];
2024
activeEntry;
2125
projectsForm: FormGroup;
26+
showClockIn: boolean;
27+
updateEntrySubscription: Subscription;
2228

23-
constructor(private formBuilder: FormBuilder, private store: Store<ProjectState>) {
24-
this.projectsForm = this.formBuilder.group({
25-
project_id: '',
26-
});
29+
constructor(private formBuilder: FormBuilder, private store: Store<ProjectState>, private actionsSubject$: ActionsSubject) {
30+
this.projectsForm = this.formBuilder.group({ project_id: null, });
2731
}
2832

2933
ngOnInit(): void {
@@ -33,35 +37,63 @@ export class ProjectListHoverComponent implements OnInit {
3337
this.listProjects = projects;
3438
this.loadActiveTimeEntry();
3539
});
40+
41+
this.updateEntrySubscription = this.actionsSubject$.pipe(
42+
filter((action: any) => (
43+
action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS
44+
)
45+
)
46+
).subscribe((action) => {
47+
this.activeEntry = action.payload;
48+
this.setSelectedProject();
49+
});
50+
3651
}
3752

38-
private loadActiveTimeEntry() {
53+
loadActiveTimeEntry() {
3954
this.store.dispatch(new entryActions.LoadActiveEntry());
4055
const activeEntry$ = this.store.pipe(select(getActiveTimeEntry));
4156
activeEntry$.subscribe((activeEntry) => {
4257
this.activeEntry = activeEntry;
4358
if (activeEntry) {
44-
this.projectsForm.setValue({
45-
project_id: activeEntry.project_id,
46-
});
59+
this.showClockIn = false;
60+
this.setSelectedProject();
4761
} else {
48-
this.projectsForm.setValue({
49-
project_id: '-1',
50-
});
62+
this.showClockIn = true;
63+
this.projectsForm.setValue({ project_id: null });
64+
}
65+
});
66+
}
67+
68+
setSelectedProject() {
69+
this.listProjects.forEach( (project) => {
70+
if (project.id === this.activeEntry.project_id) {
71+
this.projectsForm.setValue(
72+
{ project_id: `${project.customer_name} - ${project.name}`, }
73+
);
5174
}
5275
});
5376
}
5477

55-
clockIn() {
56-
const selectedProject = this.projectsForm.get('project_id').value;
57-
if (this.activeEntry) {
58-
const entry = { id: this.activeEntry.id, project_id: selectedProject };
59-
this.store.dispatch(new entryActions.UpdateEntryRunning(entry));
60-
} else {
61-
const newEntry = { project_id: selectedProject, start_date: new Date().toISOString() };
62-
this.store.dispatch(new entryActions.CreateEntry(newEntry));
63-
}
64-
this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1 ));
78+
clockIn(selectedProject, customerName, name) {
79+
const entry = { project_id: selectedProject, start_date: new Date().toISOString() };
80+
this.store.dispatch(new entryActions.CreateEntry(entry));
81+
this.projectsForm.setValue( { project_id: `${customerName} - ${name}`, } );
6582
}
6683

84+
updateProject(selectedProject) {
85+
const entry = { id: this.activeEntry.id, project_id: selectedProject };
86+
this.store.dispatch(new entryActions.UpdateEntryRunning(entry));
87+
this.store.dispatch(new entryActions.LoadActiveEntry());
88+
}
89+
90+
switch(selectedProject, customerName, name) {
91+
this.store.dispatch(new entryActions.StopTimeEntryRunning(this.activeEntry.id));
92+
this.clockIn(selectedProject, customerName, name);
93+
this.store.dispatch(new entryActions.LoadActiveEntry());
94+
}
95+
96+
ngOnDestroy(): void {
97+
this.updateEntrySubscription.unsubscribe();
98+
}
6799
}

src/app/modules/time-clock/pages/time-clock.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<app-time-entries-summary></app-time-entries-summary>
22

3-
<div style="width: 60%;">
3+
<div style="width: 70%;">
44

55
<div class="row pb-4">
66
<div class="col-12">

0 commit comments

Comments
 (0)