Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatListModule } from '@angular/material/list';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { NgxPaginationModule } from 'ngx-pagination';
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
Expand Down Expand Up @@ -90,6 +93,9 @@ import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mo
import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login';
import { GoogleLoginProvider } from 'angularx-social-login';
import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component';
import { TimeRangeCustomComponent } from './modules/reports/components/time-range-custom/time-range-custom.component';
import { TimeRangeHeaderComponent } from './modules/reports/components/time-range-custom/time-range-header/time-range-header.component';
import { TimeRangeOptionsComponent } from './modules/reports/components/time-range-custom/time-range-options/time-range-options.component';

const maskConfig: Partial<IConfig> = {
validation: false,
Expand Down Expand Up @@ -146,6 +152,9 @@ const maskConfig: Partial<IConfig> = {
CalendarComponent,
DropdownComponent,
DarkModeComponent,
TimeRangeCustomComponent,
TimeRangeHeaderComponent,
TimeRangeOptionsComponent,
],
imports: [
NgxMaskModule.forRoot(maskConfig),
Expand All @@ -165,6 +174,9 @@ const maskConfig: Partial<IConfig> = {
NgxMaterialTimepickerModule,
UiSwitchModule,
DragDropModule,
MatIconModule,
MatCheckboxModule,
MatListModule,
StoreModule.forRoot(reducers, {
metaReducers,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<form (ngSubmit)="onSubmit()" class="date-range-form">
<label class="col-form-label my-1">Select your Date Range:</label>
<mat-form-field appearance="fill">
<mat-label>Enter a date range</mat-label>
<mat-date-range-input [formGroup]="range" [rangePicker]="picker">
<input matStartDate formControlName="start" placeholder="Start date">
<input matEndDate formControlName="end" placeholder="End date">
</mat-date-range-input>
<mat-hint>MM/DD/YYYY – MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker [calendarHeaderComponent]="customHeader"></mat-date-range-picker>

<mat-error *ngIf="range.controls.start.hasError('matStartDateInvalid')">Invalid start date</mat-error>
<mat-error *ngIf="range.controls.end.hasError('matEndDateInvalid')">Invalid end date</mat-error>
</mat-form-field>
<div class="col-12 col-md-2 my-1">
<button type="submit" class="btn btn-primary">Search</button>
</div>
</form>

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}

.date-range-form {
display: flex !important;
justify-content: space-between !important;
width: 100% !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TimeRangeCustomComponent } from './time-range-custom.component';
import { ToastrService } from 'ngx-toastr';
import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer';
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<TimeRangeCustomComponent>;
let store: MockStore<EntryState>;
const toastrServiceStub = {
error: () => {
return 'test error';
}
};

const timeEntry = {
id: '1',
start_date: new Date(),
end_date: new Date(),
activity_id: '1',
technologies: ['react', 'redux'],
comments: 'any comment',
uri: 'TT-123',
project_id: '1'
};

const state = {
active: timeEntry,
entryList: [timeEntry],
isLoading: false,
message: 'test',
createError: false,
updateError: false,
timeEntriesSummary: null,
entriesForReport: [timeEntry],
};

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [ TimeRangeCustomComponent ],
providers: [
provideMockStore({ initialState: state }),
{ provide: ToastrService, useValue: toastrServiceStub }
],
})
.compileComponents();
store = TestBed.inject(MockStore);
});

beforeEach(() => {
fixture = TestBed.createComponent(TimeRangeCustomComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('setInitialDataOnScreen on ngOnInit', () => {
spyOn(component, 'setInitialDataOnScreen');

component.ngOnInit();

expect(component.setInitialDataOnScreen).toHaveBeenCalled();
});

it('LoadEntriesByTimeRange action is triggered when start date is before end date', () => {
const end = moment(new Date()).subtract(1, 'days');
const start = moment(new Date());
spyOn(store, 'dispatch');
component.range.controls.start.setValue(end);
component.range.controls.end.setValue(start);

component.onSubmit();

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', () => {
spyOn(toastrServiceStub, 'error');
const yesterday = moment(new Date()).subtract(2, 'days');
const today = moment(new Date());
spyOn(store, 'dispatch');
component.range.controls.start.setValue(today);
component.range.controls.end.setValue(yesterday);

component.onSubmit();

expect(toastrServiceStub.error).toHaveBeenCalled();
});

it('setInitialDataOnScreen sets dates in form', () => {
spyOn(component.range.controls.start, 'setValue');
spyOn(component.range.controls.end, 'setValue');

component.setInitialDataOnScreen();

expect(component.range.controls.start.setValue).toHaveBeenCalled();
expect(component.range.controls.end.setValue).toHaveBeenCalled();

});

it('triggers onSubmit to set initial data', () => {
spyOn(component, 'onSubmit');

component.setInitialDataOnScreen();

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)});

expect(component.onSubmit).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { formatDate } from '@angular/common';
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';
import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer';
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',
styleUrls: ['./time-range-custom.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TimeRangeCustomComponent implements OnInit, OnChanges {
@Input() userId: string;
customHeader = TimeRangeHeaderComponent;
range = new FormGroup({
start: new FormControl(null),
end: new FormControl(null),
});

constructor(private store: Store<EntryState>, private toastrService: ToastrService) {
}

ngOnInit(): void {
this.setInitialDataOnScreen();
}

ngOnChanges(changes: SimpleChanges){
if (!changes.userId.firstChange){
this.onSubmit();
}
}

setInitialDataOnScreen() {
this.range.setValue({
start: formatDate(moment().startOf('isoWeek').format('l'), DATE_FORMAT, 'en'),
end: formatDate(moment().format('l'), DATE_FORMAT, 'en')
});
this.onSubmit();
}

onSubmit() {
const startDate = moment(this.range.getRawValue().start).startOf('day');
const endDate = moment(this.range.getRawValue().end).endOf('day');
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));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<app-time-range-options></app-time-range-options>
<div class="time-range-header">
<button mat-icon-button class="time-range-double-arrow" (click)="previousClicked('year')">
<mat-icon>keyboard_arrow_left</mat-icon>
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
<button mat-icon-button (click)="previousClicked('month')" class="time-range-month">
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
<span class="time-range-header-label">{{periodLabel}}</span>
<button mat-icon-button (click)="nextClicked('month')" class="time-range-month-next">
<mat-icon>keyboard_arrow_right</mat-icon>
</button>
<button mat-icon-button class="time-range-double-arrow time-range-double-arrow-next" (click)="nextClicked('year')">
<mat-icon>keyboard_arrow_right</mat-icon>
<mat-icon>keyboard_arrow_right</mat-icon>
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.time-range-header {
display: flex;
align-items: center;
padding: 0.5em;
}

.time-range-header-label {
flex: 1;
height: 1em;
font-weight: 500;
text-align: center;
}

.time-range-double-arrow .mat-icon {
margin: -22%;
}
Loading