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
feat: #385 Allowing restart entries
  • Loading branch information
Juan Gabriel Guzman committed Jun 23, 2020
commit f3c5b4f36fab78e6a28bf9bf9df36cf17bd2bf04
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<form [formGroup]="entryForm" (ngSubmit)="onSubmit()">
<label><input id='isEntryRunning' type="checkbox" (change)="onIsRunningChange($event)" [checked]="isEntryRunning"> I am working on
<label><input id='isEntryRunning' type="checkbox" (change)="onGoingToWorkOnThisChange($event)" [checked]="goingToWorkOnThis"> I am working on
this</label>

<div class="input-group input-group-sm mb-3">
Expand Down Expand Up @@ -88,11 +88,11 @@
required
aria-describedby="inputGroup-sizing-sm"
/>
<div class="input-group-prepend" *ngIf="!isEntryRunning">
<div class="input-group-prepend" *ngIf="!goingToWorkOnThis">
<span class="input-group-text span-width">Time out</span>
</div>
<input
*ngIf="!isEntryRunning"
*ngIf="!goingToWorkOnThis"
[clearIfNotMatch]="true"
[showMaskTyped]="true"
[dropSpecialCharacters]="false"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EntryActionTypes } from './../../../time-clock/store/entry.actions';
import { TechnologiesComponent } from './../technologies/technologies.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatePipe, formatDate } from '@angular/common';
import { ActionsSubject } from '@ngrx/store';
Expand All @@ -13,7 +13,8 @@ import { ProjectState } from '../../../customer-management/components/projects/c
import { getCustomerProjects } from '../../../customer-management/components/projects/components/store/project.selectors';
import { EntryState } from '../../../time-clock/store/entry.reducer';
import * as entryActions from '../../../time-clock/store/entry.actions';
import { getUpdateError, getCreateError } from 'src/app/modules/time-clock/store/entry.selectors';
import { getCreateError, getUpdateError } from 'src/app/modules/time-clock/store/entry.selectors';
import { SaveEntryEvent } from './save-entry-event';

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

const state = {
projects: {
projects: [{ id: 'id', name: 'name', project_type_id: '' }],
customerProjects: [{ id: 'id', name: 'name', description: 'description', project_type_id: '123' }],
projects: [{id: 'id', name: 'name', project_type_id: ''}],
customerProjects: [{id: 'id', name: 'name', description: 'description', project_type_id: '123'}],
isLoading: false,
message: '',
projectToEdit: undefined,
},
technologies: {
technologyList: { items: [{ name: 'java' }] },
technologyList: {items: [{name: 'java'}]},
isLoading: false,
},
activities: {
data: [{ id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc' }],
data: [{id: 'fc5fab41-a21e-4155-9d05-511b956ebd05', tenant_id: 'ioet', deleted: null, name: 'abc'}],
isLoading: false,
message: 'Data fetch successfully!',
activityIdToEdit: '',
Expand All @@ -66,7 +67,7 @@ describe('DetailsFieldsComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DetailsFieldsComponent, TechnologiesComponent],
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
providers: [provideMockStore({initialState: state}), {provide: ActionsSubject, useValue: actionSub}],
imports: [FormsModule, ReactiveFormsModule],
}).compileComponents();
store = TestBed.inject(MockStore);
Expand Down Expand Up @@ -106,8 +107,8 @@ describe('DetailsFieldsComponent', () => {
});

[
{ actionType: EntryActionTypes.CREATE_ENTRY_SUCCESS },
{ actionType: EntryActionTypes.UPDATE_ENTRY_SUCCESS },
{actionType: EntryActionTypes.CREATE_ENTRY_SUCCESS},
{actionType: EntryActionTypes.UPDATE_ENTRY_SUCCESS},
].map((param) => {
it(`cleanForm after an action type ${param.actionType} is received`, () => {
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
Expand Down Expand Up @@ -217,84 +218,91 @@ describe('DetailsFieldsComponent', () => {
technology: '',
});
component.onSubmit();
const data = {
project_id: '',
activity_id: '',
technologies: [],
description: '',
start_date: '2020-02-05T00:00:11',
end_date: '2020-02-05T00:01:01',
uri: '',
const data: SaveEntryEvent = {
entry: {
project_id: '',
activity_id: '',
technologies: [],
description: '',
start_date: '2020-02-05T00:00:11',
end_date: '2020-02-05T00:01:01',
uri: ''
},
shouldRestartEntry: false
};
expect(component.saveEntry.emit).toHaveBeenCalledWith(data);
});

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

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

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

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

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

expect(component.isEntryRunning).toBeFalse();
expect(component.goingToWorkOnThis).toBeFalse();
});

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

fixture.componentInstance.ngOnChanges();

expect(component.isEntryRunning).toBeTrue();
expect(component.goingToWorkOnThis).toBeTrue();
});

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

fixture.componentInstance.ngOnChanges();

expect(component.isEntryRunning).toBeFalse();
expect(component.goingToWorkOnThis).toBeFalse();
});

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

fixture.componentInstance.ngOnChanges();

expect(component.isEntryRunning).toBeFalse();
expect(component.goingToWorkOnThis).toBeFalse();
});

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

component.entryForm.setValue({ ...formValues, entry_date: '2020-06-11' });
component.entryForm.setValue({...formValues, entry_date: '2020-06-11'});
component.onSubmit();
const data = {
project_id: '',
activity_id: '',
technologies: [],
description: '',
start_date: '2020-06-11T00:00:10',
uri: 'ticketUri',
const data: SaveEntryEvent = {
entry: {
project_id: '',
activity_id: '',
technologies: [],
description: '',
start_date: '2020-06-11T00:00:10',
uri: 'ticketUri',
},
shouldRestartEntry: false
};

expect(component.saveEntry.emit).toHaveBeenCalledWith(data);
expect(component.saveEntry.emit
).toHaveBeenCalledWith(data);
});

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

Expand All @@ -305,4 +313,15 @@ describe('DetailsFieldsComponent', () => {
const endHourInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_hour');
expect(endHourInput.value).toEqual(currentTime);
});

it('given going to work on this and the entry is not currently running, when submitting form then the entry should be restarted', () => {
component.goingToWorkOnThis = false;
component.entryToEdit = {...entryToEdit, running: false};

const checkIsEntryRunning: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#isEntryRunning');
checkIsEntryRunning.click();
fixture.detectChanges();

expect(component.shouldRestartEntry).toBeTrue();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { filter } from 'rxjs/operators';
import { NumberFormatter } from './../../formatters/number.formatter';
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { select, Store, ActionsSubject } from '@ngrx/store';
import { ActionsSubject, select, Store } from '@ngrx/store';
import { formatDate } from '@angular/common';

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

type Merged = TechnologyState & ProjectState & ActivityState & EntryState;

Expand All @@ -25,15 +26,15 @@ type Merged = TechnologyState & ProjectState & ActivityState & EntryState;
})
export class DetailsFieldsComponent implements OnChanges, OnInit {
@Input() entryToEdit: Entry;
@Input() formType: string;
@Output() saveEntry = new EventEmitter();
@Output() saveEntry = new EventEmitter<SaveEntryEvent>();
@ViewChild('closeModal') closeModal: ElementRef;
entryForm: FormGroup;
selectedTechnologies: string[] = [];
isLoading = false;
listProjects: Project[] = [];
activities: Activity[] = [];
isEntryRunning = false;
goingToWorkOnThis = false;
shouldRestartEntry = false;

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

ngOnChanges(): void {
this.isEntryRunning = this.entryToEdit ? this.entryToEdit.running : false;
this.goingToWorkOnThis = this.entryToEdit ? this.entryToEdit.running : false;
if (this.entryToEdit) {
this.selectedTechnologies = this.entryToEdit.technologies;
this.entryForm.setValue({
Expand Down Expand Up @@ -160,10 +161,10 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
end_date: `${entryDate}T${this.entryForm.value.end_hour.trim()}:01`,
uri: this.entryForm.value.uri,
};
if (this.isEntryRunning) {
if (this.goingToWorkOnThis) {
delete entry.end_date;
}
this.saveEntry.emit(entry);
this.saveEntry.emit({entry, shouldRestartEntry: this.shouldRestartEntry});
}

getElapsedSeconds(date: Date): string {
Expand All @@ -175,10 +176,11 @@ export class DetailsFieldsComponent implements OnChanges, OnInit {
}
}

onIsRunningChange(event: any) {
this.isEntryRunning = event.currentTarget.checked;
if (!this.isEntryRunning) {
this.entryForm.patchValue({ end_hour: formatDate(new Date(), 'HH:mm', 'en') });
onGoingToWorkOnThisChange(event: any) {
this.goingToWorkOnThis = event.currentTarget.checked;
if (!this.goingToWorkOnThis) {
this.entryForm.patchValue({end_hour: formatDate(new Date(), 'HH:mm', 'en')});
}
this.shouldRestartEntry = !this.entryToEdit?.running && this.goingToWorkOnThis;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SaveEntryEvent {
entry: any;
shouldRestartEntry: boolean;
}
2 changes: 1 addition & 1 deletion src/app/modules/shared/models/entry.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface Entry {
running?: boolean;
id: string;
start_date: Date;
end_date: Date;
end_date?: Date;
activity_id?: string;
technologies: string[];
uri?: string;
Expand Down
9 changes: 9 additions & 0 deletions src/app/modules/time-clock/services/entry.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,13 @@ describe('EntryService', () => {
expect(loadEntryRequest.request.params.get('end_date')).toBe(pipe.transform(today, EntryService.TIME_ENTRIES_DATE_TIME_FORMAT));
expect(loadEntryRequest.request.params.get('user_id')).toEqual('123');
});

it('when restarting entry, a POST is triggered', () => {
const entry = 'entryId';

service.restartEntry(entry).subscribe();

const restartEntryRequest = httpMock.expectOne( `${service.baseUrl}/${entry}/restart`);
expect(restartEntryRequest.request.method).toBe('POST');
});
});
6 changes: 6 additions & 0 deletions src/app/modules/time-clock/services/entry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Observable } from 'rxjs';
import { environment } from './../../../../environments/environment';
import { TimeEntriesTimeRange } from '../models/time-entries-time-range';
import { DatePipe } from '@angular/common';
import { Entry } from '../../shared/models';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -45,6 +46,11 @@ export class EntryService {
return this.http.post(url, null);
}

restartEntry(idEntry: string): Observable<Entry> {
const url = `${this.baseUrl}/${idEntry}/restart`;
return this.http.post<Entry>(url, null);
}

summary(): Observable<TimeEntriesSummary> {
const timeOffset = new Date().getTimezoneOffset();
const summaryUrl = `${this.baseUrl}/summary?time_offset=${timeOffset}`;
Expand Down
Loading