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 @@
-
-
-
-
+
+
+
+
+
+
+ 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 @@