Skip to content

Commit a006e26

Browse files
author
Juan Gabriel Guzman
committed
feat: #333 Allow edit current time entry
1 parent c828171 commit a006e26

File tree

4 files changed

+145
-52
lines changed

4 files changed

+145
-52
lines changed

src/app/modules/shared/components/details-fields/details-fields.component.html

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<form [formGroup]="entryForm" (ngSubmit)="onSubmit()">
2+
<label><input id='isEntryRunning' type="checkbox" (change)="onIsRunningChange($event)" [checked]="isEntryRunning"> I am working on
3+
this</label>
4+
25
<div class="input-group input-group-sm mb-3">
6+
37
<div class="input-group-prepend">
48
<span class="input-group-text span-width">Project</span>
59
</div>
@@ -11,13 +15,14 @@
1115
formControlName="project_id"
1216
>
1317
<option value="" selected="selected"></option>
14-
<option *ngFor="let project of listProjects" value="{{ project.id }}">{{ project.customer_name }} - {{ project.name }}</option>
18+
<option *ngFor="let project of listProjects" value="{{ project.id }}">{{ project.customer_name }}
19+
- {{ project.name }}</option>
1520
</select>
1621
</div>
1722

1823
<div class="input-group input-group-sm mb-3">
1924
<div class="input-group-prepend">
20-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Activity</span>
25+
<span class="input-group-text span-width">Activity</span>
2126
</div>
2227
<select
2328
[class.is-invalid]="activity_id.invalid && activity_id.touched"
@@ -37,7 +42,7 @@
3742

3843
<div class="input-group input-group-sm mb-3">
3944
<div class="input-group-prepend">
40-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Ticket</span>
45+
<span class="input-group-text span-width">Ticket</span>
4146
</div>
4247
<input
4348
formControlName="uri"
@@ -51,7 +56,7 @@
5156

5257
<div class="input-group input-group-sm mb-3">
5358
<div class="input-group-prepend">
54-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Date</span>
59+
<span class="input-group-text span-width">Date</span>
5560
</div>
5661
<input
5762
formControlName="entry_date"
@@ -66,11 +71,11 @@
6671
</div>
6772
<div class="input-group input-group-sm mb-3">
6873
<div class="input-group-prepend">
69-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Time in</span>
74+
<span class="input-group-text span-width">Time in</span>
7075
</div>
7176
<input
72-
[clearIfNotMatch]="true"
73-
[showMaskTyped] = "true"
77+
[clearIfNotMatch]="true"
78+
[showMaskTyped]="true"
7479
[dropSpecialCharacters]="false"
7580
matInput
7681
mask="Hh:m0"
@@ -83,12 +88,13 @@
8388
required
8489
aria-describedby="inputGroup-sizing-sm"
8590
/>
86-
<div class="input-group-prepend">
87-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Time out</span>
91+
<div class="input-group-prepend" *ngIf="!isEntryRunning">
92+
<span class="input-group-text span-width">Time out</span>
8893
</div>
8994
<input
90-
[clearIfNotMatch]="true"
91-
[showMaskTyped] = "true"
95+
*ngIf="!isEntryRunning"
96+
[clearIfNotMatch]="true"
97+
[showMaskTyped]="true"
9298
[dropSpecialCharacters]="false"
9399
matInput
94100
mask="Hh:m0"
@@ -112,11 +118,12 @@
112118

113119
<div class="form-group text-left">
114120
<label for="NotesTextarea">Description</label>
115-
<textarea maxlength="1500" formControlName="description" class="form-control" id="NotesTextarea" rows="3"></textarea>
121+
<textarea maxlength="1500" formControlName="description" class="form-control" id="NotesTextarea"
122+
rows="3"></textarea>
116123
</div>
117124
<div class="modal-footer">
118125
<button type="submit" class="btn btn-primary" [disabled]="!entryForm.valid">Save</button>
119-
<button #closeModal type="button" class="btn btn-secondary" data-dismiss="modal" >
126+
<button #closeModal type="button" class="btn btn-secondary" data-dismiss="modal">
120127
Close
121128
</button>
122129
</div>

src/app/modules/shared/components/details-fields/details-fields.component.spec.ts

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { TechnologiesComponent } from './../technologies/technologies.component'
22
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
33
import { provideMockStore, MockStore } from '@ngrx/store/testing';
44
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5-
import { formatDate } from '@angular/common';
5+
import { DatePipe, formatDate } from '@angular/common';
66

77
import { TechnologyState } from '../../store/technology.reducers';
88
import { allTechnologies } from '../../store/technology.selectors';
@@ -12,6 +12,7 @@ import { getCustomerProjects } from '../../../customer-management/components/pro
1212
import { EntryState } from '../../../time-clock/store/entry.reducer';
1313
import * as entryActions from '../../../time-clock/store/entry.actions';
1414
import { getUpdateError, getCreateError } from 'src/app/modules/time-clock/store/entry.selectors';
15+
import DateTimeFormat = Intl.DateTimeFormat;
1516

1617
describe('DetailsFieldsComponent', () => {
1718
type Merged = TechnologyState & ProjectState & EntryState;
@@ -22,21 +23,23 @@ describe('DetailsFieldsComponent', () => {
2223
let mockProjectsSelector;
2324
let mockEntriesUpdateErrorSelector;
2425
let mockEntriesCreateErrorSelector;
26+
let entryToEdit;
27+
let formValues;
2528

2629
const state = {
2730
projects: {
28-
projects: [{ id: 'id', name: 'name', project_type_id: '' }],
29-
customerProjects: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }],
31+
projects: [{id: 'id', name: 'name', project_type_id: ''}],
32+
customerProjects: [{id: 'id', name: 'name', description: 'description', project_type_id: '123'}],
3033
isLoading: false,
3134
message: '',
3235
projectToEdit: undefined,
3336
},
3437
technologies: {
35-
technologyList: { items: [{ name: 'java' }] },
38+
technologyList: {items: [{name: 'java'}]},
3639
isLoading: false,
3740
},
3841
activities: {
39-
data: [{ id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc' }],
42+
data: [{id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc'}],
4043
isLoading: false,
4144
message: 'Data fetch successfully!',
4245
activityIdToEdit: '',
@@ -61,7 +64,7 @@ describe('DetailsFieldsComponent', () => {
6164
beforeEach(async(() => {
6265
TestBed.configureTestingModule({
6366
declarations: [DetailsFieldsComponent, TechnologiesComponent],
64-
providers: [provideMockStore({ initialState: state })],
67+
providers: [provideMockStore({initialState: state})],
6568
imports: [FormsModule, ReactiveFormsModule],
6669
}).compileComponents();
6770
store = TestBed.inject(MockStore);
@@ -74,28 +77,17 @@ describe('DetailsFieldsComponent', () => {
7477
beforeEach(() => {
7578
fixture = TestBed.createComponent(DetailsFieldsComponent);
7679
component = fixture.componentInstance;
77-
});
78-
79-
it('should create', () => {
80-
expect(component).toBeTruthy();
81-
});
82-
83-
it('should emit ngOnChange without data', () => {
84-
component.entryToEdit = null;
85-
component.ngOnChanges();
86-
expect(component.entryForm.value).toEqual(initialData);
87-
});
88-
89-
it('should emit ngOnChange with new data', () => {
90-
const entryToEdit = {
80+
entryToEdit = {
9181
project_id: '',
9282
activity_id: '',
9383
uri: 'ticketUri',
9484
start_date: null,
9585
end_date: null,
9686
description: '',
87+
technologies: [],
88+
id: 'xyz'
9789
};
98-
const formValue = {
90+
formValues = {
9991
project_id: '',
10092
activity_id: '',
10193
uri: 'ticketUri',
@@ -105,9 +97,24 @@ describe('DetailsFieldsComponent', () => {
10597
description: '',
10698
technology: '',
10799
};
100+
});
101+
102+
it('should create', () => {
103+
expect(component).toBeTruthy();
104+
});
105+
106+
it('should emit ngOnChange without data', () => {
107+
component.entryToEdit = null;
108+
component.ngOnChanges();
109+
expect(component.entryForm.value).toEqual(initialData);
110+
});
111+
112+
it('should emit ngOnChange with new data', () => {
108113
component.entryToEdit = entryToEdit;
114+
109115
component.ngOnChanges();
110-
expect(component.entryForm.value).toEqual(formValue);
116+
117+
expect(component.entryForm.value).toEqual(formValues);
111118
});
112119

113120
it('should emit ngOnChange with new data', () => {
@@ -170,4 +177,79 @@ describe('DetailsFieldsComponent', () => {
170177
};
171178
expect(component.saveEntry.emit).toHaveBeenCalledWith(data);
172179
});
180+
181+
it('when the current entry is not running, then the end hour input should be rendered', () => {
182+
component.isEntryRunning = false;
183+
fixture.detectChanges();
184+
185+
const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour');
186+
expect(endHourInput).toBeDefined();
187+
});
188+
189+
it('when the current entry is running, then the end hour input should not be rendered', () => {
190+
component.isEntryRunning = true;
191+
fixture.detectChanges();
192+
193+
const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour');
194+
expect(endHourInput).toBeNull();
195+
});
196+
197+
it('when creating a new entry, then the new entry should be marked as not running', () => {
198+
component.entryToEdit = null;
199+
200+
expect(component.isEntryRunning).toBeFalse();
201+
});
202+
203+
it('when editing entry that is currently running, then the entry should be marked as running', () => {
204+
component.entryToEdit = {...entryToEdit, running: true};
205+
206+
fixture.componentInstance.ngOnChanges();
207+
208+
expect(component.isEntryRunning).toBeTrue();
209+
});
210+
211+
it('when editing entry that already finished, then the entry should not be marked as running', () => {
212+
component.entryToEdit = {...entryToEdit, running: false};
213+
214+
fixture.componentInstance.ngOnChanges();
215+
216+
expect(component.isEntryRunning).toBeFalse();
217+
});
218+
219+
it('when editing entry that already finished, then the entry should not be marked as running', () => {
220+
component.entryToEdit = {...entryToEdit, running: false};
221+
222+
fixture.componentInstance.ngOnChanges();
223+
224+
expect(component.isEntryRunning).toBeFalse();
225+
});
226+
227+
it('when submitting a entry that is currently running, the end date should not be sent ', () => {
228+
component.isEntryRunning = true;
229+
spyOn(component.saveEntry, 'emit');
230+
231+
component.entryForm.setValue({...formValues, entry_date: '2020-06-11'});
232+
component.onSubmit();
233+
const data = {
234+
project_id: '',
235+
activity_id: '',
236+
technologies: [],
237+
description: '',
238+
start_date: '2020-06-11T00:00',
239+
uri: 'ticketUri',
240+
};
241+
expect(component.saveEntry.emit).toHaveBeenCalledWith(data);
242+
});
243+
244+
it('when disabling current entry is running, then the end hour should be set to the current time', () => {
245+
const datePipe: DatePipe = new DatePipe('en');
246+
const currentTime = datePipe.transform(new Date(), 'HH:mm');
247+
248+
const checkIsEntryRunning: Element = fixture.debugElement.nativeElement.querySelector('#isEntryRunning');
249+
checkIsEntryRunning.dispatchEvent(new Event('change'));
250+
fixture.detectChanges();
251+
252+
const endHourInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_hour');
253+
expect(endHourInput.value).toEqual(currentTime);
254+
});
173255
});

src/app/modules/shared/components/details-fields/details-fields.component.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
import {
2-
Component,
3-
OnChanges,
4-
OnInit,
5-
Input,
6-
Output,
7-
EventEmitter,
8-
ViewChild,
9-
ElementRef,
10-
} from '@angular/core';
1+
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, } from '@angular/core';
112
import { FormBuilder, FormGroup } from '@angular/forms';
12-
import { Store, select } from '@ngrx/store';
3+
import { select, Store } from '@ngrx/store';
134
import { formatDate } from '@angular/common';
145

15-
import { Project, Activity } from '../../models';
6+
import { Activity, Entry, Project } from '../../models';
167
import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer';
178
import { TechnologyState } from '../../store/technology.reducers';
18-
import { LoadActivities, ActivityState, allActivities } from '../../../activities-management/store';
9+
import { ActivityState, allActivities, LoadActivities } from '../../../activities-management/store';
1910
import { getProjects } from '../../../customer-management/components/projects/components/store/project.selectors';
2011
import * as projectActions from '../../../customer-management/components/projects/components/store/project.actions';
2112
import { EntryState } from '../../../time-clock/store/entry.reducer';
2213
import * as entryActions from '../../../time-clock/store/entry.actions';
23-
import { getUpdateError, getCreateError } from 'src/app/modules/time-clock/store/entry.selectors';
14+
import { getCreateError, getUpdateError } from 'src/app/modules/time-clock/store/entry.selectors';
15+
2416
type Merged = TechnologyState & ProjectState & ActivityState & EntryState;
2517

2618
@Component({
@@ -29,7 +21,7 @@ type Merged = TechnologyState & ProjectState & ActivityState & EntryState;
2921
styleUrls: ['./details-fields.component.scss'],
3022
})
3123
export class DetailsFieldsComponent implements OnChanges, OnInit {
32-
@Input() entryToEdit;
24+
@Input() entryToEdit: Entry;
3325
@Input() formType: string;
3426
@Output() saveEntry = new EventEmitter();
3527
@ViewChild('closeModal') closeModal: ElementRef;
@@ -38,8 +30,7 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
3830
isLoading = false;
3931
listProjects: Project[] = [];
4032
activities: Activity[] = [];
41-
keyword = 'name';
42-
showlist: boolean;
33+
isEntryRunning = false;
4334

4435
constructor(private formBuilder: FormBuilder, private store: Store<Merged>) {
4536
this.entryForm = this.formBuilder.group({
@@ -84,6 +75,7 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
8475
}
8576

8677
ngOnChanges(): void {
78+
this.isEntryRunning = this.entryToEdit ? this.entryToEdit.running : false;
8779
if (this.entryToEdit) {
8880
this.selectedTechnologies = this.entryToEdit.technologies;
8981
this.entryForm.setValue({
@@ -156,6 +148,16 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
156148
end_date: `${entryDate}T${this.entryForm.value.end_hour.trim()}`,
157149
uri: this.entryForm.value.uri,
158150
};
151+
if (this.isEntryRunning) {
152+
delete entry.end_date;
153+
}
159154
this.saveEntry.emit(entry);
160155
}
156+
157+
onIsRunningChange(event: any) {
158+
this.isEntryRunning = event.currentTarget.checked;
159+
if (!this.isEntryRunning) {
160+
this.entryForm.patchValue({end_hour: formatDate(new Date(), 'HH:mm', 'en')});
161+
}
162+
}
161163
}

src/app/modules/shared/models/entry.model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface Entry {
2+
running?: boolean;
23
id: string;
34
start_date: Date;
45
end_date: Date;
@@ -8,6 +9,7 @@ export interface Entry {
89
uri?: string;
910
project_id?: string;
1011
owner_email?: string;
12+
description?: string;
1113
}
1214

1315
export interface NewEntry {

0 commit comments

Comments
 (0)