Skip to content

Commit f3c5b4f

Browse files
author
Juan Gabriel Guzman
committed
feat: #385 Allowing restart entries
1 parent 6e9edb1 commit f3c5b4f

13 files changed

+324
-125
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<form [formGroup]="entryForm" (ngSubmit)="onSubmit()">
2-
<label><input id='isEntryRunning' type="checkbox" (change)="onIsRunningChange($event)" [checked]="isEntryRunning"> I am working on
2+
<label><input id='isEntryRunning' type="checkbox" (change)="onGoingToWorkOnThisChange($event)" [checked]="goingToWorkOnThis"> I am working on
33
this</label>
44

55
<div class="input-group input-group-sm mb-3">
@@ -88,11 +88,11 @@
8888
required
8989
aria-describedby="inputGroup-sizing-sm"
9090
/>
91-
<div class="input-group-prepend" *ngIf="!isEntryRunning">
91+
<div class="input-group-prepend" *ngIf="!goingToWorkOnThis">
9292
<span class="input-group-text span-width">Time out</span>
9393
</div>
9494
<input
95-
*ngIf="!isEntryRunning"
95+
*ngIf="!goingToWorkOnThis"
9696
[clearIfNotMatch]="true"
9797
[showMaskTyped]="true"
9898
[dropSpecialCharacters]="false"

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

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EntryActionTypes } from './../../../time-clock/store/entry.actions';
22
import { TechnologiesComponent } from './../technologies/technologies.component';
33
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
4-
import { provideMockStore, MockStore } from '@ngrx/store/testing';
4+
import { MockStore, provideMockStore } from '@ngrx/store/testing';
55
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
66
import { DatePipe, formatDate } from '@angular/common';
77
import { ActionsSubject } from '@ngrx/store';
@@ -13,7 +13,8 @@ import { ProjectState } from '../../../customer-management/components/projects/c
1313
import { getCustomerProjects } from '../../../customer-management/components/projects/components/store/project.selectors';
1414
import { EntryState } from '../../../time-clock/store/entry.reducer';
1515
import * as entryActions from '../../../time-clock/store/entry.actions';
16-
import { getUpdateError, getCreateError } from 'src/app/modules/time-clock/store/entry.selectors';
16+
import { getCreateError, getUpdateError } from 'src/app/modules/time-clock/store/entry.selectors';
17+
import { SaveEntryEvent } from './save-entry-event';
1718

1819
describe('DetailsFieldsComponent', () => {
1920
type Merged = TechnologyState & ProjectState & EntryState;
@@ -30,18 +31,18 @@ describe('DetailsFieldsComponent', () => {
3031

3132
const state = {
3233
projects: {
33-
projects: [{ id: 'id', name: 'name', project_type_id: '' }],
34-
customerProjects: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }],
34+
projects: [{id: 'id', name: 'name', project_type_id: ''}],
35+
customerProjects: [{id: 'id', name: 'name', description: 'description', project_type_id: '123'}],
3536
isLoading: false,
3637
message: '',
3738
projectToEdit: undefined,
3839
},
3940
technologies: {
40-
technologyList: { items: [{ name: 'java' }] },
41+
technologyList: {items: [{name: 'java'}]},
4142
isLoading: false,
4243
},
4344
activities: {
44-
data: [{ id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc' }],
45+
data: [{id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc'}],
4546
isLoading: false,
4647
message: 'Data fetch successfully!',
4748
activityIdToEdit: '',
@@ -66,7 +67,7 @@ describe('DetailsFieldsComponent', () => {
6667
beforeEach(async(() => {
6768
TestBed.configureTestingModule({
6869
declarations: [DetailsFieldsComponent, TechnologiesComponent],
69-
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
70+
providers: [provideMockStore({initialState: state}), {provide: ActionsSubject, useValue: actionSub}],
7071
imports: [FormsModule, ReactiveFormsModule],
7172
}).compileComponents();
7273
store = TestBed.inject(MockStore);
@@ -106,8 +107,8 @@ describe('DetailsFieldsComponent', () => {
106107
});
107108

108109
[
109-
{ actionType: EntryActionTypes.CREATE_ENTRY_SUCCESS },
110-
{ actionType: EntryActionTypes.UPDATE_ENTRY_SUCCESS },
110+
{actionType: EntryActionTypes.CREATE_ENTRY_SUCCESS},
111+
{actionType: EntryActionTypes.UPDATE_ENTRY_SUCCESS},
111112
].map((param) => {
112113
it(`cleanForm after an action type ${param.actionType} is received`, () => {
113114
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
@@ -217,84 +218,91 @@ describe('DetailsFieldsComponent', () => {
217218
technology: '',
218219
});
219220
component.onSubmit();
220-
const data = {
221-
project_id: '',
222-
activity_id: '',
223-
technologies: [],
224-
description: '',
225-
start_date: '2020-02-05T00:00:11',
226-
end_date: '2020-02-05T00:01:01',
227-
uri: '',
221+
const data: SaveEntryEvent = {
222+
entry: {
223+
project_id: '',
224+
activity_id: '',
225+
technologies: [],
226+
description: '',
227+
start_date: '2020-02-05T00:00:11',
228+
end_date: '2020-02-05T00:01:01',
229+
uri: ''
230+
},
231+
shouldRestartEntry: false
228232
};
229233
expect(component.saveEntry.emit).toHaveBeenCalledWith(data);
230234
});
231235

232236
it('when the current entry is not running, then the end hour input should be rendered', () => {
233-
component.isEntryRunning = false;
237+
component.goingToWorkOnThis = false;
234238
fixture.detectChanges();
235239

236240
const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour');
237241
expect(endHourInput).toBeDefined();
238242
});
239243

240244
it('when the current entry is running, then the end hour input should not be rendered', () => {
241-
component.isEntryRunning = true;
245+
component.goingToWorkOnThis = true;
242246
fixture.detectChanges();
243247

244248
const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour');
245249
expect(endHourInput).toBeNull();
246250
});
247251

248-
it('when creating a new entry, then the new entry should be marked as not running', () => {
252+
it('when creating a new entry, then the new entry should be marked as not run', () => {
249253
component.entryToEdit = null;
250254

251-
expect(component.isEntryRunning).toBeFalse();
255+
expect(component.goingToWorkOnThis).toBeFalse();
252256
});
253257

254-
it('when editing entry that is currently running, then the entry should be marked as running', () => {
255-
component.entryToEdit = { ...entryToEdit, running: true };
258+
it('when editing entry that is currently running, then the entry should be marked as I am working on this', () => {
259+
component.entryToEdit = {...entryToEdit, running: true};
256260

257261
fixture.componentInstance.ngOnChanges();
258262

259-
expect(component.isEntryRunning).toBeTrue();
263+
expect(component.goingToWorkOnThis).toBeTrue();
260264
});
261265

262266
it('when editing entry that already finished, then the entry should not be marked as running', () => {
263-
component.entryToEdit = { ...entryToEdit, running: false };
267+
component.entryToEdit = {...entryToEdit, running: false};
264268

265269
fixture.componentInstance.ngOnChanges();
266270

267-
expect(component.isEntryRunning).toBeFalse();
271+
expect(component.goingToWorkOnThis).toBeFalse();
268272
});
269273

270274
it('when editing entry that already finished, then the entry should not be marked as running', () => {
271-
component.entryToEdit = { ...entryToEdit, running: false };
275+
component.entryToEdit = {...entryToEdit, running: false};
272276

273277
fixture.componentInstance.ngOnChanges();
274278

275-
expect(component.isEntryRunning).toBeFalse();
279+
expect(component.goingToWorkOnThis).toBeFalse();
276280
});
277281

278282
it('when submitting a entry that is currently running, the end date should not be sent ', () => {
279-
component.isEntryRunning = true;
283+
component.goingToWorkOnThis = true;
280284
spyOn(component, 'getElapsedSeconds').and.returnValue('10');
281285
spyOn(component.saveEntry, 'emit');
282286

283-
component.entryForm.setValue({ ...formValues, entry_date: '2020-06-11' });
287+
component.entryForm.setValue({...formValues, entry_date: '2020-06-11'});
284288
component.onSubmit();
285-
const data = {
286-
project_id: '',
287-
activity_id: '',
288-
technologies: [],
289-
description: '',
290-
start_date: '2020-06-11T00:00:10',
291-
uri: 'ticketUri',
289+
const data: SaveEntryEvent = {
290+
entry: {
291+
project_id: '',
292+
activity_id: '',
293+
technologies: [],
294+
description: '',
295+
start_date: '2020-06-11T00:00:10',
296+
uri: 'ticketUri',
297+
},
298+
shouldRestartEntry: false
292299
};
293300

294-
expect(component.saveEntry.emit).toHaveBeenCalledWith(data);
301+
expect(component.saveEntry.emit
302+
).toHaveBeenCalledWith(data);
295303
});
296304

297-
it('when disabling current entry is running, then the end hour should be set to the current time', () => {
305+
it('when disabling going to work on this, then the end hour should be set to the current time', () => {
298306
const datePipe: DatePipe = new DatePipe('en');
299307
const currentTime = datePipe.transform(new Date(), 'HH:mm');
300308

@@ -305,4 +313,15 @@ describe('DetailsFieldsComponent', () => {
305313
const endHourInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_hour');
306314
expect(endHourInput.value).toEqual(currentTime);
307315
});
316+
317+
it('given going to work on this and the entry is not currently running, when submitting form then the entry should be restarted', () => {
318+
component.goingToWorkOnThis = false;
319+
component.entryToEdit = {...entryToEdit, running: false};
320+
321+
const checkIsEntryRunning: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#isEntryRunning');
322+
checkIsEntryRunning.click();
323+
fixture.detectChanges();
324+
325+
expect(component.shouldRestartEntry).toBeTrue();
326+
});
308327
});

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { filter } from 'rxjs/operators';
33
import { NumberFormatter } from './../../formatters/number.formatter';
44
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, } from '@angular/core';
55
import { FormBuilder, FormGroup } from '@angular/forms';
6-
import { select, Store, ActionsSubject } from '@ngrx/store';
6+
import { ActionsSubject, select, Store } from '@ngrx/store';
77
import { formatDate } from '@angular/common';
88

99
import { Activity, Entry, Project } from '../../models';
@@ -15,6 +15,7 @@ import * as projectActions from '../../../customer-management/components/project
1515
import { EntryState } from '../../../time-clock/store/entry.reducer';
1616
import * as entryActions from '../../../time-clock/store/entry.actions';
1717
import { getCreateError, getUpdateError } from 'src/app/modules/time-clock/store/entry.selectors';
18+
import { SaveEntryEvent } from './save-entry-event';
1819

1920
type Merged = TechnologyState & ProjectState & ActivityState & EntryState;
2021

@@ -25,15 +26,15 @@ type Merged = TechnologyState & ProjectState & ActivityState & EntryState;
2526
})
2627
export class DetailsFieldsComponent implements OnChanges, OnInit {
2728
@Input() entryToEdit: Entry;
28-
@Input() formType: string;
29-
@Output() saveEntry = new EventEmitter();
29+
@Output() saveEntry = new EventEmitter<SaveEntryEvent>();
3030
@ViewChild('closeModal') closeModal: ElementRef;
3131
entryForm: FormGroup;
3232
selectedTechnologies: string[] = [];
3333
isLoading = false;
3434
listProjects: Project[] = [];
3535
activities: Activity[] = [];
36-
isEntryRunning = false;
36+
goingToWorkOnThis = false;
37+
shouldRestartEntry = false;
3738

3839
constructor(private formBuilder: FormBuilder, private store: Store<Merged>, private actionsSubject$: ActionsSubject) {
3940
this.entryForm = this.formBuilder.group({
@@ -87,7 +88,7 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
8788
}
8889

8990
ngOnChanges(): void {
90-
this.isEntryRunning = this.entryToEdit ? this.entryToEdit.running : false;
91+
this.goingToWorkOnThis = this.entryToEdit ? this.entryToEdit.running : false;
9192
if (this.entryToEdit) {
9293
this.selectedTechnologies = this.entryToEdit.technologies;
9394
this.entryForm.setValue({
@@ -160,10 +161,10 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
160161
end_date: `${entryDate}T${this.entryForm.value.end_hour.trim()}:01`,
161162
uri: this.entryForm.value.uri,
162163
};
163-
if (this.isEntryRunning) {
164+
if (this.goingToWorkOnThis) {
164165
delete entry.end_date;
165166
}
166-
this.saveEntry.emit(entry);
167+
this.saveEntry.emit({entry, shouldRestartEntry: this.shouldRestartEntry});
167168
}
168169

169170
getElapsedSeconds(date: Date): string {
@@ -175,10 +176,11 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
175176
}
176177
}
177178

178-
onIsRunningChange(event: any) {
179-
this.isEntryRunning = event.currentTarget.checked;
180-
if (!this.isEntryRunning) {
181-
this.entryForm.patchValue({ end_hour: formatDate(new Date(), 'HH:mm', 'en') });
179+
onGoingToWorkOnThisChange(event: any) {
180+
this.goingToWorkOnThis = event.currentTarget.checked;
181+
if (!this.goingToWorkOnThis) {
182+
this.entryForm.patchValue({end_hour: formatDate(new Date(), 'HH:mm', 'en')});
182183
}
184+
this.shouldRestartEntry = !this.entryToEdit?.running && this.goingToWorkOnThis;
183185
}
184186
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface SaveEntryEvent {
2+
entry: any;
3+
shouldRestartEntry: boolean;
4+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export interface Entry {
22
running?: boolean;
33
id: string;
44
start_date: Date;
5-
end_date: Date;
5+
end_date?: Date;
66
activity_id?: string;
77
technologies: string[];
88
uri?: string;

src/app/modules/time-clock/services/entry.service.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,13 @@ describe('EntryService', () => {
104104
expect(loadEntryRequest.request.params.get('end_date')).toBe(pipe.transform(today, EntryService.TIME_ENTRIES_DATE_TIME_FORMAT));
105105
expect(loadEntryRequest.request.params.get('user_id')).toEqual('123');
106106
});
107+
108+
it('when restarting entry, a POST is triggered', () => {
109+
const entry = 'entryId';
110+
111+
service.restartEntry(entry).subscribe();
112+
113+
const restartEntryRequest = httpMock.expectOne( `${service.baseUrl}/${entry}/restart`);
114+
expect(restartEntryRequest.request.method).toBe('POST');
115+
});
107116
});

src/app/modules/time-clock/services/entry.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Observable } from 'rxjs';
66
import { environment } from './../../../../environments/environment';
77
import { TimeEntriesTimeRange } from '../models/time-entries-time-range';
88
import { DatePipe } from '@angular/common';
9+
import { Entry } from '../../shared/models';
910

1011
@Injectable({
1112
providedIn: 'root',
@@ -45,6 +46,11 @@ export class EntryService {
4546
return this.http.post(url, null);
4647
}
4748

49+
restartEntry(idEntry: string): Observable<Entry> {
50+
const url = `${this.baseUrl}/${idEntry}/restart`;
51+
return this.http.post<Entry>(url, null);
52+
}
53+
4854
summary(): Observable<TimeEntriesSummary> {
4955
const timeOffset = new Date().getTimezoneOffset();
5056
const summaryUrl = `${this.baseUrl}/summary?time_offset=${timeOffset}`;

0 commit comments

Comments
 (0)