Skip to content

Commit 80278d5

Browse files
committed
merging from master
2 parents 6b3a57c + e189da6 commit 80278d5

19 files changed

+765
-12
lines changed

README.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,42 @@ Install the following extensions(optional):
7777
- `Prettier - Code formatter`.
7878
- Go to user settings (`settings.json`) and enable formatting on save: `"editor.formatOnSave": true`.
7979

80-
## Development server
80+
## How to run this project
8181

82-
Yo have 2 ways to run this project in dev mode:
82+
You have two ways to run this project locally:
8383

84-
**First**:
85-
- In your project path, open your favourite command line and run the follwing commands: `make build` then `make run` and finally `make logs`. When the project is successfully compiled you can go to `http://localhost:4200/` in your browser. Remember you must have your Docker running.
84+
**First (Using Docker)**:
8685

87-
**Second**:
86+
In your project path, open your favourite command line and run the follwing commands:
87+
88+
To run the project in development mode:
89+
- `make build` to create a docker image with dependencies needed for development.
90+
- `make run` to execute the development docker container.
91+
- `make logs` to show logs of time-tracker-ui in real time.
92+
- `make stop` to stop the development docker container.
93+
94+
To run the project in production mode (only for locally testing purposes):
95+
- `make build_prod` to create a docker image with dependencies needed for production.
96+
- `make run_prod` to execute the production docker container.
97+
- `make stop_prod` to stop the production docker container.
98+
99+
When the project is successfully compiled you can go to `http://localhost:4200/` in your browser. *Remember you must have your Docker running for both cases.*
100+
101+
**Second (Without using Docker)**:
102+
103+
Note: If you're on windows, use bash to set up the environment.
88104

89-
note: If you're on windows, use para bash to set up the environment.
90105
- Set the environment variables executing the following commands:
91106
```bash
92107
set -a
93108
source .env
94109
set +a
95110
```
96-
- Run `ng serve` to run the app in dev mode. After executing this command, you can navigate to `http://localhost:4200/` to see the app working. This method is usefull when you want to run a specific branch using less time but not recommended when doing QA.
111+
112+
- Run `ng serve` to run the app in development mode.
113+
- Run `ng serve --prod` to run the app in production mode (pointing to the production backend).
114+
115+
After executing this command, you can navigate to `http://localhost:4200/` to see the app working. This method is usefull when you want to run a specific branch using less time but not recommended when doing QA.
97116

98117
In any case, the app will automatically reload if you change anything in the source files.
99118

src/app/app.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
1313
import { DragDropModule } from '@angular/cdk/drag-drop';
1414
import { MatDatepickerModule } from '@angular/material/datepicker';
1515
import { MatInputModule } from '@angular/material/input';
16+
import { MatIconModule } from '@angular/material/icon';
17+
import { MatCheckboxModule } from '@angular/material/checkbox';
18+
import { MatListModule } from '@angular/material/list';
1619
import { MatMomentDateModule } from '@angular/material-moment-adapter';
1720
import { NgxPaginationModule } from 'ngx-pagination';
1821
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
@@ -91,6 +94,9 @@ import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mo
9194
import { SocialLoginModule, SocialAuthServiceConfig } from 'angularx-social-login';
9295
import { GoogleLoginProvider } from 'angularx-social-login';
9396
import { SearchUserComponent } from './modules/shared/components/search-user/search-user.component';
97+
import { TimeRangeCustomComponent } from './modules/reports/components/time-range-custom/time-range-custom.component';
98+
import { TimeRangeHeaderComponent } from './modules/reports/components/time-range-custom/time-range-header/time-range-header.component';
99+
import { TimeRangeOptionsComponent } from './modules/reports/components/time-range-custom/time-range-options/time-range-options.component';
94100

95101
const maskConfig: Partial<IConfig> = {
96102
validation: false,
@@ -147,6 +153,9 @@ const maskConfig: Partial<IConfig> = {
147153
CalendarComponent,
148154
DropdownComponent,
149155
DarkModeComponent,
156+
TimeRangeCustomComponent,
157+
TimeRangeHeaderComponent,
158+
TimeRangeOptionsComponent,
150159
],
151160
imports: [
152161
NgxMaskModule.forRoot(maskConfig),
@@ -166,6 +175,9 @@ const maskConfig: Partial<IConfig> = {
166175
NgxMaterialTimepickerModule,
167176
UiSwitchModule,
168177
DragDropModule,
178+
MatIconModule,
179+
MatCheckboxModule,
180+
MatListModule,
169181
StoreModule.forRoot(reducers, {
170182
metaReducers,
171183
}),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<form (ngSubmit)="onSubmit()" class="date-range-form">
2+
<label class="col-form-label my-1">Select your Date Range:</label>
3+
<mat-form-field appearance="fill">
4+
<mat-label>Enter a date range</mat-label>
5+
<mat-date-range-input [formGroup]="range" [rangePicker]="picker">
6+
<input matStartDate formControlName="start" placeholder="Start date">
7+
<input matEndDate formControlName="end" placeholder="End date">
8+
</mat-date-range-input>
9+
<mat-hint>MM/DD/YYYY – MM/DD/YYYY</mat-hint>
10+
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
11+
<mat-date-range-picker #picker [calendarHeaderComponent]="customHeader"></mat-date-range-picker>
12+
13+
<mat-error *ngIf="range.controls.start.hasError('matStartDateInvalid')">Invalid start date</mat-error>
14+
<mat-error *ngIf="range.controls.end.hasError('matEndDateInvalid')">Invalid end date</mat-error>
15+
</mat-form-field>
16+
<div class="col-12 col-md-2 my-1">
17+
<button type="submit" class="btn btn-primary">Search</button>
18+
</div>
19+
</form>
20+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
:host {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
justify-content: center;
6+
height: 100%;
7+
}
8+
9+
.date-range-form {
10+
display: flex !important;
11+
justify-content: space-between !important;
12+
width: 100% !important;
13+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
3+
import { MockStore, provideMockStore } from '@ngrx/store/testing';
4+
import { TimeRangeCustomComponent } from './time-range-custom.component';
5+
import { ToastrService } from 'ngx-toastr';
6+
import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer';
7+
import * as entryActions from '../../../time-clock/store/entry.actions';
8+
import * as moment from 'moment';
9+
import { SimpleChange } from '@angular/core';
10+
11+
12+
describe('TimeRangeCustomComponent', () => {
13+
let component: TimeRangeCustomComponent;
14+
let fixture: ComponentFixture<TimeRangeCustomComponent>;
15+
let store: MockStore<EntryState>;
16+
const toastrServiceStub = {
17+
error: () => {
18+
return 'test error';
19+
}
20+
};
21+
22+
const timeEntry = {
23+
id: '1',
24+
start_date: new Date(),
25+
end_date: new Date(),
26+
activity_id: '1',
27+
technologies: ['react', 'redux'],
28+
comments: 'any comment',
29+
uri: 'TT-123',
30+
project_id: '1'
31+
};
32+
33+
const state = {
34+
active: timeEntry,
35+
entryList: [timeEntry],
36+
isLoading: false,
37+
message: 'test',
38+
createError: false,
39+
updateError: false,
40+
timeEntriesSummary: null,
41+
entriesForReport: [timeEntry],
42+
};
43+
44+
beforeEach(async () => {
45+
await TestBed.configureTestingModule({
46+
imports: [FormsModule, ReactiveFormsModule],
47+
declarations: [ TimeRangeCustomComponent ],
48+
providers: [
49+
provideMockStore({ initialState: state }),
50+
{ provide: ToastrService, useValue: toastrServiceStub }
51+
],
52+
})
53+
.compileComponents();
54+
store = TestBed.inject(MockStore);
55+
});
56+
57+
beforeEach(() => {
58+
fixture = TestBed.createComponent(TimeRangeCustomComponent);
59+
component = fixture.componentInstance;
60+
fixture.detectChanges();
61+
});
62+
63+
it('should create', () => {
64+
expect(component).toBeTruthy();
65+
});
66+
67+
it('setInitialDataOnScreen on ngOnInit', () => {
68+
spyOn(component, 'setInitialDataOnScreen');
69+
70+
component.ngOnInit();
71+
72+
expect(component.setInitialDataOnScreen).toHaveBeenCalled();
73+
});
74+
75+
it('LoadEntriesByTimeRange action is triggered when start date is before end date', () => {
76+
const end = moment(new Date()).subtract(1, 'days');
77+
const start = moment(new Date());
78+
spyOn(store, 'dispatch');
79+
component.range.controls.start.setValue(end);
80+
component.range.controls.end.setValue(start);
81+
82+
component.onSubmit();
83+
84+
expect(store.dispatch).toHaveBeenCalledWith(new entryActions.LoadEntriesByTimeRange({
85+
start_date: end.startOf('day'),
86+
end_date: start.endOf('day')
87+
}));
88+
});
89+
90+
it('shows an error when the end date is before the start date', () => {
91+
spyOn(toastrServiceStub, 'error');
92+
const yesterday = moment(new Date()).subtract(2, 'days');
93+
const today = moment(new Date());
94+
spyOn(store, 'dispatch');
95+
component.range.controls.start.setValue(today);
96+
component.range.controls.end.setValue(yesterday);
97+
98+
component.onSubmit();
99+
100+
expect(toastrServiceStub.error).toHaveBeenCalled();
101+
});
102+
103+
it('setInitialDataOnScreen sets dates in form', () => {
104+
spyOn(component.range.controls.start, 'setValue');
105+
spyOn(component.range.controls.end, 'setValue');
106+
107+
component.setInitialDataOnScreen();
108+
109+
expect(component.range.controls.start.setValue).toHaveBeenCalled();
110+
expect(component.range.controls.end.setValue).toHaveBeenCalled();
111+
112+
});
113+
114+
it('triggers onSubmit to set initial data', () => {
115+
spyOn(component, 'onSubmit');
116+
117+
component.setInitialDataOnScreen();
118+
119+
expect(component.onSubmit).toHaveBeenCalled();
120+
});
121+
122+
it('When the ngOnChanges method is called, the onSubmit method is called', () => {
123+
const userIdCalled = 'test-user-1';
124+
spyOn(component, 'onSubmit');
125+
126+
component.ngOnChanges({userId: new SimpleChange(null, userIdCalled, false)});
127+
128+
expect(component.onSubmit).toHaveBeenCalled();
129+
});
130+
131+
it('When the ngOnChanges method is the first change, the onSubmit method is not called', () => {
132+
const userIdNotCalled = 'test-user-2';
133+
spyOn(component, 'onSubmit');
134+
135+
component.ngOnChanges({userId: new SimpleChange(null, userIdNotCalled, true)});
136+
137+
expect(component.onSubmit).not.toHaveBeenCalled();
138+
});
139+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { formatDate } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
Input,
6+
OnChanges,
7+
OnInit,
8+
SimpleChanges,
9+
} from '@angular/core';
10+
import {FormGroup, FormControl} from '@angular/forms';
11+
import { Store } from '@ngrx/store';
12+
import * as moment from 'moment';
13+
import { ToastrService } from 'ngx-toastr';
14+
import { EntryState } from 'src/app/modules/time-clock/store/entry.reducer';
15+
import { DATE_FORMAT } from 'src/environments/environment';
16+
import * as entryActions from '../../../time-clock/store/entry.actions';
17+
import { TimeRangeHeaderComponent } from './time-range-header/time-range-header.component';
18+
19+
20+
@Component({
21+
selector: 'app-time-range-custom',
22+
templateUrl: './time-range-custom.component.html',
23+
styleUrls: ['./time-range-custom.component.scss'],
24+
changeDetection: ChangeDetectionStrategy.OnPush,
25+
})
26+
export class TimeRangeCustomComponent implements OnInit, OnChanges {
27+
@Input() userId: string;
28+
customHeader = TimeRangeHeaderComponent;
29+
range = new FormGroup({
30+
start: new FormControl(null),
31+
end: new FormControl(null),
32+
});
33+
34+
constructor(private store: Store<EntryState>, private toastrService: ToastrService) {
35+
}
36+
37+
ngOnInit(): void {
38+
this.setInitialDataOnScreen();
39+
}
40+
41+
ngOnChanges(changes: SimpleChanges){
42+
if (!changes.userId.firstChange){
43+
this.onSubmit();
44+
}
45+
}
46+
47+
setInitialDataOnScreen() {
48+
this.range.setValue({
49+
start: formatDate(moment().startOf('isoWeek').format('l'), DATE_FORMAT, 'en'),
50+
end: formatDate(moment().format('l'), DATE_FORMAT, 'en')
51+
});
52+
this.onSubmit();
53+
}
54+
55+
onSubmit() {
56+
const startDate = moment(this.range.getRawValue().start).startOf('day');
57+
const endDate = moment(this.range.getRawValue().end).endOf('day');
58+
if (endDate.isBefore(startDate)) {
59+
this.toastrService.error('The end date should be after the start date');
60+
} else {
61+
this.store.dispatch(new entryActions.LoadEntriesByTimeRange({
62+
start_date: moment(this.range.getRawValue().start).startOf('day'),
63+
end_date: moment(this.range.getRawValue().end).endOf('day'),
64+
}, this.userId));
65+
}
66+
}
67+
68+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<app-time-range-options></app-time-range-options>
2+
<div class="time-range-header">
3+
<button mat-icon-button class="time-range-double-arrow" (click)="previousClicked('year')">
4+
<mat-icon>keyboard_arrow_left</mat-icon>
5+
<mat-icon>keyboard_arrow_left</mat-icon>
6+
</button>
7+
<button mat-icon-button (click)="previousClicked('month')" class="time-range-month">
8+
<mat-icon>keyboard_arrow_left</mat-icon>
9+
</button>
10+
<span class="time-range-header-label">{{periodLabel}}</span>
11+
<button mat-icon-button (click)="nextClicked('month')" class="time-range-month-next">
12+
<mat-icon>keyboard_arrow_right</mat-icon>
13+
</button>
14+
<button mat-icon-button class="time-range-double-arrow time-range-double-arrow-next" (click)="nextClicked('year')">
15+
<mat-icon>keyboard_arrow_right</mat-icon>
16+
<mat-icon>keyboard_arrow_right</mat-icon>
17+
</button>
18+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.time-range-header {
2+
display: flex;
3+
align-items: center;
4+
padding: 0.5em;
5+
}
6+
7+
.time-range-header-label {
8+
flex: 1;
9+
height: 1em;
10+
font-weight: 500;
11+
text-align: center;
12+
}
13+
14+
.time-range-double-arrow .mat-icon {
15+
margin: -22%;
16+
}

0 commit comments

Comments
 (0)