diff --git a/build.gradle.kts b/build.gradle.kts index cff1bdd..ea716f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { // testImplementation("io.mockk:mockk:1.10.2") runtimeOnly("com.h2database:h2:1.4.199") + runtimeOnly("mysql:mysql-connector-java:8.0.22") compileOnly("org.projectlombok:lombok:1.18.10") } diff --git a/src/main/frontend/package-lock.json b/src/main/frontend/package-lock.json index 37c25c2..1afe657 100644 --- a/src/main/frontend/package-lock.json +++ b/src/main/frontend/package-lock.json @@ -7597,6 +7597,11 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "ngx-material-file-input": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ngx-material-file-input/-/ngx-material-file-input-2.1.1.tgz", + "integrity": "sha512-FbaIjiJnL6BZtZYWLvMSn9aSaM62AZaJegloTUphmLz5jopXPzE5W+3aC+dsf9h1IIqHSCLcyv0w+qH0ypBhMA==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json index 167b4a9..fa044c2 100644 --- a/src/main/frontend/package.json +++ b/src/main/frontend/package.json @@ -24,6 +24,7 @@ "@mdi/angular-material": "^5.7.55", "bootstrap": "^4.5.2", "copy-webpack-plugin": "^6.2.1", + "ngx-material-file-input": "^2.1.1", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/main/frontend/src/app/app-routing.module.ts b/src/main/frontend/src/app/app-routing.module.ts index a065f66..f647f8f 100644 --- a/src/main/frontend/src/app/app-routing.module.ts +++ b/src/main/frontend/src/app/app-routing.module.ts @@ -1,5 +1,6 @@ import {NgModule} from '@angular/core'; import {Routes, RouterModule} from '@angular/router'; +import {InventoryPageComponent} from "./pages/inventory-page/inventory-page.component"; const routes: Routes = [{ @@ -16,8 +17,24 @@ const routes: Routes = [{ loadChildren: () => import('./modules/groups/groups.module').then(m => m.GroupsModule) }, { path: 'inventory', - loadChildren: () => import('./modules/inventory/inventory.module').then(m => m.InventoryModule) -}]; + component: InventoryPageComponent, + children: [ + { + path: 'materialtype', + loadChildren: () => import('./modules/material-type/material-type.module').then(m => m.MaterialTypeModule), + }, + { + path: 'material', + loadChildren: () => import('./modules/material/material.module').then(m => m.MaterialModule) + }, + { + path: '', + pathMatch: 'full', + redirectTo: 'materialtype' + } + ] +}, + {path: 'material', loadChildren: () => import('./modules/material/material.module').then(m => m.MaterialModule)}]; @NgModule({ imports: [RouterModule.forRoot(routes)], diff --git a/src/main/frontend/src/app/app.component.html b/src/main/frontend/src/app/app.component.html index c25297e..a362220 100644 --- a/src/main/frontend/src/app/app.component.html +++ b/src/main/frontend/src/app/app.component.html @@ -1,10 +1,19 @@
- - -
-

Aucune connexion

-
-
+ + +
+
+ + + Erreur de connexion + + +

Le serveur est présentement hors ligne. Réessayez plus tard.

+
+ + + +
+
- diff --git a/src/main/frontend/src/app/app.component.sass b/src/main/frontend/src/app/app.component.sass index e69de29..f4a93f5 100644 --- a/src/main/frontend/src/app/app.component.sass +++ b/src/main/frontend/src/app/app.component.sass @@ -0,0 +1,13 @@ +.offline-server-card-wrapper + position: fixed + top: 0 + z-index: 100 + + .dark-background + position: fixed + top: 0 + opacity: .5 + + mat-card + left: 50vw + transform: translate(-50%, -50%) diff --git a/src/main/frontend/src/app/app.component.ts b/src/main/frontend/src/app/app.component.ts index cf62a7e..a3a66dc 100644 --- a/src/main/frontend/src/app/app.component.ts +++ b/src/main/frontend/src/app/app.component.ts @@ -1,17 +1,38 @@ import {Component, Inject, PLATFORM_ID} from '@angular/core'; import {isPlatformBrowser} from "@angular/common"; +import {AppState} from "./modules/shared/app-state"; +import {Observable} from "rxjs"; +import {SubscribingComponent} from "./modules/shared/components/subscribing.component"; @Component({ selector: 'cre-root', templateUrl: './app.component.html', styleUrls: ['./app.component.sass'] }) -export class AppComponent { - isOnline: boolean; +export class AppComponent extends SubscribingComponent { + isOnline: boolean + isServerOnline = true constructor( - @Inject(PLATFORM_ID) private platformId: object + @Inject(PLATFORM_ID) private platformId: object, + private appState: AppState ) { + super() + } + + ngOnInit() { this.isOnline = isPlatformBrowser(this.platformId) + super.ngOnInit(); + + this.subscribe( + this.appState.serverOnline$, + { + next: online => this.isServerOnline = online + } + ) + } + + reload() { + window.location.reload() } } diff --git a/src/main/frontend/src/app/app.module.ts b/src/main/frontend/src/app/app.module.ts index fdace87..81ec36f 100644 --- a/src/main/frontend/src/app/app.module.ts +++ b/src/main/frontend/src/app/app.module.ts @@ -6,10 +6,12 @@ import {AppComponent} from './app.component'; import {MatIconRegistry} from "@angular/material/icon"; import {SharedModule} from "./modules/shared/shared.module"; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import { InventoryPageComponent } from './pages/inventory-page/inventory-page.component'; @NgModule({ declarations: [ - AppComponent + AppComponent, + InventoryPageComponent ], imports: [ AppRoutingModule, diff --git a/src/main/frontend/src/app/modules/accounts/services/account.service.ts b/src/main/frontend/src/app/modules/accounts/services/account.service.ts index 99a3613..8284354 100644 --- a/src/main/frontend/src/app/modules/accounts/services/account.service.ts +++ b/src/main/frontend/src/app/modules/accounts/services/account.service.ts @@ -1,6 +1,6 @@ import {Injectable, OnDestroy} from '@angular/core'; import {Subject} from "rxjs"; -import {take, takeUntil, tap} from "rxjs/operators"; +import {take, takeUntil} from "rxjs/operators"; import {AppState} from "../../shared/app-state"; import {HttpClient, HttpResponse} from "@angular/common/http"; import {environment} from "../../../../environments/environment"; @@ -39,11 +39,16 @@ export class AccountService implements OnDestroy { ).subscribe({ next: employee => this.appState.authenticatedEmployee = employee, error: err => { - if (err.status === 404) { - console.error('No default user is defined on this computer') + if (err.status === 0 && err.statusText === "Unknown Error") { + this.appState.isServerOnline = false } else { - console.error('An error occurred while authenticating the default user') - console.error(err) + this.appState.isServerOnline = true + if (err.status === 404 || err.status === 403) { + console.error('No default user is defined on this computer') + } else { + console.error('An error occurred while authenticating the default user') + console.error(err) + } } } }) @@ -67,7 +72,14 @@ export class AccountService implements OnDestroy { this.setLoggedInEmployeeFromApi() success() }, - error: err => error(err) + error: err => { + if (err.status === 0 && err.statusText === "Unknown Error") { + this.appState.isServerOnline = false + } else { + this.appState.isServerOnline = true + error(err) + } + } }) } @@ -89,7 +101,7 @@ export class AccountService implements OnDestroy { } hasPermission(permission: EmployeePermission): boolean { - return this.appState.authenticatedEmployee.permissions.indexOf(permission) >= 0 + return this.appState.authenticatedEmployee && this.appState.authenticatedEmployee.permissions.indexOf(permission) >= 0 } private setLoggedInEmployeeFromApi() { diff --git a/src/main/frontend/src/app/modules/employees/services/employee.service.ts b/src/main/frontend/src/app/modules/employees/services/employee.service.ts index 05548cd..f0c9765 100644 --- a/src/main/frontend/src/app/modules/employees/services/employee.service.ts +++ b/src/main/frontend/src/app/modules/employees/services/employee.service.ts @@ -1,41 +1,33 @@ -import {Injectable, OnDestroy} from '@angular/core'; +import {Injectable} from '@angular/core'; import {ApiService} from "../../shared/service/api.service"; import {Employee, EmployeePermission} from "../../shared/model/employee"; -import {Observable, Subject} from "rxjs"; -import {takeUntil} from "rxjs/operators"; +import {Observable} from "rxjs"; @Injectable({ providedIn: 'root' }) -export class EmployeeService implements OnDestroy { - private _destroy$ = new Subject() - +export class EmployeeService { constructor( private api: ApiService ) { } - ngOnDestroy(): void { - this._destroy$.next(true) - this._destroy$.complete() - } - get all(): Observable { - return this.api.get('/employee', true) + return this.api.get('/employee') } get(id: number): Observable { - return this.api.get(`/employee/${id}`).pipe(takeUntil(this._destroy$)) + return this.api.get(`/employee/${id}`) } save(id: number, firstName: string, lastName: string, password: string, group: number, permissions: EmployeePermission[]): Observable { const employee = {id, firstName, lastName, password, group, permissions} - return this.api.post('/employee', employee).pipe(takeUntil(this._destroy$)) + return this.api.post('/employee', employee) } update(id: number, firstName: string, lastName: string, permissions: EmployeePermission[]): Observable { const employee = {id, firstName, lastName, permissions} - return this.api.put('/employee', employee).pipe(takeUntil(this._destroy$)) + return this.api.put('/employee', employee) } updatePassword(id: number, password: string): Observable { diff --git a/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts b/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts index 9b58e88..7afe55f 100644 --- a/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts +++ b/src/main/frontend/src/app/modules/groups/pages/list/list.component.ts @@ -36,7 +36,14 @@ export class ListComponent extends SubscribingComponent { this.groups$ = this.groupService.all.pipe(takeUntil(this.destroy$)) this.subscribe( this.groupService.defaultGroup, - {next: g => this.defaultGroup = g} + { + next: g => this.defaultGroup = g, + error: err => { + if (err.status === 404) { + console.error('No default group is defined on this computer') + } + } + } ) } diff --git a/src/main/frontend/src/app/modules/inventory/inventory-routing.module.ts b/src/main/frontend/src/app/modules/inventory/inventory-routing.module.ts deleted file mode 100644 index b8b550f..0000000 --- a/src/main/frontend/src/app/modules/inventory/inventory-routing.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; - -import {InventoryComponent} from './inventory.component'; - -const routes: Routes = [{ - path: '', - component: InventoryComponent -}, { - path: 'materialtype', - loadChildren: () => import('./modules/materialtype/materialtype.module').then(m => m.MaterialtypeModule) -}]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class InventoryRoutingModule { -} diff --git a/src/main/frontend/src/app/modules/inventory/inventory.component.html b/src/main/frontend/src/app/modules/inventory/inventory.component.html deleted file mode 100644 index cf6b65c..0000000 --- a/src/main/frontend/src/app/modules/inventory/inventory.component.html +++ /dev/null @@ -1,2 +0,0 @@ - -test diff --git a/src/main/frontend/src/app/modules/inventory/inventory.component.ts b/src/main/frontend/src/app/modules/inventory/inventory.component.ts deleted file mode 100644 index 838fa63..0000000 --- a/src/main/frontend/src/app/modules/inventory/inventory.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Component, OnInit} from '@angular/core'; -import {NavLink} from "../shared/components/nav/nav.component"; - -@Component({ - selector: 'cre-inventory', - templateUrl: './inventory.component.html', - styleUrls: ['./inventory.component.sass'] -}) -export class InventoryComponent implements OnInit { - navLinks: NavLink[] = [ - {route: 'materialtype', title: 'Types de produit', enabled: true} - ] - - constructor() { - } - - ngOnInit(): void { - } - -} diff --git a/src/main/frontend/src/app/modules/inventory/inventory.module.ts b/src/main/frontend/src/app/modules/inventory/inventory.module.ts deleted file mode 100644 index b9cd251..0000000 --- a/src/main/frontend/src/app/modules/inventory/inventory.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { InventoryRoutingModule } from './inventory-routing.module'; -import { InventoryComponent } from './inventory.component'; -import {SharedModule} from "../shared/shared.module"; -import { MaterialtypeModule } from './modules/materialtype/materialtype.module'; - - -@NgModule({ - declarations: [InventoryComponent], - imports: [ - CommonModule, - InventoryRoutingModule, - SharedModule, - MaterialtypeModule - ] -}) -export class InventoryModule { } diff --git a/src/main/frontend/src/app/modules/inventory/modules/materialtype/materialtype-routing.module.ts b/src/main/frontend/src/app/modules/inventory/modules/materialtype/materialtype-routing.module.ts deleted file mode 100644 index ea7378b..0000000 --- a/src/main/frontend/src/app/modules/inventory/modules/materialtype/materialtype-routing.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class MaterialtypeRoutingModule { } diff --git a/src/main/frontend/src/app/modules/inventory/modules/materialtype/materialtype.module.ts b/src/main/frontend/src/app/modules/inventory/modules/materialtype/materialtype.module.ts deleted file mode 100644 index d1c3dc4..0000000 --- a/src/main/frontend/src/app/modules/inventory/modules/materialtype/materialtype.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { MaterialtypeRoutingModule } from './materialtype-routing.module'; -import { ListComponent } from './pages/list/list.component'; - - -@NgModule({ - declarations: [ListComponent], - imports: [ - CommonModule, - MaterialtypeRoutingModule - ] -}) -export class MaterialtypeModule { } diff --git a/src/main/frontend/src/app/modules/material-type/material-type-routing.module.ts b/src/main/frontend/src/app/modules/material-type/material-type-routing.module.ts new file mode 100644 index 0000000..8543072 --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/material-type-routing.module.ts @@ -0,0 +1,32 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {ListComponent} from "./pages/list/list.component"; +import {AddComponent} from "./pages/add/add.component"; +import {EditComponent} from "./pages/edit/edit.component"; + + +const routes: Routes = [ + { + path: 'list', + component: ListComponent + }, + { + path: 'add', + component: AddComponent + }, + { + path: 'edit/:id', + component: EditComponent + }, + { + path: '', + redirectTo: 'list' + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MaterialTypeRoutingModule { +} diff --git a/src/main/frontend/src/app/modules/material-type/material-type.module.ts b/src/main/frontend/src/app/modules/material-type/material-type.module.ts new file mode 100644 index 0000000..591a8a8 --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/material-type.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MaterialTypeRoutingModule } from './material-type-routing.module'; +import { ListComponent } from './pages/list/list.component'; +import {SharedModule} from "../shared/shared.module"; +import { AddComponent } from './pages/add/add.component'; +import { EditComponent } from './pages/edit/edit.component'; + + +@NgModule({ + declarations: [ListComponent, AddComponent, EditComponent], + imports: [ + CommonModule, + MaterialTypeRoutingModule, + SharedModule + ] +}) +export class MaterialTypeModule { } diff --git a/src/main/frontend/src/app/modules/material-type/pages/add/add.component.html b/src/main/frontend/src/app/modules/material-type/pages/add/add.component.html new file mode 100644 index 0000000..5a022c5 --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/pages/add/add.component.html @@ -0,0 +1,8 @@ + + diff --git a/src/main/frontend/src/app/modules/material-type/pages/add/add.component.sass b/src/main/frontend/src/app/modules/material-type/pages/add/add.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/material-type/pages/add/add.component.ts b/src/main/frontend/src/app/modules/material-type/pages/add/add.component.ts new file mode 100644 index 0000000..4a9adbb --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/pages/add/add.component.ts @@ -0,0 +1,74 @@ +import {Component} from '@angular/core'; +import {FormField} from "../../../shared/components/entity-add/entity-add.component"; +import {Validators} from "@angular/forms"; +import {MaterialTypeService} from "../../service/material-type.service"; +import {SubscribingComponent} from "../../../shared/components/subscribing.component"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'cre-add', + templateUrl: './add.component.html', + styleUrls: ['./add.component.sass'] +}) +export class AddComponent extends SubscribingComponent { + formFields: FormField[] = [ + { + name: 'name', + label: 'Nom', + icon: 'form-textbox', + type: 'text', + validator: Validators.required, + errorMessages: [ + {conditionFn: (errors) => errors.required, message: 'Un nom est requis'}, + ] + }, + { + name: 'prefix', + label: 'Préfixe', + icon: 'label-variant', + type: 'text', + validator: Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(3)]), + errorMessages: [ + {conditionFn: (errors) => errors.required, message: 'Un préfixe est requis'}, + { + conditionFn: (errors) => errors.minlength || errors.maxlength, + message: 'Le préfixe doit faire exactement 3 caractères' + } + ] + }, + { + name: 'usePercentages', + label: 'Utiliser les pourcentages', + icon: 'percent', + type: 'checkbox' + } + ] + unknownError = false + errorMessage: string | null + + constructor( + private materialTypeService: MaterialTypeService, + private router: Router + ) { + super() + } + + submit(values) { + this.subscribe( + this.materialTypeService.save(values.name, values.prefix, values.usePercentages), + { + next: () => this.router.navigate(['/inventory/materialtype/list']), + error: err => { + if (err.status == 409 && err.error.id === values.name) { + this.errorMessage = `Un type de produit avec le nom '${values.name}' existe déjà` + } else if (err.status == 409 && err.error.id === values.prefix) { + this.errorMessage = `Un type de produit avec le préfixe '${values.prefix}' exists déjà` + } else { + this.unknownError = true + } + console.log(err) + } + } + ) + } +} diff --git a/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.html b/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.html new file mode 100644 index 0000000..b8db91c --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.html @@ -0,0 +1,13 @@ + + diff --git a/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.sass b/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.ts b/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.ts new file mode 100644 index 0000000..0ece7b3 --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/pages/edit/edit.component.ts @@ -0,0 +1,104 @@ +import {Component} from '@angular/core'; +import {MaterialType} from "../../../shared/model/materialtype.model"; +import {ActivatedRoute, Router} from "@angular/router"; +import {SubscribingComponent} from "../../../shared/components/subscribing.component"; +import {MaterialTypeService} from "../../service/material-type.service"; +import {FormField} from "../../../shared/components/entity-add/entity-add.component"; +import {Validators} from "@angular/forms"; + +@Component({ + selector: 'cre-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.sass'] +}) +export class EditComponent extends SubscribingComponent { + materialType: MaterialType | null + formFields: FormField[] = [ + { + name: 'name', + label: 'Nom', + icon: 'form-textbox', + type: 'text', + validator: Validators.required, + errorMessages: [ + {conditionFn: (errors) => errors.required, message: 'Un nom est requis'}, + ] + }, + { + name: 'prefix', + label: 'Préfixe', + icon: 'label-variant', + type: 'text', + validator: Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(3)]), + errorMessages: [ + {conditionFn: (errors) => errors.required, message: 'Un préfixe est requis'}, + { + conditionFn: (errors) => errors.minlength || errors.maxlength, + message: 'Le préfixe doit faire exactement 3 caractères' + } + ] + } + ] + unknownError = false + errorMessage: string | null + + constructor( + private materialTypeService: MaterialTypeService, + private router: Router, + private activatedRoute: ActivatedRoute + ) { + super() + } + + ngOnInit() { + super.ngOnInit() + + const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id')) + this.subscribe( + this.materialTypeService.get(id), + { + next: materialType => this.materialType = materialType, + error: err => { + if (err.status === 404) { + this.router.navigate(['/employee/list']) + } else { + this.unknownError = true + } + } + }, + 1 + ) + } + + submit(values) { + this.subscribe( + this.materialTypeService.update(this.materialType.id, values.name, values.prefix), + { + next: () => this.router.navigate(['/inventory/materialtype/list']), + error: err => { + if (err.status == 409 && err.error.id === values.name) { + this.errorMessage = `Un type de produit avec le nom '${values.name}' existe déjà` + } else if (err.status == 409 && err.error.id === values.prefix) { + this.errorMessage = `Un type de produit avec le préfixe '${values.prefix}' exists déjà` + } else { + this.unknownError = true + } + console.log(err) + } + } + ) + } + + delete() { + this.subscribe( + this.materialTypeService.delete(this.materialType.id), + { + next: () => this.router.navigate(['/inventory/materialtype/list']), + error: err => { + this.unknownError = true + console.log(err) + } + } + ) + } +} diff --git a/src/main/frontend/src/app/modules/material-type/pages/list/list.component.html b/src/main/frontend/src/app/modules/material-type/pages/list/list.component.html new file mode 100644 index 0000000..5663df0 --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/pages/list/list.component.html @@ -0,0 +1,6 @@ + + diff --git a/src/main/frontend/src/app/modules/material-type/pages/list/list.component.sass b/src/main/frontend/src/app/modules/material-type/pages/list/list.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/material-type/pages/list/list.component.ts b/src/main/frontend/src/app/modules/material-type/pages/list/list.component.ts new file mode 100644 index 0000000..6e24b28 --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/pages/list/list.component.ts @@ -0,0 +1,32 @@ +import {Component} from '@angular/core'; +import {MaterialTypeService} from "../../service/material-type.service"; +import {SubscribingComponent} from "../../../shared/components/subscribing.component"; +import {EmployeePermission} from "../../../shared/model/employee"; + +@Component({ + selector: 'cre-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.sass'] +}) +export class ListComponent extends SubscribingComponent { + materialTypes$ = this.materialTypeService.all + columns = [ + {def: 'name', title: 'Nom', valueFn: t => t.name}, + {def: 'prefix', title: 'Préfixe', valueFn: t => t.prefix}, + {def: 'usePercentages', title: 'Utilise les pourcentages', valueFn: t => t.usePercentages ? 'Oui' : 'Non'} + ] + buttons = [ + { + text: 'Modifier', + linkFn: t => `/inventory/materialtype/edit/${t.id}`, + permission: EmployeePermission.EDIT_MATERIAL_TYPE, + disabledFn: t => t.systemType + } + ] + + constructor( + private materialTypeService: MaterialTypeService + ) { + super() + } +} diff --git a/src/main/frontend/src/app/modules/material-type/service/material-type.service.ts b/src/main/frontend/src/app/modules/material-type/service/material-type.service.ts new file mode 100644 index 0000000..fa6119c --- /dev/null +++ b/src/main/frontend/src/app/modules/material-type/service/material-type.service.ts @@ -0,0 +1,36 @@ +import {Injectable} from '@angular/core'; +import {ApiService} from "../../shared/service/api.service"; +import {Observable} from "rxjs"; +import {MaterialType} from "../../shared/model/materialtype.model"; + +@Injectable({ + providedIn: 'root' +}) +export class MaterialTypeService { + constructor( + private api: ApiService + ) { + } + + get all(): Observable { + return this.api.get('/materialtype') + } + + get(id: number): Observable { + return this.api.get(`/materialtype/${id}`) + } + + save(name: string, prefix: string, usePercentages: boolean): Observable { + const materialType = {name, prefix, usePercentages} + return this.api.post('/materialtype', materialType) + } + + update(id: number, name: string, prefix: string): Observable { + const materialType = {id, name, prefix} + return this.api.put('/materialtype', materialType) + } + + delete(id: number): Observable { + return this.api.delete(`/materialtype/${id}`) + } +} diff --git a/src/main/frontend/src/app/modules/material/material-routing.module.ts b/src/main/frontend/src/app/modules/material/material-routing.module.ts new file mode 100644 index 0000000..fa245a6 --- /dev/null +++ b/src/main/frontend/src/app/modules/material/material-routing.module.ts @@ -0,0 +1,27 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {ListComponent} from "./pages/list/list.component"; +import {AddComponent} from "./pages/add/add.component"; +import {EditComponent} from "./pages/edit/edit.component"; + +const routes: Routes = [{ + path: 'list', + component: ListComponent +}, { + path: 'add', + component: AddComponent +}, { + path: 'edit/:id', + component: EditComponent +}, { + path: '', + pathMatch: 'full', + redirectTo: 'list' +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MaterialRoutingModule { +} diff --git a/src/main/frontend/src/app/modules/material/material.module.ts b/src/main/frontend/src/app/modules/material/material.module.ts new file mode 100644 index 0000000..f51cfcb --- /dev/null +++ b/src/main/frontend/src/app/modules/material/material.module.ts @@ -0,0 +1,20 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {MaterialRoutingModule} from './material-routing.module'; +import {ListComponent} from './pages/list/list.component'; +import {SharedModule} from "../shared/shared.module"; +import {AddComponent} from './pages/add/add.component'; +import {EditComponent} from './pages/edit/edit.component'; + + +@NgModule({ + declarations: [ListComponent, AddComponent, EditComponent], + imports: [ + CommonModule, + MaterialRoutingModule, + SharedModule + ] +}) +export class MaterialModule { +} diff --git a/src/main/frontend/src/app/modules/material/pages/add/add.component.html b/src/main/frontend/src/app/modules/material/pages/add/add.component.html new file mode 100644 index 0000000..99b06f1 --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/add/add.component.html @@ -0,0 +1,8 @@ + + diff --git a/src/main/frontend/src/app/modules/material/pages/add/add.component.sass b/src/main/frontend/src/app/modules/material/pages/add/add.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/material/pages/add/add.component.ts b/src/main/frontend/src/app/modules/material/pages/add/add.component.ts new file mode 100644 index 0000000..1d0e53f --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/add/add.component.ts @@ -0,0 +1,87 @@ +import {Component} from '@angular/core'; +import {FormField} from "../../../shared/components/entity-add/entity-add.component"; +import {Validators} from "@angular/forms"; +import {MaterialService} from "../../service/material.service"; +import {MaterialTypeService} from "../../../material-type/service/material-type.service"; +import {Router} from "@angular/router"; +import {SubscribingComponent} from "../../../shared/components/subscribing.component"; +import {map} from "rxjs/operators"; + +@Component({ + selector: 'cre-add', + templateUrl: './add.component.html', + styleUrls: ['./add.component.sass'] +}) +export class AddComponent extends SubscribingComponent { + formFields: FormField[] = [ + { + name: 'name', + label: 'Code', + icon: 'form-textbox', + type: 'text', + validator: Validators.required, + errorMessages: [ + {conditionFn: (errors) => errors.required, message: 'Un code est requis'} + ] + }, + { + name: 'inventoryQuantity', + label: 'Quantité en inventaire', + icon: 'beaker-outline', + type: 'number', + validator: Validators.compose([Validators.required, Validators.min(0)]), + errorMessages: [ + {conditionFn: errors => errors.required, message: 'Une quantité en inventaire est requise'}, + {conditionFn: errors => errors.min, message: 'La quantité doit être supérieure ou égale à 0'} + ], + step: '0.01' + }, + { + name: 'materialType', + label: 'Type de produit', + icon: 'shape-outline', + type: 'select', + validator: Validators.required, + errorMessages: [ + {conditionFn: errors => errors.required, message: 'Un type de produit est requis'} + ], + options$: this.materialTypeService.all.pipe(map(types => types.map(t => { + return {value: t.id, label: t.name} + }))) + }, + { + name: 'simdutFile', + label: 'Fiche signalitique', + icon: 'file-outline', + type: 'file', + fileType: 'application/pdf' + } + ] + unknownError = false + errorMessage: string | null + + constructor( + private materialService: MaterialService, + private materialTypeService: MaterialTypeService, + private router: Router + ) { + super() + } + + submit(values) { + this.subscribe( + this.materialService.save(values.name, values.inventoryQuantity, values.materialType, values.simdutFile), + { + next: () => this.router.navigate(['/inventory/material/list']), + error: err => { + if (err.status == 409) { + this.errorMessage = `Un produit avec le nom '${values.name}' existe déjà` + } else { + this.unknownError = true + } + console.log(err) + } + } + ) + } +} diff --git a/src/main/frontend/src/app/modules/material/pages/edit/edit.component.html b/src/main/frontend/src/app/modules/material/pages/edit/edit.component.html new file mode 100644 index 0000000..a872990 --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/edit/edit.component.html @@ -0,0 +1,35 @@ + + + + + +
+ +
+ + + {{field.label}} + + +
+
+
diff --git a/src/main/frontend/src/app/modules/material/pages/edit/edit.component.sass b/src/main/frontend/src/app/modules/material/pages/edit/edit.component.sass new file mode 100644 index 0000000..56ef602 --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/edit/edit.component.sass @@ -0,0 +1,14 @@ +.simdut-file + button + height: 43px + + .edit-simdut-file-input + width: 250px + + mat-form-field + z-index: 10 + margin-top: 10px + opacity: 0 + + button + position: absolute diff --git a/src/main/frontend/src/app/modules/material/pages/edit/edit.component.ts b/src/main/frontend/src/app/modules/material/pages/edit/edit.component.ts new file mode 100644 index 0000000..5cb779f --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/edit/edit.component.ts @@ -0,0 +1,141 @@ +import {Component, ViewChild} from '@angular/core'; +import {FormField} from "../../../shared/components/entity-add/entity-add.component"; +import {Validators} from "@angular/forms"; +import {map} from "rxjs/operators"; +import {MaterialTypeService} from "../../../material-type/service/material-type.service"; +import {MaterialService} from "../../service/material.service"; +import {ActivatedRoute, Router} from "@angular/router"; +import {SubscribingComponent} from "../../../shared/components/subscribing.component"; +import {Material} from "../../../shared/model/material.model"; +import {environment} from "../../../../../environments/environment"; + +@Component({ + selector: 'cre-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.sass'] +}) +export class EditComponent extends SubscribingComponent { + @ViewChild('simdutTemplate', {static: true}) simdutTemplateRef + @ViewChild('simdutFileInput') simdutFileInput + + material: Material | null + formFields: FormField[] = [ + { + name: 'name', + label: 'Code', + icon: 'form-textbox', + type: 'text', + validator: Validators.required, + errorMessages: [ + {conditionFn: (errors) => errors.required, message: 'Un code est requis'} + ] + }, + { + name: 'inventoryQuantity', + label: 'Quantité en inventaire', + icon: 'beaker-outline', + type: 'number', + validator: Validators.compose([Validators.required, Validators.min(0)]), + errorMessages: [ + {conditionFn: errors => errors.required, message: 'Une quantité en inventaire est requise'}, + {conditionFn: errors => errors.min, message: 'La quantité doit être supérieure ou égale à 0'} + ], + step: '0.01' + }, + { + name: 'materialType', + label: 'Type de produit', + icon: 'shape-outline', + type: 'select', + validator: Validators.required, + errorMessages: [ + {conditionFn: errors => errors.required, message: 'Un type de produit est requis'} + ], + valueFn: material => material.materialType.id, + options$: this.materialTypeService.all.pipe(map(types => types.map(t => { + return {value: t.id, label: t.name} + }))) + }, + { + name: 'simdutFile', + label: 'Fiche signalitique', + icon: 'file-outline', + type: 'file', + fileType: 'application/pdf' + } + ] + unknownError = false + errorMessage: string | null + hasSimdut = false + + constructor( + private materialService: MaterialService, + private materialTypeService: MaterialTypeService, + private router: Router, + private activatedRoute: ActivatedRoute, + ) { + super() + } + + ngOnInit() { + super.ngOnInit(); + + this.formFields[3].template = this.simdutTemplateRef + + const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id')) + this.subscribe( + this.materialService.getById(id), + { + next: material => this.material = material, + error: err => { + if (err.status === 404) { + this.router.navigate(['/inventory/material/list']) + } else { + this.unknownError = true + } + } + }, + 1 + ) + + this.subscribe( + this.materialService.hasSimdut(id), + {next: b => this.hasSimdut = b} + ) + } + + submit(values) { + this.subscribe( + this.materialService.update(this.material.id, values.name, values.inventoryQuantity, values.materialType, values.simdutFile), + { + next: () => this.router.navigate(['/inventory/material/list']), + error: err => { + if (err.status == 409) { + this.errorMessage = `Un produit avec le nom '${values.name}' existe déjà` + } else { + this.unknownError = true + } + console.log(err) + } + } + ) + } + + delete() { + this.subscribe( + this.materialService.delete(this.material.id), + { + next: () => this.router.navigate(['/inventory/material/list']), + error: err => { + this.unknownError = true + console.log(err) + } + } + ) + } + + openSimdutUrl() { + const simdutUrl = environment.apiUrl + `/material/${this.material.id}/simdut` + window.open(simdutUrl, "_blank") + } +} diff --git a/src/main/frontend/src/app/modules/material/pages/list/list.component.html b/src/main/frontend/src/app/modules/material/pages/list/list.component.html new file mode 100644 index 0000000..3aa2bae --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/list/list.component.html @@ -0,0 +1,7 @@ + + diff --git a/src/main/frontend/src/app/modules/material/pages/list/list.component.sass b/src/main/frontend/src/app/modules/material/pages/list/list.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/material/pages/list/list.component.ts b/src/main/frontend/src/app/modules/material/pages/list/list.component.ts new file mode 100644 index 0000000..1772e1a --- /dev/null +++ b/src/main/frontend/src/app/modules/material/pages/list/list.component.ts @@ -0,0 +1,65 @@ +import {Component} from '@angular/core'; +import {SubscribingComponent} from "../../../shared/components/subscribing.component"; +import {MaterialService} from "../../service/material.service"; +import {EmployeePermission} from "../../../shared/model/employee"; +import {environment} from "../../../../../environments/environment"; + +@Component({ + selector: 'cre-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.sass'] +}) +export class ListComponent extends SubscribingComponent { + materials$ = this.materialService.all + columns = [ + {def: 'name', title: 'Code', valueFn: t => t.name}, + {def: 'inventoryQuantity', title: 'Quantité', valueFn: t => t.inventoryQuantity}, + {def: 'materialType', title: 'Type de produit', valueFn: t => t.materialType.name} + ] + icons = [{ + icon: 'text-box-remove', + color: 'warn', + title: 'Ce produit n\'a pas de fiche signalitique', + disabledFn: t => this.hasSimdutMap[t.id] + }] + buttons = [{ + text: 'Modifier', + linkFn: t => `/inventory/material/edit/${t.id}`, + permission: EmployeePermission.EDIT_MATERIAL + }, { + text: 'Fiche signalitique', + linkFn: t => { + return { + externalLink: environment.apiUrl + `/material/${t.id}/simdut` + } + }, + disabledFn: t => !this.hasSimdutMap[t.id] + }] + + private hasSimdutMap: any = {} + + constructor( + private materialService: MaterialService + ) { + super() + } + + ngOnInit() { + super.ngOnInit(); + + this.subscribe( + this.materials$, + { + next: mArray => { + mArray.forEach(m => { + this.subscribe( + this.materialService.hasSimdut(m.id), + { next: b => this.hasSimdutMap[m.id] = b } + ) + }) + } + }, + 1 + ) + } +} diff --git a/src/main/frontend/src/app/modules/material/service/material.service.ts b/src/main/frontend/src/app/modules/material/service/material.service.ts new file mode 100644 index 0000000..70276d9 --- /dev/null +++ b/src/main/frontend/src/app/modules/material/service/material.service.ts @@ -0,0 +1,54 @@ +import {Injectable} from '@angular/core'; +import {ApiService} from "../../shared/service/api.service"; +import {Observable} from "rxjs"; +import {Material} from "../../shared/model/material.model"; +import {FileInput} from "ngx-material-file-input"; + +@Injectable({ + providedIn: 'root' +}) +export class MaterialService { + constructor( + private api: ApiService + ) { + } + + get all(): Observable { + return this.api.get('/material') + } + + getById(id: number): Observable { + return this.api.get(`/material/${id}`) + } + + hasSimdut(id: number): Observable { + return this.api.get(`/material/${id}/simdut/exists`) + } + + save(name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable { + const body = new FormData() + body.append('name', name) + body.append('inventoryQuantity', inventoryQuantity.toString()) + body.append('materialType', materialType.toString()) + if (simdutFile && simdutFile.files) { + body.append('simdutFile', simdutFile.files[0]) + } + return this.api.post('/material/', body) + } + + update(id: number, name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable { + const body = new FormData() + body.append('id', id.toString()) + body.append('name', name) + body.append('inventoryQuantity', inventoryQuantity.toString()) + body.append('materialType', materialType.toString()) + if (simdutFile && simdutFile.files) { + body.append('simdutFile', simdutFile.files[0]) + } + return this.api.put('/material/', body) + } + + delete(id: number): Observable { + return this.api.delete(`/material/${id}`) + } +} diff --git a/src/main/frontend/src/app/modules/shared/app-state.ts b/src/main/frontend/src/app/modules/shared/app-state.ts index 66e36fc..968559c 100644 --- a/src/main/frontend/src/app/modules/shared/app-state.ts +++ b/src/main/frontend/src/app/modules/shared/app-state.ts @@ -11,6 +11,12 @@ export class AppState { private readonly KEY_LOGGED_IN_EMPLOYEE = "logged-in-employee" authenticatedUser$ = new Subject<{ authenticated: boolean, authenticatedUser: Employee }>() + serverOnline$ = new Subject() + + set isServerOnline(isOnline: boolean) { + if (!isOnline) this.authenticatedEmployee = null + this.serverOnline$.next(isOnline); + } get isAuthenticated(): boolean { return sessionStorage.getItem(this.KEY_AUTHENTICATED) === "true" diff --git a/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.html b/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.html new file mode 100644 index 0000000..86ffa13 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.html @@ -0,0 +1,86 @@ + + + {{title}} + + +
+

Une erreur est survenue

+

{{customError}}

+
+ +
+ + + + + + + + + + +
+
+ + + + +
+ + + + {{field.label}} + + + + + {{errorMessage.message}} + + + + + + + + {{field.label}} + + + + + + {{field.label}} + + + {{option.label}} + + + + + + + + + {{field.label}} + + + diff --git a/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.sass b/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.ts b/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.ts new file mode 100644 index 0000000..063ec29 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/entity-add/entity-add.component.ts @@ -0,0 +1,78 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {SubscribingComponent} from "../subscribing.component"; +import {FormBuilder, FormControl, FormGroup, ValidatorFn} from "@angular/forms"; +import {Observable} from "rxjs"; + +@Component({ + selector: 'cre-entity-add', + templateUrl: './entity-add.component.html', + styleUrls: ['./entity-add.component.sass'] +}) +export class EntityAddComponent extends SubscribingComponent { + @Input() title: string + @Input() backButtonLink: string + @Input() unknownError: boolean = false + @Input() customError: string | null + @Input() formFields: FormField[] + @Output() submit = new EventEmitter() + + form: FormGroup | null + + constructor( + private formBuilder: FormBuilder + ) { + super() + } + + ngOnInit() { + const formGroup = {} + this.formFields.forEach(f => { + formGroup[f.name] = new FormControl(null, f.validator) + }) + this.form = this.formBuilder.group(formGroup) + + super.ngOnInit(); + } + + submitForm() { + const values = {} + this.formFields.forEach(f => { + values[f.name] = this.getControl(f.name).value + }) + this.submit.emit(values) + } + + getControl(controlName: string): FormControl { + return this.form.controls[controlName] as FormControl + } + + test(any) { + console.log(any) + } +} + +export class FormField { + constructor( + public name: string, + public label?: string, + public icon?: string, + public type?: string, + public validator?: ValidatorFn, + public errorMessages?: FormErrorMessage[], + public valueFn?: (any) => any, + public template?: any, + // Specifics to some types + public step?: string, + public options$?: Observable<{ value: any, label: string }[]>, + public fileType?: string + ) { + } +} + +export class FormErrorMessage { + constructor( + public conditionFn: (any) => boolean, + public message: string + ) { + } +} diff --git a/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.html b/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.html new file mode 100644 index 0000000..fabafb1 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.html @@ -0,0 +1,81 @@ + + + {{title}} + + +
+

Une erreur est survenue

+

{{customError}}

+
+ +
+ + + + + + + + + + +
+
+ + + + + +
+ + + + {{field.label}} + + + + + {{errorMessage.message}} + + + + + + + + {{field.label}} + + + {{option.label}} + + + + + + + + + {{field.label}} + + + + + + diff --git a/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.sass b/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.ts b/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.ts new file mode 100644 index 0000000..b13d8f5 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/entity-edit/entity-edit.component.ts @@ -0,0 +1,59 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {FormBuilder, FormControl, FormGroup} from "@angular/forms"; +import {SubscribingComponent} from "../subscribing.component"; +import {FormField} from "../entity-add/entity-add.component"; +import {EmployeePermission} from "../../model/employee"; +import {AccountService} from "../../../accounts/services/account.service"; + +@Component({ + selector: 'cre-entity-edit', + templateUrl: './entity-edit.component.html', + styleUrls: ['./entity-edit.component.sass'] +}) +export class EntityEditComponent extends SubscribingComponent { + @Input() entity: any + @Input() title: string + @Input() deleteConfirmMessage: string + @Input() backButtonLink: string + @Input() formFields: FormField[] + @Input() deletePermission: EmployeePermission + @Input() unknownError = false + @Input() customError: string | null + @Output() submit = new EventEmitter() + @Output() delete = new EventEmitter() + + form: FormGroup | null + + constructor( + private accountService: AccountService, + private formBuilder: FormBuilder + ) { + super() + } + + ngOnInit() { + const formGroup = {} + this.formFields.forEach(f => { + formGroup[f.name] = new FormControl(f.valueFn ? f.valueFn(this.entity) : this.entity[f.name], f.validator) + }) + this.form = this.formBuilder.group(formGroup) + + super.ngOnInit(); + } + + submitForm() { + const values = {} + this.formFields.forEach(f => { + values[f.name] = this.getControl(f.name).value + }) + this.submit.emit(values) + } + + getControl(controlName: string): FormControl { + return this.form.controls[controlName] as FormControl + } + + get canDelete(): boolean { + return this.accountService.hasPermission(this.deletePermission) + } +} diff --git a/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.html b/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.html new file mode 100644 index 0000000..9c4338a --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.html @@ -0,0 +1,37 @@ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
{{column.title}}{{column.valueFn(entity)}} + + + +
diff --git a/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.sass b/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.sass new file mode 100644 index 0000000..e69de29 diff --git a/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.ts b/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.ts new file mode 100644 index 0000000..0e3b513 --- /dev/null +++ b/src/main/frontend/src/app/modules/shared/components/entity-list/entity-list.component.ts @@ -0,0 +1,87 @@ +import {Component, Input} from '@angular/core'; +import {Observable} from "rxjs"; +import {SubscribingComponent} from "../subscribing.component"; +import {AccountService} from "../../../accounts/services/account.service"; +import {EmployeePermission} from "../../model/employee"; + +@Component({ + selector: 'cre-entity-list', + templateUrl: './entity-list.component.html', + styleUrls: ['./entity-list.component.sass'] +}) +export class EntityListComponent extends SubscribingComponent { + @Input() entities$: Observable + @Input() columns: TableColumn[] + @Input() icons: TableIcon[] + @Input() buttons?: TableButton[] + @Input() addLink: string + + constructor( + private accountService: AccountService + ) { + super() + } + + hasPermissionToUseButton(button: TableButton): boolean { + return !button.permission || this.accountService.hasPermission(button.permission) + } + + openExternalLink(button: TableButton, entity: T) { + let externalLink = null + // @ts-ignore + if (button.link && button.link.externalLink) { + // @ts-ignore + externalLink = button.link.externalLink + } else { + const linkFnResult = button.linkFn(entity) + // @ts-ignore + if (linkFnResult && linkFnResult.externalLink) { + // @ts-ignore + externalLink = linkFnResult.externalLink + } + } + + if (externalLink) window.open(externalLink, "_blank") + } + + get tableCols(): string[] { + const cols = this.columns.map(c => c.def) + if (this.icons) { + this.icons.forEach((_, i) => cols.push(`icon${i}`)) + } + if (this.buttons) { + this.buttons.forEach((_, i) => cols.push(`button${i}`)) + } + return cols + } +} + +export class TableColumn { + constructor( + public def: string, + public title: string, + public valueFn: (T) => string + ) { + } +} + +export class TableIcon { + constructor( + public icon: string, + public color: string, + public title: string, + public disabledFn: (T) => boolean + ) { + } +} + +export class TableButton { + constructor( + public text: string, + public link: string | { externalLink: string } | null, + public linkFn: (T) => string | { externalLink: string } | null, + public permission: EmployeePermission | null, + public disabledFn: (T) => boolean | null + ) { + } +} diff --git a/src/main/frontend/src/app/modules/shared/components/header/header.component.html b/src/main/frontend/src/app/modules/shared/components/header/header.component.html index b9a6043..20bda30 100644 --- a/src/main/frontend/src/app/modules/shared/components/header/header.component.html +++ b/src/main/frontend/src/app/modules/shared/components/header/header.component.html @@ -6,8 +6,8 @@ + [active]="activeLink.startsWith(link.route)" + (click)="activeLink = link.route"> {{ link.title }} diff --git a/src/main/frontend/src/app/modules/shared/components/header/header.component.ts b/src/main/frontend/src/app/modules/shared/components/header/header.component.ts index b24369c..e161ea8 100644 --- a/src/main/frontend/src/app/modules/shared/components/header/header.component.ts +++ b/src/main/frontend/src/app/modules/shared/components/header/header.component.ts @@ -1,53 +1,64 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; -import {Router} from "@angular/router"; +import {ResolveEnd, Router} from "@angular/router"; import {AppState} from "../../app-state"; import {Employee, EmployeePermission} from "../../model/employee"; import {AccountService} from "../../../accounts/services/account.service"; import {Subject} from "rxjs"; import {takeUntil} from "rxjs/operators"; +import {SubscribingComponent} from "../subscribing.component"; @Component({ selector: 'cre-header', templateUrl: './header.component.html', styleUrls: ['./header.component.sass'] }) -export class HeaderComponent implements OnInit, OnDestroy { +export class HeaderComponent extends SubscribingComponent { links: HeaderLink[] = [ - // {route: 'color', title: 'Couleurs', enabled: true}, - {route: 'inventory', title: 'Inventaire', enabled: true}, - new HeaderLink('employee', 'Employés', EmployeePermission.VIEW_EMPLOYEE), - new HeaderLink('group', 'Groupes', EmployeePermission.VIEW_EMPLOYEE_GROUP), - {route: 'account/login', title: 'Connexion', enabled: true}, - {route: 'account/logout', title: 'Déconnexion', enabled: false}, + {route: '/inventory', title: 'Inventaire', enabled: true}, + new HeaderLink('/employee', 'Employés', EmployeePermission.VIEW_EMPLOYEE), + new HeaderLink('/group', 'Groupes', EmployeePermission.VIEW_EMPLOYEE_GROUP), + {route: '/account/login', title: 'Connexion', enabled: true}, + {route: '/account/logout', title: 'Déconnexion', enabled: false}, ]; - _activeLink = this.links[0].route; - - private destroy$ = new Subject() + _activeLink = this.links[0].route constructor( private accountService: AccountService, private router: Router, private appState: AppState ) { + super() } ngOnInit(): void { + // Gets the current route + this.subscribe( + this.router.events, + { + next: data => { + if (data instanceof ResolveEnd) this._activeLink = data.url + } + }, + 1 + ) + + // Auth status this.accountService.checkAuthenticationStatus() this.updateEnabledLinks(this.appState.isAuthenticated, this.appState.authenticatedEmployee) + this.subscribe( + this.appState.authenticatedUser$, + {next: authentication => this.updateEnabledLinks(authentication.authenticated, authentication.authenticatedUser)} + ) - this.appState.authenticatedUser$ - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: authentication => this.updateEnabledLinks(authentication.authenticated, authentication.authenticatedUser) - }) + super.ngOnInit() } ngOnDestroy(): void { - this.destroy$.next(true) - this.destroy$.complete() this.accountService.logout(() => { console.log("Successfully logged out") }) + + super.ngOnDestroy() } set activeLink(link: string) { @@ -60,8 +71,8 @@ export class HeaderComponent implements OnInit, OnDestroy { } private updateEnabledLinks(authenticated: boolean, employee: Employee) { - this.link('account/login').enabled = !authenticated - this.link('account/logout').enabled = authenticated + this.link('/account/login').enabled = !authenticated + this.link('/account/logout').enabled = authenticated this.links.forEach(l => { if (l.requiredPermission) { diff --git a/src/main/frontend/src/app/modules/shared/components/nav/nav.component.html b/src/main/frontend/src/app/modules/shared/components/nav/nav.component.html index e5d9439..05e7976 100644 --- a/src/main/frontend/src/app/modules/shared/components/nav/nav.component.html +++ b/src/main/frontend/src/app/modules/shared/components/nav/nav.component.html @@ -1,9 +1,9 @@