diff --git a/.git-crypt/keys/PROD/0/0D57F1A8D53A81F7400B003B23FA3416519EEE05.gpg b/.git-crypt/keys/PROD/0/0D57F1A8D53A81F7400B003B23FA3416519EEE05.gpg deleted file mode 100644 index 41c46fa73..000000000 Binary files a/.git-crypt/keys/PROD/0/0D57F1A8D53A81F7400B003B23FA3416519EEE05.gpg and /dev/null differ diff --git a/.git-crypt/keys/PROD/0/9122E40E0400D921898F3B395582EDA0BCA797BC.gpg b/.git-crypt/keys/PROD/0/9122E40E0400D921898F3B395582EDA0BCA797BC.gpg new file mode 100644 index 000000000..0b2e82c54 Binary files /dev/null and b/.git-crypt/keys/PROD/0/9122E40E0400D921898F3B395582EDA0BCA797BC.gpg differ diff --git a/.git-crypt/keys/STAGE/0/0D57F1A8D53A81F7400B003B23FA3416519EEE05.gpg b/.git-crypt/keys/STAGE/0/0D57F1A8D53A81F7400B003B23FA3416519EEE05.gpg deleted file mode 100644 index 4d6c1e8d0..000000000 Binary files a/.git-crypt/keys/STAGE/0/0D57F1A8D53A81F7400B003B23FA3416519EEE05.gpg and /dev/null differ diff --git a/.git-crypt/keys/STAGE/0/9122E40E0400D921898F3B395582EDA0BCA797BC.gpg b/.git-crypt/keys/STAGE/0/9122E40E0400D921898F3B395582EDA0BCA797BC.gpg new file mode 100644 index 000000000..b1e200052 --- /dev/null +++ b/.git-crypt/keys/STAGE/0/9122E40E0400D921898F3B395582EDA0BCA797BC.gpg @@ -0,0 +1,5 @@ +.. Z˹32$q|:k tQH,9GRAw8_+KVS0R!73*#R1K+PuقV SL F2jovrrtx^j +Rp5PC = { validation: false, @@ -140,6 +142,8 @@ const maskConfig: Partial = { SubstractDatePipeDisplayAsFloat, TechnologiesComponent, SearchUserComponent, + SearchProjectComponent, + SearchActivityComponent, TimeEntriesSummaryComponent, TimeDetailsPipe, InputLabelComponent, diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html index 270c12fad..b10ea07ab 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html @@ -1,6 +1,12 @@ -
+
- + +
+
+ +
+
+ { describe('TimeEntriesTableComponent', () => { @@ -55,13 +63,13 @@ describe('Reports Page', () => { uri: 'custom uri', project_id: '123', project_name: 'Time-Tracker', - } + }, ]; const state: EntryState = { active: timeEntry, isLoading: false, - resultSumEntriesSelected: new TotalHours(), + resultSumEntriesSelected: new TotalHours(), message: '', createError: false, updateError: false, @@ -81,25 +89,37 @@ describe('Reports Page', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NgxPaginationModule, DataTablesModule], - declarations: [TimeEntriesTableComponent, SubstractDatePipe, SubstractDatePipeDisplayAsFloat], + imports: [ + NgxPaginationModule, + DataTablesModule, + MatCheckboxModule, + NgSelectModule, + FormsModule, + ReactiveFormsModule, + ], + declarations: [ + TimeEntriesTableComponent, + SubstractDatePipe, + SubstractDatePipeDisplayAsFloat, + SearchUserComponent, + SearchProjectComponent, + SearchActivityComponent, + ], providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }], }).compileComponents(); - }) ); - beforeEach( - () => { - fixture = TestBed.createComponent(TimeEntriesTableComponent); - component = fixture.componentInstance; - store = TestBed.inject(MockStore); - store.setState(state); - getReportDataSourceSelectorMock = (store.overrideSelector(getReportDataSource, state.reportDataSource), + beforeEach(() => { + fixture = TestBed.createComponent(TimeEntriesTableComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + store.setState(state); + getReportDataSourceSelectorMock = + (store.overrideSelector(getReportDataSource, state.reportDataSource), store.overrideSelector(getResultSumEntriesSelected, state.resultSumEntriesSelected)); - fixture.detectChanges(); - } - ); + fixture.detectChanges(); + }); beforeEach(() => { row = 0; @@ -144,11 +164,10 @@ describe('Reports Page', () => { const params = [ { url: 'http://example.com', expected_value: true }, { url: 'https://example.com', expected_value: true }, - { url: 'no-url-example', expected_value: false } + { url: 'no-url-example', expected_value: false }, ]; params.map((param) => { it(`Given the url ${param.url}, the method isURL should return ${param.expected_value}`, () => { - expect(component.isURL(param.url)).toEqual(param.expected_value); }); }); @@ -160,22 +179,21 @@ describe('Reports Page', () => { }); it('when the rerenderDataTable method is called and dtElement and dtInstance are defined, the destroy and next methods are called ', - () => { - spyOn(component.dtTrigger, 'next'); + () => { + spyOn(component.dtTrigger, 'next'); - component.ngAfterViewInit(); + component.ngAfterViewInit(); - component.dtElement.dtInstance.then((dtInstance) => { - expect(component.dtTrigger.next).toHaveBeenCalled(); - }); + component.dtElement.dtInstance.then((dtInstance) => { + expect(component.dtTrigger.next).toHaveBeenCalled(); }); + }); it(`When the user method is called, the emit method is called`, () => { const userId = 'abc123'; spyOn(component.selectedUserId, 'emit'); component.user(userId); expect(component.selectedUserId.emit).toHaveBeenCalled(); - }); it('Should populate the users with the payload from the action executed', () => { @@ -183,27 +201,25 @@ describe('Reports Page', () => { const usersArray = []; const action = { type: UserActionTypes.LOAD_USERS_SUCCESS, - payload: usersArray + payload: usersArray, }; actionSubject.next(action); - expect(component.users).toEqual(usersArray); }); it('The sum of the data dates is equal to {"hours": 3, "minutes":20,"seconds":0}', () => { const { hours, minutes, seconds }: TotalHours = component.sumDates(timeEntryList); expect({ hours, minutes, seconds }).toEqual({ hours: 3, minutes: 20, seconds: 0 }); - }); it('the sume of hours of entries selected is equal to {hours:0, minutes:0, seconds:0}', () => { let checked = true; - let {hours, minutes, seconds}: TotalHours = component.sumHoursEntriesSelected(timeEntryList[0], checked); + let { hours, minutes, seconds }: TotalHours = component.sumHoursEntriesSelected(timeEntryList[0], checked); checked = false; - ({hours, minutes, seconds} = component.sumHoursEntriesSelected(timeEntryList[0], checked)); - expect({hours, minutes, seconds}).toEqual({hours: 0, minutes: 0, seconds: 0}); + ({ hours, minutes, seconds } = component.sumHoursEntriesSelected(timeEntryList[0], checked)); + expect({ hours, minutes, seconds }).toEqual({ hours: 0, minutes: 0, seconds: 0 }); }); it('should export data with the correct format', () => { @@ -228,7 +244,7 @@ describe('Reports Page', () => { "ng-reflect-ng-for-of": "git" }-->` + }-->`, ]; const dataFormat = [ ' ', @@ -245,7 +261,7 @@ describe('Reports Page', () => { 'Activity_Name', ' https://ioetec.atlassian.net/browse/CB-115 ', '', - ' git ' + ' git ', ]; data.forEach((value: any, index) => { @@ -257,7 +273,7 @@ describe('Reports Page', () => { it('Should render column header called Time Zone', () => { const table = document.querySelector('table#time-entries-table'); const tableHeaderElements = Array.from(table.getElementsByTagName('th')); - const tableHeaderTitles = tableHeaderElements.map(element => (element.textContent)); + const tableHeaderTitles = tableHeaderElements.map((element) => element.textContent); expect(tableHeaderTitles).toContain('Time zone'); }); @@ -272,6 +288,45 @@ describe('Reports Page', () => { expect(cell).toContain('UTC-5'); }); + it('Should populate the projects with the payload from the action executed', () => { + const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject; + const customerObj: Customer = { name: 'name' }; + const projectsArray: Project[] = [ + { + id: 'projectId', + customer_id: 'customer_id', + customer: customerObj, + name: 'name', + description: 'proejectDescription', + project_type_id: 'project_type_id', + status: 'active', + }, + ]; + const action = { + type: ProjectActionTypes.LOAD_PROJECTS_SUCCESS, + payload: projectsArray, + }; + actionSubject.next(action); + expect(component.projects).toEqual(projectsArray); + }); + + it('Should populate the activities with the payload from the action executed', () => { + const Subject = TestBed.inject(ActionsSubject) as ActionsSubject; + const activitiesArray: Activity[] = [ + { + id: 'activityId', + name: 'activityName', + description: 'activityDescription', + status: 'string' + }, + ]; + const action = { + type: ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS, + payload: activitiesArray, + }; + Subject.next(action); + expect(component.activities).toEqual(activitiesArray); + }); afterEach(() => { fixture.destroy(); diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts index dfa94b872..2e4bcf945 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts @@ -5,7 +5,7 @@ import { DataTableDirective } from 'angular-datatables'; import * as moment from 'moment'; import { Observable, Subject, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { Entry } from 'src/app/modules/shared/models'; +import { Activity, Entry, Project } from 'src/app/modules/shared/models'; import { DataSource } from 'src/app/modules/shared/models/data-source.model'; import { EntryState } from '../../../time-clock/store/entry.reducer'; import { getReportDataSource, getResultSumEntriesSelected } from '../../../time-clock/store/entry.selectors'; @@ -13,6 +13,11 @@ import { TotalHours } from '../../models/total-hours-report'; import { User } from 'src/app/modules/users/models/users'; import { LoadUsers, UserActionTypes } from 'src/app/modules/users/store/user.actions'; import { ParseDateTimeOffset } from '../../../shared/formatters/parse-date-time-offset/parse-date-time-offset'; +import { + LoadProjects, + ProjectActionTypes, +} from 'src/app/modules/customer-management/components/projects/components/store/project.actions'; +import { ActivityManagementActionTypes, LoadActivities } from 'src/app/modules/activities-management/store'; @Component({ selector: 'app-time-entries-table', @@ -21,11 +26,15 @@ import { ParseDateTimeOffset } from '../../../shared/formatters/parse-date-time- }) export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit { @Output() selectedUserId = new EventEmitter(); + @Output() selectedProjectId = new EventEmitter(); + @Output() selectedActivityId = new EventEmitter(); selectOptionValues = [15, 30, 50, 100, -1]; selectOptionNames = [15, 30, 50, 100, 'All']; totalTimeSelected: moment.Duration; users: User[] = []; + projects: Project[] = []; + activities: Activity[] = []; removeFirstColumn = 'th:not(:first)'; dtOptions: any = { @@ -37,7 +46,7 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn { text: 'Column Visibility' + ' ▼', extend: 'colvis', - columns: ':not(.hidden-col)' + columns: ':not(.hidden-col)', }, { extend: 'print', @@ -49,27 +58,34 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn extend: 'excel', exportOptions: { format: { - body: this.bodyExportOptions + body: this.bodyExportOptions, }, columns: this.removeFirstColumn, }, text: 'Excel', - filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` + filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`, }, { extend: 'csv', exportOptions: { format: { - body: this.bodyExportOptions + body: this.bodyExportOptions, }, columns: this.removeFirstColumn, }, text: 'CSV', - filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` + filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`, }, ], - columnDefs: [{ type: 'date', targets: 3}, {orderable: false, targets: [0]}], - order: [[1, 'asc'], [2, 'desc'], [4, 'desc']] + columnDefs: [ + { type: 'date', targets: 3 }, + { orderable: false, targets: [0] }, + ], + order: [ + [1, 'asc'], + [2, 'desc'], + [4, 'desc'], + ], }; dtTrigger: Subject = new Subject(); @ViewChild(DataTableDirective, { static: false }) @@ -82,13 +98,19 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn resultSumEntriesSelected$: Observable; totalHoursSubscription: Subscription; dateTimeOffset: ParseDateTimeOffset; - - - constructor(private store: Store, private actionsSubject$: ActionsSubject, private storeUser: Store ) { - this.reportDataSource$ = this.store.pipe(select(getReportDataSource)); - this.resultSumEntriesSelected$ = this.store.pipe(select(getResultSumEntriesSelected)); - this.dateTimeOffset = new ParseDateTimeOffset(); - this.resultSumEntriesSelected = new TotalHours(); + listProjects: Project[] = []; + + constructor( + private store: Store, + private actionsSubject$: ActionsSubject, + private storeUser: Store, + private storeProject: Store, + private storeActivity: Store + ) { + this.reportDataSource$ = this.store.pipe(select(getReportDataSource)); + this.resultSumEntriesSelected$ = this.store.pipe(select(getResultSumEntriesSelected)); + this.dateTimeOffset = new ParseDateTimeOffset(); + this.resultSumEntriesSelected = new TotalHours(); } uploadUsers(): void { @@ -102,16 +124,53 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn }); } + uploadProjects(): void { + this.storeProject.dispatch(new LoadProjects()); + this.actionsSubject$ + .pipe(filter((action: any) => action.type === ProjectActionTypes.LOAD_PROJECTS_SUCCESS)) + .subscribe((action) => { + const sortProjects = [...action.payload]; + sortProjects.sort((a, b) => a.name.localeCompare(b.name)); + this.projects = sortProjects; + this.projects = this.projects.filter((project) => project.status === 'active'); + this.projects.sort((a, b) => { + const x = a.customer.name.toLowerCase(); + const y = b.customer.name.toLowerCase(); + if (x > y) {return 1; } + if (x < y) {return -1; } + return 0; + }); + this.projects.forEach((project) => { + const projectWithSearchField = { ...project }; + projectWithSearchField.search_field = `${project.customer.name} - ${project.name}`; + this.listProjects.push(projectWithSearchField); + }); + }); + } + + uploadActivities(): void { + this.storeActivity.dispatch(new LoadActivities()); + this.actionsSubject$ + .pipe(filter((action: any) => action.type === ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS)) + .subscribe((action) => { + const sortActivities = [...action.payload]; + sortActivities.sort((a, b) => a.name.localeCompare(b.name)); + this.activities = sortActivities; + }); + } + ngOnInit(): void { this.rerenderTableSubscription = this.reportDataSource$.subscribe((ds) => { this.totalHoursSubscription = this.resultSumEntriesSelected$.subscribe((actTotalHours) => { - this.resultSumEntriesSelected = actTotalHours; - this.totalTimeSelected = moment.duration(0); - }); + this.resultSumEntriesSelected = actTotalHours; + this.totalTimeSelected = moment.duration(0); + }); this.sumDates(ds.data); this.rerenderDataTable(); }); this.uploadUsers(); + this.uploadProjects(); + this.uploadActivities(); } ngAfterViewInit(): void { @@ -147,20 +206,17 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn return data.toString().replace(/<((.|\n){0,200}?)>/gi, '') || ''; } - sumDates(arrayData: Entry[]): TotalHours { this.resultSum = new TotalHours(); const arrayDurations = new Array(); - arrayData.forEach(entry => { + arrayData.forEach((entry) => { const start = moment(entry.end_date).diff(moment(entry.start_date)); arrayDurations.push(moment.utc(start).format('HH:mm:ss')); }); - const totalDurations = arrayDurations.slice(1) - .reduce((prev, cur) => { - return prev.add(cur); - }, - moment.duration(arrayDurations[0])); + const totalDurations = arrayDurations.slice(1).reduce((prev, cur) => { + return prev.add(cur); + }, moment.duration(arrayDurations[0])); const daysInHours = totalDurations.days() * 24; this.resultSum.hours = totalDurations.hours() + daysInHours; this.resultSum.minutes = totalDurations.minutes(); @@ -172,7 +228,15 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn this.selectedUserId.emit(userId); } - sumHoursEntriesSelected(entry: Entry, checked: boolean){ + project(projectId: string) { + this.selectedProjectId.emit(projectId); + } + + activity(activityId: string) { + this.selectedActivityId.emit(activityId); + } + + sumHoursEntriesSelected(entry: Entry, checked: boolean) { this.resultSumEntriesSelected = new TotalHours(); const duration = moment.duration(moment(entry.end_date).diff(moment(entry.start_date))); this.totalTimeSelected = checked ? this.totalTimeSelected.add(duration) : this.totalTimeSelected.subtract(duration); diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts index fa2e8e9d5..b9f024567 100644 --- a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.spec.ts @@ -8,7 +8,6 @@ import * as entryActions from '../../../time-clock/store/entry.actions'; import * as moment from 'moment'; import { SimpleChange } from '@angular/core'; - describe('TimeRangeCustomComponent', () => { let component: TimeRangeCustomComponent; let fixture: ComponentFixture; @@ -16,7 +15,7 @@ describe('TimeRangeCustomComponent', () => { const toastrServiceStub = { error: () => { return 'test error'; - } + }, }; const timeEntry = { @@ -27,7 +26,7 @@ describe('TimeRangeCustomComponent', () => { technologies: ['react', 'redux'], comments: 'any comment', uri: 'TT-123', - project_id: '1' + project_id: '1', }; const state = { @@ -44,13 +43,9 @@ describe('TimeRangeCustomComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [FormsModule, ReactiveFormsModule], - declarations: [ TimeRangeCustomComponent ], - providers: [ - provideMockStore({ initialState: state }), - { provide: ToastrService, useValue: toastrServiceStub } - ], - }) - .compileComponents(); + declarations: [TimeRangeCustomComponent], + providers: [provideMockStore({ initialState: state }), { provide: ToastrService, useValue: toastrServiceStub }], + }).compileComponents(); store = TestBed.inject(MockStore); }); @@ -81,10 +76,12 @@ describe('TimeRangeCustomComponent', () => { component.onSubmit(); - expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({ - start_date: end.startOf('day'), - end_date: start.endOf('day') - })); + expect(store.dispatch).toHaveBeenCalledWith( + new entryActions.LoadEntriesByTimeRange({ + start_date: end.startOf('day'), + end_date: start.endOf('day'), + }) + ); }); it('shows an error when the end date is before the start date', () => { @@ -108,7 +105,6 @@ describe('TimeRangeCustomComponent', () => { expect(component.range.controls.start.setValue).toHaveBeenCalled(); expect(component.range.controls.end.setValue).toHaveBeenCalled(); - }); it('triggers onSubmit to set initial data', () => { @@ -119,32 +115,25 @@ describe('TimeRangeCustomComponent', () => { expect(component.onSubmit).toHaveBeenCalled(); }); - it('When the ngOnChanges method is called, the onSubmit method is called', () => { - const userIdCalled = 'test-user-1'; - spyOn(component, 'onSubmit'); - - component.ngOnChanges({userId: new SimpleChange(null, userIdCalled, false)}); - - expect(component.onSubmit).toHaveBeenCalled(); - }); - it('When the ngOnChanges method is the first change, the onSubmit method is not called', () => { - const userIdNotCalled = 'test-user-2'; spyOn(component, 'onSubmit'); - component.ngOnChanges({userId: new SimpleChange(null, userIdNotCalled, true)}); + component.ngOnChanges({ + userId: new SimpleChange(null, 'userId', true), + projectId: new SimpleChange(null, 'projectId', true), + activityId: new SimpleChange(null, 'activityId', true), + }); expect(component.onSubmit).not.toHaveBeenCalled(); }); it('should call range form and delete variable local storage ', () => { spyOn(localStorage, 'removeItem').withArgs('rangeDatePicker'); - component.range.setValue({start: null, end: null}); + component.range.setValue({ start: null, end: null }); jasmine.clock().install(); component.dateRangeChange(); jasmine.clock().tick(200); expect(localStorage.removeItem).toHaveBeenCalledWith('rangeDatePicker'); jasmine.clock().uninstall(); }); - }); diff --git a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts index dded53994..5c3f0cecc 100644 --- a/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts +++ b/src/app/modules/reports/components/time-range-custom/time-range-custom.component.ts @@ -1,13 +1,6 @@ import { formatDate } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - Input, - OnChanges, - OnInit, - SimpleChanges, -} from '@angular/core'; -import {FormGroup, FormControl} from '@angular/forms'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; import { Store } from '@ngrx/store'; import * as moment from 'moment'; import { ToastrService } from 'ngx-toastr'; @@ -16,7 +9,6 @@ import { DATE_FORMAT } from 'src/environments/environment'; import * as entryActions from '../../../time-clock/store/entry.actions'; import { TimeRangeHeaderComponent } from './time-range-header/time-range-header.component'; - @Component({ selector: 'app-time-range-custom', templateUrl: './time-range-custom.component.html', @@ -25,21 +17,23 @@ import { TimeRangeHeaderComponent } from './time-range-header/time-range-header. }) export class TimeRangeCustomComponent implements OnInit, OnChanges { @Input() userId: string; + @Input() projectId: string; + @Input() activityId: string; customHeader = TimeRangeHeaderComponent; range = new FormGroup({ start: new FormControl(null), end: new FormControl(null), }); - constructor(private store: Store, private toastrService: ToastrService) { - } + constructor(private store: Store, private toastrService: ToastrService) {} ngOnInit(): void { this.setInitialDataOnScreen(); } - ngOnChanges(changes: SimpleChanges){ - if (!changes.userId.firstChange){ + ngOnChanges(changes: SimpleChanges) { + const firstChange = Object.values(changes)[0].firstChange; + if (!firstChange) { this.onSubmit(); } } @@ -47,7 +41,7 @@ export class TimeRangeCustomComponent implements OnInit, OnChanges { setInitialDataOnScreen() { this.range.setValue({ start: formatDate(moment().startOf('isoWeek').format('l'), DATE_FORMAT, 'en'), - end: formatDate(moment().format('l'), DATE_FORMAT, 'en') + end: formatDate(moment().format('l'), DATE_FORMAT, 'en'), }); localStorage.setItem('rangeDatePicker', 'custom'); this.onSubmit(); @@ -59,10 +53,17 @@ export class TimeRangeCustomComponent implements OnInit, OnChanges { if (endDate.isBefore(startDate)) { this.toastrService.error('The end date should be after the start date'); } else { - this.store.dispatch(new entryActions.LoadEntriesByTimeRange({ - start_date: moment(this.range.getRawValue().start).startOf('day'), - end_date: moment(this.range.getRawValue().end).endOf('day'), - }, this.userId)); + this.store.dispatch( + new entryActions.LoadEntriesByTimeRange( + { + start_date: moment(this.range.getRawValue().start).startOf('day'), + end_date: moment(this.range.getRawValue().end).endOf('day'), + }, + this.userId, + this.projectId, + this.activityId + ) + ); } } diff --git a/src/app/modules/reports/components/time-range-form/time-range-form.component.ts b/src/app/modules/reports/components/time-range-form/time-range-form.component.ts index 5abb95bc8..09c2d37c6 100644 --- a/src/app/modules/reports/components/time-range-form/time-range-form.component.ts +++ b/src/app/modules/reports/components/time-range-form/time-range-form.component.ts @@ -16,6 +16,8 @@ import { DateAdapter } from '@angular/material/core'; export class TimeRangeFormComponent implements OnInit, OnChanges { @Input() userId: string; + @Input() projectId: string; + @Input() activityId: string; public reportForm: FormGroup; private startDate = new FormControl(''); @@ -33,8 +35,9 @@ export class TimeRangeFormComponent implements OnInit, OnChanges { this.setInitialDataOnScreen(); } - ngOnChanges(changes: SimpleChanges){ - if (!changes.userId.firstChange){ + ngOnChanges(changes: SimpleChanges) { + const firstChange = Object.values(changes)[0].firstChange; + if (!firstChange) { this.onSubmit(); } } @@ -56,7 +59,7 @@ export class TimeRangeFormComponent implements OnInit, OnChanges { this.store.dispatch(new entryActions.LoadEntriesByTimeRange({ start_date: moment(this.startDate.value).startOf('day'), end_date: moment(this.endDate.value).endOf('day'), - }, this.userId)); + }, this.userId, this.projectId, this.activityId)); } } } diff --git a/src/app/modules/reports/components/time-range-form/time-range.component.spec.ts b/src/app/modules/reports/components/time-range-form/time-range.component.spec.ts index 89436d6c7..58bb5f4bd 100644 --- a/src/app/modules/reports/components/time-range-form/time-range.component.spec.ts +++ b/src/app/modules/reports/components/time-range-form/time-range.component.spec.ts @@ -17,7 +17,7 @@ describe('Reports Page', () => { let store: MockStore; const toastrServiceStub = { - error: (message?: string, title?: string, override?: Partial) => { } + error: (message?: string, title?: string, override?: Partial) => {}, }; const timeEntry = { @@ -28,7 +28,7 @@ describe('Reports Page', () => { technologies: ['react', 'redux'], comments: 'any comment', uri: 'custom uri', - project_id: '123' + project_id: '123', }; const state = { @@ -42,18 +42,20 @@ describe('Reports Page', () => { entriesForReport: [timeEntry], }; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [FormsModule, ReactiveFormsModule], - declarations: [TimeRangeFormComponent, InputDateComponent], - providers: [ - provideMockStore({ initialState: state }), - { provide: ToastrService, useValue: toastrServiceStub }, - { provide: DateAdapter, useClass: DateAdapter } - ], - }).compileComponents(); - store = TestBed.inject(MockStore); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [FormsModule, ReactiveFormsModule], + declarations: [TimeRangeFormComponent, InputDateComponent], + providers: [ + provideMockStore({ initialState: state }), + { provide: ToastrService, useValue: toastrServiceStub }, + { provide: DateAdapter, useClass: DateAdapter }, + ], + }).compileComponents(); + store = TestBed.inject(MockStore); + }) + ); beforeEach(() => { fixture = TestBed.createComponent(TimeRangeFormComponent); @@ -74,10 +76,12 @@ describe('Reports Page', () => { component.onSubmit(); - expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({ - start_date: yesterday.startOf('day'), - end_date: today.endOf('day') - })); + expect(store.dispatch).toHaveBeenCalledWith( + new entryActions.LoadEntriesByTimeRange({ + start_date: yesterday.startOf('day'), + end_date: today.endOf('day'), + }) + ); }); it('setInitialDataOnScreen on ngOnInit', () => { @@ -127,20 +131,14 @@ describe('Reports Page', () => { expect(component.onSubmit).toHaveBeenCalled(); }); - it('When the ngOnChanges method is called, the onSubmit method is called', () => { - const userId = 'abcd'; - spyOn(component, 'onSubmit'); - - component.ngOnChanges({userId: new SimpleChange(null, userId, false)}); - - expect(component.onSubmit).toHaveBeenCalled(); - }); - it('When the ngOnChanges method is the first change, the onSubmit method is not called', () => { - const userId = 'abcd'; spyOn(component, 'onSubmit'); - component.ngOnChanges({userId: new SimpleChange(null, userId, true)}); + component.ngOnChanges({ + userId: new SimpleChange(null, 'user_id', true), + projectId: new SimpleChange(null, 'project_id', true), + activityId: new SimpleChange(null, 'activity_id', true), + }); expect(component.onSubmit).not.toHaveBeenCalled(); }); diff --git a/src/app/modules/reports/pages/reports.component.html b/src/app/modules/reports/pages/reports.component.html index a9b65ded9..931c46752 100644 --- a/src/app/modules/reports/pages/reports.component.html +++ b/src/app/modules/reports/pages/reports.component.html @@ -1,2 +1,2 @@ - - \ No newline at end of file + + \ No newline at end of file diff --git a/src/app/modules/reports/pages/reports.component.ts b/src/app/modules/reports/pages/reports.component.ts index 6b02adef8..1885b8c53 100644 --- a/src/app/modules/reports/pages/reports.component.ts +++ b/src/app/modules/reports/pages/reports.component.ts @@ -8,8 +8,16 @@ import { Component } from '@angular/core'; export class ReportsComponent { userId: string; + projectId: string; + activityId: string; user(userId: string){ this.userId = userId; } + activity(activityId: string){ + this.activityId = activityId; + } + project(projectId: string){ + this.projectId = projectId; + } } 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 d0c4ea2dc..77fc6d3e6 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 @@ -151,7 +151,7 @@ diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.ts b/src/app/modules/shared/components/details-fields/details-fields.component.ts index ca66f7cbf..e41801a7f 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.ts +++ b/src/app/modules/shared/components/details-fields/details-fields.component.ts @@ -333,6 +333,14 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { } onSubmit() { + + // Debounce submit (Save) button + const button = document.querySelector('#submitButton'); + button.setAttribute('disabled', 'true'); + setTimeout(() => { + button.removeAttribute('disabled'); + }, 3000); + const emptyValue = ''; const { project_name, uri, description } = this.entryForm.value; const areEmptyValues = [uri, description].every(item => item === emptyValue); diff --git a/src/app/modules/shared/components/search-activity/search-activity.component.html b/src/app/modules/shared/components/search-activity/search-activity.component.html new file mode 100644 index 000000000..f1960b694 --- /dev/null +++ b/src/app/modules/shared/components/search-activity/search-activity.component.html @@ -0,0 +1,7 @@ +
+ + + {{activity.name}} + + +
\ No newline at end of file diff --git a/src/app/modules/shared/components/search-activity/search-activity.component.scss b/src/app/modules/shared/components/search-activity/search-activity.component.scss new file mode 100644 index 000000000..d21fef41b --- /dev/null +++ b/src/app/modules/shared/components/search-activity/search-activity.component.scss @@ -0,0 +1,9 @@ +label { + width: 225px; +} +.selectActivity { + display: inline-block; + width: 300px; + padding: 0 12px 15px 12px; +} + diff --git a/src/app/modules/shared/components/search-activity/search-activity.component.spec.ts b/src/app/modules/shared/components/search-activity/search-activity.component.spec.ts new file mode 100644 index 000000000..f1f8a9cb6 --- /dev/null +++ b/src/app/modules/shared/components/search-activity/search-activity.component.spec.ts @@ -0,0 +1,34 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; + +import { SearchActivityComponent } from './search-activity.component'; +import { NgSelectModule } from '@ng-select/ng-select'; + +describe('SearchActivityComponent', () => { + let component: SearchActivityComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ FormsModule, NgSelectModule ], + declarations: [ SearchActivityComponent ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchActivityComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit changedFilterValue event #changeFilterValue', () => { + component.selectedActivity = 'angular'; + spyOn(component.selectedActivityId, 'emit'); + component.updateActivity(); + expect(component.selectedActivityId.emit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/modules/shared/components/search-activity/search-activity.component.ts b/src/app/modules/shared/components/search-activity/search-activity.component.ts new file mode 100644 index 000000000..6ab8b55a7 --- /dev/null +++ b/src/app/modules/shared/components/search-activity/search-activity.component.ts @@ -0,0 +1,22 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-search-activity', + templateUrl: './search-activity.component.html', + styleUrls: ['./search-activity.component.scss'], +}) + +export class SearchActivityComponent { + + readonly ALLOW_SELECT_MULTIPLE = false; + selectedActivity: string; + + @Input() activities: string[] = []; + + @Output() selectedActivityId = new EventEmitter(); + + updateActivity() { + this.selectedActivityId.emit(this.selectedActivity || '*' ); + } +} + diff --git a/src/app/modules/shared/components/search-project/search-project.component.html b/src/app/modules/shared/components/search-project/search-project.component.html new file mode 100644 index 000000000..c97e53194 --- /dev/null +++ b/src/app/modules/shared/components/search-project/search-project.component.html @@ -0,0 +1,7 @@ +
+ + + {{project.customer.name}} - {{project.name}} + + +
\ No newline at end of file diff --git a/src/app/modules/shared/components/search-project/search-project.component.scss b/src/app/modules/shared/components/search-project/search-project.component.scss new file mode 100644 index 000000000..786299355 --- /dev/null +++ b/src/app/modules/shared/components/search-project/search-project.component.scss @@ -0,0 +1,9 @@ +label { + width: 225px; +} +.selectProject { + display: inline-block; + width: 600px; + padding: 0 12px 15px 12px; +} + diff --git a/src/app/modules/shared/components/search-project/search-project.component.spec.ts b/src/app/modules/shared/components/search-project/search-project.component.spec.ts new file mode 100644 index 000000000..276c39049 --- /dev/null +++ b/src/app/modules/shared/components/search-project/search-project.component.spec.ts @@ -0,0 +1,34 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; + +import { SearchProjectComponent } from './search-project.component'; +import { NgSelectModule } from '@ng-select/ng-select'; + +describe('SearchActivityComponent', () => { + let component: SearchProjectComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ FormsModule, NgSelectModule ], + declarations: [ SearchProjectComponent ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchProjectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit changedFilterValue event #changeFilterValue', () => { + component.selectedProject = 'angular'; + spyOn(component.selectedProjectId, 'emit'); + component.updateProject(); + expect(component.selectedProjectId.emit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/modules/shared/components/search-project/search-project.component.ts b/src/app/modules/shared/components/search-project/search-project.component.ts new file mode 100644 index 000000000..97e04197e --- /dev/null +++ b/src/app/modules/shared/components/search-project/search-project.component.ts @@ -0,0 +1,19 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-search-project', + templateUrl: './search-project.component.html', + styleUrls: ['./search-project.component.scss'], +}) +export class SearchProjectComponent { + readonly ALLOW_SELECT_MULTIPLE = false; + selectedProject: string; + + @Input() projects: string[] = []; + + @Output() selectedProjectId = new EventEmitter(); + + updateProject() { + this.selectedProjectId.emit(this.selectedProject || '*'); + } +} diff --git a/src/app/modules/shared/components/search-user/search-user.component.html b/src/app/modules/shared/components/search-user/search-user.component.html index b141cf65b..2b9c7916d 100644 --- a/src/app/modules/shared/components/search-user/search-user.component.html +++ b/src/app/modules/shared/components/search-user/search-user.component.html @@ -1,5 +1,4 @@
- 👤{{user.name}}📨{{ user.email}} diff --git a/src/app/modules/shared/components/search-user/search-user.component.scss b/src/app/modules/shared/components/search-user/search-user.component.scss index 6d3b6756d..d36c9b4d5 100644 --- a/src/app/modules/shared/components/search-user/search-user.component.scss +++ b/src/app/modules/shared/components/search-user/search-user.component.scss @@ -3,7 +3,7 @@ label { } .selectUser { display: inline-block; - width: 350px; + width: 500px; padding: 0 12px 15px 12px; } diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts index 66207c63d..301aa33d5 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts @@ -106,6 +106,18 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { } clockIn(selectedProject, customerName, name) { + + // Debounce 'Clock In' buttons + const buttons = document.getElementsByClassName('btn btn-sm btn-primary btn-select'); + for (const button of buttons) { + button.setAttribute('disabled', 'true'); + } + setTimeout(() => { + for (const button of buttons) { + button.removeAttribute('disabled'); + } + }, 3000); + const entry = { project_id: selectedProject, start_date: new Date().toISOString(), @@ -113,6 +125,7 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { technologies: [], activity_id: head(this.activities).id, }; + this.store.dispatch(new entryActions.ClockIn(entry)); this.projectsForm.setValue({ project_id: `${customerName} - ${name}` }); setTimeout(() => { diff --git a/src/app/modules/time-clock/services/entry.service.ts b/src/app/modules/time-clock/services/entry.service.ts index c93aba502..feda2f9d6 100644 --- a/src/app/modules/time-clock/services/entry.service.ts +++ b/src/app/modules/time-clock/services/entry.service.ts @@ -11,15 +11,24 @@ import { Entry } from '../../shared/models'; import * as moment from 'moment'; +interface QueryParams { + start_date: string; + end_date: string; + user_id: string | string[]; + limit: string; + timezone_offset: string; + project_id?: string; + activity_id?: string; +} + export const MAX_NUMBER_OF_ENTRIES_FOR_REPORTS = 9999; + @Injectable({ providedIn: 'root', }) export class EntryService { - - constructor(private http: HttpClient, private datePipe: DatePipe) { - } + constructor(private http: HttpClient, private datePipe: DatePipe) {} static TIME_ENTRIES_DATE_TIME_FORMAT = 'yyyy-MM-ddTHH:mm:ssZZZZZ'; baseUrl = `${environment.timeTrackerApiUrl}/time-entries`; @@ -39,7 +48,7 @@ export class EntryService { } updateEntry(entryData): Observable { - const {id} = entryData; + const { id } = entryData; return this.http.put(`${this.baseUrl}/${id}`, entryData); } @@ -49,8 +58,9 @@ export class EntryService { } stopEntryRunning(idEntry: string): Observable { - return (this.urlInProductionLegacy ? - this.http.post(`${this.baseUrl}/${idEntry}/stop/`, null) : this.http.put(`${this.baseUrl}/stop/`, null) ); + return this.urlInProductionLegacy + ? this.http.post(`${this.baseUrl}/${idEntry}/stop/`, null) + : this.http.put(`${this.baseUrl}/stop/`, null); } restartEntry(idEntry: string): Observable { @@ -71,23 +81,32 @@ export class EntryService { return this.http.get(findEntriesByProjectURL); } - loadEntriesByTimeRange(range: TimeEntriesTimeRange, userId: string[] | string ): Observable { + + loadEntriesByTimeRange( + range: TimeEntriesTimeRange, + userId: string[] | string, + projectId?: string, + activityId?: string + ): Observable { + const loadEntriesByTimeRangeURL = this.urlInProductionLegacy ? this.baseUrl : this.baseUrl + '/report/'; - return this.http.get(loadEntriesByTimeRangeURL, - { - params: { - start_date: this.datePipe.transform(range.start_date, EntryService.TIME_ENTRIES_DATE_TIME_FORMAT), - end_date: this.datePipe.transform(range.end_date, EntryService.TIME_ENTRIES_DATE_TIME_FORMAT), - user_id: userId, - limit: `${MAX_NUMBER_OF_ENTRIES_FOR_REPORTS}`, - timezone_offset : new Date().getTimezoneOffset().toString(), - } - } - ); + const queryParams: QueryParams = { + start_date: this.datePipe.transform(range.start_date, EntryService.TIME_ENTRIES_DATE_TIME_FORMAT), + end_date: this.datePipe.transform(range.end_date, EntryService.TIME_ENTRIES_DATE_TIME_FORMAT), + user_id: userId, + limit: `${MAX_NUMBER_OF_ENTRIES_FOR_REPORTS}`, + timezone_offset: new Date().getTimezoneOffset().toString(), + }; + if (projectId !== '*') {queryParams.project_id = projectId; } + if (activityId !== '*') {queryParams.activity_id = activityId; } + + return this.http.get(loadEntriesByTimeRangeURL, { + params: { ...queryParams }, + }); } getDateLastMonth() { - return (moment().subtract(1, 'months')).format(); + return moment().subtract(1, 'months').format(); } getCurrentDate() { diff --git a/src/app/modules/time-clock/store/entry.actions.ts b/src/app/modules/time-clock/store/entry.actions.ts index f58378ffa..734515c59 100644 --- a/src/app/modules/time-clock/store/entry.actions.ts +++ b/src/app/modules/time-clock/store/entry.actions.ts @@ -70,8 +70,7 @@ export class LoadEntriesSummary implements Action { export class LoadEntriesSummarySuccess implements Action { readonly type = EntryActionTypes.LOAD_ENTRIES_SUMMARY_SUCCESS; - constructor(readonly payload: TimeEntriesSummary) { - } + constructor(readonly payload: TimeEntriesSummary) {} } export class LoadEntriesSummaryFail implements Action { @@ -85,43 +84,37 @@ export class LoadActiveEntry implements Action { export class LoadActiveEntrySuccess implements Action { readonly type = EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS; - constructor(readonly payload) { - } + constructor(readonly payload) {} } export class LoadActiveEntryFail implements Action { public readonly type = EntryActionTypes.LOAD_ACTIVE_ENTRY_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export class LoadEntries implements Action { public readonly type = EntryActionTypes.LOAD_ENTRIES; - constructor(public month: number, public year: number) { - } + constructor(public month: number, public year: number) {} } export class LoadEntriesSuccess implements Action { readonly type = EntryActionTypes.LOAD_ENTRIES_SUCCESS; - constructor(readonly payload: Entry[]) { - } + constructor(readonly payload: Entry[]) {} } export class LoadEntriesFail implements Action { public readonly type = EntryActionTypes.LOAD_ENTRIES_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export class CreateEntry implements Action { public readonly type = EntryActionTypes.CREATE_ENTRY; - constructor(public payload: NewEntry) { - } + constructor(public payload: NewEntry) {} } export class CreateEntrySuccess implements Action { @@ -132,104 +125,89 @@ export class CreateEntrySuccess implements Action { export class CreateEntryFail implements Action { public readonly type = EntryActionTypes.CREATE_ENTRY_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export class DeleteEntry implements Action { public readonly type = EntryActionTypes.DELETE_ENTRY; - constructor(public entryId: string) { - } + constructor(public entryId: string) {} } export class DeleteEntrySuccess implements Action { public readonly type = EntryActionTypes.DELETE_ENTRY_SUCCESS; - constructor(public entryId: string) { - } + constructor(public entryId: string) {} } export class DeleteEntryFail implements Action { public readonly type = EntryActionTypes.DELETE_ENTRY_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export class UpdateEntryRunning implements Action { public readonly type = EntryActionTypes.UPDATE_ENTRY_RUNNING; - constructor(public payload) { - } + constructor(public payload) {} } export class UpdateEntry implements Action { public readonly type = EntryActionTypes.UPDATE_ENTRY; - constructor(public payload) { - } + constructor(public payload) {} } export class UpdateEntrySuccess implements Action { public readonly type = EntryActionTypes.UPDATE_ENTRY_SUCCESS; - constructor(public payload: Entry) { - } + constructor(public payload: Entry) {} } export class UpdateEntryFail implements Action { public readonly type = EntryActionTypes.UPDATE_ENTRY_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export class UpdateCurrentOrLastEntry implements Action { public readonly type = EntryActionTypes.UPDATE_CURRENT_OR_LAST_ENTRY; - constructor(public payload) { - } + constructor(public payload) {} } export class UpdateCurrentOrLastEntryFail implements Action { public readonly type = EntryActionTypes.UPDATE_CURRENT_OR_LAST_ENTRY_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export class StopTimeEntryRunning implements Action { public readonly type = EntryActionTypes.STOP_TIME_ENTRY_RUNNING; - constructor(readonly payload: string) { - } + constructor(readonly payload: string) {} } export class StopTimeEntryRunningSuccess implements Action { public readonly type = EntryActionTypes.STOP_TIME_ENTRY_RUNNING_SUCCESS; - constructor(readonly payload) { - } + constructor(readonly payload) {} } export class StopTimeEntryRunningFail implements Action { public readonly type = EntryActionTypes.STOP_TIME_ENTRY_RUNNING_FAILED; - constructor(public error: string) { - } + constructor(public error: string) {} } export class CleanEntryCreateError implements Action { public readonly type = EntryActionTypes.CLEAN_ENTRY_CREATE_ERROR; - constructor(public error: boolean) { - } + constructor(public error: boolean) {} } export class CleanEntryUpdateError implements Action { public readonly type = EntryActionTypes.CLEAN_ENTRY_UPDATE_ERROR; - constructor(public error: boolean) { - } + constructor(public error: boolean) {} } export class DefaultEntry implements Action { @@ -239,15 +217,18 @@ export class DefaultEntry implements Action { export class LoadEntriesByTimeRange implements Action { public readonly type = EntryActionTypes.LOAD_ENTRIES_BY_TIME_RANGE; - constructor(readonly timeRange: TimeEntriesTimeRange, readonly userId: string = '*') { - } + constructor( + readonly timeRange: TimeEntriesTimeRange, + readonly userId: string = '*', + readonly projectId: string = '*', + readonly activityId: string = '*' + ) {} } export class LoadEntriesByTimeRangeSuccess implements Action { readonly type = EntryActionTypes.LOAD_ENTRIES_BY_TIME_RANGE_SUCCESS; - constructor(readonly payload: Entry[]) { - } + constructor(readonly payload: Entry[]) {} } export class LoadEntriesByTimeRangeFail implements Action { @@ -257,22 +238,19 @@ export class LoadEntriesByTimeRangeFail implements Action { export class RestartEntry implements Action { readonly type = EntryActionTypes.RESTART_ENTRY; - constructor(readonly entry: Entry) { - } + constructor(readonly entry: Entry) {} } export class RestartEntrySuccess implements Action { readonly type = EntryActionTypes.RESTART_ENTRY_SUCCESS; - constructor(readonly payload: Entry) { - } + constructor(readonly payload: Entry) {} } export class RestartEntryFail implements Action { public readonly type = EntryActionTypes.RESTART_ENTRY_FAIL; - constructor(public error: string) { - } + constructor(public error: string) {} } export type EntryActions = diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts index 1fea3ba58..180893600 100644 --- a/src/app/modules/time-clock/store/entry.effects.ts +++ b/src/app/modules/time-clock/store/entry.effects.ts @@ -258,7 +258,7 @@ export class EntryEffects { ofType(actions.EntryActionTypes.LOAD_ENTRIES_BY_TIME_RANGE), map((action: actions.LoadEntriesByTimeRange) => action), mergeMap((action) => - this.entryService.loadEntriesByTimeRange(action.timeRange, action.userId).pipe( + this.entryService.loadEntriesByTimeRange(action.timeRange, action.userId, action.projectId, action.activityId).pipe( map((response) => { if (response.length >= MAX_NUMBER_OF_ENTRIES_FOR_REPORTS){ this.toastrService.warning( diff --git a/src/app/modules/time-entries/pages/time-entries.component.ts b/src/app/modules/time-entries/pages/time-entries.component.ts index 99a3845b8..9ce87dff5 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -146,7 +146,7 @@ export class TimeEntriesComponent implements OnInit, OnDestroy, AfterViewInit { this.checkIfActiveEntryOverlapping(isEditingEntryEqualToActiveEntry, startDateAsLocalDate); if (!isEditingEntryEqualToActiveEntry && isTimeEntryOverlapping || this.isActiveEntryOverlapping ) { const message = this.isActiveEntryOverlapping ? 'try another "Time in"' : 'try with earlier times'; - this.toastrService.error(`You are on the clock and this entry overlaps it, ${message}.`); + this.toastrService.error(`There is an overlap with another time entry, please modify the time.`); this.isActiveEntryOverlapping = false; } else { this.doSave(event); diff --git a/tsconfig.json b/tsconfig.json index 80a8e1cb8..bf3e900b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "dom.Iterable" ] }, "angularCompilerOptions": {