diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts
index 8e22c7e..e5f2f05 100644
--- a/src/app/home/home.module.ts
+++ b/src/app/home/home.module.ts
@@ -1,8 +1,9 @@
-import { CommonModule } from "@angular/common";
+import { CommonModule, DatePipe } from "@angular/common";
import { NgModule } from "@angular/core";
import { CoreModule } from "../core/core.module";
import { HomeComponent } from "./components/home/home.component";
import { HomeRoutingModule } from "./home-routing.module";
+import { UnitsService } from "./services/units.service";
@NgModule({
declarations: [HomeComponent],
@@ -11,6 +12,6 @@ import { HomeRoutingModule } from "./home-routing.module";
HomeRoutingModule,
CoreModule
],
- providers: [],
+ providers: [UnitsService, DatePipe],
})
export class HomeModule { }
\ No newline at end of file
diff --git a/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.html b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.html
index ee8e538..6fc7f5b 100644
--- a/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.html
+++ b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.html
@@ -13,6 +13,10 @@
Add Meeting Notes for {{ personData.name }}
formControlName="comments">
+
+
Questions
+
+
\ No newline at end of file
diff --git a/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.ts b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.ts
index 0b44169..0ff4c7c 100644
--- a/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.ts
+++ b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.ts
@@ -6,6 +6,7 @@ import { YesNoToBooleanMapper } from 'src/app/shared/mappers/yes-no.mapper';
import { AttritionRisk, MeetingNotes, YesNo } from '../../models/meeting-notes.model';
import { PersonService } from '../../services/person.service';
import * as moment from 'moment';
+import { inputValidator } from 'src/app/shared/validators/field.validator';
@Component({
selector: 'app-modal',
@@ -35,7 +36,7 @@ export class AddMeetingModalComponent implements OnDestroy {
) {
this.meetingForm = this.formBuilder.group({
date: ['', [Validators.required]],
- comments: [''],
+ comments: ['', null, [inputValidator]],
questions: [''],
managerActionItems: [''],
subordinateActionItems: [''],
@@ -62,6 +63,10 @@ export class AddMeetingModalComponent implements OnDestroy {
return this.meetingForm.get('comments')!.value;
}
+ get commentsControl() {
+ return this.meetingForm.get("comments");
+ }
+
get questions() {
return this.meetingForm.get('questions')!.value;
}
diff --git a/src/app/home/people/components/edit-meeting/edit-meeting.component.ts b/src/app/home/people/components/edit-meeting/edit-meeting.component.ts
index dbbbaae..e2f6d8b 100644
--- a/src/app/home/people/components/edit-meeting/edit-meeting.component.ts
+++ b/src/app/home/people/components/edit-meeting/edit-meeting.component.ts
@@ -3,6 +3,10 @@ import { AttritionRisk, MeetingNotes } from '../../models/meeting-notes.model';
import { FormBuilder, FormGroup } from '@angular/forms';
import { YesNoToBooleanMapper } from 'src/app/shared/mappers/yes-no.mapper';
import { Location } from '@angular/common';
+import { debounceTime, switchMap } from 'rxjs';
+import { Store } from '@ngrx/store';
+import { selectUserId } from 'src/app/login/store/login.selectors';
+import { UnitsService } from 'src/app/home/services/units.service';
@Component({
selector: 'app-edit-meeting',
@@ -12,6 +16,7 @@ import { Location } from '@angular/common';
export class EditMeetingComponent implements OnInit {
meetingData!: MeetingNotes;
+ notesId?: string;
meetingForm!: FormGroup;
cdkAutosizeMinRows = 1;
cdkAutosizeMaxRows = 4;
@@ -25,10 +30,13 @@ export class EditMeetingComponent implements OnInit {
constructor(
private formBuilder: FormBuilder,
private yesNoMapper: YesNoToBooleanMapper,
- private location: Location) { }
+ private location: Location,
+ private store: Store,
+ private unitsService: UnitsService) { }
ngOnInit(): void {
this.meetingData = history.state;
+ this.notesId = this.meetingData.notesId;
this.meetingForm = this.formBuilder.group({
comments: [this.meetingData.comments],
questions: [this.meetingData.questions],
@@ -42,6 +50,40 @@ export class EditMeetingComponent implements OnInit {
attritionRisk: [this.meetingData.attritionRisk],
oneToOneReportSent: [this.yesNoMapper.toModel(this.meetingData.oneToOneReportSent)]
});
+
+ let userId: string | undefined;
+ this.store.select(selectUserId)
+ .subscribe(value => userId = value);
+
+ if (!userId) {
+ throw new Error('userId not found in Store');
+ }
+
+ // Below subscription to valueChanges event was implemented to exercise switchMap operator
+ // Behavior in UI: when user edits Comments form field, request is sent to fetch data from BE
+ // and update Questions form field if no other Observable is emitted from valueChanges for 2 seconds
+ this.comments.valueChanges
+ .pipe(
+ debounceTime(2000),
+ switchMap(() => this.unitsService.getUnits(userId!))
+ )
+ .subscribe(units => {
+ const meetingData = units
+ .flatMap(unit => unit.people)
+ .flatMap(person => person.meetings)
+ .find(meeting => meeting.notesId === this.notesId)!;
+
+ this.questions.setValue(meetingData.questions);
+ }
+ );
+ }
+
+ get comments() {
+ return this.meetingForm.get('comments')!;
+ }
+
+ get questions() {
+ return this.meetingForm.get('questions')!;
}
onSubmit() {
diff --git a/src/app/home/people/components/people/people.component.html b/src/app/home/people/components/people/people.component.html
index 325c0cd..44d8a82 100644
--- a/src/app/home/people/components/people/people.component.html
+++ b/src/app/home/people/components/people/people.component.html
@@ -1,5 +1,6 @@
+
\ No newline at end of file
diff --git a/src/app/home/people/components/people/people.component.ts b/src/app/home/people/components/people/people.component.ts
index e9b6f45..7550afe 100644
--- a/src/app/home/people/components/people/people.component.ts
+++ b/src/app/home/people/components/people/people.component.ts
@@ -1,10 +1,12 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
-import { Observable, Subscription, of } from 'rxjs';
+import { Subscription, of } from 'rxjs';
import { Unit } from 'src/app/home/people/models/unit.model';
import { ModalService } from '../../services/modal.service';
-import { UnitsService } from '../../services/units.service';
+
import { Store } from '@ngrx/store';
import { selectUserId } from 'src/app/login/store/login.selectors';
+import { UnitsService } from 'src/app/home/services/units.service';
+import { AttritionRisk } from '../../models/meeting-notes.model';
@Component({
selector: 'app-people',
@@ -13,14 +15,15 @@ import { selectUserId } from 'src/app/login/store/login.selectors';
})
export class PeopleComponent implements OnInit, OnDestroy {
- $units!: Observable;
+ $units!: Subscription;
+ units: Unit[] = [];
$userId?: Subscription;
constructor(
private readonly unitsService: UnitsService,
private modalService: ModalService,
private store: Store
- ) { }
+ ) { }
ngOnInit(): void {
let userId: string | undefined;
@@ -31,14 +34,61 @@ export class PeopleComponent implements OnInit, OnDestroy {
throw new Error('userId not found in Store');
}
- this.$units = this.unitsService.getUnits(userId);
+ this.$units = this.unitsService.getUnits(userId)
+ .subscribe(units => this.units.push(...units));
this.modalService.subscribe({
- afterClosed: data => this.$units = userId ? this.unitsService.getUnits(userId) : of([])
+ afterClosed: data => {
+ userId && data ?
+ this.unitsService.getUnits(userId).subscribe(units => this.units = units) :
+ of([...this.units]).subscribe(units => this.units = units)
+ }
})
}
ngOnDestroy(): void {
this.$userId?.unsubscribe();
+ this.$units.unsubscribe();
+ }
+
+ // When method is invoked in the template, it creates a new array with old units and adds one new unit.
+ // UnitComponents which hold multiple PersonComponents have ChangeDetectionStrategy.OnPush
+ // When 'Add new Unit' button is clicked, change detection is run only for new component and not for existing components.
+ // So instead of running change detection multiple times, Angular runs it only once for better performance.
+ addNewUnit() {
+ this.units = [...this.units, this.newUnit];
}
+ get newUnit() {
+ return {
+ id: `${Date.now()}`,
+ name: `Name ${Date.now()}`,
+ people: [
+ {
+ id: `${Date.now()}`,
+ name: `Person ${Date.now()}`,
+ grade: 'T9000',
+ specializations: ['Everything'],
+ location: 'everywhere',
+ meetings: [
+ {
+ date: new Date().toString(),
+ personId: '',
+ notesId: '',
+ comments: '',
+ questions: '',
+ managerActionItems: '',
+ subordinateActionItems: '',
+ importantAgreements: '',
+ satisfaction: '',
+ plans: '',
+ feedback: '',
+ issues: '',
+ attritionRisk: AttritionRisk.HIGH,
+ oneToOneReportSent: false
+ }
+ ]
+ }
+ ]
+ };
+ }
}
diff --git a/src/app/home/people/components/person/person.component.html b/src/app/home/people/components/person/person.component.html
index fa3590b..c08d086 100644
--- a/src/app/home/people/components/person/person.component.html
+++ b/src/app/home/people/components/person/person.component.html
@@ -1,5 +1,5 @@
-
{{ personData.name }}
+
{{ personData.name }} {{ runChangeDetection }}
-
diff --git a/src/app/home/people/components/person/person.component.ts b/src/app/home/people/components/person/person.component.ts
index ce92303..fcb2821 100644
--- a/src/app/home/people/components/person/person.component.ts
+++ b/src/app/home/people/components/person/person.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { AddMeetingModalComponent } from 'src/app/home/people/components/add-meeting-modal/add-meeting-modal.component';
import { Person } from '../../models/unit.model';
import { ModalService } from '../../services/modal.service';
@@ -6,7 +6,8 @@ import { ModalService } from '../../services/modal.service';
@Component({
selector: 'app-person',
templateUrl: './person.component.html',
- styleUrls: ['./person.component.css']
+ styleUrls: ['./person.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class PersonComponent implements OnInit {
@@ -25,6 +26,11 @@ export class PersonComponent implements OnInit {
constructor(private modalService: ModalService) { }
+ get runChangeDetection() {
+ console.log('Person component - checking view');
+ return '';
+ }
+
ngOnInit(): void {
this.initializePagination();
}
diff --git a/src/app/home/people/components/unit/unit.component.ts b/src/app/home/people/components/unit/unit.component.ts
index f6b8a8b..3f7f08d 100644
--- a/src/app/home/people/components/unit/unit.component.ts
+++ b/src/app/home/people/components/unit/unit.component.ts
@@ -1,10 +1,11 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Unit } from 'src/app/home/people/models/unit.model';
@Component({
selector: 'app-unit',
templateUrl: './unit.component.html',
- styleUrls: ['./unit.component.css']
+ styleUrls: ['./unit.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class UnitComponent implements OnInit {
diff --git a/src/app/home/people/people.module.ts b/src/app/home/people/people.module.ts
index be7ca8d..7d3d167 100644
--- a/src/app/home/people/people.module.ts
+++ b/src/app/home/people/people.module.ts
@@ -3,7 +3,6 @@ import { NgModule } from "@angular/core";
import { PeopleComponent } from "./components/people/people.component";
import { UnitComponent } from "./components/unit/unit.component";
import { PeopleRoutingModule } from "./people-routing.module";
-import { UnitsService } from "./services/units.service";
import { PersonComponent } from './components/person/person.component';
import { MeetingComponent } from './components/meeting/meeting.component';
import { NgMaterialModule } from "src/app/ng-material/ng-material.module";
@@ -32,6 +31,6 @@ import { EditMeetingComponent } from './components/edit-meeting/edit-meeting.com
ReactiveFormsModule,
SharedModule
],
- providers: [UnitsService],
+ providers: [],
})
export class PeopleModule { }
\ No newline at end of file
diff --git a/src/app/home/people/services/units.service.spec.ts b/src/app/home/services/units.service.spec.ts
similarity index 100%
rename from src/app/home/people/services/units.service.spec.ts
rename to src/app/home/services/units.service.spec.ts
diff --git a/src/app/home/people/services/units.service.ts b/src/app/home/services/units.service.ts
similarity index 51%
rename from src/app/home/people/services/units.service.ts
rename to src/app/home/services/units.service.ts
index 867307d..e32d7a5 100644
--- a/src/app/home/people/services/units.service.ts
+++ b/src/app/home/services/units.service.ts
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
-import { Observable } from 'rxjs';
+import { Observable, ReplaySubject, of, switchMap } from 'rxjs';
import { ConfigService } from 'src/app/core/services/config.service';
import { Unit } from 'src/app/home/people/models/unit.model';
@@ -8,6 +8,7 @@ import { Unit } from 'src/app/home/people/models/unit.model';
export class UnitsService {
private readonly apiBaseUrl;
+ private history$ = new ReplaySubject();
constructor(
private http: HttpClient,
@@ -17,6 +18,18 @@ export class UnitsService {
}
getUnits(userId: string): Observable {
- return this.http.get(`${this.apiBaseUrl}/${userId}/units`);
+ return this.http.get(`${this.apiBaseUrl}/${userId}/units`).pipe(
+ switchMap(units => {
+ this.history$.next(units);
+ return of(units);
+ })
+ );
+ }
+
+ // Method uses ReplaySubject which emits 'historical' values for new subscribers
+ // SummaryComponent uses it to display history of meeting entries count which is updated
+ // after every request sent to /units endpoint
+ getHistoricalData(subscription: any) {
+ return this.history$.asObservable().subscribe(subscription);
}
}
diff --git a/src/app/home/summary/components/summary/summary.component.html b/src/app/home/summary/components/summary/summary.component.html
index eb0512c..886dca5 100644
--- a/src/app/home/summary/components/summary/summary.component.html
+++ b/src/app/home/summary/components/summary/summary.component.html
@@ -1 +1,12 @@
-
summary works!
+
+
+
+ | Number of meetings |
+
+
+
+
+ | {{ entry.meetings.length }} |
+
+
+
diff --git a/src/app/home/summary/components/summary/summary.component.ts b/src/app/home/summary/components/summary/summary.component.ts
index 011993b..f7f6b65 100644
--- a/src/app/home/summary/components/summary/summary.component.ts
+++ b/src/app/home/summary/components/summary/summary.component.ts
@@ -1,15 +1,40 @@
-import { Component, OnInit } from '@angular/core';
+import { DatePipe } from '@angular/common';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { MeetingNotes } from 'src/app/home/people/models/meeting-notes.model';
+import { Unit } from 'src/app/home/people/models/unit.model';
+import { UnitsService } from 'src/app/home/services/units.service';
@Component({
selector: 'app-summary',
templateUrl: './summary.component.html',
styleUrls: ['./summary.component.css']
})
-export class SummaryComponent implements OnInit {
+export class SummaryComponent implements OnInit, OnDestroy {
- constructor() { }
+ units$?: Subscription;
+ meetings: MeetingNotes[] = [];
+ history: MeetingHistory[] = [];
+
+ constructor(private readonly unitsService: UnitsService, private datePipe: DatePipe) { }
+
+ ngOnDestroy(): void {
+ this.units$?.unsubscribe();
+ }
ngOnInit(): void {
+ this.units$ = this.unitsService.getHistoricalData(
+ (units: Unit[]) => {
+ this.history.push({
+ meetings: units
+ .flatMap(unit => unit.people)
+ .flatMap(person => person.meetings)
+ });
+ }
+ );
}
+}
+export interface MeetingHistory {
+ meetings: MeetingNotes[];
}
diff --git a/src/app/home/summary/summary.module.ts b/src/app/home/summary/summary.module.ts
index a8a3a3a..6cfd2b5 100644
--- a/src/app/home/summary/summary.module.ts
+++ b/src/app/home/summary/summary.module.ts
@@ -2,12 +2,14 @@ import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { SummaryComponent } from "./components/summary/summary.component";
import { SummaryRoutingModule } from "./summary-routing.module";
+import { NgMaterialModule } from "src/app/ng-material/ng-material.module";
@NgModule({
declarations: [SummaryComponent],
imports: [
CommonModule,
- SummaryRoutingModule
+ SummaryRoutingModule,
+ NgMaterialModule
],
providers: [],
})
diff --git a/src/app/shared/validators/field.validator.ts b/src/app/shared/validators/field.validator.ts
new file mode 100644
index 0000000..2ecaee8
--- /dev/null
+++ b/src/app/shared/validators/field.validator.ts
@@ -0,0 +1,14 @@
+import { AbstractControl, ValidationErrors } from "@angular/forms";
+import { Observable, of, switchMap, timer } from "rxjs";
+
+export function inputValidator(control: AbstractControl): Observable {
+
+ const value = control.value;
+
+ return timer(2000)
+ .pipe(
+ switchMap(() => value.length === 0 || value.includes("!") ?
+ of({ 'validInput': true, 'requiredValue': 'Not allowed: empty and !' }) :
+ of(null))
+ );
+}
\ No newline at end of file