Qualifikationen Bearbeiten #67
					 11 changed files with 379 additions and 172 deletions
				
			
		|  | @ -32,8 +32,9 @@ export const routes: Routes = [ | |||
|     component: EmployeeDetailComponent, | ||||
|   }, | ||||
|   { | ||||
|     path: "qualifikationbearbeiten", | ||||
|     path: "qualifikationbearbeiten/:id", | ||||
|     component: QualifikatonBearbeitenViewComponent, | ||||
|     canActivate: [AuthGuard], | ||||
|   }, | ||||
|   { | ||||
|     path: "**", | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ export class MitarbeiterFormComponent { | |||
|     Object.keys(this.mitarbeiterForm.controls).forEach(field => { | ||||
|       const control = this.mitarbeiterForm.get(field); | ||||
| 
 | ||||
|       if (control && control.errors && control.touched) { | ||||
|       if (control && control.errors) { | ||||
|         this.errorMessages[field] = Object.keys(control.errors) | ||||
|           .map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`) | ||||
|           .join(' '); | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  | @ -0,0 +1,41 @@ | |||
| <div class="container"> | ||||
|   <form [formGroup]="skillForm"> | ||||
|     <button 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> | ||||
|  | @ -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(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,126 @@ | |||
| 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); | ||||
|     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); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
|     } | ||||
| 
 | ||||
|     this.skill.skill = this.skillForm.get("name")?.value; | ||||
|     this.skillChange.emit(this.skill); | ||||
| 
 | ||||
|     this.router.navigate(["/qualifikationen"]); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(): void { | ||||
|     this.setUpForm(); | ||||
|     this.updateEmployeeLists(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.setUpForm(); | ||||
|     this.updateEmployeeLists(); | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -1,36 +1 @@ | |||
| <div class="container"> | ||||
|   <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> | ||||
| 
 | ||||
| <app-qualifikation-form [(skill)]="skill" (skillChange)="submitted($event)"></app-qualifikation-form> | ||||
|  |  | |||
|  | @ -1,12 +1,38 @@ | |||
| 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({ | ||||
|   selector: 'app-qualifikaton-bearbeiten-view', | ||||
|   standalone: true, | ||||
|   imports: [], | ||||
|   imports: [QualifikationFormComponent], | ||||
|   templateUrl: './qualifikaton-bearbeiten-view.component.html', | ||||
|   styleUrl: './qualifikaton-bearbeiten-view.component.css' | ||||
| }) | ||||
| 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; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -54,10 +54,20 @@ export class EmployeeService { | |||
|     return this.http.get<EmployeeResponseDTO>(`${SkillService.BASE_URL}/employees/${id}`); | ||||
|   } | ||||
| 
 | ||||
|   removeSkillFromEmployee(skillId: number, employee: EmployeeResponseDTO) { | ||||
|     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(); | ||||
|   } | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| import { HttpClient } from "@angular/common/http"; | ||||
| import { Injectable } from "@angular/core"; | ||||
| import { QualificationGetDTO } from "../models/skill"; | ||||
| import { EmployeesForAQualificationDTO, QualificationGetDTO, QualificationPostDTO } from "../models/skill"; | ||||
| import { Observable } from "rxjs"; | ||||
| import { EmployeeNameDataDTO } from "../models/mitarbeiter"; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
|  | @ -10,11 +11,25 @@ export class SkillService { | |||
| 
 | ||||
|   public static readonly BASE_URL = "http://localhost:8089"; | ||||
| 
 | ||||
|   getToPutDto(skill: QualificationGetDTO): QualificationPostDTO { | ||||
|     return { | ||||
|       skill: skill.skill, | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   constructor(private http: HttpClient) { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   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`); | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Reference in a new issue