diff --git a/.firebase/hosting.ZGlzdC9zcGVjLWxlYWQtbm90ZXMtdWk.cache b/.firebase/hosting.ZGlzdC9zcGVjLWxlYWQtbm90ZXMtdWk.cache
new file mode 100644
index 0000000..747ed74
--- /dev/null
+++ b/.firebase/hosting.ZGlzdC9zcGVjLWxlYWQtbm90ZXMtdWk.cache
@@ -0,0 +1,11 @@
+779.3a16a70e6feab9ef.js,1681973347662,cbdc2e4aa98fbb74938b9e6882f54573c9687bf6a6335152f3944d1a84c3dd4b
+favicon.ico,1681973347676,a2aee5309d0b59b9b66384ccc3969a07fab1b320d0bf76f1b25047a051d7f774
+index.html,1681973347940,6955b21a0bbb9ea697d5f43a38ba3a234b7dc605f74bc96c51f544bf41cedebf
+3rdpartylicenses.txt,1681973347662,34fafdb813b41f27dc4679432a4567e1e56c103b599c2990fa352b55081ea25f
+runtime.4b0dcc27aadf0004.js,1681973347662,33d7df3ee2a48e6bcade6b113fa5f3ceb3d93e360363a62e765ed904e8702f74
+assets/config/development.json,1681973347680,e5a94c4f75f89616e7b367e378f5bf5d3946c8f3eba7e863820ddec9c9eec409
+889.20471295d25361dc.js,1681973347662,a0b8eb15f3903c617c7f4c380a8e23c1135f88b774049277a87515e64ee73217
+assets/config/production.json,1681973347681,3f39cb9c9165c8979333b76b0dc07c657a130a77250659a6e0b5c89bbc65d7b4
+polyfills.7332c30408754df9.js,1681973347662,b2d7eab49d5673c6034d25abec71fa80e2f1b4da5257d5809ef9cc6704f45b9d
+styles.80d7f9de54c91add.css,1681973347662,b06e7465958bc5aa8c673e3d9312a5727706df5df03d5ee8addb1314e165e7cb
+main.465a62794bd2dab7.js,1681973347662,8ccc1a8756326cdef236bb8032c16db8de96a925709028a117cef68524173dfd
diff --git a/.firebase/hosting.cHVibGlj.cache b/.firebase/hosting.cHVibGlj.cache
new file mode 100644
index 0000000..6f936f0
--- /dev/null
+++ b/.firebase/hosting.cHVibGlj.cache
@@ -0,0 +1,2 @@
+404.html,1681733826663,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
+index.html,1681733826720,5afcaf5d317644156faaaaad8d411463d17883d3e2b165ee16f7fc051e142643
diff --git a/.firebaserc b/.firebaserc
new file mode 100644
index 0000000..2414acf
--- /dev/null
+++ b/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "spec-lead-notes"
+ }
+}
diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml
new file mode 100644
index 0000000..4e177b5
--- /dev/null
+++ b/.github/workflows/firebase-hosting-merge.yml
@@ -0,0 +1,20 @@
+# This file was auto-generated by the Firebase CLI
+# https://github.com/firebase/firebase-tools
+
+name: Deploy to Firebase Hosting on merge
+'on':
+ push:
+ branches:
+ - master
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: npm install && npm run build
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ repoToken: '${{ secrets.GITHUB_TOKEN }}'
+ firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_SPEC_LEAD_NOTES }}'
+ channelId: live
+ projectId: spec-lead-notes
diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml
new file mode 100644
index 0000000..85d34cf
--- /dev/null
+++ b/.github/workflows/firebase-hosting-pull-request.yml
@@ -0,0 +1,17 @@
+# This file was auto-generated by the Firebase CLI
+# https://github.com/firebase/firebase-tools
+
+name: Deploy to Firebase Hosting on PR
+'on': pull_request
+jobs:
+ build_and_preview:
+ if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: npm install && npm run build
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ repoToken: '${{ secrets.GITHUB_TOKEN }}'
+ firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_SPEC_LEAD_NOTES }}'
+ projectId: spec-lead-notes
diff --git a/.gitignore b/.gitignore
index 0711527..34e6848 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,6 @@ testem.log
# System files
.DS_Store
Thumbs.db
+
+# Firebase
+firebase*.log
\ No newline at end of file
diff --git a/README.md b/README.md
index e0459b3..56d43cb 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,15 @@
# SpecLeadNotesUi
+This project is an Angular exercise project serving an additional purpose of potentially useful application to track 1 on 1 meeting agreements in your organisation.
-This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.5.
+Information entered in the system are intended to be reviewed by Specialization Lead (SL) **only**.
-## Development server
+## How To Run
+Project can be run in `development` mode on your `localhost` or in Firebase in production mode.
-Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
+### Localhost
+Run `npm start` to build and run the application in `development` mode. API calls will be sent against back-end server running on `localhost` also.
-## Code scaffolding
+### Firebase
+Run `npm run deploy` to build and depploy the application in `production` mode on [Firebase server](https://spec-lead-notes.web.app/). API calls will be sent against back-end server running on [Render](https://spec-lead-notes-be.onrender.com)
-Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
-
-## Build
-
-Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
-
-## Running unit tests
-
-Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
-
-## Running end-to-end tests
-
-Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
-
-## Further help
-
-To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
+You can also test your application before deploying to firebase with local firebase deploy by running `npm run deploy:local`
\ No newline at end of file
diff --git a/angular.json b/angular.json
index ff79024..52f5913 100644
--- a/angular.json
+++ b/angular.json
@@ -23,6 +23,7 @@
"src/assets"
],
"styles": [
+ "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.css"
],
"scripts": []
@@ -33,7 +34,7 @@
{
"type": "initial",
"maximumWarning": "500kb",
- "maximumError": "1mb"
+ "maximumError": "2mb"
},
{
"type": "anyComponentStyle",
@@ -62,6 +63,10 @@
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "spec-lead-notes-ui:build",
+ "proxyConfig": "src/proxy.conf.json"
+ },
"configurations": {
"production": {
"browserTarget": "spec-lead-notes-ui:build:production"
@@ -90,6 +95,7 @@
"src/assets"
],
"styles": [
+ "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.css"
],
"scripts": []
diff --git a/firebase.json b/firebase.json
new file mode 100644
index 0000000..a17b801
--- /dev/null
+++ b/firebase.json
@@ -0,0 +1,16 @@
+{
+ "hosting": {
+ "public": "dist/spec-lead-notes-ui",
+ "ignore": [
+ "firebase.json",
+ "**/.*",
+ "**/node_modules/**"
+ ],
+ "rewrites": [
+ {
+ "source": "**",
+ "destination": "/index.html"
+ }
+ ]
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 35663dc..bb071b9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,13 +9,18 @@
"version": "0.0.0",
"dependencies": {
"@angular/animations": "^14.2.0",
+ "@angular/cdk": "^13.0.0",
"@angular/common": "^14.2.0",
"@angular/compiler": "^14.2.0",
"@angular/core": "^14.2.0",
"@angular/forms": "^14.2.0",
+ "@angular/material": "^13.0.0",
+ "@angular/material-moment-adapter": "^13.0.0",
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
+ "@ngrx/store": "^14.2.0",
+ "moment": "^2.29.4",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
@@ -349,6 +354,28 @@
"@angular/core": "14.2.12"
}
},
+ "node_modules/@angular/cdk": {
+ "version": "13.3.9",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.9.tgz",
+ "integrity": "sha512-XCuCbeuxWFyo3EYrgEYx7eHzwl76vaWcxtWXl00ka8d+WAOtMQ6Tf1D98ybYT5uwF9889fFpXAPw98mVnlo3MA==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "optionalDependencies": {
+ "parse5": "^5.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^13.0.0 || ^14.0.0-0",
+ "@angular/core": "^13.0.0 || ^14.0.0-0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/cdk/node_modules/parse5": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+ "optional": true
+ },
"node_modules/@angular/cli": {
"version": "14.2.10",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.10.tgz",
@@ -481,6 +508,35 @@
"rxjs": "^6.5.3 || ^7.4.0"
}
},
+ "node_modules/@angular/material": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.0.0.tgz",
+ "integrity": "sha512-v9TB0LHRweSwafPM6BP+pPGi398jlH5SUIqRNfR8Fbg9nujm/mvDNvxFDOJMRLrob69Fqztt7Uw+sK2FSzkjrA==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/animations": "^13.0.0 || ^14.0.0-0",
+ "@angular/cdk": "13.0.0",
+ "@angular/common": "^13.0.0 || ^14.0.0-0",
+ "@angular/core": "^13.0.0 || ^14.0.0-0",
+ "@angular/forms": "^13.0.0 || ^14.0.0-0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/material-moment-adapter": {
+ "version": "13.3.9",
+ "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-13.3.9.tgz",
+ "integrity": "sha512-H1mHd3oXFI0tnzWewlnXyO0qBe5xnl64KdDZW0KA089ElAXZf91/C6pWB7rxtJk8u7aWvg6shXg/9hSOoGn/4g==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "^13.0.0 || ^14.0.0-0",
+ "@angular/material": "13.3.9",
+ "moment": "^2.18.1"
+ }
+ },
"node_modules/@angular/platform-browser": {
"version": "14.2.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.2.12.tgz",
@@ -2701,6 +2757,18 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
+ "node_modules/@ngrx/store": {
+ "version": "14.3.3",
+ "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-14.3.3.tgz",
+ "integrity": "sha512-VhPDR2a5OQJfrVRah3vdJgL/F6UC8NU/X7lxKFqBW3NC+pmlIeFO/y8jLrZOKBXwG45tY9wrg15S70nEGoZtHA==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "^14.0.0",
+ "rxjs": "^6.5.3 || ^7.5.0"
+ }
+ },
"node_modules/@ngtools/webpack": {
"version": "14.2.10",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
@@ -7955,6 +8023,14 @@
"node": ">=10"
}
},
+ "node_modules/moment": {
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -12152,6 +12228,23 @@
"tslib": "^2.3.0"
}
},
+ "@angular/cdk": {
+ "version": "13.3.9",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.9.tgz",
+ "integrity": "sha512-XCuCbeuxWFyo3EYrgEYx7eHzwl76vaWcxtWXl00ka8d+WAOtMQ6Tf1D98ybYT5uwF9889fFpXAPw98mVnlo3MA==",
+ "requires": {
+ "parse5": "^5.0.0",
+ "tslib": "^2.3.0"
+ },
+ "dependencies": {
+ "parse5": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+ "optional": true
+ }
+ }
+ },
"@angular/cli": {
"version": "14.2.10",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.10.tgz",
@@ -12230,6 +12323,22 @@
"tslib": "^2.3.0"
}
},
+ "@angular/material": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.0.0.tgz",
+ "integrity": "sha512-v9TB0LHRweSwafPM6BP+pPGi398jlH5SUIqRNfR8Fbg9nujm/mvDNvxFDOJMRLrob69Fqztt7Uw+sK2FSzkjrA==",
+ "requires": {
+ "tslib": "^2.3.0"
+ }
+ },
+ "@angular/material-moment-adapter": {
+ "version": "13.3.9",
+ "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-13.3.9.tgz",
+ "integrity": "sha512-H1mHd3oXFI0tnzWewlnXyO0qBe5xnl64KdDZW0KA089ElAXZf91/C6pWB7rxtJk8u7aWvg6shXg/9hSOoGn/4g==",
+ "requires": {
+ "tslib": "^2.3.0"
+ }
+ },
"@angular/platform-browser": {
"version": "14.2.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.2.12.tgz",
@@ -13737,6 +13846,14 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
+ "@ngrx/store": {
+ "version": "14.3.3",
+ "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-14.3.3.tgz",
+ "integrity": "sha512-VhPDR2a5OQJfrVRah3vdJgL/F6UC8NU/X7lxKFqBW3NC+pmlIeFO/y8jLrZOKBXwG45tY9wrg15S70nEGoZtHA==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
"@ngtools/webpack": {
"version": "14.2.10",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
@@ -17675,6 +17792,11 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
+ "moment": {
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
+ },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/package.json b/package.json
index 434a257..d130875 100644
--- a/package.json
+++ b/package.json
@@ -4,20 +4,27 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
- "build": "ng build",
+ "build": "ng build --configuration production",
"watch": "ng build --watch --configuration development",
- "test": "ng test"
+ "test": "ng test",
+ "deploy:local": "npm install && npm run build && firebase emulators:start",
+ "deploy": "npm install && npm run build && firebase deploy"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.2.0",
+ "@angular/cdk": "^13.0.0",
"@angular/common": "^14.2.0",
"@angular/compiler": "^14.2.0",
"@angular/core": "^14.2.0",
"@angular/forms": "^14.2.0",
+ "@angular/material": "^13.0.0",
+ "@angular/material-moment-adapter": "^13.0.0",
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
+ "@ngrx/store": "^14.2.0",
+ "moment": "^2.29.4",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
@@ -35,4 +42,4 @@
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.7.2"
}
-}
+}
\ No newline at end of file
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 2a0fbf1..90c6b64 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,484 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ title }} app is running!
-
-
-
-
-
-
-
Resources
-
Here are some links to help you get started:
-
-
-
-
-
Next Steps
-
What do you want to do next with your app?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ng generate component xyz
-
ng add @angular/material
-
ng add @angular/pwa
-
ng add _____
-
ng test
-
ng build
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index b1c6c96..475aa2f 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,8 +1,21 @@
-import { NgModule } from '@angular/core';
+import { APP_INITIALIZER, NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
-
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
+import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { LoginModule } from './login/login.module';
+import { AuthInterceptor } from './core/interceptors/auth.interceptor';
+import { HomeModule } from './home/home.module';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { NgMaterialModule } from './ng-material/ng-material.module';
+import { StoreModule } from '@ngrx/store';
+import { authReducer } from './login/store/login.reducer';
+import { ConfigService } from './core/services/config.service';
+
+export function initializeConfig(configService: ConfigService) {
+ return () => configService.init();
+}
@NgModule({
declarations: [
@@ -10,9 +23,28 @@ import { AppComponent } from './app.component';
],
imports: [
BrowserModule,
- AppRoutingModule
+ AppRoutingModule,
+ ReactiveFormsModule,
+ HttpClientModule,
+ BrowserAnimationsModule,
+ HomeModule,
+ LoginModule,
+ NgMaterialModule,
+ StoreModule.forRoot({ '[Auth] Login': authReducer })
+ ],
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: AuthInterceptor,
+ multi: true
+ },
+ {
+ provide: APP_INITIALIZER,
+ useFactory: initializeConfig,
+ deps: [ConfigService],
+ multi: true
+ }
],
- providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
diff --git a/src/assets/.gitkeep b/src/app/core/components/header/header.component.css
similarity index 100%
rename from src/assets/.gitkeep
rename to src/app/core/components/header/header.component.css
diff --git a/src/app/core/components/header/header.component.html b/src/app/core/components/header/header.component.html
new file mode 100644
index 0000000..31023a8
--- /dev/null
+++ b/src/app/core/components/header/header.component.html
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/src/app/core/components/header/header.component.spec.ts b/src/app/core/components/header/header.component.spec.ts
new file mode 100644
index 0000000..1fd30b4
--- /dev/null
+++ b/src/app/core/components/header/header.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HeaderComponent } from './header.component';
+
+describe('HeaderComponent', () => {
+ let component: HeaderComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ HeaderComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(HeaderComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/core/components/header/header.component.ts b/src/app/core/components/header/header.component.ts
new file mode 100644
index 0000000..37ecb4a
--- /dev/null
+++ b/src/app/core/components/header/header.component.ts
@@ -0,0 +1,25 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { AuthService } from 'src/app/shared/services/auth/auth.service';
+
+@Component({
+ selector: 'app-header',
+ templateUrl: './header.component.html',
+ styleUrls: ['./header.component.css']
+})
+export class HeaderComponent implements OnInit {
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) { }
+
+ ngOnInit(): void {
+ }
+
+ logout() {
+ this.authService.logout();
+ this.router.navigate(['login']);
+ }
+
+}
diff --git a/src/app/core/core-routing.module.ts b/src/app/core/core-routing.module.ts
new file mode 100644
index 0000000..b466ddd
--- /dev/null
+++ b/src/app/core/core-routing.module.ts
@@ -0,0 +1,10 @@
+import { NgModule } from "@angular/core";
+import { Routes, RouterModule } from "@angular/router";
+
+const routes: Routes = [];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class CoreRoutingModule { }
\ No newline at end of file
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
new file mode 100644
index 0000000..d0563e0
--- /dev/null
+++ b/src/app/core/core.module.ts
@@ -0,0 +1,15 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { HeaderComponent } from "../core/components/header/header.component";
+import { CoreRoutingModule } from "./core-routing.module";
+
+@NgModule({
+ declarations: [HeaderComponent],
+ imports: [
+ CommonModule,
+ CoreRoutingModule
+ ],
+ providers: [],
+ exports: [HeaderComponent]
+})
+export class CoreModule { }
\ No newline at end of file
diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts
new file mode 100644
index 0000000..f5af5d7
--- /dev/null
+++ b/src/app/core/interceptors/auth.interceptor.ts
@@ -0,0 +1,30 @@
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
+import { Injectable } from "@angular/core";
+import { Store } from "@ngrx/store";
+import { Observable } from "rxjs";
+import { selectBearerToken } from "src/app/login/store/login.selectors";
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+
+ constructor(private store: Store) {}
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ let jwtBearerToken;
+ this.store.select(selectBearerToken)
+ .subscribe(result => {
+ jwtBearerToken = result;
+ });
+
+ if (jwtBearerToken) {
+ const clone = req.clone({
+ headers: req.headers.set('Authorization', 'Bearer ' + jwtBearerToken)
+ });
+
+ return next.handle(clone);
+ } else {
+ return next.handle(req);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/core/services/config.service.spec.ts b/src/app/core/services/config.service.spec.ts
new file mode 100644
index 0000000..aca64dd
--- /dev/null
+++ b/src/app/core/services/config.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ConfigService } from './config.service';
+
+describe('ConfigService', () => {
+ let service: ConfigService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(ConfigService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/core/services/config.service.ts b/src/app/core/services/config.service.ts
new file mode 100644
index 0000000..fd80a34
--- /dev/null
+++ b/src/app/core/services/config.service.ts
@@ -0,0 +1,38 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { environment } from '../../../environments/environment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ConfigService {
+
+ private env?: string;
+ private config: any;
+
+ constructor(private http: HttpClient) { }
+
+ public init(): Promise {
+ return new Promise((resolve, reject) => {
+ this.env = environment.production ? 'production' : 'development';
+ this.http.get(`./assets/config/${this.env}.json`)
+ .subscribe({
+ next: res => {
+ this.config = res;
+ console.log(res);
+ resolve();
+ },
+ error: (err: any) => reject(`Error while reading config for environment: ${this.env}, ${err}`)
+ })
+
+ })
+ }
+
+ public getApiBaseUrl() {
+ return this.get('apiBaseUrl');
+ }
+
+ private get(key: string) {
+ return this.config[key];
+ }
+}
diff --git a/src/app/home/components/home/home.component.css b/src/app/home/components/home/home.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/home/components/home/home.component.html b/src/app/home/components/home/home.component.html
new file mode 100644
index 0000000..7beb1a0
--- /dev/null
+++ b/src/app/home/components/home/home.component.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/app/home/components/home/home.component.spec.ts b/src/app/home/components/home/home.component.spec.ts
new file mode 100644
index 0000000..5075be7
--- /dev/null
+++ b/src/app/home/components/home/home.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HomeComponent } from './home.component';
+
+describe('HomeComponent', () => {
+ let component: HomeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ HomeComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(HomeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/components/home/home.component.ts b/src/app/home/components/home/home.component.ts
new file mode 100644
index 0000000..1cc6d53
--- /dev/null
+++ b/src/app/home/components/home/home.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from "@angular/core";
+
+@Component({
+ selector: 'app-home',
+ templateUrl: './home.component.html',
+ styleUrls: ['./home.component.css']
+})
+export class HomeComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/home/home-routing.module.ts b/src/app/home/home-routing.module.ts
new file mode 100644
index 0000000..6d7cde0
--- /dev/null
+++ b/src/app/home/home-routing.module.ts
@@ -0,0 +1,30 @@
+import { NgModule } from "@angular/core";
+import { Routes, RouterModule } from "@angular/router";
+import { AuthGuardService } from "../shared/services/guards/auth-guard.service";
+import { HomeComponent } from "./components/home/home.component";
+
+const routes: Routes = [
+ {
+ path: '', component: HomeComponent,
+ canActivate: [AuthGuardService],
+ canActivateChild: [AuthGuardService],
+ children: [
+ {
+ path: 'people',
+ title: 'Meeting Notes',
+ loadChildren: () => import('./people/people.module').then(m => m.PeopleModule)
+ },
+ {
+ path: 'summary',
+ title: 'Summary',
+ loadChildren: () => import('./summary/summary.module').then(m => m.SummaryModule)
+ }
+ ]
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class HomeRoutingModule { }
\ No newline at end of file
diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts
new file mode 100644
index 0000000..8e22c7e
--- /dev/null
+++ b/src/app/home/home.module.ts
@@ -0,0 +1,16 @@
+import { CommonModule } 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";
+
+@NgModule({
+ declarations: [HomeComponent],
+ imports: [
+ CommonModule,
+ HomeRoutingModule,
+ CoreModule
+ ],
+ providers: [],
+})
+export class HomeModule { }
\ No newline at end of file
diff --git a/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.css b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.css
new file mode 100644
index 0000000..1fe04af
--- /dev/null
+++ b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.css
@@ -0,0 +1,9 @@
+form {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+button {
+ margin-bottom: 20px;
+}
\ 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
new file mode 100644
index 0000000..ee8e538
--- /dev/null
+++ b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.html
@@ -0,0 +1,108 @@
+
+ Add Meeting Notes for {{ personData.name }}
+
+
\ No newline at end of file
diff --git a/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.spec.ts b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.spec.ts
new file mode 100644
index 0000000..b91a19e
--- /dev/null
+++ b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AddMeetingModalComponent } from './add-meeting-modal.component';
+
+describe('ModalComponent', () => {
+ let component: AddMeetingModalComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ AddMeetingModalComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AddMeetingModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 0000000..0b44169
--- /dev/null
+++ b/src/app/home/people/components/add-meeting-modal/add-meeting-modal.component.ts
@@ -0,0 +1,126 @@
+import { Component, Inject, OnDestroy, Optional } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { Subscription } from 'rxjs';
+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';
+
+@Component({
+ selector: 'app-modal',
+ templateUrl: './add-meeting-modal.component.html',
+ styleUrls: ['./add-meeting-modal.component.css']
+})
+export class AddMeetingModalComponent implements OnDestroy {
+
+ private static readonly DATE_FORMAT = 'MM/DD/YYYY';
+ meetingForm: FormGroup;
+ cdkAutosizeMinRows = 1;
+ cdkAutosizeMaxRows = 4;
+ attritionRiskValues = [
+ AttritionRisk.NONE,
+ AttritionRisk.LOW,
+ AttritionRisk.MEDIUM,
+ AttritionRisk.HIGH
+ ];
+ addMeetingNotes$?: Subscription;
+
+ constructor(
+ private dialogRef: MatDialogRef,
+ private formBuilder: FormBuilder,
+ private personService: PersonService,
+ private yesNoMapper: YesNoToBooleanMapper,
+ @Optional() @Inject(MAT_DIALOG_DATA) protected personData: { id: string, name: string, unitId: string }
+ ) {
+ this.meetingForm = this.formBuilder.group({
+ date: ['', [Validators.required]],
+ comments: [''],
+ questions: [''],
+ managerActionItems: [''],
+ subordinateActionItems: [''],
+ importantAgreements: [''],
+ satisfaction: [''],
+ plans: [''],
+ feedback: [''],
+ issues: [''],
+ attritionRisk: [AttritionRisk.NONE],
+ oneToOneReportSent: [YesNo.NO]
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.addMeetingNotes$?.unsubscribe();
+ }
+
+ get date() {
+ const date: moment.Moment = this.meetingForm.get('date')!.value.date;
+ return date.format(AddMeetingModalComponent.DATE_FORMAT);
+ }
+
+ get comments() {
+ return this.meetingForm.get('comments')!.value;
+ }
+
+ get questions() {
+ return this.meetingForm.get('questions')!.value;
+ }
+
+ get managerActionItems() {
+ return this.meetingForm.get('managerActionItems')!.value;
+ }
+
+ get subordinateActionItems() {
+ return this.meetingForm.get('subordinateActionItems')!.value;
+ }
+
+ get importantAgreements() {
+ return this.meetingForm.get('importantAgreements')!.value;
+ }
+
+ get satisfaction() {
+ return this.meetingForm.get('satisfaction')!.value;
+ }
+
+ get plans() {
+ return this.meetingForm.get('plans')!.value;
+ }
+
+ get feedback() {
+ return this.meetingForm.get('feedback')!.value;
+ }
+
+ get issues() {
+ return this.meetingForm.get('issues')!.value;
+ }
+
+ get attritionRisk() {
+ return this.meetingForm.get('attritionRisk')!.value;
+ }
+
+ get oneToOneReportSent() {
+ return this.meetingForm.get('oneToOneReportSent')!.value;
+ }
+
+ onSubmit() {
+ const meetingNotes: MeetingNotes = {
+ date: this.date,
+ personId: this.personData.id,
+ comments: this.comments,
+ questions: this.questions,
+ managerActionItems: this.managerActionItems,
+ subordinateActionItems: this.subordinateActionItems,
+ importantAgreements: this.importantAgreements,
+ satisfaction: this.satisfaction,
+ plans: this.plans,
+ feedback: this.feedback,
+ issues: this.issues,
+ attritionRisk: this.attritionRisk,
+ oneToOneReportSent: this.yesNoMapper.toClient(this.oneToOneReportSent)
+ }
+
+ this.addMeetingNotes$ = this.personService.addMeetingNotes(this.personData.unitId, meetingNotes)
+ .subscribe(response => this.dialogRef.close({ meetingNotes }));
+ }
+
+}
diff --git a/src/app/home/people/components/edit-meeting/edit-meeting.component.css b/src/app/home/people/components/edit-meeting/edit-meeting.component.css
new file mode 100644
index 0000000..3df2ac1
--- /dev/null
+++ b/src/app/home/people/components/edit-meeting/edit-meeting.component.css
@@ -0,0 +1,9 @@
+form {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.buttons {
+ display: inline;
+}
\ No newline at end of file
diff --git a/src/app/home/people/components/edit-meeting/edit-meeting.component.html b/src/app/home/people/components/edit-meeting/edit-meeting.component.html
new file mode 100644
index 0000000..1d0d44d
--- /dev/null
+++ b/src/app/home/people/components/edit-meeting/edit-meeting.component.html
@@ -0,0 +1,120 @@
+
+
+
Edit Meeting Notes
+
+
+
\ No newline at end of file
diff --git a/src/app/home/people/components/edit-meeting/edit-meeting.component.spec.ts b/src/app/home/people/components/edit-meeting/edit-meeting.component.spec.ts
new file mode 100644
index 0000000..8640d45
--- /dev/null
+++ b/src/app/home/people/components/edit-meeting/edit-meeting.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { EditMeetingComponent } from './edit-meeting.component';
+
+describe('EditMeetingComponent', () => {
+ let component: EditMeetingComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ EditMeetingComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(EditMeetingComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 0000000..dbbbaae
--- /dev/null
+++ b/src/app/home/people/components/edit-meeting/edit-meeting.component.ts
@@ -0,0 +1,55 @@
+import { Component, OnInit } from '@angular/core';
+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';
+
+@Component({
+ selector: 'app-edit-meeting',
+ templateUrl: './edit-meeting.component.html',
+ styleUrls: ['./edit-meeting.component.css']
+})
+export class EditMeetingComponent implements OnInit {
+
+ meetingData!: MeetingNotes;
+ meetingForm!: FormGroup;
+ cdkAutosizeMinRows = 1;
+ cdkAutosizeMaxRows = 4;
+ attritionRiskValues = [
+ AttritionRisk.NONE,
+ AttritionRisk.LOW,
+ AttritionRisk.MEDIUM,
+ AttritionRisk.HIGH
+ ];
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private yesNoMapper: YesNoToBooleanMapper,
+ private location: Location) { }
+
+ ngOnInit(): void {
+ this.meetingData = history.state;
+ this.meetingForm = this.formBuilder.group({
+ comments: [this.meetingData.comments],
+ questions: [this.meetingData.questions],
+ managerActionItems: [this.meetingData.managerActionItems],
+ subordinateActionItems: [this.meetingData.subordinateActionItems],
+ importantAgreements: [this.meetingData.importantAgreements],
+ satisfaction: [this.meetingData.satisfaction],
+ plans: [this.meetingData.plans],
+ feedback: [this.meetingData.feedback],
+ issues: [this.meetingData.issues],
+ attritionRisk: [this.meetingData.attritionRisk],
+ oneToOneReportSent: [this.yesNoMapper.toModel(this.meetingData.oneToOneReportSent)]
+ });
+ }
+
+ onSubmit() {
+ // todo: implement
+ }
+
+ back() {
+ this.location.back();
+ }
+
+}
diff --git a/src/app/home/people/components/meeting/meeting.component.css b/src/app/home/people/components/meeting/meeting.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/home/people/components/meeting/meeting.component.html b/src/app/home/people/components/meeting/meeting.component.html
new file mode 100644
index 0000000..36ca377
--- /dev/null
+++ b/src/app/home/people/components/meeting/meeting.component.html
@@ -0,0 +1,53 @@
+
+
+ - Comments
+ - {{ meetingData.comments }}
+
+
+
+ - Questions
+ - {{ meetingData.questions }}
+
+
+
+ - Their Action Items
+ - {{ meetingData.subordinateActionItems }}
+
+
+
+ - Important Agreements
+ - {{ meetingData.importantAgreements }}
+
+
+
+ - Satisfaction
+ - {{ meetingData.satisfaction }}
+
+
+
+ - Plans
+ - {{ meetingData.plans }}
+
+
+
+ - Feedback
+ - {{ meetingData.feedback }}
+
+
+
+ - Issues
+ - {{ meetingData.issues }}
+
+
+
+ - Attrition Risk
+ - {{ meetingData.attritionRisk }}
+
+
+
+ - 1-1 Sent
+ - {{ meetingData.oneToOneReportSent | yesNo }}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/home/people/components/meeting/meeting.component.spec.ts b/src/app/home/people/components/meeting/meeting.component.spec.ts
new file mode 100644
index 0000000..ab77fce
--- /dev/null
+++ b/src/app/home/people/components/meeting/meeting.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MeetingComponent } from './meeting.component';
+
+describe('MeetingComponent', () => {
+ let component: MeetingComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ MeetingComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(MeetingComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/components/meeting/meeting.component.ts b/src/app/home/people/components/meeting/meeting.component.ts
new file mode 100644
index 0000000..2adc2e9
--- /dev/null
+++ b/src/app/home/people/components/meeting/meeting.component.ts
@@ -0,0 +1,19 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { MeetingNotes } from '../../models/meeting-notes.model';
+
+@Component({
+ selector: 'app-meeting',
+ templateUrl: './meeting.component.html',
+ styleUrls: ['./meeting.component.css']
+})
+export class MeetingComponent implements OnInit {
+
+ @Input()
+ meetingData!: MeetingNotes;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/home/people/components/people/people.component.css b/src/app/home/people/components/people/people.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/home/people/components/people/people.component.html b/src/app/home/people/components/people/people.component.html
new file mode 100644
index 0000000..325c0cd
--- /dev/null
+++ b/src/app/home/people/components/people/people.component.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/src/app/home/people/components/people/people.component.spec.ts b/src/app/home/people/components/people/people.component.spec.ts
new file mode 100644
index 0000000..fea0f06
--- /dev/null
+++ b/src/app/home/people/components/people/people.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PeopleComponent } from './people.component';
+
+describe('PeopleComponent', () => {
+ let component: PeopleComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ PeopleComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PeopleComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/components/people/people.component.ts b/src/app/home/people/components/people/people.component.ts
new file mode 100644
index 0000000..e9b6f45
--- /dev/null
+++ b/src/app/home/people/components/people/people.component.ts
@@ -0,0 +1,44 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Observable, 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';
+
+@Component({
+ selector: 'app-people',
+ templateUrl: './people.component.html',
+ styleUrls: ['./people.component.css']
+})
+export class PeopleComponent implements OnInit, OnDestroy {
+
+ $units!: Observable;
+ $userId?: Subscription;
+
+ constructor(
+ private readonly unitsService: UnitsService,
+ private modalService: ModalService,
+ private store: Store
+ ) { }
+
+ ngOnInit(): void {
+ let userId: string | undefined;
+ this.$userId = this.store.select(selectUserId)
+ .subscribe(value => userId = value);
+
+ if (!userId) {
+ throw new Error('userId not found in Store');
+ }
+
+ this.$units = this.unitsService.getUnits(userId);
+ this.modalService.subscribe({
+ afterClosed: data => this.$units = userId ? this.unitsService.getUnits(userId) : of([])
+ })
+ }
+
+ ngOnDestroy(): void {
+ this.$userId?.unsubscribe();
+ }
+
+}
diff --git a/src/app/home/people/components/person-tooltip/person-tooltip.component.css b/src/app/home/people/components/person-tooltip/person-tooltip.component.css
new file mode 100644
index 0000000..2a0532d
--- /dev/null
+++ b/src/app/home/people/components/person-tooltip/person-tooltip.component.css
@@ -0,0 +1,4 @@
+.person-tooltip {
+ position: fixed;
+}
+
\ No newline at end of file
diff --git a/src/app/home/people/components/person-tooltip/person-tooltip.component.html b/src/app/home/people/components/person-tooltip/person-tooltip.component.html
new file mode 100644
index 0000000..b8a2a76
--- /dev/null
+++ b/src/app/home/people/components/person-tooltip/person-tooltip.component.html
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/src/app/home/people/components/person-tooltip/person-tooltip.component.spec.ts b/src/app/home/people/components/person-tooltip/person-tooltip.component.spec.ts
new file mode 100644
index 0000000..018d29d
--- /dev/null
+++ b/src/app/home/people/components/person-tooltip/person-tooltip.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PersonTooltipComponent } from './person-tooltip.component';
+
+describe('PersonTooltipComponent', () => {
+ let component: PersonTooltipComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ PersonTooltipComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PersonTooltipComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/components/person-tooltip/person-tooltip.component.ts b/src/app/home/people/components/person-tooltip/person-tooltip.component.ts
new file mode 100644
index 0000000..e74be41
--- /dev/null
+++ b/src/app/home/people/components/person-tooltip/person-tooltip.component.ts
@@ -0,0 +1,21 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Person } from '../../models/unit.model';
+
+@Component({
+ selector: 'app-person-tooltip',
+ templateUrl: './person-tooltip.component.html',
+ styleUrls: ['./person-tooltip.component.css']
+})
+export class PersonTooltipComponent implements OnInit {
+
+ @Input()
+ personData!: Person;
+ left = 0;
+ top = 0;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/home/people/components/person/person.component.css b/src/app/home/people/components/person/person.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/home/people/components/person/person.component.html b/src/app/home/people/components/person/person.component.html
new file mode 100644
index 0000000..fa3590b
--- /dev/null
+++ b/src/app/home/people/components/person/person.component.html
@@ -0,0 +1,16 @@
+
+
{{ personData.name }}
+
+
+
\ No newline at end of file
diff --git a/src/app/home/people/components/person/person.component.spec.ts b/src/app/home/people/components/person/person.component.spec.ts
new file mode 100644
index 0000000..23c7b09
--- /dev/null
+++ b/src/app/home/people/components/person/person.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PersonComponent } from './person.component';
+
+describe('PersonComponent', () => {
+ let component: PersonComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ PersonComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PersonComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/components/person/person.component.ts b/src/app/home/people/components/person/person.component.ts
new file mode 100644
index 0000000..ce92303
--- /dev/null
+++ b/src/app/home/people/components/person/person.component.ts
@@ -0,0 +1,79 @@
+import { 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';
+
+@Component({
+ selector: 'app-person',
+ templateUrl: './person.component.html',
+ styleUrls: ['./person.component.css']
+})
+export class PersonComponent implements OnInit {
+
+ @Input()
+ personData!: Person;
+ @Input()
+ unitId!: string;
+
+ pagination: Pagination = {
+ config: {
+ itemsOnPage: 3,
+ currentPageIndex: 0,
+ },
+ pages: []
+ }
+
+ constructor(private modalService: ModalService) { }
+
+ ngOnInit(): void {
+ this.initializePagination();
+ }
+
+ openAddMeetingModal() {
+ this.modalService
+ .open(AddMeetingModalComponent, {
+ data: { id: this.personData.id, name: this.personData.name, unitId: this.unitId }
+ })
+ .subscribe({
+ afterClosed: data => console.log('Data from Modal', data)
+ });
+ }
+
+ updatePageNumber(index: number) {
+ this.pagination.config.currentPageIndex = +index;
+ }
+
+ private initializePagination() {
+ let pageIndex = 0;
+ this.pagination.pages.push({ data: [] });
+ for (let i = 0; i < this.personData.meetings.length; i++) {
+ this.pagination.pages[pageIndex].data.push(this.personData.meetings[i]);
+ if (this.isEndOfPage(i, pageIndex) && this.hasMoreData(i, this.personData.meetings)) {
+ pageIndex++;
+ this.pagination.pages.push({ data: [] });
+ }
+ }
+ }
+
+ isEndOfPage(i: number, pageIndex: number) {
+ return i >= (this.pagination.config.itemsOnPage - 1) + pageIndex * this.pagination.config.itemsOnPage;
+ }
+
+ hasMoreData(i: number, data: any[]) {
+ return data.length > i + 1;
+ }
+}
+
+export interface Pagination {
+ config: PaginationConfig,
+ pages: Page[]
+}
+
+export interface PaginationConfig {
+ itemsOnPage: number,
+ currentPageIndex: number,
+}
+
+export interface Page {
+ data: any[]
+}
\ No newline at end of file
diff --git a/src/app/home/people/components/unit/unit.component.css b/src/app/home/people/components/unit/unit.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/home/people/components/unit/unit.component.html b/src/app/home/people/components/unit/unit.component.html
new file mode 100644
index 0000000..3f69317
--- /dev/null
+++ b/src/app/home/people/components/unit/unit.component.html
@@ -0,0 +1,8 @@
+
+ Unit Name: {{ unitData.name | uppercase }}
+
+
\ No newline at end of file
diff --git a/src/app/home/people/components/unit/unit.component.spec.ts b/src/app/home/people/components/unit/unit.component.spec.ts
new file mode 100644
index 0000000..c45ef42
--- /dev/null
+++ b/src/app/home/people/components/unit/unit.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UnitComponent } from './unit.component';
+
+describe('UnitComponent', () => {
+ let component: UnitComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ UnitComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(UnitComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/components/unit/unit.component.ts b/src/app/home/people/components/unit/unit.component.ts
new file mode 100644
index 0000000..f6b8a8b
--- /dev/null
+++ b/src/app/home/people/components/unit/unit.component.ts
@@ -0,0 +1,18 @@
+import { 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']
+})
+export class UnitComponent implements OnInit {
+
+ @Input() unitData!: Unit;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/home/people/directives/person.tooltip.directive.ts b/src/app/home/people/directives/person.tooltip.directive.ts
new file mode 100644
index 0000000..2ca0a25
--- /dev/null
+++ b/src/app/home/people/directives/person.tooltip.directive.ts
@@ -0,0 +1,59 @@
+import { ApplicationRef, ComponentRef, Directive, ElementRef, EmbeddedViewRef, HostListener, Injector, Input, NgModuleRef, OnDestroy, ViewContainerRef } from "@angular/core";
+import { PersonTooltipComponent } from "../components/person-tooltip/person-tooltip.component";
+import { Person } from "../models/unit.model";
+
+@Directive({
+ selector: '[tooltip]'
+})
+export class PersonTooltipDirective implements OnDestroy {
+
+ @Input() data!: Person;
+
+ private componentRef: ComponentRef | null = null;
+
+ constructor(
+ private elementRef: ElementRef,
+ private appRef: ApplicationRef,
+ private viewContainerRef: ViewContainerRef,
+ private injector: Injector) { }
+
+ ngOnDestroy(): void {
+ this.destroy();
+ }
+
+ @HostListener('mouseenter')
+ public onMouseEnter() {
+ if (this.componentRef === null) {
+ this.componentRef = this.viewContainerRef.createComponent(PersonTooltipComponent,
+ { injector: this.injector });
+ const hv = this.componentRef.hostView;
+ const embeddedViewref = hv as EmbeddedViewRef;
+ const domElement = embeddedViewref.rootNodes[0] as HTMLElement
+ document.body.appendChild(domElement);
+ this.setTooltipComponentProperties();
+ }
+ }
+
+ private setTooltipComponentProperties() {
+ if (this.componentRef) {
+ this.componentRef.instance.personData = this.data;
+ const { left, bottom } = this.elementRef.nativeElement.getBoundingClientRect();
+ this.componentRef.instance.left = left;
+ this.componentRef.instance.top = bottom;
+ }
+ }
+
+ @HostListener('mouseleave')
+ public onMouseLeave() {
+ this.destroy();
+ }
+
+ private destroy() {
+ if (this.componentRef) {
+ this.appRef.detachView(this.componentRef.hostView);
+ this.componentRef.destroy();
+ this.componentRef = null;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/home/people/models/meeting-notes.model.ts b/src/app/home/people/models/meeting-notes.model.ts
new file mode 100644
index 0000000..5b6d90a
--- /dev/null
+++ b/src/app/home/people/models/meeting-notes.model.ts
@@ -0,0 +1,24 @@
+export interface MeetingNotes {
+ date: string;
+ personId: string;
+ notesId?: string;
+ comments: string;
+ questions: string;
+ managerActionItems: string;
+ subordinateActionItems: string;
+ importantAgreements: string;
+ satisfaction: string;
+ plans: string;
+ feedback: string;
+ issues: string;
+ attritionRisk: AttritionRisk;
+ oneToOneReportSent: boolean;
+}
+
+export enum AttritionRisk {
+ NONE = 'None', LOW = 'Low', MEDIUM = 'Medium', HIGH = 'High'
+}
+
+export enum YesNo {
+ YES = 'Yes', NO = 'No'
+}
\ No newline at end of file
diff --git a/src/app/home/people/models/unit.model.ts b/src/app/home/people/models/unit.model.ts
new file mode 100644
index 0000000..95824ae
--- /dev/null
+++ b/src/app/home/people/models/unit.model.ts
@@ -0,0 +1,16 @@
+import { MeetingNotes } from "./meeting-notes.model";
+
+export interface Unit {
+ id: string;
+ name: string;
+ people: Person[];
+}
+
+export interface Person {
+ id: string;
+ name: string;
+ grade: string;
+ specializations: string[];
+ location: string;
+ meetings: MeetingNotes[];
+}
\ No newline at end of file
diff --git a/src/app/home/people/people-routing.module.ts b/src/app/home/people/people-routing.module.ts
new file mode 100644
index 0000000..c575cc3
--- /dev/null
+++ b/src/app/home/people/people-routing.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from "@angular/core";
+import { Routes, RouterModule } from "@angular/router";
+import { PeopleComponent } from "./components/people/people.component";
+import { EditMeetingComponent } from "./components/edit-meeting/edit-meeting.component";
+
+const routes: Routes = [
+ { path: '', component: PeopleComponent },
+ { path: 'meeting/:id', component: EditMeetingComponent }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class PeopleRoutingModule { }
\ No newline at end of file
diff --git a/src/app/home/people/people.module.ts b/src/app/home/people/people.module.ts
new file mode 100644
index 0000000..be7ca8d
--- /dev/null
+++ b/src/app/home/people/people.module.ts
@@ -0,0 +1,37 @@
+import { CommonModule } from "@angular/common";
+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";
+import { AddMeetingModalComponent } from "./components/add-meeting-modal/add-meeting-modal.component";
+import { ReactiveFormsModule } from "@angular/forms";
+import { SharedModule } from "src/app/shared/shared.module";
+import { PersonTooltipDirective } from "./directives/person.tooltip.directive";
+import { PersonTooltipComponent } from './components/person-tooltip/person-tooltip.component';
+import { EditMeetingComponent } from './components/edit-meeting/edit-meeting.component';
+
+@NgModule({
+ declarations: [
+ PeopleComponent,
+ UnitComponent,
+ PersonComponent,
+ MeetingComponent,
+ AddMeetingModalComponent,
+ PersonTooltipDirective,
+ PersonTooltipComponent,
+ EditMeetingComponent
+ ],
+ imports: [
+ CommonModule,
+ PeopleRoutingModule,
+ NgMaterialModule,
+ ReactiveFormsModule,
+ SharedModule
+ ],
+ providers: [UnitsService],
+})
+export class PeopleModule { }
\ No newline at end of file
diff --git a/src/app/home/people/services/modal.service.spec.ts b/src/app/home/people/services/modal.service.spec.ts
new file mode 100644
index 0000000..0ad21ad
--- /dev/null
+++ b/src/app/home/people/services/modal.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ModalService } from './modal.service';
+
+describe('ModalService', () => {
+ let service: ModalService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(ModalService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/services/modal.service.ts b/src/app/home/people/services/modal.service.ts
new file mode 100644
index 0000000..690503d
--- /dev/null
+++ b/src/app/home/people/services/modal.service.ts
@@ -0,0 +1,35 @@
+import { Injectable } from '@angular/core';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { of } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ModalService {
+
+ private matDialogRef?: MatDialogRef;
+ private subscribers: Subscriber[] = [];
+
+ constructor(private dialog: MatDialog) { }
+
+ open(modalComponent: any, data: any): ModalService {
+ this.matDialogRef = this.dialog.open(modalComponent, data);
+ return this;
+ }
+
+ afterClosed() {
+ this.matDialogRef ?
+ this.matDialogRef.afterClosed().subscribe(
+ (data) => this.subscribers.forEach(subscriber => subscriber.afterClosed(data)))
+ : of();
+ }
+
+ subscribe(subscriber: Subscriber) {
+ this.subscribers.push(subscriber);
+ this.afterClosed();
+ }
+}
+
+export interface Subscriber {
+ afterClosed: (input: any) => any;
+}
\ No newline at end of file
diff --git a/src/app/home/people/services/person.service.spec.ts b/src/app/home/people/services/person.service.spec.ts
new file mode 100644
index 0000000..88fe19e
--- /dev/null
+++ b/src/app/home/people/services/person.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { PersonService } from './person.service';
+
+describe('PersonService', () => {
+ let service: PersonService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(PersonService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/services/person.service.ts b/src/app/home/people/services/person.service.ts
new file mode 100644
index 0000000..0c8b2db
--- /dev/null
+++ b/src/app/home/people/services/person.service.ts
@@ -0,0 +1,27 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { MeetingNotes } from '../models/meeting-notes.model';
+import { ConfigService } from 'src/app/core/services/config.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PersonService {
+
+ private readonly apiBaseUrl;
+
+ constructor(
+ private http: HttpClient,
+ private configService: ConfigService
+ ) {
+ this.apiBaseUrl = this.configService.getApiBaseUrl();
+ }
+
+ addMeetingNotes(unitId: string, meetingNotes: MeetingNotes): Observable {
+ return this.http.post(`${this.apiBaseUrl}/person`, meetingNotes, {
+ params: { unitId: unitId }
+ })
+ }
+
+}
diff --git a/src/app/home/people/services/units.service.spec.ts b/src/app/home/people/services/units.service.spec.ts
new file mode 100644
index 0000000..2840d44
--- /dev/null
+++ b/src/app/home/people/services/units.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UnitsService } from './units.service';
+
+describe('UnitsService', () => {
+ let service: UnitsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(UnitsService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/home/people/services/units.service.ts b/src/app/home/people/services/units.service.ts
new file mode 100644
index 0000000..867307d
--- /dev/null
+++ b/src/app/home/people/services/units.service.ts
@@ -0,0 +1,22 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { ConfigService } from 'src/app/core/services/config.service';
+import { Unit } from 'src/app/home/people/models/unit.model';
+
+@Injectable()
+export class UnitsService {
+
+ private readonly apiBaseUrl;
+
+ constructor(
+ private http: HttpClient,
+ private configService: ConfigService
+ ) {
+ this.apiBaseUrl = this.configService.getApiBaseUrl();
+ }
+
+ getUnits(userId: string): Observable {
+ return this.http.get(`${this.apiBaseUrl}/${userId}/units`);
+ }
+}
diff --git a/src/app/home/summary/components/summary/summary.component.css b/src/app/home/summary/components/summary/summary.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/home/summary/components/summary/summary.component.html b/src/app/home/summary/components/summary/summary.component.html
new file mode 100644
index 0000000..eb0512c
--- /dev/null
+++ b/src/app/home/summary/components/summary/summary.component.html
@@ -0,0 +1 @@
+summary works!
diff --git a/src/app/home/summary/components/summary/summary.component.spec.ts b/src/app/home/summary/components/summary/summary.component.spec.ts
new file mode 100644
index 0000000..0ce9d33
--- /dev/null
+++ b/src/app/home/summary/components/summary/summary.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SummaryComponent } from './summary.component';
+
+describe('SummaryComponent', () => {
+ let component: SummaryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ SummaryComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(SummaryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/home/summary/components/summary/summary.component.ts b/src/app/home/summary/components/summary/summary.component.ts
new file mode 100644
index 0000000..011993b
--- /dev/null
+++ b/src/app/home/summary/components/summary/summary.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-summary',
+ templateUrl: './summary.component.html',
+ styleUrls: ['./summary.component.css']
+})
+export class SummaryComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/home/summary/summary-routing.module.ts b/src/app/home/summary/summary-routing.module.ts
new file mode 100644
index 0000000..7347f65
--- /dev/null
+++ b/src/app/home/summary/summary-routing.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from "@angular/core";
+import { Routes, RouterModule } from "@angular/router";
+import { SummaryComponent } from "./components/summary/summary.component";
+
+const routes: Routes = [
+ { path: '', component: SummaryComponent }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class SummaryRoutingModule { }
\ No newline at end of file
diff --git a/src/app/home/summary/summary.module.ts b/src/app/home/summary/summary.module.ts
new file mode 100644
index 0000000..a8a3a3a
--- /dev/null
+++ b/src/app/home/summary/summary.module.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { SummaryComponent } from "./components/summary/summary.component";
+import { SummaryRoutingModule } from "./summary-routing.module";
+
+@NgModule({
+ declarations: [SummaryComponent],
+ imports: [
+ CommonModule,
+ SummaryRoutingModule
+ ],
+ providers: [],
+})
+export class SummaryModule { }
\ No newline at end of file
diff --git a/src/app/login/components/login/login.component.css b/src/app/login/components/login/login.component.css
new file mode 100644
index 0000000..c3b5c02
--- /dev/null
+++ b/src/app/login/components/login/login.component.css
@@ -0,0 +1,14 @@
+.login-form, .error-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+p {
+ display: flex;
+ justify-content: center;
+}
+
+label {
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/src/app/login/components/login/login.component.html b/src/app/login/components/login/login.component.html
new file mode 100644
index 0000000..7c05dbc
--- /dev/null
+++ b/src/app/login/components/login/login.component.html
@@ -0,0 +1,23 @@
+
+
+
\ No newline at end of file
diff --git a/src/app/login/components/login/login.component.spec.ts b/src/app/login/components/login/login.component.spec.ts
new file mode 100644
index 0000000..10eca24
--- /dev/null
+++ b/src/app/login/components/login/login.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+ let component: LoginComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ LoginComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(LoginComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/login/components/login/login.component.ts b/src/app/login/components/login/login.component.ts
new file mode 100644
index 0000000..5864ec6
--- /dev/null
+++ b/src/app/login/components/login/login.component.ts
@@ -0,0 +1,58 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { Subscription, catchError, of, throwError } from 'rxjs';
+import { AuthData } from 'src/app/shared/models/auth.service.model';
+import { AuthService } from 'src/app/shared/services/auth/auth.service';
+
+@Component({
+ selector: 'app-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit, OnDestroy {
+
+ loginForm: FormGroup;
+ login$?: Subscription;
+ loginError = false;
+ errorMessage?: string;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private authService: AuthService,
+ private router: Router
+ ) {
+ this.loginForm = this.formBuilder.group({
+ username: ['', [Validators.required]],
+ password: ['', Validators.required]
+ });
+ }
+
+ ngOnInit(): void {
+ }
+
+ ngOnDestroy(): void {
+ this.login$?.unsubscribe();
+ }
+
+ onSubmit() {
+ const val = this.loginForm.value;
+
+ if (val.username && val.password) {
+ this.login$ = this.authService.login(val.username, val.password)
+ .pipe(
+ catchError(err => {
+ this.errorMessage = `Error occured! ${err.message}`;
+ return throwError(() => err);
+ })
+ )
+ .subscribe({
+ next: (val: AuthData) => this.router.navigate(['']),
+ error: (val: any) => this.loginError = true
+ })
+ } else {
+ alert('Please provide login and password');
+ }
+ }
+
+}
diff --git a/src/app/login/login-routing.module.ts b/src/app/login/login-routing.module.ts
new file mode 100644
index 0000000..2daf580
--- /dev/null
+++ b/src/app/login/login-routing.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from "@angular/core";
+import { Routes, RouterModule } from "@angular/router";
+import { LoginComponent } from "./components/login/login.component";
+
+const routes: Routes = [
+ { path: 'login', component: LoginComponent, title: 'Login Page' }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class LoginRoutingModule { }
\ No newline at end of file
diff --git a/src/app/login/login.module.ts b/src/app/login/login.module.ts
new file mode 100644
index 0000000..c6dc8a1
--- /dev/null
+++ b/src/app/login/login.module.ts
@@ -0,0 +1,20 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { ReactiveFormsModule } from "@angular/forms";
+import { SharedModule } from "../shared/shared.module";
+import { LoginComponent } from "./components/login/login.component";
+import { LoginRoutingModule } from "./login-routing.module";
+import { NgMaterialModule } from "../ng-material/ng-material.module";
+
+@NgModule({
+ declarations: [LoginComponent],
+ imports: [
+ CommonModule,
+ LoginRoutingModule,
+ ReactiveFormsModule,
+ SharedModule,
+ NgMaterialModule
+ ],
+ providers: [],
+})
+export class LoginModule { }
\ No newline at end of file
diff --git a/src/app/login/store/login.actions.ts b/src/app/login/store/login.actions.ts
new file mode 100644
index 0000000..e260aa4
--- /dev/null
+++ b/src/app/login/store/login.actions.ts
@@ -0,0 +1,7 @@
+import { createAction, props } from "@ngrx/store";
+import { AuthData } from "src/app/shared/models/auth.service.model";
+
+export const setAuthDataAction = createAction('[Auth] Set Data',
+ props<{ authData: AuthData}>());
+
+export const removeAuthDataAction = createAction('[Auth] Remove Data');
\ No newline at end of file
diff --git a/src/app/login/store/login.reducer.ts b/src/app/login/store/login.reducer.ts
new file mode 100644
index 0000000..d25e759
--- /dev/null
+++ b/src/app/login/store/login.reducer.ts
@@ -0,0 +1,19 @@
+import { createReducer, on } from "@ngrx/store";
+import { setAuthDataAction, removeAuthDataAction } from "./login.actions";
+import { AuthData } from "src/app/shared/models/auth.service.model";
+
+export const initialState: AuthData = {
+ userId: undefined,
+ jwtBearerToken: undefined,
+ expiresIn: undefined
+}
+
+export const authReducer = createReducer(
+ initialState,
+ on(setAuthDataAction, (state, { authData: authResult }) => {
+ return authResult;
+ }),
+ on(removeAuthDataAction, (state) => {
+ return initialState;
+ })
+);
\ No newline at end of file
diff --git a/src/app/login/store/login.selectors.ts b/src/app/login/store/login.selectors.ts
new file mode 100644
index 0000000..655ceb5
--- /dev/null
+++ b/src/app/login/store/login.selectors.ts
@@ -0,0 +1,19 @@
+import { createFeatureSelector, createSelector } from "@ngrx/store";
+import { AuthData } from "src/app/shared/models/auth.service.model";
+
+export const selectAuthResult = createFeatureSelector('[Auth] Login');
+
+export const selectBearerToken = createSelector(
+ selectAuthResult,
+ authResult => authResult.jwtBearerToken
+);
+
+export const selectExpiration = createSelector(
+ selectAuthResult,
+ authResult => authResult.expiresIn
+)
+
+export const selectUserId = createSelector(
+ selectAuthResult,
+ authResult => authResult.userId
+)
\ No newline at end of file
diff --git a/src/app/ng-material/ng-material.module.ts b/src/app/ng-material/ng-material.module.ts
new file mode 100644
index 0000000..357b0f0
--- /dev/null
+++ b/src/app/ng-material/ng-material.module.ts
@@ -0,0 +1,29 @@
+import { NgModule } from '@angular/core';
+import { MatDialogModule } from '@angular/material/dialog'
+import { MatButtonModule } from '@angular/material/button'
+import { TextFieldModule } from '@angular/cdk/text-field'
+import { MatFormFieldModule } from '@angular/material/form-field'
+import { MatInputModule } from '@angular/material/input'
+import { MatSelectModule } from '@angular/material/select'
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatDatepickerModule} from '@angular/material/datepicker'
+import { MatMomentDateModule } from '@angular/material-moment-adapter';
+
+const modules = [
+ MatDialogModule,
+ MatButtonModule,
+ TextFieldModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ MatCheckboxModule,
+ MatDatepickerModule,
+ MatMomentDateModule
+]
+
+@NgModule({
+ declarations: [],
+ imports: [...modules],
+ exports: [...modules]
+})
+export class NgMaterialModule { }
diff --git a/src/app/shared/components/datepicker/datepicker.component.css b/src/app/shared/components/datepicker/datepicker.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/shared/components/datepicker/datepicker.component.html b/src/app/shared/components/datepicker/datepicker.component.html
new file mode 100644
index 0000000..0ce1451
--- /dev/null
+++ b/src/app/shared/components/datepicker/datepicker.component.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/app/shared/components/datepicker/datepicker.component.spec.ts b/src/app/shared/components/datepicker/datepicker.component.spec.ts
new file mode 100644
index 0000000..bac8817
--- /dev/null
+++ b/src/app/shared/components/datepicker/datepicker.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DatepickerComponent } from './datepicker.component';
+
+describe('DatepickerComponent', () => {
+ let component: DatepickerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ DatepickerComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(DatepickerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/components/datepicker/datepicker.component.ts b/src/app/shared/components/datepicker/datepicker.component.ts
new file mode 100644
index 0000000..cf0f06e
--- /dev/null
+++ b/src/app/shared/components/datepicker/datepicker.component.ts
@@ -0,0 +1,78 @@
+import { Component, OnDestroy, forwardRef } from '@angular/core';
+import { AbstractControl, ControlValueAccessor, FormBuilder, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
+import { Subscription } from 'rxjs';
+import * as moment from 'moment';
+import { validDate } from './datepicker.validators';
+
+@Component({
+ selector: 'app-datepicker',
+ templateUrl: './datepicker.component.html',
+ styleUrls: ['./datepicker.component.css'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DatepickerComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => DatepickerComponent),
+ multi: true
+ }
+ ]
+})
+export class DatepickerComponent implements ControlValueAccessor, Validator, OnDestroy {
+
+ public readonly datepickerForm: FormGroup;
+
+ onChangeSubscriptions: Subscription[] = [];
+
+ constructor(private formBuilder: FormBuilder) {
+ this.datepickerForm = this.formBuilder.group({
+ date: ['', validDate('MM/DD/YYYY')]
+ });
+ }
+
+ ngOnInit(): void {
+ }
+
+ get date() {
+ return this.datepickerForm.get('date');
+ }
+
+ ngOnDestroy(): void {
+ this.onChangeSubscriptions.forEach(sub => sub.unsubscribe());
+ }
+
+ writeValue(date: moment.Moment | string | null): void {
+ date = date === "" ? null : moment();
+ this.datepickerForm.get('date')?.setValue(date);
+ }
+
+ onChange = (date: string | null) => { };
+ registerOnChange(fn: any): void {
+ const subscription = this.datepickerForm.valueChanges.subscribe(fn);
+ this.onChangeSubscriptions.push(subscription);
+ }
+
+ onTouched = () => { };
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState?(isDisabled: boolean): void {
+ isDisabled ? this.datepickerForm.disable() : this.datepickerForm.enable();
+ }
+
+ validate(control: AbstractControl): ValidationErrors | null {
+ return this.datepickerForm.valid ? null : this.getControlErrors();
+ }
+
+ private getControlErrors(): any[] {
+ const errors: any[] = [];
+ Object.keys(this.datepickerForm.controls)
+ .map(key => this.datepickerForm.controls[key])
+ .forEach(control => errors.push(control.errors))
+ return errors;
+ }
+}
diff --git a/src/app/shared/components/datepicker/datepicker.validators.ts b/src/app/shared/components/datepicker/datepicker.validators.ts
new file mode 100644
index 0000000..b0c9573
--- /dev/null
+++ b/src/app/shared/components/datepicker/datepicker.validators.ts
@@ -0,0 +1,17 @@
+import { AbstractControl, ValidationErrors } from "@angular/forms";
+import * as moment from "moment";
+
+export function validDate(dateFormat: string) {
+ return (control: AbstractControl): ValidationErrors | null => {
+
+ const value = control.value;
+
+ const valid = moment(value, dateFormat, true).isValid();
+
+ if (!valid) {
+ return { dateMustBeValid: { date: value, requiredFormat: dateFormat } };
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/app/shared/components/error-box/error-box.component.css b/src/app/shared/components/error-box/error-box.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/shared/components/error-box/error-box.component.html b/src/app/shared/components/error-box/error-box.component.html
new file mode 100644
index 0000000..48406a0
--- /dev/null
+++ b/src/app/shared/components/error-box/error-box.component.html
@@ -0,0 +1,3 @@
+
+ {{ errorMessage }}
+
\ No newline at end of file
diff --git a/src/app/shared/components/error-box/error-box.component.spec.ts b/src/app/shared/components/error-box/error-box.component.spec.ts
new file mode 100644
index 0000000..638a608
--- /dev/null
+++ b/src/app/shared/components/error-box/error-box.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ErrorBoxComponent } from './error-box.component';
+
+describe('ErrorBoxComponent', () => {
+ let component: ErrorBoxComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ErrorBoxComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ErrorBoxComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/components/error-box/error-box.component.ts b/src/app/shared/components/error-box/error-box.component.ts
new file mode 100644
index 0000000..cd29857
--- /dev/null
+++ b/src/app/shared/components/error-box/error-box.component.ts
@@ -0,0 +1,18 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-error-box',
+ templateUrl: './error-box.component.html',
+ styleUrls: ['./error-box.component.css']
+})
+export class ErrorBoxComponent implements OnInit {
+
+ @Input()
+ errorMessage?: string;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/shared/mappers/mapper.ts b/src/app/shared/mappers/mapper.ts
new file mode 100644
index 0000000..0155102
--- /dev/null
+++ b/src/app/shared/mappers/mapper.ts
@@ -0,0 +1,4 @@
+export abstract class Mapper {
+ public abstract toClient(value: any): any;
+ public abstract toModel(value: any): any;
+}
\ No newline at end of file
diff --git a/src/app/shared/mappers/yes-no.mapper.ts b/src/app/shared/mappers/yes-no.mapper.ts
new file mode 100644
index 0000000..848248c
--- /dev/null
+++ b/src/app/shared/mappers/yes-no.mapper.ts
@@ -0,0 +1,27 @@
+import { Mapper } from "./mapper";
+
+export class YesNoToBooleanMapper extends Mapper {
+ public toClient(yesNo: string): boolean {
+ switch(yesNo) {
+ case 'Yes':
+ case 'yes':
+ return true;
+ case 'No':
+ case 'no':
+ return false;
+ default:
+ throw new Error(`Unknown value = ${yesNo}`);
+ }
+ }
+ public toModel(booleanValue: boolean): string {
+ switch(booleanValue) {
+ case true:
+ return 'Yes';
+ case false:
+ return 'No';
+ default:
+ throw new Error(`Unknown value = ${booleanValue}`);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/shared/models/auth.service.model.ts b/src/app/shared/models/auth.service.model.ts
new file mode 100644
index 0000000..d1bebce
--- /dev/null
+++ b/src/app/shared/models/auth.service.model.ts
@@ -0,0 +1,5 @@
+export interface AuthData {
+ userId?: string;
+ jwtBearerToken?: string;
+ expiresIn?: number
+}
\ No newline at end of file
diff --git a/src/app/shared/pipes/yes-no.pipe.spec.ts b/src/app/shared/pipes/yes-no.pipe.spec.ts
new file mode 100644
index 0000000..f34ec96
--- /dev/null
+++ b/src/app/shared/pipes/yes-no.pipe.spec.ts
@@ -0,0 +1,9 @@
+import { YesNoToBooleanMapper } from '../mappers/yes-no.mapper';
+import { YesNoPipe } from './yes-no.pipe';
+
+describe('YesNoPipe', () => {
+ it('create an instance', () => {
+ const pipe = new YesNoPipe(new YesNoToBooleanMapper());
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/pipes/yes-no.pipe.ts b/src/app/shared/pipes/yes-no.pipe.ts
new file mode 100644
index 0000000..896ea99
--- /dev/null
+++ b/src/app/shared/pipes/yes-no.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { YesNoToBooleanMapper } from '../mappers/yes-no.mapper';
+
+@Pipe({
+ name: 'yesNo'
+})
+export class YesNoPipe implements PipeTransform {
+
+ constructor(private readonly yesNoToBooleanMapper: YesNoToBooleanMapper) {
+
+ }
+
+ transform(value: boolean): string {
+ return this.yesNoToBooleanMapper.toModel(value);
+ }
+
+}
diff --git a/src/app/shared/services/auth/auth.service.spec.ts b/src/app/shared/services/auth/auth.service.spec.ts
new file mode 100644
index 0000000..f1251ca
--- /dev/null
+++ b/src/app/shared/services/auth/auth.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AuthService } from './auth.service';
+
+describe('AuthService', () => {
+ let service: AuthService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(AuthService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts
new file mode 100644
index 0000000..cce981e
--- /dev/null
+++ b/src/app/shared/services/auth/auth.service.ts
@@ -0,0 +1,76 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable, OnDestroy } from '@angular/core';
+import * as moment from 'moment';
+import { map, Observable, shareReplay, Subscription } from 'rxjs';
+import { AuthData } from '../../models/auth.service.model';
+import { Store } from '@ngrx/store';
+import { setAuthDataAction, removeAuthDataAction } from 'src/app/login/store/login.actions';
+import { selectExpiration } from 'src/app/login/store/login.selectors';
+import { ConfigService } from 'src/app/core/services/config.service';
+
+@Injectable()
+export class AuthService implements OnDestroy {
+
+ private readonly apiBaseUrl;
+ private expiration$?: Subscription;
+
+ constructor(
+ private http: HttpClient,
+ private store: Store,
+ private configService: ConfigService
+ ) {
+ this.apiBaseUrl = this.configService.getApiBaseUrl();
+ }
+
+ login(username: string, password: string): Observable {
+ return this.http.post(`${this.apiBaseUrl}/login`, { username, password }).pipe(
+ map(res => this.setSession(res)),
+ shareReplay()
+ );
+ }
+
+ private setSession(authResult: AuthData): AuthData {
+ const expiresIn = moment().add(authResult.expiresIn, 'second');
+
+ this.store.dispatch(setAuthDataAction({
+ authData: {
+ userId: authResult.userId,
+ jwtBearerToken: authResult.jwtBearerToken,
+ expiresIn: +JSON.stringify(expiresIn.valueOf())
+ }
+ }));
+
+ return authResult;
+ }
+
+ logout(): void {
+ this.store.dispatch(removeAuthDataAction());
+ }
+
+ public isLoggedIn(): boolean {
+ const expiration = this.getExpiration();
+ return !!expiration && moment().isBefore(expiration);
+ }
+
+ isLoggedOut() {
+ return !this.isLoggedIn();
+ }
+
+ getExpiration(): moment.Moment | null {
+ let expiration;
+
+ this.expiration$ = this.store.select(selectExpiration)
+ .subscribe(value => expiration = value);
+
+ if (!expiration) {
+ return null;
+ }
+
+ const expiresIn = JSON.parse(expiration);
+ return moment(expiresIn);
+ }
+
+ ngOnDestroy(): void {
+ this.expiration$?.unsubscribe();
+ }
+}
diff --git a/src/app/shared/services/guards/auth-guard.service.spec.ts b/src/app/shared/services/guards/auth-guard.service.spec.ts
new file mode 100644
index 0000000..35afd37
--- /dev/null
+++ b/src/app/shared/services/guards/auth-guard.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AuthGuardService } from './auth-guard.service';
+
+describe('AuthGuardService', () => {
+ let service: AuthGuardService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(AuthGuardService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/services/guards/auth-guard.service.ts b/src/app/shared/services/guards/auth-guard.service.ts
new file mode 100644
index 0000000..bae8d24
--- /dev/null
+++ b/src/app/shared/services/guards/auth-guard.service.ts
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
+import { Observable } from 'rxjs';
+import { AuthService } from '../auth/auth.service';
+
+@Injectable()
+export class AuthGuardService implements CanActivate, CanActivateChild {
+
+ constructor(private authService: AuthService, private router: Router) { }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
+ if (!this.authService.isLoggedIn()) {
+ alert('You are not logged in');
+ this.router.navigate(['login']);
+ return false;
+ }
+
+ return true;
+ }
+
+ canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
+ return this.canActivate(childRoute, state);
+ }
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
new file mode 100644
index 0000000..0250fca
--- /dev/null
+++ b/src/app/shared/shared.module.ts
@@ -0,0 +1,37 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { YesNoToBooleanMapper } from "./mappers/yes-no.mapper";
+import { AuthService } from "./services/auth/auth.service";
+import { AuthGuardService } from "./services/guards/auth-guard.service";
+import { YesNoPipe } from './pipes/yes-no.pipe';
+import { ErrorBoxComponent } from './components/error-box/error-box.component';
+import { DatepickerComponent } from './components/datepicker/datepicker.component';
+import { NgMaterialModule } from "../ng-material/ng-material.module";
+import { ReactiveFormsModule } from "@angular/forms";
+
+@NgModule({
+ declarations: [
+ YesNoPipe,
+ ErrorBoxComponent,
+ DatepickerComponent
+ ],
+ imports: [
+ CommonModule,
+ NgMaterialModule,
+ ReactiveFormsModule
+ ],
+ providers: [
+ AuthService,
+ AuthGuardService,
+ {
+ provide: YesNoToBooleanMapper,
+ useClass: YesNoToBooleanMapper
+ }
+ ],
+ exports: [
+ YesNoPipe,
+ ErrorBoxComponent,
+ DatepickerComponent
+ ]
+})
+export class SharedModule { }
\ No newline at end of file
diff --git a/src/assets/config/development.json b/src/assets/config/development.json
new file mode 100644
index 0000000..a525d38
--- /dev/null
+++ b/src/assets/config/development.json
@@ -0,0 +1,4 @@
+{
+ "debugging": true,
+ "apiBaseUrl": "http://localhost:3000"
+}
\ No newline at end of file
diff --git a/src/assets/config/production.json b/src/assets/config/production.json
new file mode 100644
index 0000000..b650291
--- /dev/null
+++ b/src/assets/config/production.json
@@ -0,0 +1,4 @@
+{
+ "debugging": false,
+ "apiBaseUrl": "https://spec-lead-notes-be.onrender.com"
+}
\ No newline at end of file
diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts
new file mode 100644
index 0000000..ffe8aed
--- /dev/null
+++ b/src/environments/environment.test.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: false
+};
diff --git a/src/index.html b/src/index.html
index cee43a4..a4482c6 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6,8 +6,12 @@
+
+
+
+
-
+
diff --git a/src/proxy.conf.json b/src/proxy.conf.json
new file mode 100644
index 0000000..9b04b36
--- /dev/null
+++ b/src/proxy.conf.json
@@ -0,0 +1,6 @@
+{
+ "/api/*": {
+ "target": "http://localhost:3000",
+ "secure": false
+ }
+}
\ No newline at end of file
diff --git a/src/styles.css b/src/styles.css
index 90d4ee0..7e7239a 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1 +1,4 @@
/* You can add global styles to this file, and also import other style files */
+
+html, body { height: 100%; }
+body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
diff --git a/tsconfig.json b/tsconfig.json
index ff06eae..87716a3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
- "outDir": "./dist/out-tsc",
+ "outDir": "public/dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,