Compare commits

...
This repository has been archived on 2025-04-26. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.

107 commits

Author SHA1 Message Date
84b855b8aa Merge pull request 'employee-detail-english' (#83) from employee-detail-english into main
All checks were successful
Playwright Tests / test (push) Successful in 2m32s
Reviewed-on: #83
Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
2025-01-22 14:15:50 +00:00
1e42614b22 Merge pull request 'refactor: remove required validator from newSkill field' (#84) from fix/fix-save-not-working into main
All checks were successful
Playwright Tests / test (push) Successful in 2m25s
Reviewed-on: #84
2025-01-22 11:36:25 +00:00
2582c5b568
refactor(mitarbeiter-form): remove console log statements
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m20s
2025-01-22 12:35:10 +01:00
f51619e172
refactor: remove required validator from newSkill field
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m12s
2025-01-22 12:31:09 +01:00
6efe952831 Merge pull request 'Fix selecting nothing' (#81) from fix/fix-selecting-nothing-as-skill into main
All checks were successful
Playwright Tests / test (push) Successful in 1m55s
Reviewed-on: #81
2025-01-22 11:17:44 +00:00
2e9b48be7c fix(qualifikation-form): prevent adding invalid employee ID
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m7s
2025-01-22 11:14:55 +00:00
60c5d41161 fix(mitarbeiter-form): prevent adding skill with invalid id 2025-01-22 11:14:55 +00:00
951ab76128 Changed employee detail page to english
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m40s
2025-01-22 12:10:40 +01:00
f941fff8d6 Changed employee detail page to english 2025-01-22 12:09:58 +01:00
e5b2d7789f Merge pull request 'Employee Details page' (#80) from employees-detailseite into main
All checks were successful
Playwright Tests / test (push) Successful in 2m24s
Reviewed-on: #80
2025-01-22 11:04:44 +00:00
300520093d
refactor(employee-detail): restructure HTML layout with container
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m49s
2025-01-22 12:01:44 +01:00
fb90c770f6
test: remove outdated edit redirect test case
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m8s
2025-01-22 11:54:20 +01:00
9b90c735f5
IDK 2025-01-22 11:54:10 +01:00
ebd4508743 Merge pull request 'feat(navigation): add navigation bar to employee views' (#79) from feature/add-sidebar into main
All checks were successful
Playwright Tests / test (push) Successful in 2m3s
Playwright Tests / test (pull_request) Successful in 2m39s
Reviewed-on: #79
2025-01-22 09:23:07 +00:00
5cc396f552
feat(navigation): add navigation bar to employee views
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m16s
2025-01-22 10:16:08 +01:00
f3738ff631 Merge pull request 'refactor: remove unused employee fields from template' (#77) from fix/remove-extra-collumns-from-employees into main
All checks were successful
Playwright Tests / test (push) Successful in 2m10s
Reviewed-on: #77
2025-01-22 08:32:14 +00:00
fbec66debc Merge pull request 'feat(qualifikationsverwaltung): add error handling for delete skill' (#76) from fix/skill-deletion-logi into main
All checks were successful
Playwright Tests / test (push) Successful in 2m25s
Reviewed-on: #76
2025-01-22 08:29:36 +00:00
4dc253b508
refactor: remove unused employee fields from template
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m45s
2025-01-22 09:28:17 +01:00
5f18aa207d
feat(qualifikationsverwaltung): add error handling for delete skill
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m12s
2025-01-22 09:26:46 +01:00
2a337f3586 Merge pull request 'feat: add qualification creation component and route' (#75) from feature/create-skill into main
All checks were successful
Playwright Tests / test (push) Successful in 2m13s
Reviewed-on: #75
2025-01-22 08:09:43 +00:00
471a675117
fix: correct selector and template URLs in component
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m2s
2025-01-22 09:07:13 +01:00
9b80777fc3
feat: add qualification creation component and route
Some checks failed
Playwright Tests / test (pull_request) Failing after 1m19s
2025-01-22 09:03:52 +01:00
08adf21da9 Merge pull request 'chore(deps): lock file maintenance' (#52) from renovate/lock-file-maintenance into main
All checks were successful
Playwright Tests / test (push) Successful in 2m7s
Reviewed-on: #52
2025-01-18 08:49:10 +00:00
158c7acb55 chore(deps): lock file maintenance
All checks were successful
Playwright Tests / test (pull_request) Successful in 3m24s
2025-01-17 14:03:07 +00:00
11da909e3f Merge pull request 'feat: add Playwright testing framework configuration and tests' (#71) from feature/add-playwright into main
All checks were successful
Playwright Tests / test (push) Successful in 2m7s
Reviewed-on: #71
2025-01-17 13:34:51 +00:00
d920f8d774
test: remove redundant employee creation test
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m48s
2025-01-17 14:32:27 +01:00
421740910e
chore: update CI workers to improve test concurrency
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m3s
2025-01-17 14:29:31 +01:00
d6f68afd64
test: Update delete button wait conditions in tests
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m20s
2025-01-17 14:25:07 +01:00
d3b9362023
test: Add wait for delete button in creation test
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m24s
2025-01-17 14:19:35 +01:00
e91dc49804
test: Add timeouts for safety in employee tests
Some checks failed
Playwright Tests / test (pull_request) Failing after 2m16s
2025-01-17 14:05:30 +01:00
b61e00e647
test: add wait for delete button visibility before click
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m56s
2025-01-17 13:59:47 +01:00
bac1f3cfac
test: Remove old employee test and add new tests
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m42s
2025-01-17 13:55:25 +01:00
a6047e2720
test: add tests for mitarbeiter creation and validation
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m36s
2025-01-17 13:32:11 +01:00
3882cde8ca
test: add initial tests for qualifications management
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m3s
2025-01-17 12:17:27 +01:00
56a2ef77fa
test: add search functionality test case
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m58s
2025-01-17 12:04:25 +01:00
d4cdfba01a
test: rename test for user deletion scenario
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m52s
2025-01-17 11:51:55 +01:00
c1de527057
style(tests): format code for better readability
All checks were successful
Playwright Tests / test (pull_request) Successful in 2m33s
2025-01-17 11:48:26 +01:00
007a2f041b
test: add test for user deletion functionality
Some checks failed
Playwright Tests / test (pull_request) Failing after 2m34s
2025-01-17 11:30:24 +01:00
c29ae4ef18
ci: remove unnecessary container names in workflow config
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m51s
2025-01-17 11:19:35 +01:00
9e70e2a090
fix: correct Angular build target in workflows and config
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m56s
2025-01-17 11:06:11 +01:00
b70d7c8e0e
feat: add pipeline configuration to angular.json
Some checks failed
Playwright Tests / test (pull_request) Has been cancelled
2025-01-17 11:04:27 +01:00
f21d03dda3
ci: update command to start Angular server in workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 54s
2025-01-17 11:00:33 +01:00
f3a61c10ad
ci: update angular server command in workflow file
Some checks failed
Playwright Tests / test (pull_request) Failing after 51s
2025-01-17 10:58:25 +01:00
3a25307a36
fix: update BASE_URL to use 127.0.0.1
Some checks failed
Playwright Tests / test (pull_request) Failing after 2m28s
2025-01-17 10:54:56 +01:00
00fefab60b
ci: update Angular server command in workflow file
Some checks failed
Playwright Tests / test (pull_request) Failing after 2m12s
2025-01-17 10:51:41 +01:00
2ee45c699a
build: update Angular CLI before building project
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m27s
2025-01-17 10:47:32 +01:00
b693ea7706
build: update build command to use ng instead of npm
Some checks failed
Playwright Tests / test (pull_request) Failing after 24s
2025-01-17 10:45:44 +01:00
0c475e51a9
build: update build command for Angular project
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m37s
2025-01-17 10:41:42 +01:00
17f20461f8
feat(ci): add CI environment configuration and replace URL 2025-01-17 10:41:01 +01:00
aceb2e953c
ci: remove unnecessary debug steps from workflow
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m27s
2025-01-17 10:25:06 +01:00
370b1f54f5
ci: update telnet command in Playwright workflow
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m32s
2025-01-17 10:22:50 +01:00
5a4e14c2dd
fix(skill.service): update BASE_URL for production use
Some checks failed
Playwright Tests / test (pull_request) Failing after 14s
2025-01-17 10:20:58 +01:00
db0966fdd9
ci: add port mapping to Playwright workflow configuration
Some checks failed
Playwright Tests / test (pull_request) Failing after 15s
2025-01-17 10:18:08 +01:00
c292380fcd
chore: update telnet installation to use -y flag
Some checks failed
Playwright Tests / test (pull_request) Failing after 15s
2025-01-17 10:16:15 +01:00
c76749f3fd
ci: update apt before installing telnet in workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 14s
2025-01-17 10:15:36 +01:00
a058720a6b
ci: add telnet installation to workflow debug step
Some checks failed
Playwright Tests / test (pull_request) Failing after 10s
2025-01-17 10:14:50 +01:00
a598a68d34
ci: add telnet command for debugging in workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 10s
2025-01-17 10:13:53 +01:00
8a3fb2aaf0
ci: remove unnecessary port mappings from workflow config
Some checks failed
Playwright Tests / test (pull_request) Failing after 2m58s
2025-01-17 10:07:24 +01:00
03a8586c38
ci: update Playwright CI configuration for employee service
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m27s
2025-01-17 10:05:42 +01:00
ab1c9ac76f
ci: add healthcheck to employee service in workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m1s
2025-01-17 08:24:44 +01:00
182c014564
chore: update environment to env in workflow file
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m10s
2025-01-17 08:18:52 +01:00
b8f1dab9f4
ci: add PostgreSQL and employee service to workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m4s
2025-01-17 07:59:08 +01:00
408644ddad
ci(workflows): ensure Playwright runs in CI mode always
Some checks failed
Playwright Tests / test (pull_request) Failing after 2m56s
2025-01-17 07:56:29 +01:00
c0ccc736bc
fix(skill.service): update BASE_URL to use localhost IP 2025-01-17 07:55:35 +01:00
1daed7ec65
ci: update Playwright workflow to use Docker compose
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m3s
2025-01-17 07:46:22 +01:00
2d062da74f
feat(ci): add PostgreSQL and employee service to workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m16s
2025-01-16 19:40:54 +01:00
433001001a
ci: add docker compose boot step to workflow
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m15s
2025-01-16 18:10:58 +01:00
ae78af5440
test: add tests for mitarbeiter functionality
Some checks failed
Playwright Tests / test (pull_request) Failing after 3m8s
2025-01-16 18:06:37 +01:00
769c775be4
test: Add login redirection test for employees page
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m51s
2025-01-16 17:42:00 +01:00
59b210ff46
ci: update Angular server start command in background
All checks were successful
Playwright Tests / test (pull_request) Successful in 1m27s
2025-01-16 17:24:43 +01:00
0cecd1cd2b
ci: update Playwright workflow for Angular project setup
Some checks failed
Playwright Tests / test (pull_request) Has been cancelled
2025-01-16 17:22:43 +01:00
6c1ec524ec
chore: update Playwright tests and remove examples
Some checks failed
Playwright Tests / test (pull_request) Failing after 1m36s
2025-01-16 17:19:38 +01:00
b30a40fe07
feat: add Playwright testing framework configuration and tests
Some checks failed
Playwright Tests / test (pull_request) Failing after 1m43s
2025-01-16 16:46:30 +01:00
27eca287e2 Merge pull request 'feature/add-skill-list' (#70) from feature/add-skill-list into main
Reviewed-on: #70
2025-01-16 15:42:39 +00:00
3d2f049312
feat(qualifikation): add skill management features and fixes 2025-01-16 16:42:02 +01:00
8a4a617c33
feat: add qualifications management component and routes 2025-01-16 11:06:30 +01:00
b7e1ec2d57 Merge pull request 'Qualifikationen Bearbeiten' (#67) from feature/edit-skills into main
Reviewed-on: #67
2025-01-16 09:48:44 +00:00
303dadfc68 Merge pull request 'Redo skill editing logic' (#69) from refactor/redo-skill-editing-logic into feature/edit-skills
Reviewed-on: #69
2025-01-16 09:48:21 +00:00
703d644799
feat: add error handling for skill form validation 2025-01-16 10:46:27 +01:00
6119621040
feat(qualifikation-form): implement submit functionality and navigation 2025-01-15 20:45:17 +01:00
8d778ae773
refactor: simplify employee management logic in component 2025-01-15 20:19:21 +01:00
c9a4b5063f
EOD 2025-01-15 16:29:11 +01:00
c11772f08b
feat: add qualification editing functionality and styles 2025-01-15 15:57:31 +01:00
61597627c6
feat(qualifikation-form): add qualification form component 2025-01-15 14:20:59 +01:00
4de394acb0 Merge pull request 'Mitarbeiter erstellen' (#66) from feature/create-employee into main
Reviewed-on: #66
2025-01-15 13:06:06 +00:00
ca26b84cc6
feat: add createEmployee method and button click handler 2025-01-15 14:05:28 +01:00
582e115285
feat: add employee creation component and route 2025-01-15 14:01:27 +01:00
b6919107da
feat(mitarbeiter-erstellen): implement employee form functionality 2025-01-15 13:48:46 +01:00
2dc8956142
feat(mitarbeiter-erstellen): add new component files 2025-01-15 13:48:44 +01:00
a0672a03a1 Merge pull request 'Logik/Backend-für-Mitarbeiter-Seite-&-Qualifikationsseite-59' (#65) from Logik/Backend-für-Mitarbeiter-Seite-&-Qualifikationsseite-59 into main
Reviewed-on: #65
2025-01-15 12:45:34 +00:00
d80c334e29
style: Update button styles in employee table 2025-01-15 13:40:48 +01:00
c7f3bf5c54
feat: implement employee deletion functionality 2025-01-15 13:39:00 +01:00
4c0371efb3
feat(mitarbeiterverwaltung): add edit employee functionality 2025-01-15 13:32:37 +01:00
d80f98f2a0
feat(search): implement search functionality for employees 2025-01-15 13:29:30 +01:00
9219b92049
refactor: restructure employee-related components and services 2025-01-15 12:53:31 +01:00
5d90b71ba5
refactor: simplify component imports and structure 2025-01-15 12:44:05 +01:00
mehdiboudjoudi
9807f73f1a
test 2025-01-15 12:34:19 +01:00
mehdiboudjoudi
67179669c7
BACK UP COMMIT 2025-01-15 12:32:57 +01:00
mehdiboudjoudi
e18436a57c
90% done 2025-01-15 12:32:51 +01:00
d736143ad6 Merge pull request 'Add edit employee form' (#63) from forms-pages into main
Reviewed-on: #63
2025-01-15 11:29:56 +00:00
f10fe2455d
style: Remove unnecessary console.log statements 2025-01-15 12:25:17 +01:00
3b8ed21b27
feat(mitarbeiter-form): add validation error messages display 2025-01-15 12:24:39 +01:00
feaf8f54d2
feat: add navigation to employee overview after update 2025-01-15 11:40:51 +01:00
8abc5855dd
feat(mitarbeiter-bearbeiten): add employee update functionality 2025-01-15 11:30:39 +01:00
d86af94ac8
feat: add employee management and skill handling features 2025-01-15 11:02:12 +01:00
0bcc7d4684
refactor: move form to seperate component as it will be used 2 times 2025-01-15 07:10:50 +01:00
5ff00d7510
feat(routes): add route for MitarbeiterBearbeitenViewComponent 2025-01-15 07:02:38 +01:00
57 changed files with 2964 additions and 4178 deletions

71
.github/workflows/playwright.yml vendored Normal file
View file

@ -0,0 +1,71 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
postgres-employee:
image: postgres:13.3
env:
POSTGRES_DB: employee_db
POSTGRES_USER: employee
POSTGRES_PASSWORD: secret
employee:
image: berndheidemann/employee-management-service:1.1.3
# image: berndheidemann/employee-management-service_without_keycloak:1.1
env:
spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db
spring.datasource.username: employee
spring.datasource.password: secret
steps:
# Checkout the repository
- uses: actions/checkout@v4
# Set up Node.js
- uses: actions/setup-node@v4
with:
node-version: lts/*
# Install project dependencies
- name: Install dependencies
run: npm ci
# Build the Angular project
- name: Build Angular Project
run: |
npm install -g @angular/cli
ng build --configuration=pipeline
# Start Angular development server
- name: Start Angular Development Server in Background
run: ng serve --configuration=pipeline > angular.log 2>&1 &
env:
PORT: 4200 # Ensure the server runs on a predictable port
# Install Playwright and dependencies
- name: Install Playwright Browsers
run: npx playwright install --with-deps
# Run Playwright tests
- name: Run Playwright Tests
run: npx playwright test
env:
CI: true # Ensures Playwright runs in CI mode
# Upload Playwright report
- uses: actions/upload-artifact@v3
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

5
.gitignore vendored
View file

@ -40,3 +40,8 @@ testem.log
# System files # System files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View file

@ -52,6 +52,14 @@
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
},
"pipeline": {
"fileReplacements": [
{
"replace": "src/app/environments/environment.ts",
"with": "src/app/environments/environment.ci.ts"
}
]
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@ -64,6 +72,9 @@
}, },
"development": { "development": {
"buildTarget": "employeeService:build:development" "buildTarget": "employeeService:build:development"
},
"pipeline": {
"buildTarget": "employeeService:build:pipeline"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@ -93,5 +104,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View file

@ -6,14 +6,10 @@ services:
postgres-employee: postgres-employee:
container_name: postgres_employee container_name: postgres_employee
image: postgres:13.3 image: postgres:13.3
volumes:
- employee_postgres_data:/var/lib/postgresql/data
environment: environment:
POSTGRES_DB: employee_db POSTGRES_DB: employee_db
POSTGRES_USER: employee POSTGRES_USER: employee
POSTGRES_PASSWORD: secret POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
employee: employee:
container_name: employee container_name: employee

4704
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,9 @@
"@angular-devkit/build-angular": "^18.2.0", "@angular-devkit/build-angular": "^18.2.0",
"@angular/cli": "^18.2.0", "@angular/cli": "^18.2.0",
"@angular/compiler-cli": "^18.2.0", "@angular/compiler-cli": "^18.2.0",
"@playwright/test": "^1.49.1",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^22.10.7",
"jasmine-core": "~5.2.0", "jasmine-core": "~5.2.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",

69
playwright.config.ts Normal file
View file

@ -0,0 +1,69 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 8 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -1,25 +1,10 @@
import {Component} from '@angular/core'; import { Component } from '@angular/core';
import {RouterOutlet} from '@angular/router'; import { RouterOutlet } from '@angular/router';
import {LoginViewComponent} from "./components/login-view/login-view.component";
import {
MitarbeiterverwaltungViewComponent
} from "./components/mitarbeiterverwaltung-view/mitarbeiterverwaltung-view.component";
import {NavigationBarComponent} from './components/navigation-bar/navigation-bar.component';
import {EmployeeDetailComponent} from './components/employee-detail/employee-detail.component';
import {
MitarbeiterBearbeitenViewComponent
} from "./components/mitarbeiter-bearbeiten-view/mitarbeiter-bearbeiten-view.component";
import {
QualifikatonBearbeitenViewComponent
} from "./components/qualifikaton-bearbeiten-view/qualifikaton-bearbeiten-view.component";
import {QualifikatonDetailComponent} from "./components/qualifikaton-detail/qualifikaton-detail.component";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [RouterOutlet, NavigationBarComponent, EmployeeDetailComponent, LoginViewComponent, MitarbeiterverwaltungViewComponent, MitarbeiterBearbeitenViewComponent, QualifikatonBearbeitenViewComponent, QualifikatonDetailComponent], imports: [RouterOutlet],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css' styleUrl: './app.component.css'
}) })

View file

@ -2,7 +2,7 @@ import { APP_INITIALIZER, ApplicationConfig, provideZoneChangeDetection } from '
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular"; import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from "keycloak-angular";
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
export const initializeKeycloak = (keycloak: KeycloakService) => async () => export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
@ -28,20 +28,20 @@ function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideRouter(routes), provideRouter(routes),
KeycloakAngularModule, KeycloakAngularModule,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initializeApp, useFactory: initializeApp,
multi: true, multi: true,
deps: [KeycloakService] deps: [KeycloakService]
}, },
KeycloakService, KeycloakService,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor, useClass: KeycloakBearerInterceptor,
multi: true multi: true
} }
] ]
}; };

View file

@ -3,8 +3,11 @@ import { LoginViewComponent } from "./components/login-view/login-view.component
import { MitarbeiterverwaltungViewComponent } from "./components/mitarbeiterverwaltung-view/mitarbeiterverwaltung-view.component"; import { MitarbeiterverwaltungViewComponent } from "./components/mitarbeiterverwaltung-view/mitarbeiterverwaltung-view.component";
import { EmployeeDetailComponent } from "./components/employee-detail/employee-detail.component"; import { EmployeeDetailComponent } from "./components/employee-detail/employee-detail.component";
import { QualifikatonBearbeitenViewComponent } from "./components/qualifikaton-bearbeiten-view/qualifikaton-bearbeiten-view.component"; import { QualifikatonBearbeitenViewComponent } from "./components/qualifikaton-bearbeiten-view/qualifikaton-bearbeiten-view.component";
import { KeycloakAuthGuard } from "keycloak-angular"; import { MitarbeiterBearbeitenViewComponent } from "./components/mitarbeiter-bearbeiten-view/mitarbeiter-bearbeiten-view.component";
import { AuthGuard } from "./service/auth.service"; import { AuthGuard } from "./service/auth.service";
import { MitarbeiterErstellenComponent } from "./components/mitarbeiter-erstellen/mitarbeiter-erstellen.component";
import { QualifikationsverwaltungComponent } from "./components/qualifikationsverwaltung/qualifikationsverwaltung.component";
import { QulifikationErstellenComponent } from "./components/qualifikation-erstellen/qualifikation-erstellen.component";
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -17,12 +20,34 @@ export const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
{ {
path: "mitarbeiterdetails", path: "qualifikationerstellen",
component: EmployeeDetailComponent, component: QulifikationErstellenComponent,
canActivate: [AuthGuard],
}, },
{ {
path: "qualifikationbearbeiten", path: "mitarbeitererstellen",
component: MitarbeiterErstellenComponent,
canActivate: [AuthGuard],
},
{
path: "mitarbeiterbearbeiten/:id",
component: MitarbeiterBearbeitenViewComponent,
canActivate: [AuthGuard],
},
{
path: "qualifikationsverwaltung",
component: QualifikationsverwaltungComponent,
canActivate: [AuthGuard],
},
{
path: "mitarbeiterdetails/:id",
component: EmployeeDetailComponent,
canActivate: [AuthGuard],
},
{
path: "qualifikationbearbeiten/:id",
component: QualifikatonBearbeitenViewComponent, component: QualifikatonBearbeitenViewComponent,
canActivate: [AuthGuard],
}, },
{ {
path: "**", path: "**",

View file

@ -1,28 +1,29 @@
<div class="d-flex flex-col" style="height: 100%;"> <div class="container">
<app-navigation-bar></app-navigation-bar> <div class="d-flex flex-col" style="height: 100%;">
<div class="p-3" style="width: 100%;"> <div class="p-3" style="width: 100%;">
<a href="" class="btn btn-primary">Zurück</a> <button class="btn btn-primary" (click)="goToEmployeePage()">Back</button>
<div class="row align-items-start pt-3"> <div class="row align-items-start pt-3">
<div class="col"> <div class="col">
<h1>Name des Mitarbeiters</h1> <h1>{{ employee.firstName }} {{ employee.lastName }} </h1>
<p><strong>Straße: </strong>Straße des Benutzers</p> <p><strong>Street: </strong>{{ employee.street }}</p>
<p><strong>Postleitzahl: </strong>Postleitzahl des Benutzers</p> <p><strong>Postal Code: </strong>{{ employee.postcode }}</p>
<p><strong>Stadt: </strong>Stadt des Benutzers</p> <p><strong>City: </strong>{{ employee.city }}</p>
<p><strong>Telefonnummer: </strong>Telefonnummer des Benutzers</p> <p><strong>Phone number: </strong>{{ employee.phone }}s</p>
<button class="btn btn-danger">Löschen</button> <button class="btn btn-danger" (click)="deleteEmployee(currentId)">Delete</button>
<button class="ms-3 btn btn-primary">Bearbeiten</button> <button class="ms-3 btn btn-primary" (click)="editEmployee(currentId)">Edit</button>
</div>
<div class="col">
<h2>Qualifikationen</h2>
<ul class="list-group" style="width: fit-content;">
<li class="list-group-item">Qualifikation 1</li>
<li class="list-group-item">Qualifikation 2</li>
<li class="list-group-item">Qualifikation 3</li>
<li class="list-group-item">Qualifikation 4</li>
<li class="list-group-item">Qualifikation 5</li>
</ul>
</div>
</div>
</div>
<div class="col">
<h2>Qualifikationen</h2>
<ul class="list-group" style="width: fit-content;">
@for(skill of skillSet; track skill) {
<li class="list-group-item">{{ skill }}</li>
}
</ul>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,11 @@
import { Component } from '@angular/core'; import {Component} from '@angular/core';
import { NavigationBarComponent } from '../navigation-bar/navigation-bar.component'; import {NavigationBarComponent} from '../navigation-bar/navigation-bar.component';
import {EmployeeNameAndSkillDataDTO, EmployeeResponseDTO} from "../../models/mitarbeiter";
import {EmployeeService} from "../../service/employee.service";
import {ActivatedRoute, Router} from "@angular/router";
import {Observable} from "rxjs";
import {QualificationGetDTO} from "../../models/skill";
import {SkillService} from "../../service/skill.service";
@Component({ @Component({
selector: 'app-employee-detail', selector: 'app-employee-detail',
@ -9,5 +15,31 @@ import { NavigationBarComponent } from '../navigation-bar/navigation-bar.compone
styleUrl: './employee-detail.component.css' styleUrl: './employee-detail.component.css'
}) })
export class EmployeeDetailComponent { export class EmployeeDetailComponent {
employee!: EmployeeResponseDTO;
skillSet: string[] = [];
currentId: number = 0;
constructor(private employeeService: EmployeeService, private router: Router, private route: ActivatedRoute, private skillService: SkillService) {
this.currentId = this.route.snapshot.params['id'];
this.employeeService.getEmployeeById(this.route.snapshot.params['id']).subscribe(employee => {
this.employee = employee;
this.skillSet = this.employee.skillSet?.map(skill => skill.skill) || [];
});
}
editEmployee(id: number) {
this.router.navigate([`/mitarbeiterbearbeiten/${id}`]);
}
goToEmployeePage(){
this.router.navigate(['mitarbeiter']);
}
deleteEmployee(id: number) {
this.employeeService.deleteEmployee(id);
this.router.navigate([`mitarbeiter`]);
}
protected readonly EmployeeService = EmployeeService;
} }

View file

@ -3,6 +3,6 @@
<img src="img.png" alt="Logout Icon" class="logo-image" /> <img src="img.png" alt="Logout Icon" class="logo-image" />
<form> <form>
<button (click)="login()" type="submit">Mit KeyCLoak anmelden</button> <button (click)="login()" type="submit">Login with Keycloak</button>
</form> </form>
</div> </div>

View file

@ -1,120 +0,0 @@
body {
font-family: sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
}
h1, h2 {
font-size: 2rem;
margin-bottom: 20px;
}
.back-button {
background-color: #ccc;
color: #333;
border: none;
padding: 8px 12px;
border-radius: 3px;
margin-bottom: 15px;
}
.user-info {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 5px;
}
input[type="text"] {
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
input[type="text"]::placeholder {
color: #999;
}
.save-button {
background-color: #007bff;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.skills-container {
border: 1px solid #ccc;
padding: 20px;
border-radius: 3px;
}
.skill-controls select {
padding: 10px 10px;
border: none;
border-radius: 3px;
background-color: #007bff;
color: #fff;
cursor: pointer;
}
.skill-list {
list-style: none;
padding: 0;
}
.skill-list li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.skill-icon {
margin-right: 5px;
}
.delete-skill-button {
color: #fff;
padding: 5px 8px;
border: none;
cursor: pointer;
margin-left: 10px;
border-radius: 3px;
}
.delete-skill-button img {
width: 15px;
height: 15px;
}
.add-skill-button{
background-color: #06a63b;
color: #fff;
padding: 8px 10px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-left: 10px;
}

View file

@ -1,49 +1 @@
<div class="container"> <app-mitarbeiter-form [(mitarbeiter)]="mitarbeiter" (mitarbeiterChange)="submitted($event)"></app-mitarbeiter-form>
<button class="back-button">Back</button>
<div class="user-info">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" id="firstName" placeholder="First Name">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" id="lastName" placeholder="Last Name">
</div>
<div class="form-group">
<label for="street">Street</label>
<input type="text" id="street" placeholder="Street">
</div>
<div class="form-group">
<label for="postcode">Postcode</label>
<input type="text" id="postcode" placeholder="Postcode">
</div>
<div class="form-group">
<label for="city">City</label>
<input type="text" id="city" placeholder="City">
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="text" id="phone" placeholder="Phone Number">
</div>
</div>
<div class="skills-container">
<h2>Skills</h2>
<ul class="skill-list">
<li>
<span class="skill-name">Skill 1</span>
<button class="delete-skill-button">
<img src="Delete-button.svg" alt="Delete">
</button>
</li>
</ul>
<div class="skill-controls">
<select>
<option value="">Option 1</option>
<option value="">Option 2</option>
</select>
<button class="add-skill-button">Add qualification</button>
</div>
</div>
<button class="save-button">Save</button>
</div>

View file

@ -1,12 +1,43 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MitarbeiterFormComponent } from '../mitarbeiter-form/mitarbeiter-form.component';
import { EmployeeResponseDTO } from '../../models/mitarbeiter';
import { ActivatedRoute, Router } from '@angular/router';
import { EmployeeService } from '../../service/employee.service';
@Component({ @Component({
selector: 'app-mitarbeiter-bearbeiten-view', selector: 'app-mitarbeiter-bearbeiten-view',
standalone: true, standalone: true,
imports: [], imports: [MitarbeiterFormComponent],
templateUrl: './mitarbeiter-bearbeiten-view.component.html', templateUrl: './mitarbeiter-bearbeiten-view.component.html',
styleUrl: './mitarbeiter-bearbeiten-view.component.css' styleUrl: './mitarbeiter-bearbeiten-view.component.css'
}) })
export class MitarbeiterBearbeitenViewComponent { export class MitarbeiterBearbeitenViewComponent {
public mitarbeiter!: EmployeeResponseDTO;
constructor(private employeeService: EmployeeService, private route: ActivatedRoute, private router: Router) { }
submitted(mitarbeiter: EmployeeResponseDTO) {
this.employeeService.updateEmployee(mitarbeiter);
this.returnToEmployeeOverview();
}
returnToEmployeeOverview() {
this.router.navigate(["mitarbeiter"]);
}
ngOnInit(): void {
this.mitarbeiter = {
id: 0,
firstName: '',
lastName: '',
street: '',
phone: '',
skillSet: [],
postcode: '',
city: '',
}
this.employeeService.getEmployeeById(this.route.snapshot.params['id']).subscribe(employee => {
this.mitarbeiter = employee;
});
}
} }

View file

@ -0,0 +1 @@
<app-mitarbeiter-form [(mitarbeiter)]="mitarbeiter" (mitarbeiterChange)="submitted($event)"></app-mitarbeiter-form>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MitarbeiterErstellenComponent } from './mitarbeiter-erstellen.component';
describe('MitarbeiterErstellenComponent', () => {
let component: MitarbeiterErstellenComponent;
let fixture: ComponentFixture<MitarbeiterErstellenComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MitarbeiterErstellenComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MitarbeiterErstellenComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,44 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { EmployeeResponseDTO } from '../../models/mitarbeiter';
import { EmployeeService } from '../../service/employee.service';
import { MitarbeiterFormComponent } from '../mitarbeiter-form/mitarbeiter-form.component';
@Component({
selector: 'app-mitarbeiter-erstellen',
standalone: true,
imports: [MitarbeiterFormComponent],
templateUrl: './mitarbeiter-erstellen.component.html',
styleUrl: './mitarbeiter-erstellen.component.css'
})
export class MitarbeiterErstellenComponent {
public mitarbeiter!: EmployeeResponseDTO;
constructor(private employeeService: EmployeeService, private route: ActivatedRoute, private router: Router) { }
submitted(mitarbeiter: EmployeeResponseDTO) {
console.log(mitarbeiter)
this.employeeService.createEmployee(mitarbeiter);
this.returnToEmployeeOverview();
}
returnToEmployeeOverview() {
this.router.navigate(["mitarbeiter"]);
}
ngOnInit(): void {
this.mitarbeiter = {
id: 0,
firstName: '',
lastName: '',
street: '',
phone: '',
skillSet: [],
postcode: '',
city: '',
}
this.employeeService.getEmployeeById(this.route.snapshot.params['id']).subscribe(employee => {
this.mitarbeiter = employee;
});
}
}

View file

@ -0,0 +1,120 @@
body {
font-family: sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
}
h1, h2 {
font-size: 2rem;
margin-bottom: 20px;
}
.back-button {
background-color: #ccc;
color: #333;
border: none;
padding: 8px 12px;
border-radius: 3px;
margin-bottom: 15px;
}
.user-info {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 5px;
}
input[type="text"] {
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
input[type="text"]::placeholder {
color: #999;
}
.save-button {
background-color: #007bff;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.skills-container {
border: 1px solid #ccc;
padding: 20px;
border-radius: 3px;
}
.skill-controls select {
padding: 10px 10px;
border: none;
border-radius: 3px;
background-color: #007bff;
color: #fff;
cursor: pointer;
}
.skill-list {
list-style: none;
padding: 0;
}
.skill-list li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.skill-icon {
margin-right: 5px;
}
.delete-skill-button {
color: #fff;
padding: 5px 8px;
border: none;
cursor: pointer;
margin-left: 10px;
border-radius: 3px;
}
.delete-skill-button img {
width: 15px;
height: 15px;
}
.add-skill-button{
background-color: #06a63b;
color: #fff;
padding: 8px 10px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-left: 10px;
}

View file

@ -0,0 +1,78 @@
<form [formGroup]="mitarbeiterForm">
<div class="container">
<button (click)="returnToEmployeeOverview()" class="back-button">Back</button>
<div class="user-info">
<div class="form-group">
@if (errorMessages['firstName']) {
<div class="alert alert-danger">{{errorMessages['firstName']}}</div>
}
<label for="firstName">First Name</label>
<input type="text" id="firstName" placeholder="First Name" formControlName="firstName">
</div>
<div class="form-group">
@if (errorMessages['lastName']) {
<div class="alert alert-danger">{{errorMessages['lastName']}}</div>
}
<label for="lastName">Last Name</label>
<input type="text" id="lastName" placeholder="Last Name" formControlName="lastName">
</div>
<div class="form-group">
@if (errorMessages['street']) {
<div class="alert alert-danger">{{errorMessages['street']}}</div>
}
<label for="street">Street</label>
<input type="text" id="street" placeholder="Street" formControlName="street">
</div>
<div class="form-group">
@if (errorMessages['postcode']) {
<div class="alert alert-danger">{{errorMessages['postcode']}}</div>
}
<label for="postcode">Postcode</label>
<input type="text" id="postcode" placeholder="Postcode" formControlName="postcode">
</div>
<div class="form-group">
@if (errorMessages['city']) {
<div class="alert alert-danger">{{errorMessages['city']}}</div>
}
<label for="city">City</label>
<input type="text" id="city" placeholder="City" formControlName="city">
</div>
<div class="form-group">
@if (errorMessages['phone']) {
<div class="alert alert-danger">{{errorMessages['phone']}}</div>
}
<label for="phone">Phone Number</label>
<input type="text" id="phone" placeholder="Phone Number" formControlName="phone">
</div>
</div>
<div class="skills-container">
<h2>Skills</h2>
<ul class="skill-list">
@for (skill of mitarbeiter.skillSet; track skill) {
<li>
<span class="skill-name">{{skill.skill}}</span>
<button (click)="removeSkill(skill.id)" class="delete-skill-button">
<img src="Delete-button.svg" alt="Delete">
</button>
</li>
}
</ul>
@if (!hasAllSkills) {
<div class="skill-controls">
<select formControlName="newSkill">
@for (skill of allSkills | async; track skill) {
@if (!hasSkill(skill.id)) {
<option value="{{skill.id}}">{{skill.skill}}</option>
}
}
</select>
<button (click)="addSkill()" class="add-skill-button">Add qualification</button>
</div>
}
</div>
<button (click)="submit()" class="save-button">Save</button>
</div>
</form>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MitarbeiterFormComponent } from './mitarbeiter-form.component';
describe('MitarbeiterFormComponent', () => {
let component: MitarbeiterFormComponent;
let fixture: ComponentFixture<MitarbeiterFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MitarbeiterFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MitarbeiterFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,133 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { EmployeeNameAndSkillDataDTO, EmployeeRequestPutDTO, EmployeeResponseDTO } from '../../models/mitarbeiter';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { AsyncPipe, NgFor, Location } from '@angular/common';
import { SkillService } from '../../service/skill.service';
import { QualificationGetDTO } from '../../models/skill';
import { Observable, of } from 'rxjs';
@Component({
selector: 'app-mitarbeiter-form',
standalone: true,
imports: [ReactiveFormsModule, NgFor, AsyncPipe],
templateUrl: './mitarbeiter-form.component.html',
styleUrl: './mitarbeiter-form.component.css'
})
export class MitarbeiterFormComponent {
@Input() mitarbeiter!: EmployeeResponseDTO;
@Output() mitarbeiterChange = new EventEmitter<EmployeeResponseDTO>();
public mitarbeiterForm!: FormGroup;
public allSkills: Observable<Array<QualificationGetDTO>> = of([]);
public hasAllSkills: boolean = false;
errorMessages: Record<string, string> = {};
constructor(public http: HttpClient, public router: Router, private skillService: SkillService, private location: Location) {}
returnToEmployeeOverview() {
this.location.back();
}
private setupForm() {
this.mitarbeiterForm = new FormGroup({
lastName: new FormControl(this.mitarbeiter.lastName, Validators.required),
firstName: new FormControl(this.mitarbeiter.firstName, Validators.required),
street: new FormControl(this.mitarbeiter.street, Validators.required),
postcode: new FormControl(this.mitarbeiter.postcode, [Validators.required, Validators.minLength(5), Validators.maxLength(5)]),
city: new FormControl(this.mitarbeiter.city, Validators.required),
phone: new FormControl(this.mitarbeiter.phone, [Validators.required, Validators.pattern('^[- +()0-9]+$')]),
newSkill: new FormControl(null)
});
}
ngOnChanges(): void {
this.setupForm();
}
skillsChanged() {
this.allSkills.subscribe(skills => {
this.hasAllSkills = this.checkAllSkills(skills);
});
}
ngOnInit(): void {
this.allSkills = this.skillService.getAllSkills();
this.skillsChanged();
this.setupForm();
}
removeSkill(id?: number) {
this.mitarbeiter.skillSet = this.mitarbeiter.skillSet?.filter(skill => skill.id !== id);
this.skillsChanged();
}
checkAllSkills(skills: Array<QualificationGetDTO>): boolean {
const skillSet = this.mitarbeiter.skillSet || [];
return skills.every(skill =>
skillSet.some(givenSkill => skill.id === givenSkill.id)
);
}
hasSkill(id: number): boolean {
for (const skill of this.mitarbeiter.skillSet || []) {
if (skill.id == id) {
return true;
}
}
return false;
}
addSkill() {
const id = Number(this.mitarbeiterForm.get("newSkill")?.value);
if (!id) {
return;
}
this.allSkills.subscribe(skills => {
const newSkill = skills.filter(skill => skill.id == id)[0];
this.mitarbeiter.skillSet?.push(newSkill);
this.skillsChanged();
this.mitarbeiterForm.get("newSkill")?.reset(null); // Added
});
}
private validationErrorMessages: Record<string, string> = {
required: "This field is required",
minlength: "The value is too short",
maxlength: "The value is too long",
pattern: "This field must be a valid phone number",
};
updateErrorMessages(): void {
this.errorMessages = {};
Object.keys(this.mitarbeiterForm.controls).forEach(field => {
const control = this.mitarbeiterForm.get(field);
if (control && control.errors) {
this.errorMessages[field] = Object.keys(control.errors)
.map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`)
.join(' ');
}
});
}
submit() {
this.updateErrorMessages();
if (!this.mitarbeiterForm.valid) {
return;
}
this.mitarbeiter.firstName = this.mitarbeiterForm.get("firstName")?.value;
this.mitarbeiter.lastName = this.mitarbeiterForm.get("lastName")?.value;
this.mitarbeiter.street = this.mitarbeiterForm.get("street")?.value;
this.mitarbeiter.postcode = this.mitarbeiterForm.get("postcode")?.value;
this.mitarbeiter.city = this.mitarbeiterForm.get("city")?.value;
this.mitarbeiter.phone = this.mitarbeiterForm.get("phone")?.value;
this.mitarbeiterChange.emit(this.mitarbeiter);
}
}

View file

@ -19,7 +19,7 @@ h1 {
.search-bar { .search-bar {
display: flex; display: flex;
margin-bottom: 20px; margin-bottom: 10px;
align-items: center; align-items: center;
} }

View file

@ -1,49 +1,52 @@
<div class="container"> <div class="d-flex flex-row">
<div class="header"> <app-navigation-bar [route]="'employee'" class="row" style="height: 100vh;"></app-navigation-bar>
<div class="dropdown position-absolute top-0 end-0 m-3"> <div class="container">
<button <div class="header">
class="btn align-items-center d-flex" <div class="dropdown position-absolute top-0 end-0 m-3">
type="button" <button class="btn align-items-center d-flex" type="button" id="userDropdown" data-bs-toggle="dropdown"
id="userDropdown" aria-expanded="false">
data-bs-toggle="dropdown" <img src="user.svg" alt="User Icon" class="rounded-circle" style="width: 30px; height: 30px;">
aria-expanded="false" </button>
> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<img <li><a class="dropdown-item" href="/logout">Log out</a></li>
src="user.svg" </ul>
alt="User Icon" </div>
class="rounded-circle" <h1>Employees</h1>
style="width: 30px; height: 30px;"
>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item" href="/logout">Log out</a>
</li>
</ul>
</div> </div>
<h1>Employees</h1>
</div>
<div class="header-actions">
<div class="search-bar">
<input type="text" placeholder="Search employee">
<button>Search</button>
</div>
<button class="add-button">Add employee</button>
</div>
<table class="employee-table"> <div class="header-actions">
<thead> <form [formGroup]="searchForm">
<tr> <div class="search-bar">
<th><span class="sortable">First Name</span></th> <input type="text" placeholder="Search employee" formControlName="search">
<th><span class="sortable">Last Name</span></th> <button (click)="submit()">Search</button>
<th>Street</th> </div>
<th>Postcode</th> <p class="text-body-tertiary">Search for a propertiy of an Employee. eg. First Name</p>
<th>City</th> </form>
<th>Phone</th> <button (click)="createEmployee()" class="add-button">Add employee</button>
<th>Actions</th> </div>
</tr>
</thead> <table class="employee-table">
<tbody> <thead>
</tbody> <tr>
</table> <th><span class="sortable">First Name</span></th>
<th><span class="sortable">Last Name</span></th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if (employees) {
@for (employee of employees; track employee) {
<tr>
<td>{{ employee.firstName }}</td>
<td>{{ employee.lastName }}</td>
<td>
<button class="btn btn-primary me-2" (click)="goToEmployeeDetailPage(employee.id)">Details</button>
<button class="btn btn-danger" (click)="deleteEmployee(employee.id)">Delete</button>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div> </div>

View file

@ -1,12 +1,75 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable, of } from 'rxjs';
import { EmployeeResponseDTO } from '../../models/mitarbeiter';
import { EmployeeService } from '../../service/employee.service';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { NavigationBarComponent } from '../navigation-bar/navigation-bar.component';
@Component({ @Component({
selector: 'app-mitarbeiterverwaltung-view', selector: 'app-mitarbeiterverwaltung-view',
standalone: true,
imports: [],
templateUrl: './mitarbeiterverwaltung-view.component.html', templateUrl: './mitarbeiterverwaltung-view.component.html',
styleUrl: './mitarbeiterverwaltung-view.component.css' styleUrls: ['./mitarbeiterverwaltung-view.component.css'],
standalone: true,
imports: [CommonModule, ReactiveFormsModule, NavigationBarComponent]
}) })
export class MitarbeiterverwaltungViewComponent { export class MitarbeiterverwaltungViewComponent implements OnInit {
employees: Array<EmployeeResponseDTO> = [];
searchForm!: FormGroup;
constructor(private employeeService: EmployeeService, private router: Router) {
this.updateEmployees();
}
submit() {
const searchTerm = this.searchForm.get("search")?.value || '';
this.employeeService.getAllEmployees().subscribe(employees => {
let foundEmployees: Array<EmployeeResponseDTO> = [];
for (const employee of employees) {
if (
employee.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.street.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.postcode.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.city.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.phone.toLowerCase().includes(searchTerm.toLowerCase()) ||
searchTerm == ''
) {
foundEmployees.push(employee);
}
}
this.employees = foundEmployees;
});
}
editEmployee(id: number) {
this.router.navigate([`/mitarbeiterbearbeiten/${id}`]);
}
createEmployee() {
this.router.navigate(['/mitarbeitererstellen']);
}
deleteEmployee(id: number) {
this.employeeService.deleteEmployee(id);
this.employees = this.employees.filter(employee => employee.id != id);
}
goToEmployeeDetailPage(id: number){
this.router.navigate([`/mitarbeiterdetails/${id}`]);
}
updateEmployees() {
this.employeeService.getAllEmployees().subscribe(employees => this.employees = employees);
}
ngOnInit(): void {
this.searchForm = new FormGroup({
search: new FormControl(''),
});
this.updateEmployees();
}
} }

View file

@ -5,18 +5,13 @@
</a> </a>
<hr> <hr>
<ul class="nav nav-pills flex-column mb-auto"> <ul class="nav nav-pills flex-column mb-auto">
<li class="nav-item">
<a href="#" class="nav-link active text-black" aria-current="page">
Dashboard
</a>
</li>
<li> <li>
<a href="#" class="nav-link text-black"> <a [routerLink]="['/mitarbeiter']" (click)="employees()" [class.active]="route == 'employee'" class="nav-link text-black" aria-current="page">
Mitarbeiterverwaltung Mitarbeiterverwaltung
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="nav-link text-black"> <a [routerLink]="['/qualifikationsverwaltung']" [class.active]="route == 'skill'" class="nav-link text-black">
Qualifikationsverwaltung Qualifikationsverwaltung
</a> </a>
</li> </li>

View file

@ -1,12 +1,18 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@Component({ @Component({
selector: 'app-navigation-bar', selector: 'app-navigation-bar',
standalone: true, standalone: true,
imports: [], imports: [RouterLink],
templateUrl: './navigation-bar.component.html', templateUrl: './navigation-bar.component.html',
styleUrl: './navigation-bar.component.css' styleUrl: './navigation-bar.component.css'
}) })
export class NavigationBarComponent { export class NavigationBarComponent {
constructor(private router: Router) { }
@Input() route: string = "";
employees() {
this.router.navigate(['mitarbeiter']);
}
} }

View file

@ -0,0 +1 @@
<app-qualifikation-form [(skill)]="skill" (skillChange)="submitted($event)"></app-qualifikation-form>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QulifikationErstellenComponent } from './qulifikation-erstellen.component';
describe('QulifikationErstellenComponent', () => {
let component: QulifikationErstellenComponent;
let fixture: ComponentFixture<QulifikationErstellenComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [QulifikationErstellenComponent]
})
.compileComponents();
fixture = TestBed.createComponent(QulifikationErstellenComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { QualifikationFormComponent } from '../qualifikation-form/qualifikation-form.component';
import { QualificationGetDTO } from '../../models/skill';
import { SkillService } from '../../service/skill.service';
@Component({
selector: 'app-qulifikation-erstellen',
standalone: true,
imports: [QualifikationFormComponent],
templateUrl: './qualifikation-erstellen.component.html',
styleUrl: './qualifikation-erstellen.component.css'
})
export class QulifikationErstellenComponent {
public skill!: QualificationGetDTO;
constructor(private skillService: SkillService) { }
submitted(skill: QualificationGetDTO) {
console.log(skill);
this.skillService.createSkill(skill);
}
ngOnInit(): void {
this.skill = {
id: 0,
skill: '',
};
}
}

View file

@ -0,0 +1,131 @@
body {
font-family: sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
}
h1, h2 {
font-size: 1.8rem;
margin-bottom: 15px;
}
.back-button {
background-color: #ccc;
color: #333;
border: none;
padding: 8px 12px;
border-radius: 3px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 1px;
}
.employee-list {
list-style: none;
padding: 0;
}
.employee-list li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.employee-icon {
margin-right: 5px;
}
.add-employee-section {
margin-top: 15px;
}
.add-employee-section select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
margin-right: 10px;
}
.add-employee-button {
background-color: #06a63b;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-top: 10px;
}
.save-button {
background-color: #007bff;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-top: 20px;
display: block;
width: 100%;
}
.delete-skill-button {
color: #fff;
padding: 5px 8px;
border: none;
cursor: pointer;
margin-left: 4px;
margin-right: 4px;
border-radius: 3px;
}
.delete-skill-button img {
width: 15px;
height: 15px;
}
.add-employee-section {
margin-top: 15px;
}
.add-employee-section label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.add-employee-section input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
.employee-container {
border: 1px solid #ccc;
padding: 20px;
border-radius: 1px;
margin-bottom: 20px;
}

View file

@ -0,0 +1,41 @@
<div class="container">
<form [formGroup]="skillForm">
<button (click)="returnToSkillList()" class="back-button">Back</button>
<div class="form-group">
@if (errorMessages['name']) {
<div class="alert alert-danger">{{errorMessages['name']}}</div>
}
<label for="name">Name</label>
<input type="text" id="name" formControlName="name">
</div>
<div class="employee-container">
<h2>Employees possessing the qualification</h2>
<ul class="employee-list">
@for (employee of addedEmployees; track employee) {
<li>
<button (click)="removeEmployee(employee.id)" class="delete-skill-button">
<img src="Delete-button.svg" alt="Delete">
</button>
<span class="employee-name">{{employee.firstName}} {{employee.lastName}}</span>
</li>
}
</ul>
@if (addableEmployees.length > 0) {
<div class="add-employee-section">
<select formControlName="newEmployee">
@for (employee of addableEmployees; track employee) {
<option value="{{employee.id}}">{{employee.firstName}} {{employee.lastName}}</option>
}
</select>
<button (click)="addEmployee()" class="add-employee-button">Add employee</button>
</div>
}
</div>
<button (click)="submit()" class="save-button">Save</button>
</form>
</div>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QualifikationFormComponent } from './qualifikation-form.component';
describe('QualifikationFormComponent', () => {
let component: QualifikationFormComponent;
let fixture: ComponentFixture<QualifikationFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [QualifikationFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(QualifikationFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,133 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { EmployeesForAQualificationDTO, QualificationGetDTO } from '../../models/skill';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { SkillService } from '../../service/skill.service';
import { EmployeeService } from '../../service/employee.service';
import { concatMap, every, from, lastValueFrom, Observable, of, switchMap, take, tap } from 'rxjs';
import { EmployeeNameDataDTO, EmployeeResponseDTO } from '../../models/mitarbeiter';
import { AsyncPipe } from '@angular/common';
import { Router } from '@angular/router';
@Component({
selector: 'app-qualifikation-form',
standalone: true,
imports: [ReactiveFormsModule, AsyncPipe],
templateUrl: './qualifikation-form.component.html',
styleUrl: './qualifikation-form.component.css'
})
export class QualifikationFormComponent {
@Input() skill!: QualificationGetDTO;
@Output() skillChange = new EventEmitter<QualificationGetDTO>();
public skillForm!: FormGroup;
public addableEmployees: Array<EmployeeResponseDTO> = [];
public addedEmployees: Array<EmployeeNameDataDTO> = [];
errorMessages: Record<string, string> = {};
constructor(private skillService: SkillService, private employeeService: EmployeeService, private router: Router) { }
private validationErrorMessages: Record<string, string> = {
required: "This field is required",
};
updateErrorMessages(): void {
this.errorMessages = {};
Object.keys(this.skillForm.controls).forEach(field => {
const control = this.skillForm.get(field);
if (control && control.errors) {
this.errorMessages[field] = Object.keys(control.errors)
.map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`)
.join(' ');
}
});
}
setUpForm() {
this.skillForm = new FormGroup({
name: new FormControl(this.skill.skill, Validators.required),
newEmployee: new FormControl(),
});
}
addEmployee() {
const employeeId = Number(this.skillForm.get("newEmployee")?.value);
if (!employeeId) {
return;
}
const employee = this.addableEmployees.find(emp => emp.id === employeeId);
if (employee) {
this.addableEmployees = this.addableEmployees.filter(emp => emp.id !== employeeId);
this.addedEmployees.push(employee);
}
}
removeEmployee(employeeId: number) {
const employee = this.addedEmployees.find(emp => emp.id === employeeId);
if (employee) {
this.addedEmployees = this.addedEmployees.filter(emp => emp.id !== employeeId);
this.employeeService.getEmployeeById(employee.id).subscribe(employeeDto => {
this.addableEmployees.push(employeeDto);
});
}
}
returnToSkillList() {
this.router.navigate(["qualifikationsverwaltung"]);
}
updateEmployeeLists() {
if (this.skill.id != -1) {
this.employeeService.getAllEmployees().subscribe(employees => {
this.addableEmployees = employees;
this.skillService.getEmployeesBySkill(this.skill.id).subscribe(addedEmployeesResponse => {
this.addedEmployees = addedEmployeesResponse.employees;
this.addableEmployees = this.addableEmployees.filter(employee => {
return !this.addedEmployees.some(added => added.id === employee.id);
});
});
});
}
}
submit() {
this.updateErrorMessages();
if (!this.skillForm.valid) {
return;
}
for (const employee of this.addedEmployees) {
this.employeeService.getEmployeeById(employee.id).subscribe(employeeResponse => {
this.employeeService.addSkillToEmployee(this.skill.id, employeeResponse);
});
}
for (const employee of this.addableEmployees) {
this.employeeService.removeSkillFromEmployee(this.skill.id, employee.id);
}
this.skill.skill = this.skillForm.get("name")?.value;
this.skillChange.emit(this.skill);
this.router.navigate(["/qualifikationsverwaltung"]);
}
ngOnChanges(): void {
this.setUpForm();
this.updateEmployeeLists();
}
ngOnInit() {
this.setUpForm();
this.updateEmployeeLists();
}
}

View file

@ -0,0 +1,90 @@
body {
font-family: sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
width: 100%;
margin: 0 auto;
background-color: #fff;
padding: 20px;
}
h1 {
font-size: 2rem;
margin-bottom: 20px;
}
.search-bar {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.search-bar input[type="text"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
flex-grow: 1;
margin-right: 10px;
}
.search-bar button {
padding: 10px 15px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
.add-button {
background-color: #07af16;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
float: left;
margin-bottom: 10px;
}
.employee-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.employee-table th,
.employee-table td {
padding: 12px 15px;
border: 1px solid #ddd;
text-align: left;
}
.employee-table th {
background-color: #f0f0f0;
font-weight: bold;
}
.sortable {
cursor: pointer;
}
.logout-button {
background-color: transparent;
border: none;
cursor: pointer;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
float: right;
}
.logout-button img {
width: 20px;
height: 20px;
}

View file

@ -0,0 +1,60 @@
<div class="d-flex flex-row">
<app-navigation-bar [route]="'skill'" class="row" style="height: 100vh;"></app-navigation-bar>
<div class="container">
<div class="header">
<div class="dropdown position-absolute top-0 end-0 m-3">
<button class="btn align-items-center d-flex" type="button" id="userDropdown" data-bs-toggle="dropdown"
aria-expanded="false">
<img src="user.svg" alt="User Icon" class="rounded-circle" style="width: 30px; height: 30px;">
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="/logout">Log out</a></li>
</ul>
</div>
<h1>Qualifications</h1>
</div>
<div class="d-flex flex-column">
<div class="row header-actions">
<form [formGroup]="searchForm">
<div class="search-bar">
<input type="text" placeholder="Search employee" formControlName="search">
<button (click)="submit()">Search</button>
</div>
<p class="text-body-tertiary">Search for a propertiy of a Qualification. eg. Name</p>
</form>
<div class="d-flex">
<button (click)="createSkill()" class="add-button me-auto">Add qualification</button>
</div>
</div>
@if (errorDeletingSkill) {
<div class="row">
<p class="alert alert-danger">This Qualification can not be deleted because it still has employees.</p>
</div>
}
</div>
<table class="employee-table">
<thead>
<tr>
<th><span class="sortable">Name</span></th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if (skills) {
@for (skill of skills; track skill) {
<tr>
<td>{{ skill.skill }}</td>
<td>
<button class="btn btn-primary me-2" (click)="editSkill(skill.id)">Edit</button>
<button class="btn btn-danger" (click)="deleteSkill(skill.id)">Delete</button>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QualifikationsverwaltungComponent } from './qualifikationsverwaltung.component';
describe('QualifikationsverwaltungComponent', () => {
let component: QualifikationsverwaltungComponent;
let fixture: ComponentFixture<QualifikationsverwaltungComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [QualifikationsverwaltungComponent]
})
.compileComponents();
fixture = TestBed.createComponent(QualifikationsverwaltungComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,66 @@
import { Component } from '@angular/core';
import { QualificationGetDTO } from '../../models/skill';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { SkillService } from '../../service/skill.service';
import { Router } from '@angular/router';
import { NavigationBarComponent } from '../navigation-bar/navigation-bar.component';
@Component({
selector: 'app-qualifikationsverwaltung',
standalone: true,
imports: [ReactiveFormsModule, NavigationBarComponent],
templateUrl: './qualifikationsverwaltung.component.html',
styleUrl: './qualifikationsverwaltung.component.css'
})
export class QualifikationsverwaltungComponent {
skills: Array<QualificationGetDTO> = [];
public searchForm!: FormGroup;
public errorDeletingSkill = false;
constructor(private skillService: SkillService, private router: Router) { }
submit() {
const searchTerm = this.searchForm.get("search")?.value || '';
this.skillService.getAllSkills().subscribe(skills => {
let foundSkills: Array<QualificationGetDTO> = [];
for (const skill of skills) {
if (
skill.skill.toLowerCase().includes(searchTerm.toLowerCase()) ||
searchTerm == ''
) {
foundSkills.push(skill);
}
}
this.skills = foundSkills;
});
}
editSkill(id: number) {
this.router.navigate([`/qualifikationbearbeiten/${id}`]);
}
createSkill() {
this.router.navigate(['/qualifikationerstellen']);
}
deleteSkill(id: number) {
this.skillService.getEmployeesBySkill(id).subscribe(employees => {
this.errorDeletingSkill = false;
if (employees.employees.length == 0) {
this.skillService.deleteSkill(id);
this.skills = this.skills.filter(skill => skill.id != id);
} else {
this.errorDeletingSkill = true;
}
});
}
ngOnInit(): void {
this.searchForm = new FormGroup({
search: new FormControl(''),
});
this.skillService.getAllSkills().subscribe(skills => this.skills = skills);
}
}

View file

@ -1,131 +0,0 @@
body {
font-family: sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
}
h1, h2 {
font-size: 1.8rem;
margin-bottom: 15px;
}
.back-button {
background-color: #ccc;
color: #333;
border: none;
padding: 8px 12px;
border-radius: 3px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 1px;
}
.employee-list {
list-style: none;
padding: 0;
}
.employee-list li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.employee-icon {
margin-right: 5px;
}
.add-employee-section {
margin-top: 15px;
}
.add-employee-section select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
margin-right: 10px;
}
.add-employee-button {
background-color: #06a63b;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-top: 10px;
}
.save-button {
background-color: #007bff;
color: #fff;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-top: 20px;
display: block;
width: 100%;
}
.delete-skill-button {
color: #fff;
padding: 5px 8px;
border: none;
cursor: pointer;
margin-left: 4px;
margin-right: 4px;
border-radius: 3px;
}
.delete-skill-button img {
width: 15px;
height: 15px;
}
.add-employee-section {
margin-top: 15px;
}
.add-employee-section label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.add-employee-section input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
.employee-container {
border: 1px solid #ccc;
padding: 20px;
border-radius: 1px;
margin-bottom: 20px;
}

View file

@ -1,36 +1 @@
<div class="container"> <app-qualifikation-form [(skill)]="skill" (skillChange)="submitted($event)"></app-qualifikation-form>
<button class="back-button">Back</button>
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" value="(Hier kommt name der gewählten qualification hin)">
</div>
<div class="employee-container">
<h2>Employees possessing the qualification</h2>
<ul class="employee-list">
<li>
<button class="delete-skill-button">
<img src="Delete-button.svg" alt="Delete">
</button>
<span class="employee-name">Max Mustermann</span>
</li>
<li>
<button class="delete-skill-button">
<img src="Delete-button.svg" alt="Delete">
</button>
<span class="employee-name">Mehdi Boudjoudi</span>
</li>
</ul>
<div class="add-employee-section">
<label for="employeeSearch">Search for employee</label>
<input type="text" id="employeeSearch" placeholder="Last name of employee">
<button class="add-employee-button">Add employee</button>
</div>
</div>
<button class="save-button">Save</button>
</div>

View file

@ -1,12 +1,38 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { QualifikationFormComponent } from '../qualifikation-form/qualifikation-form.component';
import { SkillService } from '../../service/skill.service';
import { FormGroup } from '@angular/forms';
import { QualificationGetDTO } from '../../models/skill';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'app-qualifikaton-bearbeiten-view', selector: 'app-qualifikaton-bearbeiten-view',
standalone: true, standalone: true,
imports: [], imports: [QualifikationFormComponent],
templateUrl: './qualifikaton-bearbeiten-view.component.html', templateUrl: './qualifikaton-bearbeiten-view.component.html',
styleUrl: './qualifikaton-bearbeiten-view.component.css' styleUrl: './qualifikaton-bearbeiten-view.component.css'
}) })
export class QualifikatonBearbeitenViewComponent { export class QualifikatonBearbeitenViewComponent {
public skill!: QualificationGetDTO;
constructor(private skillService: SkillService, private route: ActivatedRoute) { }
submitted(skill: QualificationGetDTO) {
this.skillService.updateSkill(skill);
}
ngOnInit(): void {
this.skill = {
id: -1,
skill: '',
};
this.skillService.getAllSkills().subscribe(skills => {
for (const skill of skills) {
if (skill.id == Number(this.route.snapshot.params['id'])) {
this.skill = skill;
}
}
});
}
} }

View file

@ -0,0 +1,4 @@
export class Environment {
public static readonly BASE_URL = "http://employee:8089";
}

View file

@ -0,0 +1,4 @@
export class Environment {
public static readonly BASE_URL = "http://127.0.0.1:8089";
}

View file

@ -0,0 +1,47 @@
import { QualificationGetDTO, QualificationPostDTO } from "./skill";
export interface EmployeeRequestPutDTO {
lastName: string,
firstName: string,
street: string,
postcode: string,
city: string,
phone: string,
skillSet: Array<number>,
}
export interface EmployeeResponseDTO {
id: number,
lastName: string,
firstName: string,
street: string,
postcode: string,
city: string,
phone: string,
skillSet?: Array<QualificationGetDTO>,
}
export interface EmployeeRequestDTO {
lastName: string,
firstName: string,
street: string,
postcode: string,
city: string,
phone: string,
skillSet?: Array<number>,
}
export interface EmployeeNameAndSkillDataDTO {
id: number,
lastName: string,
firstName: string,
skillSet: Array<QualificationPostDTO>,
}
export interface EmployeeNameDataDTO {
id: number,
lastName: string,
firstName: string,
}

15
src/app/models/skill.ts Normal file
View file

@ -0,0 +1,15 @@
import { EmployeeNameDataDTO } from "./mitarbeiter";
export interface QualificationGetDTO {
id: number,
skill: string,
}
export interface QualificationPostDTO {
skill: string,
}
export interface EmployeesForAQualificationDTO {
qualification: QualificationGetDTO,
employees: Array<EmployeeNameDataDTO>,
}

View file

@ -0,0 +1,76 @@
import { Injectable } from "@angular/core";
import { EmployeeRequestDTO, EmployeeRequestPutDTO, EmployeeResponseDTO } from "../models/mitarbeiter";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { SkillService } from "./skill.service";
@Injectable({
providedIn: 'root'
})
export class EmployeeService {
constructor(private http: HttpClient) { }
responseToRequestDto(employee: EmployeeResponseDTO): EmployeeRequestDTO {
return {
firstName: employee.firstName,
lastName: employee.lastName,
street: employee.street,
postcode: employee.postcode,
city: employee.city,
phone: employee.phone,
skillSet: employee.skillSet?.map(skill => skill.id) || [],
}
}
responseDtoToPutDto(employee: EmployeeResponseDTO): EmployeeRequestPutDTO {
return {
firstName: employee.firstName,
lastName: employee.lastName,
street: employee.street,
postcode: employee.postcode,
city: employee.city,
phone: employee.phone,
skillSet: employee.skillSet?.map(skill => skill.id) || [],
}
}
createEmployee(employee: EmployeeResponseDTO) {
this.http.post(`${SkillService.BASE_URL}/employees`, this.responseToRequestDto(employee)).subscribe();
}
updateEmployee(employee: EmployeeResponseDTO) {
this.http.put(`${SkillService.BASE_URL}/employees/${employee.id}`, this.responseDtoToPutDto(employee)).subscribe();
}
getAllEmployees(): Observable<Array<EmployeeResponseDTO>> {
return this.http.get<Array<EmployeeResponseDTO>>(`${SkillService.BASE_URL}/employees`);
}
deleteEmployee(id: number) {
this.http.delete(`${SkillService.BASE_URL}/employees/${id}`).subscribe();
}
getEmployeeById(id: number): Observable<EmployeeResponseDTO> {
return this.http.get<EmployeeResponseDTO>(`${SkillService.BASE_URL}/employees/${id}`);
}
removeSkillFromEmployee(skillId: number, employeeId: number) {
this.getEmployeeById(employeeId).subscribe(employee => {
let employeePut = this.responseDtoToPutDto(employee);
if (employeePut.skillSet.indexOf(skillId) != 1) {
employeePut.skillSet = employeePut.skillSet.filter(skill => skill != skillId);
}
this.http.put(`${SkillService.BASE_URL}/employees/${employee.id}`, employeePut).subscribe();
});
}
addSkillToEmployee(skillId: number, employee: EmployeeResponseDTO) {
let employeePut = this.responseDtoToPutDto(employee);
if (employeePut.skillSet.indexOf(skillId) == -1) {
employeePut.skillSet.push(skillId);
}
this.http.put(`${SkillService.BASE_URL}/employees/${employee.id}`, employeePut).subscribe();
}
}

View file

@ -0,0 +1,55 @@
import {HttpClient} from "@angular/common/http";
import {Injectable} from "@angular/core";
import {EmployeesForAQualificationDTO, QualificationGetDTO, QualificationPostDTO} from "../models/skill";
import {Observable} from "rxjs";
import {EmployeeNameAndSkillDataDTO, EmployeeNameDataDTO} from "../models/mitarbeiter";
import {EmployeeService} from "./employee.service";
import {Environment} from "../environments/environment";
@Injectable({
providedIn: 'root'
})
export class SkillService {
deleteSkill(id: number) {
this.getEmployeesBySkill(id).subscribe(employees => {
for (let employee of employees.employees) {
this.employeeService.removeSkillFromEmployee(id, employee.id);
}
});
this.http.delete(`${SkillService.BASE_URL}/qualifications/${id}`).subscribe();
}
public static readonly BASE_URL = Environment.BASE_URL;
getToPutDto(skill: QualificationGetDTO): QualificationPostDTO {
return {
skill: skill.skill,
}
}
createSkill(skill: QualificationGetDTO) {
this.http.post(`${SkillService.BASE_URL}/qualifications`, this.getToPutDto(skill)).subscribe();
}
constructor(private http: HttpClient, private employeeService: EmployeeService) {
}
updateSkill(skill: QualificationGetDTO) {
this.http.put(`${SkillService.BASE_URL}/qualifications/${skill.id}`, this.getToPutDto(skill)).subscribe();
}
getAllSkills(): Observable<Array<QualificationGetDTO>> {
return this.http.get<Array<QualificationGetDTO>>(`${SkillService.BASE_URL}/qualifications`);
}
getEmployeesBySkill(id: number): Observable<EmployeesForAQualificationDTO> {
return this.http.get<EmployeesForAQualificationDTO>(`${SkillService.BASE_URL}/qualifications/${id}/employees`);
}
getSkillsOfEmployee(id: number): Observable<EmployeeNameAndSkillDataDTO> {
return this.http.get<EmployeeNameAndSkillDataDTO>(`${SkillService.BASE_URL}/employees/${id}/qualifications`);
}
}

View file

@ -1,6 +1,7 @@
import { HttpClientModule } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig) bootstrapApplication(AppComponent , appConfig,)
.catch((err) => console.error(err)); .catch((err) => console.error(err));

46
tests/login.spec.ts Normal file
View file

@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test';
import { async } from 'rxjs';
test('LoginPageShouldRender', async ({ page }) => {
await page.goto('http://localhost:4200');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/EmployeeService/);
});
test('LoginPageShouldHaveCorrectHeading', async ({ page }) => {
await page.goto('http://localhost:4200');
const heading = page.getByRole('heading');
await expect(heading).toHaveText('Hi-Tec GmbH');
});
test('LoginPageShouldHaveLoginButton', async ({ page }) => {
await page.goto('http://localhost:4200');
const button = page.getByRole('button');
await expect(button).toHaveText('Login with Keycloak');
});
test('LoginPageButtonShouldRedirectToKeycloak', async ({ page }) => {
await page.goto('http://localhost:4200');
const button = page.getByText("Login with Keycloak");
await button.click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
expect(page.url()).toContain("keycloak.szut.dev");
});
test('AfterLoginUserShouldBeRedirectedToEmployees', async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
expect(page.url()).toContain('localhost');
expect(page.url()).toContain('mitarbeiter');
});

50
tests/mitarbeiter.spec.ts Normal file
View file

@ -0,0 +1,50 @@
import { test, expect } from "@playwright/test";
test.describe('mitarbeiter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/mitarbeiter');
});
test('ShouldLoad', async ({ page }) => {
await expect(page.getByRole('heading')).toHaveText("Employees");
});
test('ShouldLoadEmployees', async ({ page }) => {
expect(page.getByText('Max')).toBeTruthy();
});
test('AddEmployeeShouldRedirect', async ({ page }) => {
await page.getByText('Add employee').click();
expect(page.url()).toContain('mitarbeitererstellen');
});
test('DeleteShouldBeThere', async ({ page }) => {
const button = page.getByText('Delete').first();
const users = page.getByText('Delete');
await users.first().waitFor({ state: "visible" });
expect(await users.count()).toBeGreaterThan(1);
expect(button).toBeTruthy();
});
test('SearchShouldWork', async ({ page }) => {
const searchField = page.getByRole('textbox');
const searchButton = page.getByText('Search').first();
await searchField.fill('Max');
await searchButton.click();
const hiddenItem = page.getByText('MusterFrau');
await expect(hiddenItem).toHaveCount(0);
});
});

View file

@ -0,0 +1,61 @@
import { test, expect } from "@playwright/test";
test.describe('mitarbeiter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/mitarbeitererstellen');
});
test('pageShouldLoad', async ({ page }) => {
await expect(page.getByText('Save')).toHaveCount(1);
});
test('backButtonShouldGoBack', async ({ page }) => {
await page.getByText('Back').click();
expect(page.url().includes('erstellen')).toBeFalsy();
});
test('EveryFieldShouldValidateEmptiness', async ({ page }) => {
await page.getByText('Save').click();
const errors = page.getByText('This field is required');
await expect(errors).toHaveCount(6);
});
test('PhoneNumberShouldBeValidated', async ({ page }) => {
await page.getByLabel('Phone').fill("asd");
await page.getByText('Save').click();
const error = page.getByText('This field must be a valid phone number');
await expect(error).toHaveCount(1);
});
test('PostCodeShouldValidateTooShort', async ({ page }) => {
await page.getByLabel('Postcode').fill("1");
await page.getByText('Save').click();
const error = page.getByText('The value is too short');
await expect(error).toHaveCount(1);
});
test('PostCodeShouldValidateTooLong', async ({ page }) => {
await page.getByLabel('Postcode').fill("123456");
await page.getByText('Save').click();
const error = page.getByText('The value is too long');
await expect(error).toHaveCount(1);
});
});

View file

@ -0,0 +1,24 @@
import { test, expect } from "@playwright/test";
test.describe('mitarbeiterbearbeiten', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/mitarbeiterbearbeiten/1');
});
test('ShouldLoad', async ({ page }) => {
expect(page.getByText("Save")).toBeTruthy();
});
test('FieldsShouldHaveValues', async ({page}) => {
await expect(page.getByLabel('First Name')).toHaveValue('Max');
});
});

View file

@ -0,0 +1,23 @@
import { test, expect } from "@playwright/test";
test.describe('qualifikationbearbeiten', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/qualifikationbearbeiten/1');
});
test('ShouldLoad', async ({ page }) => {
expect(page.getByText("Save")).toBeTruthy();
});
test('FieldsShouldHaveValues', async ({page}) => {
await expect(page.getByLabel('Name')).toHaveValue('Java');
});
});

View file

@ -0,0 +1,58 @@
import { test, expect } from "@playwright/test";
test.describe('qualifikationen', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4200');
await page.getByRole('button').click();
await page.waitForFunction(() => window.location.href.includes('keycloak'));
await page.getByLabel('Username or email').fill('user');
await page.getByLabel('Password').fill('test');
await page.click('#kc-login');
await page.goto('http://localhost:4200/qualifikationsverwaltung');
});
test('ShouldLoad', async ({ page }) => {
await expect(page.getByRole('heading')).toHaveText("Qualifications");
});
test('ShouldLoadQualifications', async ({ page }) => {
expect(page.getByText('Java')).toBeTruthy();
});
// TODO
// test('AddQualificationShouldRedirect', async ({ page }) => {
// await page.getByText('Add qualification').click();
// expect(page.url()).toContain('mitarbeitererstellen');
// });
test('EditShouldRedirectToCorrespondingPage', async ({ page }) => {
const button = page.getByText('Edit').first();
await button.click();
expect(page.url()).toContain('qualifikationbearbeiten');
expect(page.url()).toContain('1');
});
test('DeleteShouldBeThere', async ({ page }) => {
const button = page.getByText('Delete').first();
const users = page.getByText('Delete');
await users.first().waitFor({ state: "visible" });
expect(await users.count()).toBe(2);
expect(button).toBeTruthy();
});
test('SearchShouldWork', async ({ page }) => {
const searchField = page.getByRole('textbox');
const searchButton = page.getByText('Search').first();
await searchField.fill('Java');
await searchButton.click();
const hiddenItem = page.getByText('Angular');
await expect(hiddenItem).toHaveCount(0);
});
});