diff --git a/package.json b/package.json index e8fcb27ed..3377397be 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@ngrx/store": "^10.0.1", "@ngrx/store-devtools": "^10.0.1", "@types/datatables.net-buttons": "^1.4.3", + "angular-calendar": "^0.28.24", + "date-fns": "^2.22.1", "angular-datatables": "^9.0.2", "bootstrap": "^4.4.1", "datatables.net": "^1.10.21", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e31aad87e..d0bca2dfd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,6 +16,8 @@ import { MatInputModule } from '@angular/material/input'; import { MatMomentDateModule } from '@angular/material-moment-adapter'; import { NgxPaginationModule } from 'ngx-pagination'; import { AutocompleteLibModule } from 'angular-ng-autocomplete'; +import { CalendarModule, DateAdapter } from 'angular-calendar'; +import { adapterFactory } from 'angular-calendar/date-adapters/date-fns'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -81,6 +83,7 @@ import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker'; // tslint:disable-next-line: max-line-length import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component'; import { TechnologyReportComponent } from './modules/technology-report/pages/technology-report.component'; +import { CalendarComponent } from './modules/time-entries/components/calendar/calendar.component'; const maskConfig: Partial = { validation: false, @@ -133,6 +136,7 @@ const maskConfig: Partial = { UsersListComponent, TechnologyReportComponent, TechnologyReportTableComponent, + CalendarComponent, ], imports: [ NgxMaskModule.forRoot(maskConfig), @@ -171,6 +175,10 @@ const maskConfig: Partial = { UserEffects, ]), ToastrModule.forRoot(), + CalendarModule.forRoot({ + provide: DateAdapter, + useFactory: adapterFactory, + }), ], providers: [ { 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 9f1cb151f..0cdc3d5dc 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,12 +1,14 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import * as moment from 'moment'; -import { NEVER } from 'rxjs'; +import { CookieService } from 'ngx-cookie-service'; +import { FeatureToggle } from 'src/environments/enum'; import { MonthPickerComponent } from './month-picker.component'; describe('MonthPickerComponent', () => { let component: MonthPickerComponent; let fixture: ComponentFixture; + let cookieService: CookieService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ MonthPickerComponent ] @@ -16,6 +18,7 @@ describe('MonthPickerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MonthPickerComponent); + cookieService = TestBed.inject(CookieService); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -73,4 +76,203 @@ describe('MonthPickerComponent', () => { expect(component.monthEnable(monthFuture)).toBeTrue(); }); + it('call cookieService.get() when component was create', () => { + spyOn(cookieService, 'get'); + + component.ngOnInit(); + + expect(cookieService.get).toHaveBeenCalledWith(FeatureToggle.TIME_TRACKER_CALENDAR); + }); + + it('set true in isFeatureToggleCalendarActive when component was create', () => { + const expectCookieValue = true; + spyOn(cookieService, 'get').and.returnValue(`${ expectCookieValue }`); + + component.ngOnInit(); + + expect(component.isFeatureToggleCalendarActive).toEqual(expectCookieValue); + }); + + it('set false in isFeatureToggleCalendarActive when component was create', () => { + const expectCookieValue = false; + spyOn(cookieService, 'get').and.returnValue(`${ expectCookieValue }`); + + component.ngOnInit(); + + expect(component.isFeatureToggleCalendarActive).toEqual(expectCookieValue); + }); + + it('set false in isFeatureToggleCalendarActive when cookie does not exist', () => { + const expectCookieValue = false; + spyOn(cookieService, 'get'); + + component.ngOnInit(); + + expect(component.isFeatureToggleCalendarActive).toEqual(expectCookieValue); + }); + + it('call refresData when updating the value of selectedDate', () => { + const fakeSelectedDate: moment.Moment = moment(new Date()); + spyOn(component, 'refreshDate'); + + component.selectedDate = fakeSelectedDate; + + expect(component.refreshDate).toHaveBeenCalledWith(fakeSelectedDate); + }); + + it('not set value of selectedDate in selectedDateMoment when isFeatureToggleCalendarActive is false', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-05')); + component.isFeatureToggleCalendarActive = false; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedDateMoment).not.toEqual(fakeSelectedDate); + }); + + it('set value of selectedDate in selectedDateMoment when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-05')); + component.isFeatureToggleCalendarActive = true; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedDateMoment).toEqual(fakeSelectedDate); + }); + + + it('set current Month index in selectedMonthIndex when isFeatureToggleCalendarActive is false', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-05')); + const currentDate: moment.Moment = moment(new Date()); + component.isFeatureToggleCalendarActive = false; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedMonthIndex).toEqual(currentDate.month()); + }); + + it('set month of selectedDate in selectedMonthIndex when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-05')); + component.isFeatureToggleCalendarActive = true; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedMonthIndex).toEqual(fakeSelectedDate.month()); + }); + + it('set current year as a text in selectedYearText when isFeatureToggleCalendarActive is false', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2020-07-05')); + const currentDate: number = moment(new Date()).year(); + component.isFeatureToggleCalendarActive = false; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedYearText).toEqual(`${currentDate}`); + }); + + it('set year as a text of selectedDate in selectedYearText when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('1999-07-05')); + const expectedYear = '1999'; + component.isFeatureToggleCalendarActive = true; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedYearText).toEqual(expectedYear); + }); + + it('set current year in selectedYear when isFeatureToggleCalendarActive is false', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2020-07-05')); + const currentDate: number = moment(new Date()).year(); + component.isFeatureToggleCalendarActive = false; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedYear).toEqual(currentDate); + }); + + it('set year as a text of selectedDate in selectedYear when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('1999-07-05')); + const expectedYear = 1999; + component.isFeatureToggleCalendarActive = true; + + component.refreshDate(fakeSelectedDate); + + expect(component.selectedYear).toEqual(expectedYear); + }); + + it('not true in showArrowNext when isFeatureToggleCalendarActive is false', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('1999-07-05')); + component.isFeatureToggleCalendarActive = false; + + component.refreshDate(fakeSelectedDate); + + expect(component.showArrowNext).not.toEqual(true); + }); + + it('false in showArrowNext when isFeatureToggleCalendarActive is false', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('1999-07-05')); + component.isFeatureToggleCalendarActive = false; + + component.refreshDate(fakeSelectedDate); + + expect(component.showArrowNext).toEqual(false); + }); + + it('set true in showArrowNext when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('1999-07-05')); + component.isFeatureToggleCalendarActive = true; + + component.refreshDate(fakeSelectedDate); + + expect(component.showArrowNext).toEqual(true); + }); + + it('set false in showArrowNext when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date()).add(1, 'month'); + component.isFeatureToggleCalendarActive = true; + + component.refreshDate(fakeSelectedDate); + + expect(component.showArrowNext).toEqual(false); + }); + + it('isSelectedMonth returns true when isFeatureToggleCalendarActive is true', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-06')); + const expectedReturn = true; + const fakeFeatureToggleValue = true; + component.selectedMonthIndex = fakeSelectedDate.month(); + component.selectedYear = fakeSelectedDate.year(); + component.selectedDateMoment = fakeSelectedDate; + component.isFeatureToggleCalendarActive = fakeFeatureToggleValue; + + const response = component.isSelectedMonth(fakeSelectedDate.month()); + + expect(response).toEqual(expectedReturn); + }); + + it('isSelectedMonth returns false when isFeatureToggleCalendarActive is true and the months are not the same', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-06')); + const expectedReturn = false; + const fakeFeatureToggleValue = true; + component.selectedMonthIndex = fakeSelectedDate.month(); + component.selectedYear = fakeSelectedDate.year(); + component.selectedDateMoment = fakeSelectedDate; + component.isFeatureToggleCalendarActive = fakeFeatureToggleValue; + + const response = component.isSelectedMonth(fakeSelectedDate.add(1, 'month').month()); + + expect(response).toEqual(expectedReturn); + }); + + it('isSelectedMonth returns false when isFeatureToggleCalendarActive is true and the years are not the same', () => { + const fakeSelectedDate: moment.Moment = moment(new Date('2021-07-06')); + const expectedReturn = false; + const fakeFeatureToggleValue = true; + component.selectedMonthIndex = fakeSelectedDate.month(); + component.selectedYear = fakeSelectedDate.year(); + component.selectedDateMoment = fakeSelectedDate.add(1, 'year'); + component.isFeatureToggleCalendarActive = fakeFeatureToggleValue; + + const response = component.isSelectedMonth(fakeSelectedDate.month()); + + expect(response).toEqual(expectedReturn); + }); }); 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 156f7a02f..282d7a0e5 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,5 +1,7 @@ -import { Component, OnInit, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core'; import * as moment from 'moment'; +import { CookieService } from 'ngx-cookie-service'; +import { FeatureToggle } from 'src/environments/enum'; @Component({ selector: 'app-month-picker', @@ -7,6 +9,10 @@ import * as moment from 'moment'; styleUrls: ['./month-picker.component.scss'] }) export class MonthPickerComponent implements OnInit { + @Input() + set selectedDate(selectedDateMoment: moment.Moment){ + this.refreshDate(selectedDateMoment); + } @Output() dateSelected = new EventEmitter<{ monthIndex: number; year: number; @@ -21,7 +27,9 @@ export class MonthPickerComponent implements OnInit { currentYear = new Date().getFullYear(); showArrowNext = false; monthCurrent = moment().month(); - constructor() { + selectedDateMoment: moment.Moment = moment(); + isFeatureToggleCalendarActive: boolean; + constructor(private cookiesService: CookieService) { this.selectedYearMoment = moment(); this.selectedMonthMoment = moment(); this.months = moment.months(); @@ -31,9 +39,20 @@ export class MonthPickerComponent implements OnInit { } ngOnInit() { + this.isFeatureToggleCalendarActive = (this.cookiesService.get(FeatureToggle.TIME_TRACKER_CALENDAR) === 'true'); this.selectDate(this.selectedMonthIndex, this.selectedYear); } + refreshDate(newDate: moment.Moment){ + if (this.isFeatureToggleCalendarActive){ + this.selectedDateMoment = newDate; + this.selectedMonthIndex = this.selectedDateMoment.month(); + this.selectedYearText = moment(this.selectedDateMoment).format('YYYY'); + this.selectedYear = this.selectedDateMoment.year(); + this.showArrowNext = this.selectedYear < this.currentYear; + } + } + changeYear(changeAction: string) { this.selectedYearMoment[changeAction](1, 'year'); this.selectedYearText = moment(this.selectedYearMoment).format('YYYY'); @@ -56,6 +75,12 @@ export class MonthPickerComponent implements OnInit { ); } isSelectedMonth(monthIndex: number) { + if (this.isFeatureToggleCalendarActive) { + return ( + this.selectedMonthIndex === monthIndex && + this.selectedYear === this.selectedDateMoment.year() + ); + } return ( this.selectedMonthIndex === monthIndex && this.selectedYear === this.selectedYearMoment.year() diff --git a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts index 4b91ccb37..2d63c43d0 100644 --- a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts +++ b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.spec.ts @@ -59,4 +59,40 @@ describe('SubstractDatePipe', () => { expect(diff).toBe('--:--'); }); + + it('returns 0 if fromDate is null when call transformInMinutes', () => { + const fromDate = null; + const substractDate = new Date('2011-04-11T08:00:30Z'); + + const diffInMinutes = new SubstractDatePipe().transformInMinutes(fromDate, substractDate); + + expect(diffInMinutes).toBe(0); + }); + + it('returns 0 if substractDate is null when call transformInMinutes', () => { + const fromDate = new Date('2011-04-11T08:00:30Z'); + const substractDate = null; + + const diffInMinutes = new SubstractDatePipe().transformInMinutes(fromDate, substractDate); + + expect(diffInMinutes).toBe(0); + }); + + it('returns the date diff in minutes when call transformInMinutes', () => { + [ + { endDate: '2021-04-11T10:20:00Z', startDate: '2021-04-11T08:00:00Z', expectedDiff: 140 }, + { endDate: '2021-04-11T17:40:00Z', startDate: '2021-04-11T17:10:00Z', expectedDiff: 30 }, + { endDate: '2021-04-11T18:18:00Z', startDate: '2021-04-11T18:00:00Z', expectedDiff: 18 }, + { endDate: '2021-04-12T12:18:00Z', startDate: '2021-04-11T10:00:00Z', expectedDiff: 1578 }, + { endDate: '2021-04-12T10:01:00Z', startDate: '2021-04-12T10:00:00Z', expectedDiff: 1 }, + { endDate: '2021-04-11T11:27:00Z', startDate: '2021-04-11T10:03:00Z', expectedDiff: 84 }, + ].forEach(({ startDate, endDate, expectedDiff }) => { + const fromDate = new Date(endDate); + const substractDate = new Date(startDate); + + const diffInMinutes = new SubstractDatePipe().transformInMinutes(fromDate, substractDate); + + expect(diffInMinutes).toBe(expectedDiff); + }); + }); }); diff --git a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts index 3bbaf1466..2fc6ed473 100644 --- a/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts +++ b/src/app/modules/shared/pipes/substract-date/substract-date.pipe.ts @@ -41,4 +41,17 @@ export class SubstractDatePipe implements PipeTransform { formatTime(time: number): string { return new NumberFormatter(time).getAsAtLeastTwoDigitString(); } + + transformInMinutes(fromDate: Date, substractDate: Date): number{ + + if (fromDate === null || substractDate === null) { + return 0; + } + + const startDate = moment(substractDate); + const endDate = moment(fromDate); + const duration = this.getTimeDifference(startDate, endDate); + + return duration.asMinutes(); + } } diff --git a/src/app/modules/time-entries/components/calendar/calendar.component.html b/src/app/modules/time-entries/components/calendar/calendar.component.html new file mode 100644 index 000000000..2423c71cd --- /dev/null +++ b/src/app/modules/time-entries/components/calendar/calendar.component.html @@ -0,0 +1,150 @@ + +
+

+ {{ timeEntries.project_name }} +

+
+

+ {{ timeEntries.activity_name }} +

+

+ + {{ timeEntries.start_date | date: 'HH:mm' }} - + {{ timeEntries.end_date | date: 'HH:mm' }} +

+
+
+

+ {{ timeEntries.description }} +

+
+
+
+ + +
+ +
+
+ + +
+ {{ day.date | calendarDate: 'monthViewDayNumber':locale }} +
+
+ + +
+
+ + +
+ + +
+
+ +
+
+
+
+ + + +
+
+
+

+ {{ currentDate | date: 'EEEE' }} +

+

+ {{ currentDate | date: 'MMM d' }} +

+
+
+
+ + + +
+
+
+ +
+ + + + + + +
+
diff --git a/src/app/modules/time-entries/components/calendar/calendar.component.scss b/src/app/modules/time-entries/components/calendar/calendar.component.scss new file mode 100644 index 000000000..1bba9520d --- /dev/null +++ b/src/app/modules/time-entries/components/calendar/calendar.component.scss @@ -0,0 +1,72 @@ +@import '/src/styles/colors.scss'; + +@function calculate-border-color($base-background-color) { + @return darken(desaturate(adjust-hue($base-background-color, 6), 18), 27); +} + +@function calculate-bold-text-color($text-color) { + @return darken(adjust-hue($text-color, 1), 35); +} + +.container-time-entries { + margin: 2px; + div.time-entries { + padding: 8px; + height: 100%; + border-radius: 7px; + color: $primary-text; + margin: 2px 6px 0px 5px; + background-color: $background-card-entry; + border-left: 5px solid calculate-border-color($background-card-entry); + -webkit-box-shadow: 0px 2px 5px 0px lighten($shadow-card-entry, 50%); + -moz-box-shadow: 0px 2px 5px 0px lighten($shadow-card-entry, 50%); + box-shadow: 0px 2px 5px 0px lighten($shadow-card-entry, 50%); + p { + margin: 0px; + } + p.project-name { + color: calculate-bold-text-color($primary-text); + font-weight: bold; + line-height: 1; + } + p.additional { + color: calculate-bold-text-color($primary-text); + } + } + + div.running-entry { + border-left: 5px solid calculate-border-color($background-card-entry-activate); + background-color: $background-card-entry-activate; + color: $activate-text; + p.project-name { + color: calculate-bold-text-color($activate-text); + } + p.additional { + color: calculate-bold-text-color($activate-text); + } + } +} + +.container-time-entries-adapt-height { + height: calc(100% - 2px); + min-height: 40px; +} + +.close-icon { + font-size: 0.7em; + width: 20px; + height: 20px; + border-radius: 10px; + color: white; + top: -5px; + right: -5px; + -webkit-box-shadow: 0px 2px 5px 0px lighten($shadow-card-entry, 50%); + -moz-box-shadow: 0px 2px 5px 0px lighten($shadow-card-entry, 50%); + box-shadow: 0px 2px 5px 0px lighten($shadow-card-entry, 50%); +} + +.currentDate { + p { + margin: 0; + } +} diff --git a/src/app/modules/time-entries/components/calendar/calendar.component.spec.ts b/src/app/modules/time-entries/components/calendar/calendar.component.spec.ts new file mode 100644 index 000000000..570446115 --- /dev/null +++ b/src/app/modules/time-entries/components/calendar/calendar.component.spec.ts @@ -0,0 +1,291 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { CalendarEvent, CalendarView } from 'angular-calendar'; +import * as moment from 'moment'; +import { Observable, of } from 'rxjs'; +import { Entry } from 'src/app/modules/shared/models'; +import { DataSource } from 'src/app/modules/shared/models/data-source.model'; + +import { CalendarComponent } from './calendar.component'; + +describe('CalendarComponent', () => { + let component: CalendarComponent; + let fixture: ComponentFixture; + let currentDate: moment.Moment; + let fakeEntry: Entry; + let fakeEntryRunning: Entry; + + beforeEach(waitForAsync( () => { + TestBed.configureTestingModule({ + declarations: [ CalendarComponent ] + }) + .compileComponents(); + + currentDate = moment(); + fakeEntry = { + id: 'entry_1', + project_id: 'abc', + project_name: 'Time-tracker', + start_date: new Date('2020-02-05T15:36:15.887Z'), + end_date: new Date('2020-02-05T18:36:15.887Z'), + customer_name: 'ioet Inc.', + activity_id: 'development', + technologies: ['Angular', 'TypeScript'], + description: 'No comments', + uri: 'EY-25', + }; + fakeEntryRunning = { + id: 'entry_1', + project_id: 'abc', + project_name: 'Time-tracker', + start_date: new Date('2020-02-05T15:36:15.887Z'), + end_date: null, + customer_name: 'ioet Inc.', + activity_id: 'development', + technologies: ['Angular', 'TypeScript'], + description: 'No comments', + uri: 'EY-25', + }; + + jasmine.clock().mockDate(currentDate.toDate()); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CalendarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('call castEntryToCalendarEvent when set timeEntries$', () => { + const fakeTimeEntries = new Observable>(); + spyOn(component, 'castEntryToCalendarEvent'); + + component.timeEntries$ = fakeTimeEntries; + + expect(component.castEntryToCalendarEvent).toHaveBeenCalledWith(fakeTimeEntries); + }); + + it('set [] in timeEntriesAsEvent when call castEntryToCalendarEvent and timeEntries is empty', () => { + const expectedtimeEntriesAsEvent = []; + const fakeTimeEntries = new Observable>(); + + component.castEntryToCalendarEvent(fakeTimeEntries); + + expect(component.timeEntriesAsEvent).toEqual(expectedtimeEntriesAsEvent); + }); + + it('set [entry] in timeEntriesAsEvent when call castEntryToCalendarEvent and timeEntries is not empty', () => { + const fakeTimeEntryAsEvent: CalendarEvent = { + start: fakeEntry.start_date, + end: fakeEntry.end_date, + title: fakeEntry.description, + id: fakeEntry.id, + meta: fakeEntry + }; + const fakeDatasource = { + isLoading: false, + data: [fakeEntry] + }; + const fakeTimeEntries = of(fakeDatasource); + const expectedtimeEntriesAsEvent = [fakeTimeEntryAsEvent]; + + component.castEntryToCalendarEvent(fakeTimeEntries); + + expect(component.timeEntriesAsEvent).toEqual(expectedtimeEntriesAsEvent); + expect(component.timeEntriesAsEvent.length).toEqual(1); + }); + + it('set [entry] without endDate in timeEntriesAsEvent when call castEntryToCalendarEvent and timeEntries is not empty', () => { + const fakeTimeEntryAsEvent: CalendarEvent = { + start: fakeEntryRunning.start_date, + end: fakeEntryRunning.end_date, + title: fakeEntryRunning.description, + id: fakeEntryRunning.id, + meta: fakeEntryRunning + }; + const fakeDatasource = { + isLoading: false, + data: [fakeEntryRunning] + }; + const fakeTimeEntries = of(fakeDatasource); + const expectedtimeEntriesAsEvent = [fakeTimeEntryAsEvent]; + + component.castEntryToCalendarEvent(fakeTimeEntries); + + expect(component.timeEntriesAsEvent).toEqual(expectedtimeEntriesAsEvent); + expect(component.timeEntriesAsEvent.length).toEqual(1); + }); + + it('Call navigationEnable when call ngOnInit()', () => { + spyOn(component, 'navigationEnable'); + + component.ngOnInit(); + + expect(component.navigationEnable).toHaveBeenCalledWith(CalendarView.Month); + }); + + it('emit time entry id when call handleEditEvent', () => { + const fakeTimeEntryAsEvent: CalendarEvent = { + start: fakeEntry.start_date, + end: fakeEntry.end_date, + title: fakeEntry.description, + id: fakeEntry.id, + meta: fakeEntry + }; + const fakeValueEmit = { + id: fakeEntry.id, + }; + spyOn(component.viewModal, 'emit'); + + component.handleEditEvent(fakeTimeEntryAsEvent); + + expect(component.viewModal.emit).toHaveBeenCalledWith(fakeValueEmit); + }); + + it('emit time entry meta when call handleDeleteEvent', () => { + const fakeTimeEntryAsEvent: CalendarEvent = { + start: fakeEntry.start_date, + end: fakeEntry.end_date, + title: fakeEntry.description, + id: fakeEntry.id, + meta: fakeEntry + }; + const fakeValueEmit = { + timeEntry: fakeEntry, + }; + spyOn(component.deleteTimeEntry, 'emit'); + + component.handleDeleteEvent(fakeTimeEntryAsEvent); + + expect(component.deleteTimeEntry.emit).toHaveBeenCalledWith(fakeValueEmit); + }); + + it('emit current date and call navigationEnable when call handleChangeDateEvent', () => { + const fakeValueEmit = { + date: currentDate.toDate(), + }; + const calendarView = CalendarView.Month; + spyOn(component, 'navigationEnable'); + spyOn(component.changeDate, 'emit'); + + component.handleChangeDateEvent(); + + expect(component.navigationEnable).toHaveBeenCalledWith(calendarView); + expect(component.changeDate.emit).toHaveBeenCalledWith(fakeValueEmit); + }); + + it('set incoming calendarView in calendarView when call changeCalendarView', () => { + const fakeCalendarView: CalendarView = CalendarView.Day; + + component.changeCalendarView(CalendarView.Day); + + expect(component.calendarView).toEqual(fakeCalendarView); + }); + + it('set false in nextDateDisabled when call navigationEnable and calendarView != Month and currentDate + 1 day is not greater to initialDate', () => { + component.currentDate = moment().subtract(2, 'day').toDate(); + component.initialDate = moment().toDate(); + + component.navigationEnable(CalendarView.Week); + + expect(component.nextDateDisabled).toBeFalse(); + }); + + it('false in nextDateDisabled when call navigationEnable and calendarView == Month and currentDate.month != initialDate.month', () => { + component.currentDate = moment().subtract(2, 'month').toDate(); + component.initialDate = moment().toDate(); + + component.navigationEnable(CalendarView.Month); + + expect(component.nextDateDisabled).toBeFalse(); + }); + + it('set false in nextDateDisabled when call navigationEnable and calendarView == Month and currentDate.year != initialDate.year', () => { + component.currentDate = moment().subtract(2, 'year').toDate(); + component.initialDate = moment().toDate(); + + component.navigationEnable(CalendarView.Month); + + expect(component.nextDateDisabled).toBeFalse(); + }); + + it('set true in nextDateDisabled when call navigationEnable and calendarView == Month and currentDate equal to initialDate', () => { + component.currentDate = moment().toDate(); + component.initialDate = moment().add(2, 'day').toDate(); + + component.navigationEnable(CalendarView.Month); + + expect(component.nextDateDisabled).toBeTrue(); + }); + + it('set true in nextDateDisabled when call navigationEnable and calendarView != Month and currentDate isGreater than initialDate', () => { + component.currentDate = moment().toDate(); + component.initialDate = moment().toDate(); + + component.navigationEnable(CalendarView.Week); + + expect(component.nextDateDisabled).toBeTrue(); + }); + + it('return 30 when call getTimeWork and end date is null', () => { + const expectedValue = 30; + + const response = component.getTimeWork(fakeEntryRunning.start_date, fakeEntryRunning.end_date); + + expect(response).toEqual(expectedValue); + }); + + it('return subtraction between start date an end date in minutes when call getTimeWork', () => { + const expectedValue = 20; + const minutesToBeAdded = 20; + fakeEntry.start_date = moment().toDate(); + fakeEntry.end_date = moment().add(minutesToBeAdded, 'minute').toDate(); + + const response = component.getTimeWork(fakeEntry.start_date, fakeEntry.end_date); + + expect(response).toEqual(expectedValue); + }); + + it('return true when call timeIsGreaterThan and subtraction between start date an end date is greater than incoming timeRange', () => { + const minutesToBeAdded = 20; + const timeRangeIncoming = 10; + fakeEntry.start_date = moment().toDate(); + fakeEntry.end_date = moment().add(minutesToBeAdded, 'minute').toDate(); + + const response = component.timeIsGreaterThan(fakeEntry.start_date, fakeEntry.end_date, timeRangeIncoming); + + expect(response).toBeTrue(); + }); + + it('return false when call timeIsGreaterThan and subtraction between start date an end date is less than incoming timeRange', () => { + const minutesToBeAdded = 10; + const timeRangeIncoming = 20; + fakeEntry.start_date = moment().toDate(); + fakeEntry.end_date = moment().add(minutesToBeAdded, 'minute').toDate(); + + const response = component.timeIsGreaterThan(fakeEntry.start_date, fakeEntry.end_date, timeRangeIncoming); + + expect(response).toBeFalse(); + }); + + it('return true when call isVisibleForCurrentView and currentCalendarView is equal to desiredView', () => { + const currentCalendarView: CalendarView = CalendarView.Week; + const desiredView: CalendarView = CalendarView.Week; + + const response = component.isVisibleForCurrentView(currentCalendarView, desiredView); + + expect(response).toBeTrue(); + }); + + it('return false when call isVisibleForCurrentView and currentCalendarView is different to desiredView', () => { + const currentCalendarView: CalendarView = CalendarView.Day; + const desiredView: CalendarView = CalendarView.Week; + + const response = component.isVisibleForCurrentView(currentCalendarView, desiredView); + + expect(response).toBeFalse(); + }); +}); diff --git a/src/app/modules/time-entries/components/calendar/calendar.component.ts b/src/app/modules/time-entries/components/calendar/calendar.component.ts new file mode 100644 index 000000000..71331ec5b --- /dev/null +++ b/src/app/modules/time-entries/components/calendar/calendar.component.ts @@ -0,0 +1,122 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { + CalendarEvent, + CalendarView, +} from 'angular-calendar'; +import { Observable } from 'rxjs'; +import * as moment from 'moment'; +import { DataSource } from '../../../shared/models/data-source.model'; +import { Entry } from 'src/app/modules/shared/models'; +import { map } from 'rxjs/operators'; +import { SubstractDatePipe } from 'src/app/modules/shared/pipes/substract-date/substract-date.pipe'; + +@Component({ + selector: 'app-calendar', + templateUrl: './calendar.component.html', + styleUrls: ['./calendar.component.scss'], +}) +export class CalendarComponent implements OnInit { + @Input() set timeEntries$(timeEntries: Observable>){ + this.castEntryToCalendarEvent(timeEntries); + } + @Input() calendarView: CalendarView = CalendarView.Month; + @Input() currentDate: Date = new Date(); + + @Output() viewModal: EventEmitter = new EventEmitter(); + @Output() deleteTimeEntry: EventEmitter = new EventEmitter(); + @Output() changeDate: EventEmitter = new EventEmitter<{ + date: Date + }>(); + + initialDate: Date; + previusDate: Date; + timeEntriesAsEvent: CalendarEvent[]; + nextDateDisabled: boolean; + + constructor() { + this.initialDate = new Date(); + this.previusDate = new Date(); + this.timeEntriesAsEvent = []; + this.nextDateDisabled = true; + } + + ngOnInit(): void { + this.navigationEnable(this.calendarView); + } + + get CalendarViewEnum(): typeof CalendarView{ + return CalendarView; + } + + castEntryToCalendarEvent(timeEntries$: Observable>) { + timeEntries$.pipe( + map((timeEntriesDatasorce) => timeEntriesDatasorce.data.map( + (timeEntries) => ({ + start: new Date(timeEntries.start_date), + end: timeEntries.end_date === null ? null : new Date(timeEntries.end_date), + title: timeEntries.description, + id: timeEntries.id, + meta: timeEntries + } as CalendarEvent) + ) + ) + ) + .subscribe((timeEntriesAsEvent) => { + this.timeEntriesAsEvent = [...timeEntriesAsEvent].reverse(); + }); + } + + handleEditEvent(timeEntryAsEvent: CalendarEvent): void { + this.viewModal.emit( { + id: timeEntryAsEvent.id + }); + } + + handleDeleteEvent(timeEntryAsEvent: CalendarEvent): void { + this.deleteTimeEntry.emit({ + timeEntry: timeEntryAsEvent.meta + }); + } + + handleChangeDateEvent(): void{ + const date = this.currentDate; + this.navigationEnable(this.calendarView); + this.changeDate.emit({date}); + } + + changeCalendarView(calendarView: CalendarView){ + this.calendarView = calendarView; + } + + navigationEnable(calendarView: CalendarView){ + let enable = false; + const currentDate = moment(this.currentDate); + const initialDate = moment(this.initialDate); + if (calendarView === CalendarView.Month){ + if (currentDate.month() === initialDate.month() && currentDate.year() === initialDate.year()) { + enable = true; + } + } + currentDate.add(1, 'day'); + if (currentDate > initialDate){ + enable = true; + } + this.nextDateDisabled = enable; + } + + getTimeWork(startDate: Date, endDate: Date): number{ + if (endDate === null){ + return 30; + } + return new SubstractDatePipe().transformInMinutes( endDate , startDate); + } + + timeIsGreaterThan(startDate: Date, endDate: Date, timeRange: number ): boolean{ + const timeWorkInMinutes = this.getTimeWork(startDate, endDate); + return timeWorkInMinutes > timeRange; + } + + isVisibleForCurrentView(currentCalendarView: CalendarView, desiredView: CalendarView ): boolean{ + return currentCalendarView === desiredView; + } +} 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 989a15b9a..6029f6097 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.html +++ b/src/app/modules/time-entries/pages/time-entries.component.html @@ -1,63 +1,91 @@
-
-
- -
+
+ +
- +
- - - - - - - - - - - - - - - - - - - - - - - - -
DateTime in - outDurationCustomerProjectActivity
{{ entry.start_date | date: 'MM/dd/yyyy' }}{{ entry.start_date | date: 'HH:mm' }} - {{ entry.end_date | date: 'HH:mm' }}{{ entry.end_date | substractDate: entry.start_date }}{{ entry.customer_name }}{{ entry.project_name }}{{ entry.activity_name }} - - -
+
+ +
+
+ + + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
DateTime in - outDurationCustomerProjectActivity
{{ entry.start_date | date: 'MM/dd/yyyy' }}{{ entry.start_date | date: 'HH:mm' }} - {{ entry.end_date | date: 'HH:mm' }}{{ entry.end_date | substractDate: entry.start_date }}{{ entry.customer_name }}{{ entry.project_name }}{{ entry.activity_name }} + + +
+
+