Feature: Employee Edit and Create #18
4 changed files with 329 additions and 3 deletions
|
@ -1,4 +1,11 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { DashboardComponent } from '@app/views/dashboard/dashboard.component';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
|
||||
export const routes: Routes = [{path: '', component: DashboardComponent, title: 'Home'}];
|
||||
import { EmployeeDetailComponent } from './views/employee-detail/employee-detail.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: DashboardComponent, title: 'Home' },
|
||||
{ path: 'employee/new', component: EmployeeDetailComponent, title: 'New Employee', canActivate: [AuthService] },
|
||||
{ path: 'employee/:id', component: EmployeeDetailComponent, title: 'Edit Employee', canActivate: [AuthService] }
|
||||
|
||||
];
|
||||
|
|
91
src/app/views/employee-detail/employee-detail.component.html
Normal file
91
src/app/views/employee-detail/employee-detail.component.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
<form class="employee-detail" [formGroup]="employeeForm" appearance="outline">
|
||||
<div class="employee-detail__info-view">
|
||||
<div class="employee-detail__info-view__base">
|
||||
<h2>Info</h2>
|
||||
<div class="employee-detail__info-view__base__content">
|
||||
<mat-form-field>
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-error>First Name can not be Empty</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<mat-error>Last Name can not be Empty</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Postcode</mat-label>
|
||||
<mat-icon matSuffix>money</mat-icon>
|
||||
<input matInput formControlName="postcode" required minlength="5" maxlength="5">
|
||||
<mat-error>Postcode has to be exactly 5 long</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>City</mat-label>
|
||||
<mat-icon matSuffix>location_city</mat-icon>
|
||||
<input matInput formControlName="city" required>
|
||||
<mat-error>City can not be Empty</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Street</mat-label>
|
||||
<mat-icon matSuffix>house</mat-icon>
|
||||
<input matInput formControlName="street" required>
|
||||
<mat-error>Street can not be Empty</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Phone</mat-label>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<input matInput formControlName="phone" required>
|
||||
<mat-error>Phone can not be Empty</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="employee-detail__info-view__skills">
|
||||
<h2>Skills</h2>
|
||||
<div class="employee-detail__info-view__skills__action-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Skill</mat-label>
|
||||
<mat-select #skillSelect [disabled]="!($unusedSkills | async)?.length">
|
||||
@for (skill of ($unusedSkills|async); track skill) {
|
||||
<mat-option [value]="skill">{{skill.skill}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-fab class="shadowless" (click)="onAddSkill(skillSelect)">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-table [dataSource]="skillsDataSource">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Id</th>
|
||||
<td mat-cell *matCellDef="let skill">{{skill.id}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="skill">
|
||||
<th mat-header-cell *matHeaderCellDef>Skill</th>
|
||||
<td mat-cell *matCellDef="let skill">
|
||||
<p>{{skill.skill}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="action">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let skill">
|
||||
<button mat-mini-fab class="warn shadowless" (click)="onRemoveSkill(skill)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-row *matRowDef="let row; columns: skillsDisplayedColumns;"></tr>
|
||||
</mat-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="employee-detail__action-row">
|
||||
<a mat-flat-button class="abort" routerLink="/">Abort</a>
|
||||
@if (isNewEmployee) {
|
||||
<button mat-flat-button (click)="onCreate()">Create</button>
|
||||
}
|
||||
@else {
|
||||
<button mat-flat-button (click)="onSave()">Save</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
</form>
|
91
src/app/views/employee-detail/employee-detail.component.scss
Normal file
91
src/app/views/employee-detail/employee-detail.component.scss
Normal file
|
@ -0,0 +1,91 @@
|
|||
@use '@angular/material' as mat;
|
||||
|
||||
.employee-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
|
||||
// Calculate Height and Spacing based on the Form Fields
|
||||
$inner-form-height: 56px;
|
||||
$form-height: 75.5px;
|
||||
$inner-form-spacing: 4.5px;
|
||||
$form-conter-padding: calc(($form-height + $inner-form-spacing - $inner-form-height) / 2);
|
||||
$form-conter-padding-fab: calc(($form-conter-padding - $inner-form-spacing) / 2);
|
||||
|
||||
&__info-view {
|
||||
display: flex;
|
||||
gap: 4rem;
|
||||
|
||||
>* {
|
||||
min-width: 0;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $inner-form-spacing;
|
||||
|
||||
input {
|
||||
font-size: var(--mat-sys-body-large-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__skills {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
|
||||
&__action-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
mat-form-field:first-of-type {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td.mat-column-skill {
|
||||
width: 100%;
|
||||
justify-self: center;
|
||||
padding: $form-conter-padding 0;
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
height: $inner-form-height;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
font-size: var(--mat-sys-body-large-size);
|
||||
}
|
||||
}
|
||||
|
||||
td.mat-column-action {
|
||||
width: 0;
|
||||
vertical-align: middle;
|
||||
padding-right: 8px; // Half the difference to the Width of a FAB
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
td.mat-column-skill {
|
||||
padding-top: $inner-form-spacing;
|
||||
}
|
||||
|
||||
td.mat-column-action {
|
||||
padding-bottom: $form-conter-padding-fab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
137
src/app/views/employee-detail/employee-detail.component.ts
Normal file
137
src/app/views/employee-detail/employee-detail.component.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelect, MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Employee, EmployeeService, NewEmployee, Qualification, QualificationService, UpdatedEmployee } from '@core/ems';
|
||||
import { NotificationService, NotificationType } from '@core/notification/notification.service';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-employee-detail',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
MatIcon,
|
||||
RouterLink,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
MatFormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './employee-detail.component.html',
|
||||
styleUrl: './employee-detail.component.scss'
|
||||
})
|
||||
export class EmployeeDetailComponent {
|
||||
isNewEmployee: boolean;
|
||||
employeeForm: FormGroup;
|
||||
skillsDataSource: MatTableDataSource<Qualification> = new MatTableDataSource<Qualification>([]);
|
||||
skillsDisplayedColumns = ['skill', 'action'];
|
||||
$unusedSkills: Observable<Array<Qualification>>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private employeeService: EmployeeService,
|
||||
private qualificationService: QualificationService,
|
||||
private formBuilder: FormBuilder,
|
||||
private notificationService: NotificationService
|
||||
) {
|
||||
const idParam = this.route.snapshot.paramMap.get('id');
|
||||
this.isNewEmployee = idParam == null;
|
||||
|
||||
this.employeeForm = this.formBuilder.group<Employee>({
|
||||
id: 0,
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
postcode: '',
|
||||
city: '',
|
||||
street: '',
|
||||
phone: '',
|
||||
skillSet: []
|
||||
});
|
||||
|
||||
this.$unusedSkills = this.qualificationService.getAllQualifications();
|
||||
|
||||
if (this.isNewEmployee) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseInt(idParam as string);
|
||||
this.employeeService.getEmployee({ id }).subscribe((employee) => {
|
||||
this.employeeForm = this.formBuilder.group<Employee>(employee);
|
||||
this.skillsDataSource = new MatTableDataSource<Qualification>(employee.skillSet);
|
||||
this.$unusedSkills = this.qualificationService.getAllQualifications().pipe(
|
||||
map((allSkills: Array<Qualification>) => {
|
||||
const usedSkillIds = new Set(employee.skillSet.map(skill => skill.id));
|
||||
return allSkills?.filter(skill => !usedSkillIds.has(skill.id));
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onCreate() {
|
||||
if (!this.employeeForm.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const employee: NewEmployee = this.employeeForm.value;
|
||||
employee.skillSet = this.skillsDataSource.data.map((qualification) => qualification.id);
|
||||
this.employeeService.createEmployee({ requestBody: employee }).subscribe((response) => {
|
||||
this.notificationService.publish(`${response.firstName} ${response.lastName} was created`, NotificationType.Information);
|
||||
this.router.navigate(['']);
|
||||
});
|
||||
}
|
||||
|
||||
onSave() {
|
||||
if (!this.employeeForm.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.employeeForm.value.id;
|
||||
const employee: UpdatedEmployee = this.employeeForm.value;
|
||||
employee.skillSet = this.skillsDataSource.data.map((qualification) => qualification.id);
|
||||
|
||||
if (employee.skillSet.length == 0) {
|
||||
SZUT-Dominik
commented
Heidemanns API kann beim update zwar das Heidemanns API kann beim update zwar das `skillSet` aktualisieren, aber nur solange du versuchst es auf mindestens einen wert zu setzten. Es erkennt den Unterschied zwischen `null` und `[]` nicht, deswegen müssen wir, wenn alle Skills entfernt werden, jeden einzeln per API loslösen.
|
||||
this.employeeService.getAllEmployeeQualifications({ id }).subscribe((result) => {
|
||||
result.skillSet?.forEach((skill) => {
|
||||
this.employeeService.removeQualificationFromEmployee({ employeeId: id, qualificationId: skill.id }).subscribe(() => { });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.employeeService.updateEmployee({ id: id, requestBody: employee }).subscribe((response) => {
|
||||
this.notificationService.publish(`${response.firstName} ${response.lastName} was updated`, NotificationType.Information);
|
||||
this.router.navigate(['']);
|
||||
});
|
||||
}
|
||||
|
||||
onAddSkill(select: MatSelect) {
|
||||
if (select.value == undefined) {
|
||||
return;
|
||||
}
|
||||
const newSkill = select.value as Qualification;
|
||||
|
||||
const skills = this.skillsDataSource.data;
|
||||
skills.push(newSkill);
|
||||
this.skillsDataSource.data = skills;
|
||||
|
||||
this.$unusedSkills = this.$unusedSkills.pipe(
|
||||
map((skills) => skills.filter(skill => skill.id != newSkill.id))
|
||||
);
|
||||
}
|
||||
|
||||
onRemoveSkill(skill: Qualification) {
|
||||
const skills = this.skillsDataSource.data;
|
||||
this.skillsDataSource.data = skills.filter(filterSkill => filterSkill.id != skill.id);
|
||||
this.$unusedSkills = this.$unusedSkills.pipe(
|
||||
map(skills => [...skills, skill])
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue
Wenn der
AuthServices
als wert fürcanActivate
gesetzt ist, dann wird der Nutzer, wenn er diese Seiten besucht, ohne eingeloggt zu sein, auf die Home Seite umgeleitet.