diff --git a/package-lock.json b/package-lock.json index 6997be973..b992b7736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9255,6 +9255,11 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "moment": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz", + "integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -18537,7 +18542,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -18713,7 +18718,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -19528,7 +19533,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -19704,7 +19709,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true diff --git a/package.json b/package.json index 9f2e48125..bc7e2e3bf 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "bootstrap": "^4.4.1", "jquery": "^3.5.0", "minimist": "^1.2.5", + "moment": "^2.25.3", "msal": "^1.2.1", "ngx-pagination": "^5.0.0", "rxjs": "~6.5.4", diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.html b/src/app/modules/shared/components/details-fields/details-fields.component.html index 37c6bd1fd..5024ed021 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.html +++ b/src/app/modules/shared/components/details-fields/details-fields.component.html @@ -6,6 +6,7 @@
Activity
- +
- Jira Ticket + Ticket
+
+
+ Start/Date +
+ +
+ Start/Hour +
+ +
+
+
+ End/Date +
+ +
+ End/Hour +
+ +
Technology @@ -73,8 +119,8 @@
- - + +
diff --git a/src/app/modules/shared/components/modal/modal.component.ts b/src/app/modules/shared/components/modal/modal.component.ts index dae9ad620..df7531e2c 100644 --- a/src/app/modules/shared/components/modal/modal.component.ts +++ b/src/app/modules/shared/components/modal/modal.component.ts @@ -1,27 +1,23 @@ -import { - Component, - OnInit, - ViewChild, - ElementRef, - EventEmitter, - Output, - Input -} from '@angular/core'; +import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output, Input } from '@angular/core'; import { Project, Entry } from '../../models'; @Component({ selector: 'app-modal', templateUrl: './modal.component.html', - styleUrls: ['./modal.component.scss'] + styleUrls: ['./modal.component.scss'], }) export class ModalComponent implements OnInit { - @Input() list: Project & Entry; @Output() removeList = new EventEmitter(); @ViewChild('cancelDeleteModal') cancelDeleteModal: ElementRef; - constructor() { } + constructor() {} + + ngOnInit(): void {} - ngOnInit(): void { } + deleteAction() { + this.removeList.emit(this.list.id); + this.cancelDeleteModal.nativeElement.click(); + } } diff --git a/src/app/modules/time-clock/services/entry.service.spec.ts b/src/app/modules/time-clock/services/entry.service.spec.ts index 4c8f8b0f1..01d685a66 100644 --- a/src/app/modules/time-clock/services/entry.service.spec.ts +++ b/src/app/modules/time-clock/services/entry.service.spec.ts @@ -45,6 +45,14 @@ describe('EntryService', () => { }); }); + it('loads all Entries', () => { + service.baseUrl = 'time-entries'; + service.loadEntries().subscribe((response) => { + const loadEntryRequest = httpMock.expectOne(`${service.baseUrl}`); + expect(loadEntryRequest.request.method).toBe('GET'); + }); + }); + it('update an entry using PUT', () => { service.baseUrl = 'time-entries'; @@ -55,6 +63,15 @@ describe('EntryService', () => { }); }); + it('delete an entry using DELETE', () => { + service.baseUrl = 'time-entries'; + const entry = 'entryId'; + service.deleteEntry(entry).subscribe((response) => { + const updateEntryRequest = httpMock.expectOne(`${service.baseUrl}/${entry}`); + expect(updateEntryRequest.request.method).toBe('DELETE'); + }); + }); + it('stops an entry using POST', () => { service.baseUrl = 'time-entries'; diff --git a/src/app/modules/time-clock/services/entry.service.ts b/src/app/modules/time-clock/services/entry.service.ts index c903343f8..460264eeb 100644 --- a/src/app/modules/time-clock/services/entry.service.ts +++ b/src/app/modules/time-clock/services/entry.service.ts @@ -16,6 +16,10 @@ export class EntryService { return this.http.get(`${this.baseUrl}/running`); } + loadEntries(): Observable { + return this.http.get(`${this.baseUrl}`); + } + createEntry(entryData): Observable { return this.http.post(this.baseUrl, entryData); } @@ -25,6 +29,11 @@ export class EntryService { return this.http.put(`${this.baseUrl}/${id}`, entryData); } + deleteEntry(entryId: string): Observable { + const url = `${this.baseUrl}/${entryId}`; + return this.http.delete(url); + } + stopEntryRunning(idEntry: string): Observable { const url = `${this.baseUrl}/${idEntry}/stop`; return this.http.post(url, null); diff --git a/src/app/modules/time-clock/store/entry.actions.spec.ts b/src/app/modules/time-clock/store/entry.actions.spec.ts index 3be86a94b..cc10bec91 100644 --- a/src/app/modules/time-clock/store/entry.actions.spec.ts +++ b/src/app/modules/time-clock/store/entry.actions.spec.ts @@ -11,6 +11,16 @@ describe('Actions for Entries', () => { expect(loadActiveEntryFail.type).toEqual(actions.EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL); }); + it('LoadEntriesSuccess type is EntryActionTypes.LOAD_ENTRIES_SUCCESS', () => { + const loadEntrySuccess = new actions.LoadEntriesSuccess([]); + expect(loadEntrySuccess.type).toEqual(actions.EntryActionTypes.LOAD_ENTRIES_SUCCESS); + }); + + it('LoadEntriesFail type is EntryActionTypes.LOAD_ENTRIES_FAIL', () => { + const loadEntryFail = new actions.LoadEntriesFail('error'); + expect(loadEntryFail.type).toEqual(actions.EntryActionTypes.LOAD_ENTRIES_FAIL); + }); + it('CreateEntrySuccess type is EntryActionTypes.CREATE_ENTRY_SUCCESS', () => { const createEntrySuccess = new actions.CreateEntrySuccess({ project_id: '1', @@ -24,6 +34,16 @@ describe('Actions for Entries', () => { expect(createEntryFail.type).toEqual(actions.EntryActionTypes.CREATE_ENTRY_FAIL); }); + it('DeleteEntrySuccess type is EntryActionTypes.DELETE_ENTRY_SUCCESS', () => { + const deleteEntrySuccess = new actions.DeleteEntrySuccess('entryId'); + expect(deleteEntrySuccess.type).toEqual(actions.EntryActionTypes.DELETE_ENTRY_SUCCESS); + }); + + it('DeleteEntrySuccess type is EntryActionTypes.DELETE_ENTRY_SUCCESS', () => { + const deleteEntryFail = new actions.DeleteEntryFail('error'); + expect(deleteEntryFail.type).toEqual(actions.EntryActionTypes.DELETE_ENTRY_FAIL); + }); + it('UpdateActiveEntrySuccess type is EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS', () => { const updateActiveEntrySuccess = new actions.UpdateActiveEntrySuccess({ project_id: '1', diff --git a/src/app/modules/time-clock/store/entry.actions.ts b/src/app/modules/time-clock/store/entry.actions.ts index 36b4db8db..63fa9c2fc 100644 --- a/src/app/modules/time-clock/store/entry.actions.ts +++ b/src/app/modules/time-clock/store/entry.actions.ts @@ -1,19 +1,26 @@ import { Action } from '@ngrx/store'; -import { NewEntry } from '../../shared/models'; +import { NewEntry, Entry } from '../../shared/models'; export enum EntryActionTypes { LOAD_ACTIVE_ENTRY = '[Entry] LOAD_ACTIVE_ENTRY', LOAD_ACTIVE_ENTRY_SUCCESS = '[Entry] LOAD_ACTIVE_ENTRY_SUCCESS', LOAD_ACTIVE_ENTRY_FAIL = '[Entry] LOAD_ACTIVE_ENTRY_FAIL', + LOAD_ENTRIES = '[Entry] LOAD_ENTRIES', + LOAD_ENTRIES_SUCCESS = '[Entry] LOAD_ENTRIES_SUCCESS', + LOAD_ENTRIES_FAIL = '[Entry] LOAD_ENTRIES_FAIL', CREATE_ENTRY = '[Entry] CREATE_ENTRY', CREATE_ENTRY_SUCCESS = '[Entry] CREATE_ENTRY_SUCCESS', CREATE_ENTRY_FAIL = '[Entry] CREATE_ENTRY_FAIL', UPDATE_ACTIVE_ENTRY = '[Entry] UPDATE_ACTIVE_ENTRY', UPDATE_ACTIVE_ENTRY_SUCCESS = '[Entry] UPDATE_ACTIVE_ENTRY_SUCCESS', UPDATE_ACTIVE_ENTRY_FAIL = '[Entry] UPDATE_ACTIVE_ENTRY_FAIL', + DELETE_ENTRY = '[Entry] DELETE_ENTRY', + DELETE_ENTRY_SUCCESS = '[Entry] DELETE_ENTRY_SUCCESS', + DELETE_ENTRY_FAIL = '[Entry] DELETE_ENTRY_FAIL', STOP_TIME_ENTRY_RUNNING = '[Entry] STOP_TIME_ENTRIES_RUNNING', STOP_TIME_ENTRY_RUNNING_SUCCESS = '[Entry] STOP_TIME_ENTRIES_RUNNING_SUCCESS', STOP_TIME_ENTRY_RUNNING_FAILED = '[Entry] STOP_TIME_ENTRIES_RUNNING_FAILED', + DEFAULT_ENTRY = '[Entry] DEFAULT_ENTRY', } export class LoadActiveEntry implements Action { @@ -31,6 +38,21 @@ export class LoadActiveEntryFail implements Action { constructor(public error: string) {} } +export class LoadEntries implements Action { + public readonly type = EntryActionTypes.LOAD_ENTRIES; +} + +export class LoadEntriesSuccess implements Action { + readonly type = EntryActionTypes.LOAD_ENTRIES_SUCCESS; + constructor(readonly payload: Entry[]) {} +} + +export class LoadEntriesFail implements Action { + public readonly type = EntryActionTypes.LOAD_ENTRIES_FAIL; + + constructor(public error: string) {} +} + export class CreateEntry implements Action { public readonly type = EntryActionTypes.CREATE_ENTRY; @@ -49,6 +71,23 @@ export class CreateEntryFail implements Action { constructor(public error: string) {} } +export class DeleteEntry implements Action { + public readonly type = EntryActionTypes.DELETE_ENTRY; + + constructor(public entryId: string) {} +} + +export class DeleteEntrySuccess implements Action { + public readonly type = EntryActionTypes.DELETE_ENTRY_SUCCESS; + + constructor(public entryId: string) {} +} + +export class DeleteEntryFail implements Action { + public readonly type = EntryActionTypes.DELETE_ENTRY_FAIL; + + constructor(public error: string) {} +} export class UpdateActiveEntry implements Action { public readonly type = EntryActionTypes.UPDATE_ACTIVE_ENTRY; @@ -81,17 +120,27 @@ export class StopTimeEntryRunningFail implements Action { public readonly type = EntryActionTypes.STOP_TIME_ENTRY_RUNNING_FAILED; constructor(public error: string) {} } +export class DefaultEntry implements Action { + public readonly type = EntryActionTypes.DEFAULT_ENTRY; +} export type EntryActions = | LoadActiveEntry | LoadActiveEntrySuccess | LoadActiveEntryFail + | LoadEntries + | LoadEntriesSuccess + | LoadEntriesFail | CreateEntry | CreateEntrySuccess | CreateEntryFail | UpdateActiveEntry | UpdateActiveEntrySuccess | UpdateActiveEntryFail + | DeleteEntry + | DeleteEntrySuccess + | DeleteEntryFail | StopTimeEntryRunning | StopTimeEntryRunningSuccess - | StopTimeEntryRunningFail; + | StopTimeEntryRunningFail + | DefaultEntry; diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts index 55e622188..99e46a683 100644 --- a/src/app/modules/time-clock/store/entry.effects.ts +++ b/src/app/modules/time-clock/store/entry.effects.ts @@ -23,6 +23,17 @@ export class EntryEffects { ) ); + @Effect() + loadEntries$: Observable = this.actions$.pipe( + ofType(actions.EntryActionTypes.LOAD_ENTRIES), + mergeMap(() => + this.entryService.loadEntries().pipe( + map((entries) => new actions.LoadEntriesSuccess(entries)), + catchError((error) => of(new actions.LoadEntriesFail(error))) + ) + ) + ); + @Effect() createEntry$: Observable = this.actions$.pipe( ofType(actions.EntryActionTypes.CREATE_ENTRY), @@ -37,6 +48,18 @@ export class EntryEffects { ) ); + @Effect() + deleteEntry$: Observable = this.actions$.pipe( + ofType(actions.EntryActionTypes.DELETE_ENTRY), + map((action: actions.DeleteEntry) => action.entryId), + mergeMap((entryId) => + this.entryService.deleteEntry(entryId).pipe( + map(() => new actions.DeleteEntrySuccess(entryId)), + catchError((error) => of(new actions.DeleteEntryFail(error))) + ) + ) + ); + @Effect() updateActiveEntry$: Observable = this.actions$.pipe( ofType(actions.EntryActionTypes.UPDATE_ACTIVE_ENTRY), diff --git a/src/app/modules/time-clock/store/entry.reducer.spec.ts b/src/app/modules/time-clock/store/entry.reducer.spec.ts index 9447f068d..93b200aa7 100644 --- a/src/app/modules/time-clock/store/entry.reducer.spec.ts +++ b/src/app/modules/time-clock/store/entry.reducer.spec.ts @@ -1,12 +1,21 @@ -import { NewEntry } from './../../shared/models'; +import { NewEntry, Entry } from './../../shared/models'; import * as actions from './entry.actions'; import { entryReducer, EntryState } from './entry.reducer'; describe('entryReducer', () => { const initialState: EntryState = { active: null, entryList: [], isLoading: false, message: '' }; - - const entry: NewEntry = { start_date: 'start-date', description: - 'description', project_id: '112', technologies: ['angular', 'typescript']}; + const entry: NewEntry = { + start_date: 'start-date', + description: 'description', + project_id: '112', + technologies: ['angular', 'typescript'], + }; + + it('on Default, ', () => { + const action = new actions.DefaultEntry(); + const state = entryReducer(initialState, action); + expect(state).toEqual(initialState); + }); it('on LoadActiveEntry, isLoading is true', () => { const action = new actions.LoadActiveEntry(); @@ -29,6 +38,36 @@ describe('entryReducer', () => { expect(state.active).toBe(null); }); + it('on LoadEntries, isLoading is true', () => { + const action = new actions.LoadEntries(); + const state = entryReducer(initialState, action); + expect(state.isLoading).toEqual(true); + }); + + it('on LoadEntriesSuccess, get all Entries', () => { + const entries: Entry[] = [ + { + project_id: '123', + comments: 'description', + technologies: ['angular', 'javascript'], + uri: 'uri', + id: 'id', + start_date: new Date(), + end_date: new Date(), + activity: 'activity', + }, + ]; + const action = new actions.LoadEntriesSuccess(entries); + const state = entryReducer(initialState, action); + expect(state.entryList).toEqual(entries); + }); + + it('on LoadEntriesFail, active tobe null', () => { + const action = new actions.LoadEntriesFail('error'); + const state = entryReducer(initialState, action); + expect(state.entryList).toEqual([]); + }); + it('on CreateEntry, isLoading is true', () => { const entryToCreate: NewEntry = { project_id: '1', start_date: '2020-04-21T19:51:36.559000+00:00' }; const action = new actions.CreateEntry(entryToCreate); @@ -53,6 +92,24 @@ describe('entryReducer', () => { expect(state.isLoading).toEqual(false); }); + it('on DeleteEntry by Id, isLoading is true', () => { + const action = new actions.DeleteEntry('id'); + const state = entryReducer(initialState, action); + expect(state.isLoading).toEqual(true); + }); + + it('on DeleteEntrySuccess', () => { + const action = new actions.DeleteEntry('id'); + const state = entryReducer(initialState, action); + expect(state.entryList).toEqual([]); + }); + + it('on LoadEntriesFail, active tobe null', () => { + const action = new actions.DeleteEntryFail('error'); + const state = entryReducer(initialState, action); + expect(state.entryList).toEqual([]); + }); + it('on UpdateActiveEntry, isLoading is true', () => { const action = new actions.UpdateActiveEntry(entry); const state = entryReducer(initialState, action); @@ -105,5 +162,4 @@ describe('entryReducer', () => { expect(state.isLoading).toBeFalsy(); }); - }); diff --git a/src/app/modules/time-clock/store/entry.reducer.ts b/src/app/modules/time-clock/store/entry.reducer.ts index dc9bb7cd8..99fbbb9e6 100644 --- a/src/app/modules/time-clock/store/entry.reducer.ts +++ b/src/app/modules/time-clock/store/entry.reducer.ts @@ -39,6 +39,28 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi }; } + case EntryActionTypes.LOAD_ENTRIES: { + return { + ...state, + isLoading: true, + }; + } + case EntryActionTypes.LOAD_ENTRIES_SUCCESS: + return { + ...state, + entryList: action.payload, + isLoading: false, + }; + + case EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL: { + return { + ...state, + entryList: [], + isLoading: false, + message: 'Something went wrong fetching entries!', + }; + } + case EntryActionTypes.CREATE_ENTRY: { return { ...state, @@ -59,12 +81,37 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi case EntryActionTypes.CREATE_ENTRY_FAIL: { return { ...state, - entryList: [], isLoading: false, message: action.error, }; } + case EntryActionTypes.DELETE_ENTRY: { + return { + ...state, + isLoading: true, + }; + } + + case EntryActionTypes.DELETE_ENTRY_SUCCESS: { + const entryList = state.entryList.filter((entry) => entry.id !== action.entryId); + return { + ...state, + entryList, + isLoading: false, + message: 'ProjectType removed successfully!', + }; + } + + case EntryActionTypes.DELETE_ENTRY_FAIL: { + return { + ...state, + entryList: [], + isLoading: false, + message: 'Something went wrong deleting entry!', + }; + } + case EntryActionTypes.UPDATE_ACTIVE_ENTRY: { return { ...state, @@ -114,7 +161,7 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi }; } - default : { + default: { return state; } } diff --git a/src/app/modules/time-clock/store/entry.selectors.spec.ts b/src/app/modules/time-clock/store/entry.selectors.spec.ts index a726f454f..3cc4b80d8 100644 --- a/src/app/modules/time-clock/store/entry.selectors.spec.ts +++ b/src/app/modules/time-clock/store/entry.selectors.spec.ts @@ -7,4 +7,11 @@ describe('Entry selectors', () => { expect(selectors.getStatusMessage.projector(entryState)).toBe(anyMessage); }); + + it('should select the entry list', () => { + const entryList = []; + const entryState = { entryList }; + + expect(selectors.allEntries.projector(entryState)).toBe(entryList); + }); }); diff --git a/src/app/modules/time-clock/store/entry.selectors.ts b/src/app/modules/time-clock/store/entry.selectors.ts index ad2beeb51..bbae7cdce 100644 --- a/src/app/modules/time-clock/store/entry.selectors.ts +++ b/src/app/modules/time-clock/store/entry.selectors.ts @@ -9,3 +9,5 @@ export const getActiveTimeEntry = createSelector(getEntryState, (state: EntrySta }); export const getStatusMessage = createSelector(getEntryState, (state: EntryState) => state.message); + +export const allEntries = createSelector(getEntryState, (state: EntryState) => state.entryList); diff --git a/src/app/modules/time-entries/pages/time-entries.component.html b/src/app/modules/time-entries/pages/time-entries.component.html index b418eac54..b9dd36c38 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.html +++ b/src/app/modules/time-entries/pages/time-entries.component.html @@ -1,4 +1,17 @@
+
+
+ +
+
@@ -33,10 +46,10 @@ >
- {{ item.project }} + {{ item.id }}
- {{ item.duration }} + {{ item.time }}