diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 00000000..88151e29
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,33 @@
+module.exports = {
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "overrides": [
+ {
+ "env": {
+ "node": true
+ },
+ "files": [
+ ".eslintrc.{js,cjs}"
+ ],
+ "parserOptions": {
+ "sourceType": "script"
+ }
+ }
+ ],
+ "parser": "@angular-eslint/template-parser",
+ "parserOptions": {
+ "ecmaVersion": "latest"
+ },
+ "plugins": [
+ "@typescript-eslint",
+ "@angular-eslint/template"
+ ],
+ "rules": {
+ }
+}
diff --git a/angular.json b/angular.json
index 7da714c7..91b40fea 100644
--- a/angular.json
+++ b/angular.json
@@ -62,6 +62,27 @@
],
"outputHashing": "all"
},
+ "staging": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "1mb",
+ "maximumError": "2mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.staging.ts"
+ }
+ ],
+ "outputHashing": "all"
+ },
"development": {
"buildOptimizer": false,
"optimization": false,
@@ -79,6 +100,9 @@
"production": {
"buildTarget": "petrel.interviewer:build:production"
},
+ "staging": {
+ "buildTarget": "petrel.interviewer:build:staging"
+ },
"development": {
"buildTarget": "petrel.interviewer:build:development"
}
diff --git a/package-lock.json b/package-lock.json
index 43deb365..87c5fd39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,8 +47,8 @@
"@angular/compiler-cli": "~17.0.8",
"@types/jasmine": "~5.1.4",
"@types/node": "^20.10.5",
- "@typescript-eslint/eslint-plugin": "6.16.0",
- "@typescript-eslint/parser": "6.16.0",
+ "@typescript-eslint/eslint-plugin": "^6.18.1",
+ "@typescript-eslint/parser": "^6.18.1",
"eslint": "^8.56.0",
"jasmine-core": "~5.1.1",
"karma": "~6.4.2",
@@ -3913,16 +3913,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz",
- "integrity": "sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz",
+ "integrity": "sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "6.16.0",
- "@typescript-eslint/type-utils": "6.16.0",
- "@typescript-eslint/utils": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0",
+ "@typescript-eslint/scope-manager": "6.18.1",
+ "@typescript-eslint/type-utils": "6.18.1",
+ "@typescript-eslint/utils": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -3948,13 +3948,13 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz",
- "integrity": "sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz",
+ "integrity": "sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "6.16.0",
- "@typescript-eslint/utils": "6.16.0",
+ "@typescript-eslint/typescript-estree": "6.18.1",
+ "@typescript-eslint/utils": "6.18.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -3975,17 +3975,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.16.0.tgz",
- "integrity": "sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.1.tgz",
+ "integrity": "sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.16.0",
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/typescript-estree": "6.16.0",
+ "@typescript-eslint/scope-manager": "6.18.1",
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/typescript-estree": "6.18.1",
"semver": "^7.5.4"
},
"engines": {
@@ -4000,15 +4000,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz",
- "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz",
+ "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "6.16.0",
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/typescript-estree": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0",
+ "@typescript-eslint/scope-manager": "6.18.1",
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/typescript-estree": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1",
"debug": "^4.3.4"
},
"engines": {
@@ -4028,13 +4028,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
- "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz",
+ "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0"
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -4129,9 +4129,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz",
- "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz",
+ "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -4142,13 +4142,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
- "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz",
+ "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0",
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -4293,12 +4293,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
- "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz",
+ "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.16.0",
+ "@typescript-eslint/types": "6.18.1",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -19297,16 +19297,16 @@
}
},
"@typescript-eslint/eslint-plugin": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz",
- "integrity": "sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz",
+ "integrity": "sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "6.16.0",
- "@typescript-eslint/type-utils": "6.16.0",
- "@typescript-eslint/utils": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0",
+ "@typescript-eslint/scope-manager": "6.18.1",
+ "@typescript-eslint/type-utils": "6.18.1",
+ "@typescript-eslint/utils": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -19316,55 +19316,55 @@
},
"dependencies": {
"@typescript-eslint/type-utils": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz",
- "integrity": "sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz",
+ "integrity": "sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==",
"dev": true,
"requires": {
- "@typescript-eslint/typescript-estree": "6.16.0",
- "@typescript-eslint/utils": "6.16.0",
+ "@typescript-eslint/typescript-estree": "6.18.1",
+ "@typescript-eslint/utils": "6.18.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
}
},
"@typescript-eslint/utils": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.16.0.tgz",
- "integrity": "sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.1.tgz",
+ "integrity": "sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.16.0",
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/typescript-estree": "6.16.0",
+ "@typescript-eslint/scope-manager": "6.18.1",
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/typescript-estree": "6.18.1",
"semver": "^7.5.4"
}
}
}
},
"@typescript-eslint/parser": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz",
- "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz",
+ "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "6.16.0",
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/typescript-estree": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0",
+ "@typescript-eslint/scope-manager": "6.18.1",
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/typescript-estree": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
- "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz",
+ "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0"
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1"
}
},
"@typescript-eslint/type-utils": {
@@ -19413,19 +19413,19 @@
}
},
"@typescript-eslint/types": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz",
- "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz",
+ "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
- "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz",
+ "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "6.16.0",
- "@typescript-eslint/visitor-keys": "6.16.0",
+ "@typescript-eslint/types": "6.18.1",
+ "@typescript-eslint/visitor-keys": "6.18.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -19513,12 +19513,12 @@
}
},
"@typescript-eslint/visitor-keys": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
- "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
+ "version": "6.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz",
+ "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "6.16.0",
+ "@typescript-eslint/types": "6.18.1",
"eslint-visitor-keys": "^3.4.1"
}
},
diff --git a/package.json b/package.json
index cc77d29d..01420e14 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"start": "ng serve",
"build": "ng build",
"build-prod": "ng build --configuration=production",
+ "serve-staging": "ng serve --configuration=staging",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"test-headless-ci-only": "ng test --browsers ChromiumNoSandbox",
@@ -53,8 +54,8 @@
"@angular/compiler-cli": "~17.0.8",
"@types/jasmine": "~5.1.4",
"@types/node": "^20.10.5",
- "@typescript-eslint/eslint-plugin": "6.16.0",
- "@typescript-eslint/parser": "6.16.0",
+ "@typescript-eslint/eslint-plugin": "^6.18.1",
+ "@typescript-eslint/parser": "^6.18.1",
"eslint": "^8.56.0",
"jasmine-core": "~5.1.1",
"karma": "~6.4.2",
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 51b7a722..f623f129 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -34,6 +34,10 @@ const routes: Routes = [
path: 'candidate-cards',
loadChildren: () => import('./modules/candidate-cards/candidate-cards.module').then((m) => m.CandidateCardsModule)
},
+ {
+ path: 'salaries',
+ loadChildren: () => import('./modules/salaries/salaries.module').then((m) => m.SalariesModule)
+ },
// Fallback when no prior route is matched
{ path: '**', redirectTo: 'not-found', pathMatch: 'full' }
diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts
index 3785e15f..3964f55b 100644
--- a/src/app/components/navbar/navbar.component.ts
+++ b/src/app/components/navbar/navbar.component.ts
@@ -125,6 +125,17 @@ export class NavbarComponent implements OnInit, OnDestroy {
show: true
}
]
+ },
+ {
+ title: 'Salaries',
+ show: true,
+ links: [
+ {
+ title: 'Salaries chart',
+ url: '/salaries',
+ show: true
+ },
+ ]
}
];
diff --git a/src/app/models/salaries/company-type.ts b/src/app/models/salaries/company-type.ts
new file mode 100644
index 00000000..dc83e336
--- /dev/null
+++ b/src/app/models/salaries/company-type.ts
@@ -0,0 +1,5 @@
+export enum CompanyType {
+ Undefined = 0,
+ Local = 1,
+ Remote = 2,
+}
diff --git a/src/app/models/salaries/currency.ts b/src/app/models/salaries/currency.ts
new file mode 100644
index 00000000..6fe3a53c
--- /dev/null
+++ b/src/app/models/salaries/currency.ts
@@ -0,0 +1,4 @@
+export enum Currency {
+ Undefined = 0,
+ KZT = 1,
+}
diff --git a/src/app/models/salaries/salary.model.ts b/src/app/models/salaries/salary.model.ts
new file mode 100644
index 00000000..326dd5c6
--- /dev/null
+++ b/src/app/models/salaries/salary.model.ts
@@ -0,0 +1,15 @@
+import { DeveloperGrade } from "@models/enums";
+import { CompanyType } from "./company-type";
+import { Currency } from "./currency";
+import { UserProfession } from "./user-profession";
+
+export interface UserSalary {
+ value: number;
+ quarter: number;
+ year: number;
+ currency: Currency;
+ company: CompanyType;
+ grade: DeveloperGrade | null;
+ profession: UserProfession;
+ createdAt: Date;
+}
diff --git a/src/app/models/salaries/user-profession.ts b/src/app/models/salaries/user-profession.ts
new file mode 100644
index 00000000..d1be8d4e
--- /dev/null
+++ b/src/app/models/salaries/user-profession.ts
@@ -0,0 +1,37 @@
+export enum UserProfession {
+ Undefined = 0,
+
+ Developer = 1,
+
+ QualityAssurance = 2,
+
+ Tester = 3,
+
+ BusinessAnalyst = 4,
+
+ ProjectManager = 5,
+
+ ScrumMaster = 6,
+
+ DevOps = 7,
+
+ SystemAdministrator = 8,
+
+ ProductOwner = 9,
+
+ TeamLeader = 10,
+
+ Architect = 11,
+
+ DataScientist = 12,
+
+ DataAnalyst = 13,
+
+ DataEngineer = 14,
+
+ DataWarehouseSpecialist = 15,
+
+ DatabaseAdministrator = 16,
+
+ TechLeader = 17,
+}
diff --git a/src/app/modules/candidate-cards-shared/card-cv-files-list/card-cv-files-list.component.html b/src/app/modules/candidate-cards-shared/card-cv-files-list/card-cv-files-list.component.html
index cd65624f..0035c607 100644
--- a/src/app/modules/candidate-cards-shared/card-cv-files-list/card-cv-files-list.component.html
+++ b/src/app/modules/candidate-cards-shared/card-cv-files-list/card-cv-files-list.component.html
@@ -21,7 +21,7 @@
- 2022-10-02
+ 2024-10-02
diff --git a/src/app/modules/home/components/auth-callback/auth-callback.component.ts b/src/app/modules/home/components/auth-callback/auth-callback.component.ts
index 2488afb7..f5222b2f 100644
--- a/src/app/modules/home/components/auth-callback/auth-callback.component.ts
+++ b/src/app/modules/home/components/auth-callback/auth-callback.component.ts
@@ -34,6 +34,7 @@ export class AuthCallbackComponent implements OnInit {
this.cookieService.delete('url');
this.router.navigate([url]);
} else {
+ console.log('Redirecting to /me');
this.router.navigate([this.urlToRedirectAfterLogin]);
}
});
diff --git a/src/app/modules/home/components/privacy-policy-page/privacy-policy-page.component.html b/src/app/modules/home/components/privacy-policy-page/privacy-policy-page.component.html
index e0191c50..48ab8cd4 100644
--- a/src/app/modules/home/components/privacy-policy-page/privacy-policy-page.component.html
+++ b/src/app/modules/home/components/privacy-policy-page/privacy-policy-page.component.html
@@ -25,7 +25,7 @@
referenced, are strictly defined as:
-
- Cookie: small amount of data generated by a website and saved by your web browser. It is used to
+ Cookie: small amount of data generated by a website and saved by your web browser. It is used to
identify your browser, provide analytics, remember information about you such as your language
preference or login information.
@@ -38,11 +38,11 @@
Kazakhstan
-
- Customer: refers to the company, organization or person that signs up to use the Tech.Interview Service
+ Customer: refers to the company, organization or person that signs up to use the Tech.Interview Service
to manage the relationships with your consumers or service users.
-
- Device: any internet connected device such as a phone, tablet, computer or any other device that can be
+ Device: any internet connected device such as a phone, tablet, computer or any other device that can be
used to visit Tech.Interview and use the services.
-
@@ -51,7 +51,7 @@
to identify the location from which a device is connecting to the Internet.
-
- Personnel: refers to those individuals who are employed by Tech.Interview or are under contract to
+ Personnel: refers to those individuals who are employed by Tech.Interview or are under contract to
perform a service on behalf of one of the parties.
-
@@ -64,7 +64,7 @@
available) and on this platform.
-
- Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and
+ Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and
others who provide our content or whose products or services we think may interest you.
- Website: Tech.Interview's site, which can be accessed via this URL: interviewer.petrelai.kz
@@ -157,7 +157,7 @@
shared, it may be used to estimate general location and other technographics such as connection speed,
whether you have visited the website in a shared location, and type of the device used to visit the website.
They may aggregate information about our advertising and what you see on the website and then provide
- auditing, research and reporting for us and our advertisers. We may also disclose personal and non-personal
+ auditing, research and reporting for us and our advertisers. We may also disclose personal and non-personal
information about you to government or law enforcement officials or private parties as we, in our sole
discretion, believe necessary or appropriate in order to respond to claims, legal process (including
subpoenas), to protect our rights and interests or those of a third party, the safety of the public or any
@@ -252,10 +252,10 @@
Customers have the right to request the restriction of certain uses and disclosures of personally
- identifiable information as follows. You can contact us in order to (1) update or correct your personally
+ identifiable information as follows. You can contact us in order to (1) update or correct your personally
identifiable information, (2) change your preferences with respect to communications and other information
you receive from us, or (3) delete the personally identifiable information maintained about you on our
- systems (subject to the following paragraph), by cancelling your account. Such updates, corrections, changes
+ systems (subject to the following paragraph), by cancelling your account. Such updates, corrections, changes
and deletions will have no effect on other information that we maintain, or information that we have
provided to third parties in accordance with this Privacy Policy prior to such update, correction, change or
deletion. To protect your privacy and security, we may take reasonable steps (such as requesting a unique
@@ -340,7 +340,7 @@
Your Consent
- We've updated our Privacy Policy to provide you with complete transparency into what is being set when you
+ We've updated our Privacy Policy to provide you with complete transparency into what is being set when you
visit our site and how it's being used. By using our website, registering an account, or making a purchase,
you hereby consent to our Privacy Policy and agree to its terms.
@@ -454,7 +454,7 @@
What is GDPR?
GDPR is an EU-wide privacy and data protection law that regulates how EU residents' data is protected by
- companies and enhances the control the EU residents have, over their personal data.
+ companies and enhances the control the EU residents have, over their personal data.
The GDPR is relevant to any globally operating company and not just the EU-based businesses and EU
@@ -504,7 +504,7 @@
Individual Data Subject's Rights - Data Access, Portability and Deletion
- We are committed to helping our customers meet the data subject rights requirements of GDPR. Tech.Interview
+ We are committed to helping our customers meet the data subject rights requirements of GDPR. Tech.Interview
processes or stores all personal data in fully vetted, DPA compliant vendors. We do store all conversation
and personal data for up to 6 years unless your account is deleted. In which case, we dispose of all data in
accordance with our Terms of Service and Privacy Policy, but we will not hold it longer than 60 days.
@@ -512,7 +512,7 @@
We are aware that if you are working with EU customers, you need to be able to provide them with the ability
to access, update, retrieve and remove personal data. We got you! We've been set up as self service from the
- start and have always given you access to your data and your customers data. Our customer support team is
+ start and have always given you access to your data and your customers data. Our customer support team is
here for you to answer any questions you might have about working with the API.
diff --git a/src/app/modules/salaries/components/add-salary/add-salary-form.ts b/src/app/modules/salaries/components/add-salary/add-salary-form.ts
new file mode 100644
index 00000000..e1e02571
--- /dev/null
+++ b/src/app/modules/salaries/components/add-salary/add-salary-form.ts
@@ -0,0 +1,67 @@
+import { FormControl, FormGroup, Validators } from "@angular/forms";
+import { DeveloperGrade } from "@models/enums";
+import { CompanyType } from "@models/salaries/company-type";
+import { Currency } from "@models/salaries/currency";
+import { UserProfession } from "@models/salaries/user-profession";
+import { CreateUserSalaryRequest } from "@services/user-salaries.service";
+
+export class AddSalaryForm extends FormGroup {
+
+static readonly digitsPattern = '^[0-9]*$';
+
+ constructor() {
+ const now = new Date(Date.now());
+ const currentQuarter = Math.floor((now.getMonth() + 3) / 3);
+
+ super({
+ value: new FormControl(
+ null,
+ [
+ Validators.pattern(AddSalaryForm.digitsPattern),
+ Validators.required
+ ]),
+ quarter: new FormControl(
+ currentQuarter,
+ [
+ Validators.pattern(AddSalaryForm.digitsPattern),
+ Validators.min(1),
+ Validators.max(4),
+ Validators.required
+ ]),
+ year: new FormControl(
+ now.getFullYear(),
+ [
+ Validators.pattern(AddSalaryForm.digitsPattern),
+ Validators.min(2000),
+ Validators.max(2100),
+ Validators.required
+ ]),
+ currency: new FormControl(Currency.KZT, [Validators.required]),
+ company: new FormControl(null, [Validators.required]),
+ grade: new FormControl(null, []),
+ profession: new FormControl(null, [Validators.required]),
+ });
+ }
+
+ createRequestOrNull(): CreateUserSalaryRequest | null {
+ if (this.valid) {
+ const profession = Number(this.value.profession) as UserProfession;
+ const grade = this.value.grade != null
+ ? Number(this.value.grade) as DeveloperGrade
+ : null;
+
+ return {
+ value: Number(this.value.value),
+ quarter: Number(this.value.quarter),
+ year: Number(this.value.year),
+ currency: Number(this.value.currency) as Currency,
+ company: Number(this.value.company) as CompanyType,
+ grade: grade,
+ profession: profession,
+ };
+ }
+
+ this.markAllAsTouched();
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/app/modules/salaries/components/add-salary/add-salary.component.html b/src/app/modules/salaries/components/add-salary/add-salary.component.html
new file mode 100644
index 00000000..88b7d84f
--- /dev/null
+++ b/src/app/modules/salaries/components/add-salary/add-salary.component.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+ Для того, чтобы просматривать график зарплат, вам нужно сначала внести свою. График открывается, только если среди данных есть ваша зарплата за последний год.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/modules/salaries/components/add-salary/add-salary.component.scss b/src/app/modules/salaries/components/add-salary/add-salary.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/modules/salaries/components/add-salary/add-salary.component.spec.ts b/src/app/modules/salaries/components/add-salary/add-salary.component.spec.ts
new file mode 100644
index 00000000..035696de
--- /dev/null
+++ b/src/app/modules/salaries/components/add-salary/add-salary.component.spec.ts
@@ -0,0 +1,28 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AddSalaryComponent } from './add-salary.component';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { mostUsedImports, testUtilStubs, mostUsedServices } from '@shared/test-utils';
+
+describe('AddSalaryComponent', () => {
+ let component: AddSalaryComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [AddSalaryComponent],
+ imports: [...mostUsedImports],
+ providers: [...testUtilStubs, ...mostUsedServices],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AddSalaryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/modules/salaries/components/add-salary/add-salary.component.ts b/src/app/modules/salaries/components/add-salary/add-salary.component.ts
new file mode 100644
index 00000000..24b3073d
--- /dev/null
+++ b/src/app/modules/salaries/components/add-salary/add-salary.component.ts
@@ -0,0 +1,57 @@
+import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
+import { UserSalariesService } from '@services/user-salaries.service';
+import { AddSalaryForm } from './add-salary-form';
+import { untilDestroyed } from '@shared/subscriptions/until-destroyed';
+import { CompanyTypeSelectItem } from '@shared/select-boxes/company-type-select-item';
+import { DeveloperGradeSelectItem } from '@shared/select-boxes/developer-grade-select-item';
+import { ProfessionSelectItem } from '@shared/select-boxes/profession-select-item';
+import { UserSalary } from '@models/salaries/salary.model';
+
+@Component({
+ selector: 'app-add-salary-modal',
+ templateUrl: './add-salary.component.html',
+ styleUrl: './add-salary.component.scss'
+})
+export class AddSalaryComponent implements OnInit, OnDestroy {
+
+ @Output()
+ closed: EventEmitter = new EventEmitter();
+
+ @Output()
+ salaryAdded: EventEmitter = new EventEmitter();
+
+ addSalaryForm: AddSalaryForm | null = null;
+
+ readonly companyTypes: Array = CompanyTypeSelectItem.allItems();
+ readonly grades: Array = DeveloperGradeSelectItem.allGrades();
+ readonly professions: Array = ProfessionSelectItem.allItems();
+
+ constructor(
+ private readonly service: UserSalariesService) {}
+
+ ngOnInit(): void {
+ this.addSalaryForm = new AddSalaryForm();
+ }
+
+ addSalarySubmitAction(): void {
+ const data = this.addSalaryForm?.createRequestOrNull();
+ if (data == null) {
+ return;
+ }
+
+ this.service
+ .create(data)
+ .pipe(untilDestroyed(this))
+ .subscribe((x) => {
+ this.salaryAdded.emit(x);
+ });
+ }
+
+ ngOnDestroy(): void {
+ // ignored
+ }
+
+ close(): void {
+ this.closed.emit();
+ }
+}
diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html
new file mode 100644
index 00000000..ad95f4bb
--- /dev/null
+++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html
@@ -0,0 +1,66 @@
+Зарплаты в Казахстане
+
+
+
+
+
+ На графике можно увидеть зарплаты в Казахстане по разным специальностям IT. Данные заполняются самими пользователями.
+
+
+
+
+
+
+
+
Медианная зарплата:
+
+ {{ salariesChart.medianSalary }}
+ тенге.
+
+
+
+
+
Средняя зарплата:
+
+ {{ salariesChart.averageSalary }}
+ тенге.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.scss b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.spec.ts b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.spec.ts
new file mode 100644
index 00000000..64cb2167
--- /dev/null
+++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.spec.ts
@@ -0,0 +1,28 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SalariesChartComponent } from './salaries-chart.component';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { mostUsedImports, testUtilStubs, mostUsedServices } from '@shared/test-utils';
+
+describe('SalariesChartComponent', () => {
+ let component: SalariesChartComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [SalariesChartComponent],
+ imports: [...mostUsedImports],
+ providers: [...testUtilStubs, ...mostUsedServices],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(SalariesChartComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts
new file mode 100644
index 00000000..04acbcb5
--- /dev/null
+++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts
@@ -0,0 +1,68 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { UserSalary } from '@models/salaries/salary.model';
+import { TitleService } from '@services/title.service';
+import { UserSalariesService } from '@services/user-salaries.service';
+import { untilDestroyed } from '@shared/subscriptions/until-destroyed';
+import { SalariesChart } from './salaries-chart';
+import { AuthService } from '@shared/services/auth/auth.service';
+import { CookieService } from 'ngx-cookie-service';
+
+@Component({
+ templateUrl: './salaries-chart.component.html',
+ styleUrl: './salaries-chart.component.scss'
+})
+export class SalariesChartComponent implements OnInit, OnDestroy {
+
+ salariesChart: SalariesChart | null = null;
+
+ openAddSalaryModal = false;
+
+ constructor(
+ private readonly service: UserSalariesService,
+ title: TitleService,
+ private readonly router: Router,
+ private readonly authService: AuthService,
+ private readonly cookieService: CookieService) {
+ title.setTitle('Salaries');
+ }
+
+ ngOnInit(): void {
+ if (this.authService.isAuthenticated()) {
+ this.load();
+ return;
+ }
+
+ console.log('Saving url to cookie', this.router.url);
+ this.cookieService.set('url', this.router.url);
+ this.authService.login();
+ }
+
+ load(): void {
+ this.service.charts()
+ .pipe(untilDestroyed(this))
+ .subscribe((x) => {
+ this.salariesChart = new SalariesChart(x);
+ if (x.shouldAddOwnSalary) {
+ this.openAddSalaryModal = true;
+ }
+ });
+ }
+
+ openAddSalaryAction(): void {
+ this.openAddSalaryModal = true;
+ }
+
+ closeAddSalaryAction(): void {
+ this.openAddSalaryModal = false;
+ }
+
+ onSalaryAdded(salary: UserSalary): void {
+ this.openAddSalaryModal = false;
+ this.load();
+ }
+
+ ngOnDestroy(): void {
+ // ignored
+ }
+}
diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts b/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts
new file mode 100644
index 00000000..e32184ed
--- /dev/null
+++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts
@@ -0,0 +1,19 @@
+import { formatNumber } from "@angular/common";
+import { UserSalary } from "@models/salaries/salary.model";
+import { UserProfession } from "@models/salaries/user-profession";
+import { SalariesByProfession, SalariesChartResponse } from "@services/user-salaries.service";
+
+export class SalariesChart {
+
+ averageSalary: string;
+ medianSalary: string;
+ countOfRecords: number;
+ salariesByProfession: Array;
+
+ constructor(private readonly data: SalariesChartResponse) {
+ this.averageSalary = formatNumber(data.averageSalary, 'en-US', '1.0-2');
+ this.medianSalary = formatNumber(data.medianSalary, 'en-US', '1.0-2');
+ this.countOfRecords = data.salaries.length;
+ this.salariesByProfession = data.salariesByProfession;
+ }
+}
diff --git a/src/app/modules/salaries/salaries-routing.module.ts b/src/app/modules/salaries/salaries-routing.module.ts
new file mode 100644
index 00000000..978d3db7
--- /dev/null
+++ b/src/app/modules/salaries/salaries-routing.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { SalariesChartComponent } from './components/salaries-chart/salaries-chart.component';
+
+const routes: Routes = [
+ { path: '', component: SalariesChartComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class SalariesRoutingModule {}
diff --git a/src/app/modules/salaries/salaries.module.ts b/src/app/modules/salaries/salaries.module.ts
new file mode 100644
index 00000000..a3abcf3e
--- /dev/null
+++ b/src/app/modules/salaries/salaries.module.ts
@@ -0,0 +1,24 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SalariesRoutingModule } from './salaries-routing.module';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { NgSelectModule } from '@ng-select/ng-select';
+import { SharedModule } from '@shared/shared.module';
+import { SalariesChartComponent } from './components/salaries-chart/salaries-chart.component';
+import { AddSalaryComponent } from './components/add-salary/add-salary.component';
+
+@NgModule({
+ declarations: [
+ SalariesChartComponent,
+ AddSalaryComponent,
+ ],
+ imports: [
+ CommonModule,
+ SalariesRoutingModule,
+ SharedModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule
+ ]
+})
+export class SalariesModule { }
diff --git a/src/app/services/index.ts b/src/app/services/index.ts
index 900daf4e..3ef09487 100644
--- a/src/app/services/index.ts
+++ b/src/app/services/index.ts
@@ -19,6 +19,7 @@ import { CandidateCardsService } from './candidate-cards.service';
import { CandidatesService } from './candidates.service';
import { OrganizationLabelsService } from './organization-labels.service';
import { CandidateCvService } from './candidate-cv.service';
+import { UserSalariesService } from './user-salaries.service';
export * from './authorization.service';
export * from './api.service';
@@ -58,5 +59,6 @@ export const applicationServices = [
CandidateCardsService,
CandidatesService,
OrganizationLabelsService,
- CandidateCvService
+ CandidateCvService,
+ UserSalariesService
];
diff --git a/src/app/services/organizations.service.ts b/src/app/services/organizations.service.ts
index e173906f..7b4d6c7d 100644
--- a/src/app/services/organizations.service.ts
+++ b/src/app/services/organizations.service.ts
@@ -4,10 +4,9 @@ import { CandidateCard } from '@models/organizations/candidate-card.model';
import { Candidate } from '@models/organizations/candidate.model';
import { Organization } from '@models/organizations/organization.model';
import { defaultPageParams, PageParams } from '@models/page-params';
-import { PaginatedList, PaginatedModel } from '@models/paginated-list';
+import { PaginatedList } from '@models/paginated-list';
import { SelectBoxItem } from '@models/select-box-item';
import { ConvertObjectToHttpParams } from '@shared/value-objects/convert-object-to-http';
-import { createReadStream } from 'fs';
import { Observable } from 'rxjs';
import { ApiService } from './api.service';
import { CandidateCardsFilterPaginatedRequest } from './requests/candidate-cards-filters';
diff --git a/src/app/services/user-salaries.service.ts b/src/app/services/user-salaries.service.ts
new file mode 100644
index 00000000..9213d06a
--- /dev/null
+++ b/src/app/services/user-salaries.service.ts
@@ -0,0 +1,55 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { ApiService } from './api.service';
+import { DeveloperGrade } from '@models/enums';
+import { CompanyType } from '@models/salaries/company-type';
+import { Currency } from '@models/salaries/currency';
+import { UserSalary } from '@models/salaries/salary.model';
+import { UserProfession } from '@models/salaries/user-profession';
+
+export interface CreateUserSalaryRequest {
+ value: number;
+ quarter: number;
+ year: number;
+ currency: Currency;
+ company: CompanyType;
+ grade: DeveloperGrade | null;
+ profession: UserProfession;
+}
+
+export interface SalariesChartResponse {
+ salaries: UserSalary[];
+ shouldAddOwnSalary: boolean;
+ rangeStart: Date;
+ rangeEnd: Date;
+ averageSalary: number;
+ medianSalary: number;
+ salariesByProfession: SalariesByProfession[];
+}
+
+export interface SalariesByProfession {
+ profession: UserProfession;
+ salaries: UserSalary[];
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserSalariesService {
+ private readonly root = '/api/salaries/';
+ constructor(private readonly api: ApiService) {}
+
+ all(): Observable> {
+ // for admis
+ return this.api.get(this.root + 'all');
+ }
+
+ charts(): Observable {
+ // for admis
+ return this.api.get(this.root + 'chart');
+ }
+
+ create(data: CreateUserSalaryRequest): Observable {
+ return this.api.post(this.root, data);
+ }
+}
diff --git a/src/app/shared/guards/auth.guard.ts b/src/app/shared/guards/auth.guard.ts
index 4b2ac049..faef372e 100644
--- a/src/app/shared/guards/auth.guard.ts
+++ b/src/app/shared/guards/auth.guard.ts
@@ -14,12 +14,15 @@ export class AuthGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot | null,
state: RouterStateSnapshot | null): boolean {
+
+ console.log('State', state);
if (this.authService.isAuthenticated()) {
return true;
}
- if (state !== null && state.url !== null) {
+ if (state !== null && state.url != null) {
// set expire date + 10 hours
+ console.log('Url to redirect', state.url);
this.cookieService.set('url', state.url, Date.now(), '/');
}
diff --git a/src/app/shared/guards/permissions-service.ts b/src/app/shared/guards/permissions-service.ts
new file mode 100644
index 00000000..3d873a97
--- /dev/null
+++ b/src/app/shared/guards/permissions-service.ts
@@ -0,0 +1,37 @@
+import { Injectable, inject } from "@angular/core";
+import { Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateFn } from "@angular/router";
+import { AuthService } from '../services/auth/auth.service';
+import { CookieService } from "ngx-cookie-service";
+
+// https://stackoverflow.com/a/76107558
+@Injectable({
+ providedIn: 'root'
+})
+export class PermissionsService {
+
+ constructor(
+ private readonly router: Router,
+ private readonly authService: AuthService,
+ private readonly cookieService: CookieService) {}
+
+ canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ console.log('State', state);
+ if (this.authService.isAuthenticated()) {
+ return true;
+ }
+
+ if (state !== null && state.url != null) {
+ // set expire date + 10 hours
+ console.log('Url to redirect', state.url);
+ this.cookieService.set('url', state.url, Date.now(), '/');
+ }
+
+ this.router.navigateByUrl('/');
+ return false;
+ }
+}
+
+// TODO mgorbatyuk: replace with this implementation
+export const AuthGuard: CanActivateFn = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean => {
+ return inject(PermissionsService).canActivate(next, state);
+}
diff --git a/src/app/shared/interceptors/auth-interceptor.ts b/src/app/shared/interceptors/auth-interceptor.ts
index dcebf281..352a1990 100644
--- a/src/app/shared/interceptors/auth-interceptor.ts
+++ b/src/app/shared/interceptors/auth-interceptor.ts
@@ -59,6 +59,12 @@ export class AuthInterceptor implements HttpInterceptor {
return true;
}
+ if (error.status === 0 &&
+ error.url != null &&
+ error.url.endsWith('/health')) {
+ return false;
+ }
+
const headersStatus = error.headers.get('status');
return headersStatus != null && Number(headersStatus) === notAuthStatusCode;
}
@@ -67,6 +73,8 @@ export class AuthInterceptor implements HttpInterceptor {
// unauthorized
if (this.checkIsNotAuthorizeError(error)) {
this.authService!.signout();
+
+ console.log('Navigated by auth interceptor to main page');
this.router.navigate(['/']);
return true;
}
diff --git a/src/app/shared/select-boxes/company-type-select-item.ts b/src/app/shared/select-boxes/company-type-select-item.ts
new file mode 100644
index 00000000..dd8d0ec1
--- /dev/null
+++ b/src/app/shared/select-boxes/company-type-select-item.ts
@@ -0,0 +1,22 @@
+import { EnumHelper } from '@shared/value-objects/enum-helper';
+import { SplittedByWhitespacesString } from '@shared/value-objects/splitted-by-whitespaces-string';
+import { SelectItem } from './select-item';
+import { CompanyType } from '@models/salaries/company-type';
+
+export class CompanyTypeSelectItem implements SelectItem {
+ readonly value: string;
+ readonly label: string;
+ readonly item: CompanyType;
+
+ constructor(item: CompanyType) {
+ this.value = item.toString();
+ this.label = new SplittedByWhitespacesString(CompanyType[item]).value;
+ this.item = item;
+ }
+
+ static allItems(): CompanyTypeSelectItem[] {
+ return EnumHelper.getValues(CompanyType)
+ .filter((x) => x !== CompanyType.Undefined)
+ .map((grade) => new CompanyTypeSelectItem(grade));
+ }
+}
diff --git a/src/app/shared/select-boxes/currency-select-item.ts b/src/app/shared/select-boxes/currency-select-item.ts
new file mode 100644
index 00000000..d75747e4
--- /dev/null
+++ b/src/app/shared/select-boxes/currency-select-item.ts
@@ -0,0 +1,22 @@
+import { EnumHelper } from '@shared/value-objects/enum-helper';
+import { SplittedByWhitespacesString } from '@shared/value-objects/splitted-by-whitespaces-string';
+import { SelectItem } from './select-item';
+import { Currency } from '@models/salaries/currency';
+
+export class CurrencySelectItem implements SelectItem {
+ readonly value: string;
+ readonly label: string;
+ readonly item: Currency;
+
+ constructor(item: Currency) {
+ this.value = item.toString();
+ this.label = new SplittedByWhitespacesString(Currency[item]).value;
+ this.item = item;
+ }
+
+ static allItems(): CurrencySelectItem[] {
+ return EnumHelper.getValues(Currency)
+ .filter((x) => x !== Currency.Undefined)
+ .map((grade) => new CurrencySelectItem(grade));
+ }
+}
diff --git a/src/app/shared/select-boxes/profession-select-item.ts b/src/app/shared/select-boxes/profession-select-item.ts
new file mode 100644
index 00000000..8cebe9c3
--- /dev/null
+++ b/src/app/shared/select-boxes/profession-select-item.ts
@@ -0,0 +1,22 @@
+import { EnumHelper } from '@shared/value-objects/enum-helper';
+import { SplittedByWhitespacesString } from '@shared/value-objects/splitted-by-whitespaces-string';
+import { SelectItem } from './select-item';
+import { UserProfession } from '@models/salaries/user-profession';
+
+export class ProfessionSelectItem implements SelectItem {
+ readonly value: string;
+ readonly label: string;
+ readonly item: UserProfession;
+
+ constructor(item: UserProfession) {
+ this.value = item.toString();
+ this.label = new SplittedByWhitespacesString(UserProfession[item]).value;
+ this.item = item;
+ }
+
+ static allItems(): ProfessionSelectItem[] {
+ return EnumHelper.getValues(UserProfession)
+ .filter((x) => x !== UserProfession.Undefined)
+ .map((grade) => new ProfessionSelectItem(grade));
+ }
+}
diff --git a/src/app/shared/test-utils/mock-auth.service.ts b/src/app/shared/test-utils/mock-auth.service.ts
index 353a7759..9bda040c 100644
--- a/src/app/shared/test-utils/mock-auth.service.ts
+++ b/src/app/shared/test-utils/mock-auth.service.ts
@@ -25,10 +25,10 @@ export class MockAuthService implements IAuthService {
}
login(): Promise {
- throw new Error('Method not implemented.');
+ return Promise.resolve();
}
signout(): void {
- throw new Error('Method not implemented.');
+ // do nothing
}
}
diff --git a/src/environments/environment.staging.ts b/src/environments/environment.staging.ts
new file mode 100644
index 00000000..8ee21c9e
--- /dev/null
+++ b/src/environments/environment.staging.ts
@@ -0,0 +1,27 @@
+import { NgxGoogleAnalyticsModule, NgxGoogleAnalyticsRouterModule } from 'ngx-google-analytics';
+
+export const environment = {
+ production: true,
+ staging: false,
+ type: 'dev',
+ baseUrl: 'http://techinterview.space',
+ resourceApiURI: 'https://api.techinterview.space',
+ identityApiURI: 'https://techinterview-space.eu.auth0.com',
+ name: '',
+ auth: {
+ domain: 'techinterview-space.eu.auth0.com',
+ authority: 'https://techinterview-space.eu.auth0.com',
+ client_id: 'juNFNVr0wy1yayFgM2KTbk8374oP8MDk',
+ redirect_uri: 'http://localhost:4200/auth-callback',
+ post_logout_redirect_uri: 'http://localhost:4200/logout-callback',
+ response_type: 'id_token token',
+ scope: 'openid profile name given_name family_name email nickname',
+ filterProtocolClaims: true,
+ loadUserInfo: true,
+ automaticSilentRenew: true,
+ silent_redirect_uri: 'http://localhost:4200/silent-refresh.html'
+ },
+ googleAnalytics: {
+ imports: []
+ }
+};
diff --git a/src/index.html b/src/index.html
index 4352aaf0..9cc238e4 100644
--- a/src/index.html
+++ b/src/index.html
@@ -34,7 +34,7 @@
Tech.Interview
Kazakhstan, Almaty
-
2022, © maximgorbatyuk
+
2024, © maximgorbatyuk