diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts b/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts index 236baf097..9e39ed8e9 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts +++ b/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts @@ -245,19 +245,25 @@ describe('DetailsFieldsComponent', () => { expect(component.saveEntry.emit).toHaveBeenCalledWith(data); }); - it('when the current entry is not running, then the end hour input should be rendered', () => { + it('when the current entry is not running, then the end date and end hour inputs should be rendered', () => { component.goingToWorkOnThis = false; fixture.detectChanges(); + const endDateInput = fixture.debugElement.nativeElement.querySelector('#end_date'); const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour'); + + expect(endDateInput).toBeDefined(); expect(endHourInput).toBeDefined(); }); - it('when the current entry is running, then the end hour input should not be rendered', () => { + it('when the current entry is running, then the end date and end hour inputs should not be rendered', () => { component.goingToWorkOnThis = true; fixture.detectChanges(); + const endDateInput = fixture.debugElement.nativeElement.querySelector('#end_date'); const endHourInput = fixture.debugElement.nativeElement.querySelector('#end_hour'); + + expect(endDateInput).toBeNull(); expect(endHourInput).toBeNull(); }); 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 14d47737a..f2c7fc900 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 @@ -20,7 +20,6 @@ import { ProjectSelectedEvent } from './project-selected-event'; import { get } from 'lodash'; import { DATE_FORMAT } from 'src/environments/environment'; - type Merged = TechnologyState & ProjectState & ActivityState & EntryState; @Component({ selector: 'app-details-fields', @@ -28,7 +27,6 @@ type Merged = TechnologyState & ProjectState & ActivityState & EntryState; styleUrls: ['./details-fields.component.scss'], }) export class DetailsFieldsComponent implements OnChanges, OnInit { - keyword = 'search_field'; @Input() entryToEdit: Entry; @Input() canMarkEntryAsWIP: boolean; @@ -43,8 +41,12 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { goingToWorkOnThis = false; shouldRestartEntry = false; - constructor(private formBuilder: FormBuilder, private store: Store, - private actionsSubject$: ActionsSubject, private toastrService: ToastrService) { + constructor( + private formBuilder: FormBuilder, + private store: Store, + private actionsSubject$: ActionsSubject, + private toastrService: ToastrService + ) { this.entryForm = this.formBuilder.group({ project_id: ['', Validators.required], project_name: ['', Validators.required], @@ -66,11 +68,10 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { if (projects) { this.listProjects = []; projects.forEach((project) => { - const projectWithSearchField = {...project}; - projectWithSearchField.search_field = `${project.customer_name} - ${project.name}`; - this.listProjects.push(projectWithSearchField); - } - ); + const projectWithSearchField = { ...project }; + projectWithSearchField.search_field = `${project.customer_name} - ${project.name}`; + this.listProjects.push(projectWithSearchField); + }); } }); @@ -95,31 +96,32 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { } }); - this.actionsSubject$.pipe( - filter((action: any) => ( - action.type === EntryActionTypes.CREATE_ENTRY_SUCCESS || - action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS - )) - ).subscribe(() => { - this.cleanForm(); - }); + this.actionsSubject$ + .pipe( + filter( + (action: any) => + action.type === EntryActionTypes.CREATE_ENTRY_SUCCESS || + action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS + ) + ) + .subscribe(() => { + this.cleanForm(); + }); } onClearedComponent(event) { - this.entryForm.patchValue( - { - project_id: '', - project_name: '', - }); + this.entryForm.patchValue({ + project_id: '', + project_name: '', + }); } onSelectedProject(item) { - this.projectSelected.emit({ projectId : item.id}); - this.entryForm.patchValue( - { - project_id: item.id, - project_name: item.search_field, - }); + this.projectSelected.emit({ projectId: item.id }); + this.entryForm.patchValue({ + project_id: item.id, + project_name: item.search_field, + }); } ngOnChanges(): void { @@ -197,7 +199,7 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { this.closeModal?.nativeElement?.click(); } - dateToSubmit(date, hour){ + dateToSubmit(date, hour) { const entryFormDate = this.entryForm.value[date]; const updatedHour = this.entryForm.value[hour]; const initialDate = this.entryToEdit[date]; @@ -246,7 +248,10 @@ export class DetailsFieldsComponent implements OnChanges, OnInit { onGoingToWorkOnThisChange(event: any) { this.goingToWorkOnThis = event.currentTarget.checked; if (!this.goingToWorkOnThis) { - this.entryForm.patchValue({ end_hour: formatDate(new Date(), 'HH:mm:ss', 'en') }); + this.entryForm.patchValue({ + end_date: formatDate(get(this.entryToEdit, 'start_date', ''), DATE_FORMAT, 'en'), + end_hour: formatDate(get(this.entryToEdit, 'start_date', '00:00'), 'HH:mm', 'en'), + }); } this.shouldRestartEntry = !this.entryToEdit?.running && this.goingToWorkOnThis; } diff --git a/src/app/modules/shared/components/month-picker/month-picker.component.html b/src/app/modules/shared/components/month-picker/month-picker.component.html index 204ecb6d7..8bb7c7901 100644 --- a/src/app/modules/shared/components/month-picker/month-picker.component.html +++ b/src/app/modules/shared/components/month-picker/month-picker.component.html @@ -1,10 +1,22 @@ -
-
-
{{ month }}
+
+
+ +

+ {{selectedYearText}} +

+ +
+
+
+
+
diff --git a/src/app/modules/shared/components/month-picker/month-picker.component.scss b/src/app/modules/shared/components/month-picker/month-picker.component.scss index f4123a4b3..fc1a00665 100644 --- a/src/app/modules/shared/components/month-picker/month-picker.component.scss +++ b/src/app/modules/shared/components/month-picker/month-picker.component.scss @@ -5,6 +5,9 @@ line-height: 1.2em; font-weight: bold; } +.content-months { + margin: 0; +} .month { background: $primary; @@ -21,5 +24,7 @@ @include highlight(); border-radius: 0.2em; text-decoration: underline; - +} +.spacing { + padding: 0.1em; } diff --git a/src/app/modules/shared/components/month-picker/month-picker.component.spec.ts b/src/app/modules/shared/components/month-picker/month-picker.component.spec.ts index c8d29053e..ea46623f0 100644 --- a/src/app/modules/shared/components/month-picker/month-picker.component.spec.ts +++ b/src/app/modules/shared/components/month-picker/month-picker.component.spec.ts @@ -1,4 +1,6 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import * as moment from 'moment'; +import { NEVER } from 'rxjs'; import { MonthPickerComponent } from './month-picker.component'; @@ -23,10 +25,46 @@ describe('MonthPickerComponent', () => { expect(component).toBeTruthy(); }); - it('should emit activeMonth event', () => { - const month = 2; - spyOn(component.monthSelected, 'emit'); - component.getMonth(month); - expect(component.monthSelected.emit).toHaveBeenCalledWith(month + 1); + it('should emit monthIndex and year', () => { + const month = new Date().getMonth(); + const year = new Date().getFullYear(); + + spyOn(component.dateSelected, 'emit'); + expect(component.dateSelected.emit({ monthIndex: month, year: year })); }); + + + it('should add a year to current year', () => { + component.selectedYearMoment = moment(); + const incrementYear = moment().add(1, 'year').format('Y'); + spyOn(component, 'increment').and.callThrough(); + + component.increment(); + + expect(component.increment).toHaveBeenCalled(); + expect(component.selectedYearText).toEqual(incrementYear); + expect(component.selectedYearMoment.format('Y')).toEqual(incrementYear); + }); + + + it('should subtract a year to current year', () => { + component.selectedYearMoment = moment(); + const decrementYear = moment().subtract(1, 'year').format('Y'); + spyOn(component, 'decrement').and.callThrough(); + + component.decrement(); + + expect(component.decrement).toHaveBeenCalled(); + expect(component.selectedYearMoment.format('Y')).toEqual(decrementYear); + }); + + + it('selectMonth should call selectDates', () => { + spyOn(component, 'selectDate'); + + component.selectMonth(8); + + expect(component.selectDate).toHaveBeenCalledWith(8, 2020); + }); + }); diff --git a/src/app/modules/shared/components/month-picker/month-picker.component.ts b/src/app/modules/shared/components/month-picker/month-picker.component.ts index 1a404f058..8041c9efc 100644 --- a/src/app/modules/shared/components/month-picker/month-picker.component.ts +++ b/src/app/modules/shared/components/month-picker/month-picker.component.ts @@ -1,4 +1,6 @@ +import { SubstractDatePipe } from './../../pipes/substract-date/substract-date.pipe'; import { Component, OnInit, Output, EventEmitter } from '@angular/core'; +import * as moment from 'moment'; @Component({ selector: 'app-month-picker', @@ -6,18 +8,64 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core'; styleUrls: ['./month-picker.component.scss'] }) export class MonthPickerComponent implements OnInit { - @Output() monthSelected = new EventEmitter(); - activeMonth: number; - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + @Output() dateSelected = new EventEmitter<{ + monthIndex: number; + year: number; + }>(); - constructor() { } + selectedMonthMoment: moment.Moment; + selectedMonthIndex: number; + selectedYearMoment: moment.Moment; + selectedMonthYear: number; - ngOnInit(): void { - this.activeMonth = new Date().getMonth(); + selectedYearText: string; + months: Array = []; + years: Array = []; + + constructor() { + this.selectedYearMoment = moment(); + this.selectedMonthMoment = moment(); + this.months = moment.months(); + this.selectedMonthIndex = this.selectedMonthMoment.month(); + this.selectedMonthYear = this.selectedYearMoment.year(); + this.updateYearText(); + } + + ngOnInit() { + this.selectDate(this.selectedMonthIndex, this.selectedMonthYear); + } + + updateYearText() { + this.selectedYearText = moment(this.selectedYearMoment).format('YYYY'); + } + + increment() { + this.selectedYearMoment = this.selectedYearMoment.add(1, 'year'); + this.updateYearText(); } - getMonth(month: number) { - this.monthSelected.emit(month + 1); - this.activeMonth = month; + decrement() { + this.selectedYearMoment = this.selectedYearMoment.subtract(1, 'year'); + this.updateYearText(); } + + selectMonth(index: number) { + this.selectedMonthMoment = moment().month(index); + this.selectedMonthIndex = this.selectedMonthMoment.month(); + this.selectedMonthYear = this.selectedYearMoment.year(); + this.selectDate(this.selectedMonthIndex, this.selectedMonthYear); + } + + isSelectedMonth(monthIndex: number) { + return ( + this.selectedMonthIndex === monthIndex && + this.selectedMonthYear === this.selectedYearMoment.year() + ); + } + + selectDate(monthIndex: number, year: number) { + this.dateSelected.emit({ monthIndex: monthIndex, year: year }); + } + } + diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts index ebcc5531d..b799c4b6b 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts @@ -32,6 +32,8 @@ export class EntryFieldsComponent implements OnInit { newData; lastEntry; showTimeInbuttons = false; + month = new Date().getMonth(); + year = new Date().getFullYear(); constructor( private formBuilder: FormBuilder, @@ -50,7 +52,7 @@ export class EntryFieldsComponent implements OnInit { ngOnInit(): void { this.store.dispatch(new LoadActivities()); - this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1)); + this.store.dispatch(new entryActions.LoadEntries(this.month, this.year)); this.actionsSubject$ .pipe(filter((action: any) => action.type === ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS)) .subscribe((action) => { 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 9979c2566..681fe51e0 100644 --- a/src/app/modules/time-clock/services/entry.service.spec.ts +++ b/src/app/modules/time-clock/services/entry.service.spec.ts @@ -54,11 +54,13 @@ describe('EntryService', () => { }); it('load all Entries', () => { + const year = new Date().getFullYear(); const month = new Date().getMonth(); + const timezoneOffset = new Date().getTimezoneOffset(); - service.loadEntries(month).subscribe(); + service.loadEntries(year, month).subscribe(); - const loadEntryRequest = httpMock.expectOne(`${service.baseUrl}?month=${month}&timezone_offset=${timezoneOffset}`); + const loadEntryRequest = httpMock.expectOne(`${service.baseUrl}?month=${month}&year=${year}&timezone_offset=${timezoneOffset}`); expect(loadEntryRequest.request.method).toBe('GET'); }); diff --git a/src/app/modules/time-clock/services/entry.service.ts b/src/app/modules/time-clock/services/entry.service.ts index 6f2a96ad2..25d64ea80 100644 --- a/src/app/modules/time-clock/services/entry.service.ts +++ b/src/app/modules/time-clock/services/entry.service.ts @@ -23,9 +23,9 @@ export class EntryService { return this.http.get(`${this.baseUrl}/running`); } - loadEntries(month): Observable { + loadEntries(year, month): Observable { const timezoneOffset = new Date().getTimezoneOffset(); - return this.http.get(`${this.baseUrl}?month=${month}&timezone_offset=${timezoneOffset}`); + return this.http.get(`${this.baseUrl}?month=${month}&year=${year}&timezone_offset=${timezoneOffset}`); } createEntry(entryData): Observable { diff --git a/src/app/modules/time-clock/store/entry.actions.ts b/src/app/modules/time-clock/store/entry.actions.ts index abdb9c75f..f58378ffa 100644 --- a/src/app/modules/time-clock/store/entry.actions.ts +++ b/src/app/modules/time-clock/store/entry.actions.ts @@ -99,7 +99,7 @@ export class LoadActiveEntryFail implements Action { export class LoadEntries implements Action { public readonly type = EntryActionTypes.LOAD_ENTRIES; - constructor(public month: number) { + constructor(public month: number, public year: number) { } } diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts index 806ec9a19..ea5973c4e 100644 --- a/src/app/modules/time-clock/store/entry.effects.ts +++ b/src/app/modules/time-clock/store/entry.effects.ts @@ -87,9 +87,10 @@ export class EntryEffects { @Effect() loadEntries$: Observable = this.actions$.pipe( ofType(actions.EntryActionTypes.LOAD_ENTRIES), - map((action: actions.LoadEntries) => action.month), - mergeMap((month) => - this.entryService.loadEntries(month).pipe( + // tslint:disable-next-line:no-unused-expression + map((action: actions.LoadEntries) => (action.month, action.year)), + mergeMap((month, year) => + this.entryService.loadEntries(month, year).pipe( map((entries) => new actions.LoadEntriesSuccess(entries)), catchError((error) => { this.toastrService.warning(`The data could not be loaded`); @@ -97,6 +98,7 @@ export class EntryEffects { }) ) ) + ); @Effect() @@ -226,7 +228,7 @@ export class EntryEffects { ofType(actions.EntryActionTypes.UPDATE_CURRENT_OR_LAST_ENTRY), map((action: actions.UpdateCurrentOrLastEntry) => action.payload), switchMap((entry) => - this.entryService.loadEntries(new Date().getMonth() + 1).pipe( + this.entryService.loadEntries(new Date().getMonth() + 1, new Date().getFullYear()).pipe( map((entries) => { const lastEntry = entries[1]; const isStartTimeInLastEntry = moment(entry.start_date).isBefore(lastEntry.end_date); 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 ecc683589..dafce2ee7 100644 --- a/src/app/modules/time-clock/store/entry.reducer.spec.ts +++ b/src/app/modules/time-clock/store/entry.reducer.spec.ts @@ -107,7 +107,9 @@ describe('entryReducer', () => { }); it('on LoadEntries, isLoading is true', () => { - const action = new actions.LoadEntries(new Date().getMonth() + 1); + const month = 12; + const year = 2020; + const action = new actions.LoadEntries(month, year); const state = entryReducer(initialState, action); expect(state.timeEntriesDataSource.isLoading).toEqual(true); }); 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 8928eb656..8fabce8a2 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.html +++ b/src/app/modules/time-entries/pages/time-entries.component.html @@ -13,7 +13,7 @@
- +
diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index b2237ce1e..2d83a344e 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -126,7 +126,8 @@ describe('TimeEntriesComponent', () => { })); it('when create time entries, the time entries should be queried', () => { - const currentMonth = new Date().getMonth() + 1; + const currentMonth = new Date().getMonth(); + const year = new Date().getFullYear(); const entryToSave = { entry: { project_id: 'project-id', @@ -141,7 +142,7 @@ describe('TimeEntriesComponent', () => { component.saveEntry(entryToSave); expect(store.dispatch).toHaveBeenCalledWith(new entryActions.CreateEntry(entryToSave.entry)); - expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntries(currentMonth)); + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntries(currentMonth, year)); }); it('when creating a new entry, then entryId should be null', () => { @@ -322,11 +323,13 @@ describe('TimeEntriesComponent', () => { expect(store.dispatch).toHaveBeenCalledWith(new entryActions.DeleteEntry('id')); }); - it('should get the entry List by Month', () => { - const month = 1; + it('should get the entry List by Month and year', () => { + const month = new Date().getMonth(); + const year = new Date().getFullYear(); + spyOn(store, 'dispatch'); - component.getMonth(month); - expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntries(month)); + component.dateSelected({monthIndex: month, year: year}); + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntries(month + 1, year)); }); it('doSave when activeTimeEntry === null', waitForAsync(() => { 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 9d564946c..62503d972 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -1,3 +1,4 @@ +import { formatDate } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActionsSubject, select, Store } from '@ngrx/store'; import { ToastrService } from 'ngx-toastr'; @@ -8,6 +9,7 @@ import { SaveEntryEvent } from '../../shared/components/details-fields/save-entr import { Entry } from '../../shared/models'; import { DataSource } from '../../shared/models/data-source.model'; import * as entryActions from '../../time-clock/store/entry.actions'; +import * as moment from 'moment'; import { EntryState } from '../../time-clock/store/entry.reducer'; import { EntryActionTypes } from './../../time-clock/store/entry.actions'; import { getActiveTimeEntry, getTimeEntriesDataSource } from './../../time-clock/store/entry.selectors'; @@ -26,6 +28,12 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { entriesSubscription: Subscription; canMarkEntryAsWIP = true; timeEntriesDataSource$: Observable>; + selectedYearAsText: string; + selectedMonthIndex: number; + selectedMonthAsText: string; + + currentMonth = new Date().getMonth(); + year = new Date().getFullYear(); constructor(private store: Store, private toastrService: ToastrService, private actionsSubject$: ActionsSubject) { this.timeEntriesDataSource$ = this.store.pipe(delay(0), select(getTimeEntriesDataSource)); @@ -36,7 +44,9 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1)); + + this.store.dispatch(new entryActions.LoadEntries(this.selectedMonthIndex, this.year)); + this.loadActiveEntry(); this.entriesSubscription = this.actionsSubject$.pipe( @@ -48,7 +58,6 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { ) ).subscribe((action) => { this.loadActiveEntry(); - this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1)); }); } @@ -107,8 +116,7 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { const isEditingEntryEqualToActiveEntry = this.entryId === this.activeTimeEntry.id; const isStartDateGreaterThanActiveEntry = startDateAsLocalDate > activeEntryAsLocalDate; const isEndDateGreaterThanActiveEntry = endDateAsLocalDate > activeEntryAsLocalDate; - const isTimeEntryOverlapping = isStartDateGreaterThanActiveEntry || isEndDateGreaterThanActiveEntry; - if (!isEditingEntryEqualToActiveEntry && isTimeEntryOverlapping) { + if (!isEditingEntryEqualToActiveEntry && (isStartDateGreaterThanActiveEntry || isEndDateGreaterThanActiveEntry)){ this.toastrService.error('You are on the clock and this entry overlaps it, try with earlier times.'); } else { this.doSave(event); @@ -161,8 +169,11 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { this.showModal = false; } - getMonth(month: number) { - this.store.dispatch(new entryActions.LoadEntries(month)); + dateSelected(event: { monthIndex: number; year: number }) { + this.selectedYearAsText = event.year.toString(); + this.selectedMonthIndex = event.monthIndex; + this.selectedMonthAsText = moment().month(event.monthIndex).format('MMMM'); + this.store.dispatch(new entryActions.LoadEntries(event.monthIndex + 1, event.year)); } openModal(item: any) { diff --git a/tslint.json b/tslint.json index 37c500fe1..eda9ad204 100644 --- a/tslint.json +++ b/tslint.json @@ -76,6 +76,7 @@ "no-redundant-jsdoc": true, "no-switch-case-fall-through": true, "no-var-requires": false, + "object-literal-shorthand": false, "object-literal-key-quotes": [ true, "as-needed"