Compare commits
19 commits
feature/em
...
trunk
Author | SHA1 | Date | |
---|---|---|---|
a9c8c79d0b | |||
e532d171c7 | |||
fd62dd02c4 | |||
523e5f4eee | |||
28bb4e220e | |||
a87d420f6d | |||
16a9aef3c5 | |||
76541fa2a8 | |||
264cd3f209 | |||
488990f634 | |||
2fe272c1e4 | |||
996ed8ca05 | |||
856d3e44b9 | |||
|
748df61dc6 | ||
c08df8c2d8 | |||
|
9d3be76744 | ||
c43d73aadb | |||
|
3bffc414ec | ||
3a528b0a01 |
24 changed files with 735 additions and 53 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
|
@ -14,4 +14,11 @@ main {
|
|||
min-width: 60rem;
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {routes} from '@app/app.routes';
|
||||
import {OpenAPI} from '@core/ems/core/OpenAPI';
|
||||
import {LogLevel, provideAuth} from 'angular-auth-oidc-client';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { routes } from '@app/app.routes';
|
||||
import { OpenAPI } from '@core/ems/core/OpenAPI';
|
||||
import { LogLevel, provideAuth } from 'angular-auth-oidc-client';
|
||||
|
||||
OpenAPI.BASE = 'http://localhost:8080';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideAnimationsAsync(),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { DashboardComponent } from '@app/views/dashboard/dashboard.component';
|
||||
import {QualificationsComponent} from '@app/views/qualifications/qualifications.component';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
|
||||
import { EmployeeDetailComponent } from './views/employee-detail/employee-detail.component';
|
||||
|
@ -7,5 +8,6 @@ import { EmployeeDetailComponent } from './views/employee-detail/employee-detail
|
|||
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] }
|
||||
{ path: 'employee/:id', component: EmployeeDetailComponent, title: 'Edit Employee', canActivate: [AuthService] },
|
||||
{path: 'qualifications', component: QualificationsComponent, title: 'Qualifications', canActivate: [AuthService]}
|
||||
];
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, GuardResult, MaybeAsync, RedirectCommand, Router } from '@angular/router';
|
||||
import UserData from '@core/auth/UserData';
|
||||
import { OpenAPI } from '@core/ems';
|
||||
import { OidcSecurityService } from 'angular-auth-oidc-client';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { OpenAPI } from '../ems';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService implements CanActivate {
|
||||
public $user: Observable<UserData | undefined>;
|
||||
public $user: BehaviorSubject<UserData | undefined>;
|
||||
|
||||
constructor(private readonly oidcSecurityService: OidcSecurityService, private router: Router) {
|
||||
this.$user = new Observable((publish) => {
|
||||
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData }) => {
|
||||
publish.next(isAuthenticated ? {
|
||||
this.$user = new BehaviorSubject<UserData | undefined>(undefined);
|
||||
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken }) => {
|
||||
OpenAPI.TOKEN = accessToken;
|
||||
const isLoggedIn = isAuthenticated && userData != null && accessToken != '';
|
||||
this.$user.next(isLoggedIn ? {
|
||||
username: userData.preferred_username,
|
||||
verified: userData.email_verified
|
||||
} : undefined);
|
||||
});
|
||||
});
|
||||
this.oidcSecurityService.getAccessToken().subscribe(token => OpenAPI.TOKEN = token);
|
||||
|
||||
}
|
||||
|
||||
canActivate(): MaybeAsync<GuardResult> {
|
||||
|
|
8
src/app/delete-modal/DeleteModalData.d.ts
vendored
Normal file
8
src/app/delete-modal/DeleteModalData.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
interface DeleteModalData {
|
||||
title: string,
|
||||
description: string,
|
||||
cancel: string,
|
||||
ok: string,
|
||||
}
|
||||
|
||||
export default DeleteModalData;
|
8
src/app/delete-modal/delete-modal.component.html
Normal file
8
src/app/delete-modal/delete-modal.component.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<h2 mat-dialog-title>{{ data.title}}</h2>
|
||||
<mat-dialog-content>
|
||||
<p>{{data.description}}</p>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="delete-modal">
|
||||
<button mat-flat-button [mat-dialog-close]="false">{{data.cancel}}</button>
|
||||
<button mat-flat-button [mat-dialog-close]="true" class="warn">{{data.ok}}</button>
|
||||
</mat-dialog-actions>
|
4
src/app/delete-modal/delete-modal.component.scss
Normal file
4
src/app/delete-modal/delete-modal.component.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.delete-modal {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
32
src/app/delete-modal/delete-modal.component.ts
Normal file
32
src/app/delete-modal/delete-modal.component.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions, MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogTitle
|
||||
} from '@angular/material/dialog';
|
||||
import DeleteModalData from '@app/delete-modal/DeleteModalData';
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-model',
|
||||
imports: [
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogTitle,
|
||||
MatButton,
|
||||
MatDialogClose
|
||||
],
|
||||
templateUrl: './delete-modal.component.html',
|
||||
styleUrl: './delete-modal.component.scss'
|
||||
})
|
||||
export class DeleteModalComponent {
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) protected data: DeleteModalData,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
<mat-toolbar class="header">
|
||||
<nav>
|
||||
<a routerLink="'/'" mat-button>
|
||||
<a routerLink="" mat-button>
|
||||
<mat-icon>badge</mat-icon>
|
||||
EMS</a>
|
||||
EMS
|
||||
</a>
|
||||
@for (route of routes; track route) {
|
||||
<a mat-button routerLink="{{ route.path }}" class="{{ route.class }}">{{ route.title }}</a>
|
||||
}
|
||||
|
|
|
@ -13,6 +13,13 @@
|
|||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
gap: 0.25rem;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
a:first-of-type {
|
||||
display: none;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
|
@ -28,5 +35,11 @@
|
|||
&__login {
|
||||
min-width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
>a:last-of-type {
|
||||
display: none;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {AsyncPipe, Location, TitleCasePipe} from '@angular/common';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {MatAnchor, MatButton, MatIconButton} from '@angular/material/button';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatToolbar} from '@angular/material/toolbar';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {AuthService} from '@core/auth/auth.service';
|
||||
import { AsyncPipe, TitleCasePipe } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatToolbar } from '@angular/material/toolbar';
|
||||
import { EventType, Router, RouterLink } from '@angular/router';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
|
@ -26,22 +26,24 @@ export class HeaderComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
protected auth: AuthService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.routes = this.router.config
|
||||
const routes = this.router.config
|
||||
.filter(route => !route.path?.includes(':'))
|
||||
.filter(route => !route.path?.includes('/'))
|
||||
.map(route => ({
|
||||
.filter(route => !route.path?.includes('/'));
|
||||
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event.type != EventType.NavigationEnd) {
|
||||
return;
|
||||
}
|
||||
this.routes = routes.map(route => ({
|
||||
path: `/${route.path}`,
|
||||
title: route.title as string,
|
||||
class: `/${route.path}` == (this.location.path() || '/') ? 'active' : ''
|
||||
class: `/${route.path}` == event.url ? 'active' : ''
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
15
src/app/info-modal/info-modal.component.html
Normal file
15
src/app/info-modal/info-modal.component.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<h2 mat-dialog-title>{{ data.firstName }} {{ data.lastName }}</h2>
|
||||
<mat-dialog-content class="info-modal">
|
||||
<p>Street: {{data.street}}</p>
|
||||
<p>Location: {{data.postcode}} {{data.city}}</p>
|
||||
<p>Phone: <a href="tel:{{data.phone}}">{{data.phone}}</a></p>
|
||||
<p class="info-modal__skills-label">Skills:</p>
|
||||
<ul class="info-modal__skills">
|
||||
@for (skill of data.skillSet; track skill){
|
||||
<li>{{skill.skill}}</li>
|
||||
}
|
||||
</ul>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-flat-button [mat-dialog-close]="false">Close</button>
|
||||
</mat-dialog-actions>
|
11
src/app/info-modal/info-modal.component.scss
Normal file
11
src/app/info-modal/info-modal.component.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
@media (min-width: 800px) {
|
||||
.info-modal {
|
||||
&__skills-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__skills {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
32
src/app/info-modal/info-modal.component.ts
Normal file
32
src/app/info-modal/info-modal.component.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions, MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogTitle
|
||||
} from '@angular/material/dialog';
|
||||
import {Employee} from '@core/ems';
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-model',
|
||||
imports: [
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogTitle,
|
||||
MatButton,
|
||||
MatDialogClose
|
||||
],
|
||||
templateUrl: './info-modal.component.html',
|
||||
styleUrl: './info-modal.component.scss'
|
||||
})
|
||||
export class InfoModalComponent {
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) protected data: Employee,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -12,6 +12,11 @@
|
|||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
@media (max-width: 20rem) {
|
||||
min-width: calc(100% - 0.5rem);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&__card {
|
||||
|
|
8
src/app/views/dashboard/Filter.d.ts
vendored
Normal file
8
src/app/views/dashboard/Filter.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import {Qualification} from '@core/ems';
|
||||
|
||||
interface Filter {
|
||||
fuzzy: string,
|
||||
qualification: Qualification|undefined
|
||||
}
|
||||
|
||||
export default Filter;
|
|
@ -1,4 +1,66 @@
|
|||
<a mat-flat-button routerLink="/employee/new">New Employee</a>
|
||||
<a mat-flat-button routerLink="/employee/1">Edit Employee 3</a>
|
||||
<button mat-flat-button (click)="testInfo()">Test Info</button>
|
||||
<button mat-flat-button (click)="testError()">Test Error</button>
|
||||
<div class="dashboard">
|
||||
@if (auth.$user|async; as user) {
|
||||
<div class="dashboard__action-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Search</mat-label>
|
||||
<input matInput (keyup)="onFuzzyFilter($event)">
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Qualification</mat-label>
|
||||
<mat-select [(value)]="selectedQualificationFilter" (valueChange)="onFilterUpdate()">
|
||||
<mat-option>None</mat-option>
|
||||
@for (skill of ($qualifications|async); track skill) {
|
||||
<mat-option [value]="skill">{{ skill.skill }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-fab class="shadowless" routerLink="employee/new">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-table [dataSource]="employeeDataSource" class="dashboard__employees">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Id</th>
|
||||
<td mat-cell *matCellDef="let employee">{{ employee.id }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="first-name">
|
||||
<th mat-header-cell *matHeaderCellDef>First Name</th>
|
||||
<td mat-cell *matCellDef="let employee">{{ employee.firstName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="last-name">
|
||||
<th mat-header-cell *matHeaderCellDef>Last Name</th>
|
||||
<td mat-cell *matCellDef="let employee">{{ employee.lastName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="skills">
|
||||
<th mat-header-cell *matHeaderCellDef>Skills</th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<ul>
|
||||
@for (skill of employee.skillSet; track skill) {
|
||||
<li>{{ skill.skill }}</li>
|
||||
}
|
||||
</ul>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let employee">
|
||||
<button mat-mini-fab class="shadowless" (click)="onInfo(employee)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab class="shadowless" routerLink="employee/{{employee.id}}">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab class="shadowless warn" (click)="onDelete(employee)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="employeesDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: employeesDisplayedColumns;"></tr>
|
||||
</mat-table>
|
||||
} @else {
|
||||
<p>Log in to see more!</p>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
&__action-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__employees {
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
.mat-column-id {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.mat-column-first-name,
|
||||
.mat-column-last-name,
|
||||
.mat-column-skills {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
$action-count: 3;
|
||||
$fap-size: 40px;
|
||||
$gap-size: 1rem;
|
||||
|
||||
th.mat-column-actions {
|
||||
width: calc($fap-size * $action-count + $gap-size * ($action-count - 1));
|
||||
}
|
||||
|
||||
td.mat-column-actions {
|
||||
gap: $gap-size;
|
||||
display: flex;
|
||||
padding: 0.25rem 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
$gap-size: 0.25rem;
|
||||
|
||||
.mat-column-id,
|
||||
.mat-column-skills {
|
||||
display: none;
|
||||
}
|
||||
|
||||
th.mat-column-actions {
|
||||
width: calc($fap-size * $action-count + $gap-size * ($action-count - 1));
|
||||
}
|
||||
|
||||
td.mat-column-actions {
|
||||
gap: $gap-size;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,127 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { DeleteModalComponent } from '@app/delete-modal/delete-modal.component';
|
||||
import {InfoModalComponent} from '@app/info-modal/info-modal.component';
|
||||
import Filter from '@app/views/dashboard/Filter';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
import { Employee, EmployeeService, Qualification, QualificationService } from '@core/ems';
|
||||
import { NotificationService, NotificationType } from '@core/notification/notification.service';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
imports: [MatButtonModule, RouterLink],
|
||||
imports: [MatButtonModule, MatSelectModule, MatFormFieldModule, MatTableModule, ReactiveFormsModule, MatInputModule, MatIcon, RouterLink, AsyncPipe],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.scss'
|
||||
})
|
||||
export class DashboardComponent {
|
||||
constructor(private notifications: NotificationService) { }
|
||||
testInfo() {
|
||||
this.notifications.publish('Cake', NotificationType.Information);
|
||||
export class DashboardComponent implements OnInit {
|
||||
selectedQualificationFilter: Qualification | undefined;
|
||||
fuzzyFilter: string = '';
|
||||
|
||||
employeeDataSource: MatTableDataSource<Employee> = new MatTableDataSource<Employee>([]);
|
||||
employeesDisplayedColumns = ['id', 'first-name', 'last-name', 'skills', 'actions'];
|
||||
$qualifications: Observable<Array<Qualification> | undefined> = of();
|
||||
|
||||
constructor(
|
||||
protected auth: AuthService,
|
||||
private employeeService: EmployeeService,
|
||||
private qualificationService: QualificationService,
|
||||
private dialog: MatDialog,
|
||||
private notifications: NotificationService
|
||||
) { }
|
||||
ngOnInit(): void {
|
||||
this.auth.$user.subscribe((user) => {
|
||||
if (user === undefined) {
|
||||
return;
|
||||
}
|
||||
this.$qualifications = this.qualificationService.getAllQualifications();
|
||||
this.employeeService.getAllEmployees().subscribe((employees) => {
|
||||
this.employeeDataSource = new MatTableDataSource(employees);
|
||||
this.employeeDataSource.filterPredicate = (employee: Employee, rawFilter: string): boolean => {
|
||||
const filter = JSON.parse(rawFilter) as Filter;
|
||||
if (filter.fuzzy == '' && filter.qualification == undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
testError() {
|
||||
this.notifications.publish('Cake', NotificationType.Error);
|
||||
}}
|
||||
if (
|
||||
filter.qualification != undefined
|
||||
&& !employee.skillSet.map((skill) => skill.id).includes(filter.qualification.id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filter.fuzzy != ''
|
||||
&& !employee.id.toString().includes(filter.fuzzy)
|
||||
&& !employee.firstName.toLowerCase().includes(filter.fuzzy)
|
||||
&& !employee.lastName.toLowerCase().includes(filter.fuzzy)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onInfo(employee: Employee){
|
||||
this.dialog.open(InfoModalComponent, {
|
||||
data: employee
|
||||
});
|
||||
}
|
||||
|
||||
onDelete(employee: Employee) {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
data: {
|
||||
title: `Delete ${employee.firstName} ${employee.lastName}`,
|
||||
description: `Do you really want to delete ${employee.firstName} ${employee.lastName}?`,
|
||||
cancel: 'No',
|
||||
ok: 'Yes',
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((accepted: boolean) => {
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
this.employeeService.deleteEmployee({ id: employee.id }).subscribe(() => {
|
||||
const data = this.employeeDataSource.data;
|
||||
const i = data.indexOf(employee);
|
||||
if (i != -1) {
|
||||
data.splice(i, 1);
|
||||
this.employeeDataSource.data = data;
|
||||
}
|
||||
this.notifications.publish(`Deleted ${employee.firstName} ${employee.lastName}`, NotificationType.Information);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onFuzzyFilter(event: KeyboardEvent) {
|
||||
this.fuzzyFilter = (event.target as HTMLInputElement).value;
|
||||
this.onFilterUpdate();
|
||||
}
|
||||
|
||||
onFilterUpdate() {
|
||||
const skill = this.selectedQualificationFilter;
|
||||
const fuzzy = this.fuzzyFilter;
|
||||
if (skill == undefined && fuzzy == '') {
|
||||
this.employeeDataSource.filter = '';
|
||||
return;
|
||||
}
|
||||
this.employeeDataSource.filter = JSON.stringify({
|
||||
qualification: skill,
|
||||
fuzzy: fuzzy.toLowerCase()
|
||||
} as Filter);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
// Calculate Height and Spacing based on the Form Fields
|
||||
$inner-form-height: 56px;
|
||||
$form-height: 75.5px;
|
||||
|
@ -16,6 +20,11 @@
|
|||
display: flex;
|
||||
gap: 4rem;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
>* {
|
||||
min-width: 0;
|
||||
flex-basis: 0;
|
||||
|
|
54
src/app/views/qualifications/qualifications.component.html
Normal file
54
src/app/views/qualifications/qualifications.component.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
<div class="qualifications">
|
||||
<div class="qualifications__action-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Qualification</mat-label>
|
||||
<input matInput #qualificationInput minlength="1" maxlength="25">
|
||||
</mat-form-field>
|
||||
<button mat-fab class="shadowless" (click)="onAdd(qualificationInput)">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-table class="qualifications__view" [dataSource]="qualificationDataSource">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>Id</th>
|
||||
<td mat-cell *matCellDef="let qualification">{{qualification.id}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="skill">
|
||||
<th mat-header-cell *matHeaderCellDef>Skill</th>
|
||||
<td mat-cell *matCellDef="let qualification" [class.edit]="isEditing(qualification.id)">
|
||||
<p>{{qualification.skill}}</p>
|
||||
<form [formGroup]="getSkillFormGroup(qualification)">
|
||||
<mat-form-field>
|
||||
<mat-label>Skill</mat-label>
|
||||
<input matInput formControlName="skill" required>
|
||||
<mat-error>Skill can't be empty</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let qualification">
|
||||
@if (!isEditing(qualification.id)) {
|
||||
<button mat-mini-fab class="shadowless" (click)="startEdit(qualification.id)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab class="warn shadowless" (click)="onDelete(qualification)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-mini-fab class="shadowless" (click)="endEdit(qualification, true)">
|
||||
<mat-icon>save</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab class="warn shadowless" (click)="endEdit(qualification, false)">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="qualificationsDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: qualificationsDisplayedColumns;"></tr>
|
||||
</mat-table>
|
||||
</div>
|
||||
|
51
src/app/views/qualifications/qualifications.component.scss
Normal file
51
src/app/views/qualifications/qualifications.component.scss
Normal file
|
@ -0,0 +1,51 @@
|
|||
.qualifications{
|
||||
&__action-row{
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
mat-form-field:first-of-type{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__view{
|
||||
tr{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
.mat-column-id{
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.mat-column-skill{
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
form{
|
||||
display: none;
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
&.edit{
|
||||
p{
|
||||
display: none;
|
||||
}
|
||||
|
||||
form{
|
||||
display: inline-grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions{
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
175
src/app/views/qualifications/qualifications.component.ts
Normal file
175
src/app/views/qualifications/qualifications.component.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
|
||||
import {DeleteModalComponent} from '@app/delete-modal/delete-modal.component';
|
||||
import {
|
||||
EmployeeQualifications,
|
||||
EmployeeService,
|
||||
Qualification,
|
||||
QualificationEmployees,
|
||||
QualificationService,
|
||||
RemoveQualificationFromEmployeeResponse
|
||||
} from '@core/ems';
|
||||
import {NotificationService, NotificationType} from '@core/notification/notification.service';
|
||||
import {forkJoin, Observable} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-qualifications',
|
||||
imports: [
|
||||
MatIcon,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
MatFormFieldModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
templateUrl: './qualifications.component.html',
|
||||
styleUrl: './qualifications.component.scss'
|
||||
})
|
||||
export class QualificationsComponent {
|
||||
qualificationDataSource: MatTableDataSource<Qualification> = new MatTableDataSource<Qualification>([]);
|
||||
qualificationsDisplayedColumns = ['id', 'skill', 'actions'];
|
||||
|
||||
qualificationSkillFormGroups: Map<number, FormGroup> = new Map();
|
||||
qualificationEdits: Map<number, boolean> = new Map();
|
||||
|
||||
constructor(
|
||||
private qualificationService: QualificationService,
|
||||
private employeeService: EmployeeService,
|
||||
protected formBuilder: FormBuilder,
|
||||
private notifications: NotificationService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
this.qualificationService.getAllQualifications().subscribe((qualifications) => {
|
||||
this.qualificationDataSource = new MatTableDataSource<Qualification>(qualifications);
|
||||
});
|
||||
}
|
||||
|
||||
getSkillFormGroup(skill: Qualification): FormGroup {
|
||||
|
||||
let formGroup = this.qualificationSkillFormGroups.get(skill.id);
|
||||
if (formGroup == undefined) {
|
||||
formGroup = this.formBuilder.group(skill);
|
||||
this.qualificationSkillFormGroups.set(skill.id, formGroup);
|
||||
}
|
||||
return formGroup;
|
||||
}
|
||||
|
||||
save() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
isEditing(id: number): boolean {
|
||||
const editing = this.qualificationEdits.get(id);
|
||||
if (editing == undefined) {
|
||||
return false;
|
||||
}
|
||||
return editing;
|
||||
}
|
||||
|
||||
startEdit(id: number) {
|
||||
this.qualificationEdits.set(id, true);
|
||||
}
|
||||
|
||||
onAdd(qualificationField: HTMLInputElement){
|
||||
this.qualificationService.createQualification({requestBody: {skill: qualificationField.value}}).subscribe((qualification)=>{
|
||||
const data = this.qualificationDataSource.data;
|
||||
data.push(qualification);
|
||||
this.qualificationDataSource = new MatTableDataSource<Qualification>(data);
|
||||
this.notifications.publish(`Added ${qualification.skill}`);
|
||||
qualificationField.value='';
|
||||
});
|
||||
}
|
||||
|
||||
endEdit(oldQualification: Qualification, save: boolean) {
|
||||
|
||||
const qualificationFormGroup = this.qualificationSkillFormGroups.get(oldQualification.id);
|
||||
if (qualificationFormGroup == undefined) {
|
||||
return;
|
||||
}
|
||||
const qualification: Qualification = qualificationFormGroup.value;
|
||||
|
||||
if (!save) {
|
||||
qualificationFormGroup.setValue(oldQualification);
|
||||
this.qualificationEdits.set(oldQualification.id, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (qualificationFormGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.qualificationService.updateQualification({
|
||||
id: oldQualification.id,
|
||||
requestBody: {
|
||||
skill: qualification.skill
|
||||
}
|
||||
}).subscribe(() => {
|
||||
const data = this.qualificationDataSource.data;
|
||||
const i = data.indexOf(oldQualification);
|
||||
data[i] = qualification;
|
||||
this.qualificationDataSource.data = data;
|
||||
|
||||
this.qualificationEdits.set(oldQualification.id, false);
|
||||
this.notifications.publish(`Saved ${qualification.skill}`, NotificationType.Information);
|
||||
});
|
||||
}
|
||||
|
||||
onDelete(qualification: Qualification) {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
data: {
|
||||
title: `Delete ${qualification.skill}`,
|
||||
description: `Do you really want to delete ${qualification.skill}?`,
|
||||
cancel: 'No',
|
||||
ok: 'Yes',
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((accepted: boolean) => {
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
this.qualificationService.getAllQualificationEmployees({id: qualification.id}).subscribe((employees: QualificationEmployees) => {
|
||||
if (employees.employees.length==0){
|
||||
this.execDelete(qualification);
|
||||
return;
|
||||
}
|
||||
const requests: Array<Observable<RemoveQualificationFromEmployeeResponse>> = [];
|
||||
for (const employee of employees.employees) {
|
||||
requests.push(this.employeeService.removeQualificationFromEmployee({
|
||||
employeeId: employee.id,
|
||||
qualificationId: qualification.id
|
||||
}));
|
||||
}
|
||||
forkJoin(requests).subscribe((employeesQualifications: Array<EmployeeQualifications>)=>{
|
||||
for (const employee of employeesQualifications){
|
||||
if (employee.skillSet?.map((q)=>q.id).includes(qualification.id)){
|
||||
this.notifications.publish(`Could not remove ${qualification.skill} from ${employee.firstName} ${employee.lastName}`, NotificationType.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.execDelete(qualification);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
execDelete(qualification: Qualification){
|
||||
this.qualificationService.deleteQualification({id: qualification.id}).subscribe(() => {
|
||||
const data = this.qualificationDataSource.data;
|
||||
const i = data.indexOf(qualification);
|
||||
if (i != -1) {
|
||||
data.splice(i, 1);
|
||||
this.qualificationDataSource.data = data;
|
||||
}
|
||||
this.notifications.publish(`Deleted ${qualification.skill}`, NotificationType.Information);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue