Materials and Material types frontend

This commit is contained in:
FyloZ 2020-12-23 14:45:21 -05:00
parent c81f046804
commit f98a0064ca
75 changed files with 1669 additions and 253 deletions

View File

@ -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")
}

View File

@ -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",

View File

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

View File

@ -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)],

View File

@ -1,10 +1,19 @@
<cre-header></cre-header>
<div>
<router-outlet *ngIf="isOnline; else isOffline"></router-outlet>
<ng-template #isOffline>
<div>
<p>Aucune connexion</p>
</div>
</ng-template>
<router-outlet></router-outlet>
<div class="offline-server-card-wrapper" [hidden]="isServerOnline">
<div class="dark-background"></div>
<mat-card class="x-centered y-centered">
<mat-card-header>
<mat-card-title>Erreur de connexion</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Le serveur est présentement hors ligne. Réessayez plus tard.</p>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="accent" (click)="reload()">Réessayer</button>
</mat-card-actions>
</mat-card>
</div>
</div>
<!-- FOOTER -->

View File

@ -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%)

View File

@ -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()
}
}

View File

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

View File

@ -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() {

View File

@ -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<boolean>()
export class EmployeeService {
constructor(
private api: ApiService
) {
}
ngOnDestroy(): void {
this._destroy$.next(true)
this._destroy$.complete()
}
get all(): Observable<Employee[]> {
return this.api.get<Employee[]>('/employee', true)
return this.api.get<Employee[]>('/employee')
}
get(id: number): Observable<Employee> {
return this.api.get<Employee>(`/employee/${id}`).pipe(takeUntil(this._destroy$))
return this.api.get<Employee>(`/employee/${id}`)
}
save(id: number, firstName: string, lastName: string, password: string, group: number, permissions: EmployeePermission[]): Observable<Employee> {
const employee = {id, firstName, lastName, password, group, permissions}
return this.api.post<Employee>('/employee', employee).pipe(takeUntil(this._destroy$))
return this.api.post<Employee>('/employee', employee)
}
update(id: number, firstName: string, lastName: string, permissions: EmployeePermission[]): Observable<void> {
const employee = {id, firstName, lastName, permissions}
return this.api.put<void>('/employee', employee).pipe(takeUntil(this._destroy$))
return this.api.put<void>('/employee', employee)
}
updatePassword(id: number, password: string): Observable<void> {

View File

@ -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')
}
}
}
)
}

View File

@ -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 {
}

View File

@ -1,2 +0,0 @@
<cre-nav [links]="navLinks"></cre-nav>
test

View File

@ -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 {
}
}

View File

@ -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 { }

View File

@ -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 { }

View File

@ -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 { }

View File

@ -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 {
}

View File

@ -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 { }

View File

@ -0,0 +1,8 @@
<cre-entity-add
title="Création d'un type de produit"
backButtonLink="/inventory/materialtype/list"
[unknownError]="unknownError"
[customError]="errorMessage"
[formFields]="formFields"
(submit)="submit($event)">
</cre-entity-add>

View File

@ -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)
}
}
)
}
}

View File

@ -0,0 +1,13 @@
<cre-entity-edit
*ngIf="materialType"
title="Modifier le group {{materialType.name}}"
deleteConfirmMessage="Voulez-vous vraiment supprimer le type de produit {{materialType.name}}?"
backButtonLink="/inventory/materialtype/list"
deletePermission="REMOVE_MATERIAL_TYPE"
[entity]="materialType"
[formFields]="formFields"
[unknownError]="unknownError"
[customError]="errorMessage"
(submit)="submit($event)"
(delete)="delete()">
</cre-entity-edit>

View File

@ -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)
}
}
)
}
}

View File

@ -0,0 +1,6 @@
<cre-entity-list
[entities$]="materialTypes$"
[columns]="columns"
[buttons]="buttons"
addLink="/inventory/materialtype/add">
</cre-entity-list>

View File

@ -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()
}
}

View File

@ -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<MaterialType[]> {
return this.api.get<MaterialType[]>('/materialtype')
}
get(id: number): Observable<MaterialType> {
return this.api.get<MaterialType>(`/materialtype/${id}`)
}
save(name: string, prefix: string, usePercentages: boolean): Observable<MaterialType> {
const materialType = {name, prefix, usePercentages}
return this.api.post<MaterialType>('/materialtype', materialType)
}
update(id: number, name: string, prefix: string): Observable<void> {
const materialType = {id, name, prefix}
return this.api.put<void>('/materialtype', materialType)
}
delete(id: number): Observable<void> {
return this.api.delete<void>(`/materialtype/${id}`)
}
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -0,0 +1,8 @@
<cre-entity-add
title="Création d'un produit"
backButtonLink="/inventory/material/list"
[unknownError]="unknownError"
[customError]="errorMessage"
[formFields]="formFields"
(submit)="submit($event)">
</cre-entity-add>

View File

@ -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)
}
}
)
}
}

View File

@ -0,0 +1,35 @@
<cre-entity-edit
*ngIf="material"
title="Modifier le produit {{material.name}}"
deleteConfirmMessage="Voulez-vous vraiment supprimer le produit {{material.name}}?"
backButtonLink="/inventory/material/list"
deletePermission="REMOVE_MATERIAL"
[entity]="material"
[formFields]="formFields"
[unknownError]="unknownError"
[customError]="errorMessage"
(submit)="submit($event)"
(delete)="delete()">
</cre-entity-edit>
<ng-template
#simdutTemplate
let-control="control"
let-field="field">
<div class="simdut-file w-100 d-flex justify-content-between">
<button mat-raised-button color="primary" [disabled]="!hasSimdut"
[attr.title]="!hasSimdut ? 'Ce produit n\'a pas de fiche signalitique' : null" (click)="openSimdutUrl()">
Voir la fiche signalitique
</button>
<div class="edit-simdut-file-input">
<button mat-raised-button color="accent" type="button">Modifier la fiche
signalitique
</button>
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<ngx-mat-file-input #simdutFileInput [accept]="field.fileType" [formControl]="control"></ngx-mat-file-input>
</mat-form-field>
</div>
</div>
</ng-template>

View File

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

View File

@ -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")
}
}

View File

@ -0,0 +1,7 @@
<cre-entity-list
[entities$]="materials$"
[columns]="columns"
[icons]="icons"
[buttons]="buttons"
addLink="/inventory/material/add">
</cre-entity-list>

View File

@ -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
)
}
}

View File

@ -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<Material[]> {
return this.api.get<Material[]>('/material')
}
getById(id: number): Observable<Material> {
return this.api.get<Material>(`/material/${id}`)
}
hasSimdut(id: number): Observable<boolean> {
return this.api.get<boolean>(`/material/${id}/simdut/exists`)
}
save(name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable<void> {
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<void>('/material/', body)
}
update(id: number, name: string, inventoryQuantity: number, materialType: number, simdutFile: FileInput): Observable<void> {
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<void>('/material/', body)
}
delete(id: number): Observable<void> {
return this.api.delete<void>(`/material/${id}`)
}
}

View File

@ -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<boolean>()
set isServerOnline(isOnline: boolean) {
if (!isOnline) this.authenticatedEmployee = null
this.serverOnline$.next(isOnline);
}
get isAuthenticated(): boolean {
return sessionStorage.getItem(this.KEY_AUTHENTICATED) === "true"

View File

@ -0,0 +1,86 @@
<mat-card class="x-centered mt-5">
<mat-card-header>
<mat-card-title>{{title}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="unknownError || customError" class="alert alert-danger">
<p *ngIf="unknownError">Une erreur est survenue</p>
<p *ngIf="customError">{{customError}}</p>
</div>
<form *ngIf="form" [formGroup]="form">
<ng-container *ngFor="let field of formFields">
<ng-container
*ngIf="field.type != 'checkbox' && field.type != 'select' && field.type != 'file'"
[ngTemplateOutlet]="fieldTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
*ngIf="field.type == 'checkbox'"
[ngTemplateOutlet]="checkboxTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
*ngIf="field.type == 'select'"
[ngTemplateOutlet]="selectTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
*ngIf="field.type == 'file'"
[ngTemplateOutlet]="fileTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
</ng-container>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" [routerLink]="backButtonLink">Retour</button>
<button mat-raised-button color="accent" [disabled]="form.invalid" (click)="submitForm()">Créer</button>
</mat-card-actions>
</mat-card>
<ng-template
#fieldTemplate
let-control="control" let-field="field">
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<input matInput [type]="field.type" [formControl]="control" [step]="field.step ? field.step : null"/>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
<mat-error *ngIf="control.invalid && field.errorMessages">
<ng-container *ngFor="let errorMessage of field.errorMessages">
<span *ngIf="errorMessage.conditionFn(control.errors)">{{errorMessage.message}}</span>
</ng-container>
</mat-error>
</mat-form-field>
</ng-template>
<ng-template
#checkboxTemplate
let-control="control" let-field="field">
<mat-checkbox [formControl]="control">
{{field.label}}
</mat-checkbox>
</ng-template>
<ng-template
#selectTemplate
let-control="control" let-field="field">
<mat-form-field *ngIf="field.options$ | async as options">
<mat-label>{{field.label}}</mat-label>
<mat-select [formControl]="control">
<mat-option *ngFor="let option of options" [value]="option.value">
{{option.label}}
</mat-option>
</mat-select>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
</mat-form-field>
</ng-template>
<ng-template
#fileTemplate
let-control="control" let-field="field">
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<ngx-mat-file-input [accept]="field.fileType" [formControl]="control"></ngx-mat-file-input>
</mat-form-field>
</ng-template>

View File

@ -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<any>()
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
) {
}
}

View File

@ -0,0 +1,81 @@
<mat-card *ngIf="entity" class="mt-5 x-centered">
<mat-card-header>
<mat-card-title>{{title}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="unknownError || customError" class="alert alert-danger">
<p *ngIf="unknownError">Une erreur est survenue</p>
<p *ngIf="customError">{{customError}}</p>
</div>
<form [formGroup]="form">
<ng-container *ngFor="let field of formFields">
<ng-container
*ngIf="!field.template && field.type != 'checkbox' && field.type != 'select' && field.type != 'file'"
[ngTemplateOutlet]="fieldTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
*ngIf="field.type == 'select' && !field.template"
[ngTemplateOutlet]="selectTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
*ngIf="field.type == 'file' && !field.template"
[ngTemplateOutlet]="fileTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
[ngTemplateOutlet]="field.template"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
</ng-container>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" [routerLink]="backButtonLink">Retour</button>
<button mat-raised-button color="warn" *ngIf="canDelete" (click)="confirmBoxComponent.show()">Supprimer</button>
<button mat-raised-button color="accent" [disabled]="form.invalid" (click)="submitForm()">Enregistrer</button>
</mat-card-actions>
</mat-card>
<ng-template
#fieldTemplate
let-control="control" let-field="field">
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<input matInput [type]="field.type" [formControl]="control"/>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
<mat-error *ngIf="control.invalid && field.errorMessages">
<ng-container *ngFor="let errorMessage of field.errorMessages">
<span *ngIf="errorMessage.conditionFn(control.errors)">{{errorMessage.message}}</span>
</ng-container>
</mat-error>
</mat-form-field>
</ng-template>
<ng-template
#selectTemplate
let-control="control" let-field="field">
<mat-form-field *ngIf="field.options$ | async as options">
<mat-label>{{field.label}}</mat-label>
<mat-select [formControl]="control">
<mat-option *ngFor="let option of options" [value]="option.value">
{{option.label}}
</mat-option>
</mat-select>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
</mat-form-field>
</ng-template>
<ng-template
#fileTemplate
let-control="control" let-field="field">
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<ngx-mat-file-input [accept]="field.fileType" [formControl]="control"></ngx-mat-file-input>
</mat-form-field>
</ng-template>
<cre-confirm-box #confirmBoxComponent [message]="deleteConfirmMessage" (confirm)="delete.emit()"></cre-confirm-box>

View File

@ -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<any>()
@Output() delete = new EventEmitter<void>()
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)
}
}

View File

@ -0,0 +1,37 @@
<div class="action-bar">
<button mat-raised-button color="accent" [routerLink]="addLink">Ajouter</button>
</div>
<table class="mx-auto" *ngIf="entities$ | async as entities" mat-table [dataSource]="entities">
<!-- Columns -->
<ng-container *ngFor="let column of columns" [matColumnDef]="column.def">
<th mat-header-cell *matHeaderCellDef>{{column.title}}</th>
<td mat-cell *matCellDef="let entity">{{column.valueFn(entity)}}</td>
</ng-container>
<!-- Icons -->
<ng-container *ngFor="let icon of icons; let iconIndex = index" matColumnDef="icon{{iconIndex}}">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let entity" [class.disabled]="icon.disabledFn && icon.disabledFn(entity)">
<mat-icon [svgIcon]="icon.icon" [color]="icon.color" [title]="icon.title"></mat-icon>
</td>
</ng-container>
<!-- Buttons -->
<ng-container *ngFor="let button of buttons; let buttonIndex = index" matColumnDef="button{{buttonIndex}}">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!hasPermissionToUseButton(button)" *matCellDef="let entity">
<button
mat-raised-button
color="accent"
[routerLink]="button.link ? button.link.externalLink ? undefined : button.link : button.linkFn(entity).externalLink ? undefined : button.linkFn(entity)"
[disabled]="button.disabledFn && button.disabledFn(entity)"
(click)="openExternalLink(button, entity)">
{{button.text}}
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let row; columns: tableCols"></tr>
</table>

View File

@ -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<T> extends SubscribingComponent {
@Input() entities$: Observable<T>
@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
) {
}
}

View File

@ -6,8 +6,8 @@
<a
*ngIf="link.enabled"
mat-tab-link
(click)="activeLink = link.route"
[active]="activeLink == link.route">
[active]="activeLink.startsWith(link.route)"
(click)="activeLink = link.route">
{{ link.title }}
</a>
</ng-container>

View File

@ -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<boolean>()
_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) {

View File

@ -1,9 +1,9 @@
<nav mat-tab-nav-bar backgroundColor="primary">
<ng-container *ngFor="let link of links">
<a
*ngIf="link.enabled"
*ngIf="link.enabled && hasPermission(link)"
mat-tab-link
[active]="activeLink == link.route"
[active]="activeLink.startsWith(link.route)"
(click)="activeLink = link.route">
{{ link.title }}
</a>

View File

@ -1,7 +1,7 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Employee, EmployeePermission} from "../../model/employee";
import {AccountService} from "../../../accounts/services/account.service";
import {Router} from "@angular/router";
import {ActivatedRoute, Router} from "@angular/router";
import {takeUntil} from "rxjs/operators";
import {AppState} from "../../app-state";
import {Subject} from "rxjs";
@ -16,19 +16,17 @@ export class NavComponent implements OnInit, OnDestroy {
@Input() links: NavLink [] = []
// links: NavLink[] = [
// {route: 'materialtype', title: 'Types de produit', enabled: true}
// ]
_activeLink: string | null;
_activeLink: string;
constructor(
private accountService: AccountService,
private router: Router,
private appState: AppState
private appState: AppState,
private router: Router
) {
}
ngOnInit(): void {
this._activeLink = this.router.url
this.updateEnabledLinks(this.appState.authenticatedEmployee)
this.appState.authenticatedUser$
@ -43,6 +41,10 @@ export class NavComponent implements OnInit, OnDestroy {
this._destroy$.complete()
}
hasPermission(link: NavLink): boolean {
return !link.permission || this.accountService.hasPermission(link.permission)
}
set activeLink(link: string) {
this._activeLink = link
this.router.navigate([link])
@ -54,8 +56,8 @@ export class NavComponent implements OnInit, OnDestroy {
private updateEnabledLinks(employee: Employee) {
this.links.forEach(l => {
if (l.requiredPermission) {
l.enabled = employee && employee.permissions.indexOf(l.requiredPermission) >= 0;
if (l.permission) {
l.enabled = employee && employee.permissions.indexOf(l.permission) >= 0;
}
})
}
@ -65,8 +67,8 @@ export class NavLink {
constructor(
public route: string,
public title: string,
public requiredPermission?: EmployeePermission,
public enabled = false
public permission?: EmployeePermission,
public enabled?
) {
}
}

View File

@ -21,18 +21,24 @@ export class EmployeeGroup {
}
export enum EmployeePermission {
VIEW_MATERIAL = 'VIEW_MATERIAL',
VIEW_MATERIAL_TYPE = 'VIEW_MATERIAL_TYPE',
VIEW = 'VIEW',
VIEW_EMPLOYEE = 'VIEW_EMPLOYEE',
VIEW_EMPLOYEE_GROUP = 'VIEW_EMPLOYEE_GROUP',
VIEW = 'VIEW',
EDIT_MATERIAL = 'EDIT_MATERIAL',
EDIT_MATERIAL_TYPE = 'EDIT_MATERIAL_TYPE',
EDIT = 'EDIT',
EDIT_EMPLOYEE = 'EDIT_EMPLOYEE',
EDIT_EMPLOYEE_PASSWORD = 'EDIT_EMPLOYEE_PASSWORD',
EDIT_EMPLOYEE_GROUP = 'EDIT_EMPLOYEE_GROUP',
EDIT = 'EDIT',
REMOVE_MATERIAL = 'REMOVE_MATERIAL',
REMOVE_MATERIAL_TYPE = 'REMOVE_MATERIAL_TYPE',
REMOVE = 'REMOVE',
REMOVE_EMPLOYEE = 'REMOVE_EMPLOYEE',
REMOVE_EMPLOYEE_GROUP = 'REMOVE_EMPLOYEE_GROUP',
REMOVE = 'REMOVE',
SET_BROWSER_DEFAULT_GROUP = 'SET_BROWSER_DEFAULT_GROUP',
ADMIN = 'ADMIN'
@ -40,23 +46,89 @@ export enum EmployeePermission {
export const mapped_permissions = {
view: [
{permission: EmployeePermission.VIEW_MATERIAL, description: 'Voir les produits', impliedPermissions: []},
{
permission: EmployeePermission.VIEW_MATERIAL_TYPE,
description: 'Voir les types de produit',
impliedPermissions: []
},
{
permission: EmployeePermission.VIEW,
description: 'Voir',
impliedPermissions: [EmployeePermission.VIEW_MATERIAL, EmployeePermission.VIEW_MATERIAL_TYPE]
},
{permission: EmployeePermission.VIEW_EMPLOYEE, description: 'Voir les employés', impliedPermissions: []},
{permission: EmployeePermission.VIEW_EMPLOYEE_GROUP, description: 'Voir les groupes', impliedPermissions: []},
{permission: EmployeePermission.VIEW, description: 'Voir', impliedPermissions: []},
],
edit: [
{permission: EmployeePermission.EDIT_EMPLOYEE, description: 'Modifier les employés', impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE]},
{permission: EmployeePermission.EDIT_EMPLOYEE_PASSWORD, description: 'Modifier le mot de passe des employés', impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]},
{permission: EmployeePermission.EDIT_EMPLOYEE_GROUP, description: 'Modifier les groupes', impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE_GROUP]},
{permission: EmployeePermission.EDIT, description: 'Modifier', impliedPermissions: [EmployeePermission.VIEW]},
{
permission: EmployeePermission.EDIT_MATERIAL,
description: 'Modifier les produits',
impliedPermissions: [EmployeePermission.VIEW_MATERIAL_TYPE]
},
{
permission: EmployeePermission.EDIT_MATERIAL_TYPE,
description: 'Modifier les types de produit',
impliedPermissions: [EmployeePermission.VIEW_MATERIAL_TYPE]
},
{
permission: EmployeePermission.EDIT,
description: 'Modifier',
impliedPermissions: [EmployeePermission.EDIT_MATERIAL, EmployeePermission.EDIT_MATERIAL_TYPE, EmployeePermission.VIEW]
},
{
permission: EmployeePermission.EDIT_EMPLOYEE,
description: 'Modifier les employés',
impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]
},
{
permission: EmployeePermission.EDIT_EMPLOYEE_PASSWORD,
description: 'Modifier le mot de passe des employés',
impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]
},
{
permission: EmployeePermission.EDIT_EMPLOYEE_GROUP,
description: 'Modifier les groupes',
impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE_GROUP]
},
],
remove: [
{permission: EmployeePermission.REMOVE_EMPLOYEE, description: 'Supprimer les employés', impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]},
{permission: EmployeePermission.REMOVE_EMPLOYEE_GROUP, description: 'Supprimer les groupes', impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE_GROUP]},
{permission: EmployeePermission.REMOVE, description: 'Supprimer', impliedPermissions: [EmployeePermission.EDIT]},
{
permission: EmployeePermission.REMOVE_MATERIAL,
description: 'Supprimer des produits',
impliedPermissions: [EmployeePermission.EDIT_MATERIAL]
},
{
permission: EmployeePermission.REMOVE_MATERIAL_TYPE,
description: 'Supprimer des types de produit',
impliedPermissions: [EmployeePermission.EDIT_MATERIAL_TYPE]
},
{
permission: EmployeePermission.REMOVE,
description: 'Supprimer',
impliedPermissions: [EmployeePermission.REMOVE_MATERIAL, EmployeePermission.REMOVE_MATERIAL_TYPE, EmployeePermission.EDIT]
},
{
permission: EmployeePermission.REMOVE_EMPLOYEE,
description: 'Supprimer des employés',
impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE]
},
{
permission: EmployeePermission.REMOVE_EMPLOYEE_GROUP,
description: 'Supprimer des groupes',
impliedPermissions: [EmployeePermission.EDIT_EMPLOYEE_GROUP]
},
],
other: [
{permission: EmployeePermission.SET_BROWSER_DEFAULT_GROUP, description: 'Définir le groupe par défaut', impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE_GROUP]},
{permission: EmployeePermission.ADMIN, description: 'Administrateur', impliedPermissions: [EmployeePermission.REMOVE, EmployeePermission.SET_BROWSER_DEFAULT_GROUP, EmployeePermission.REMOVE_EMPLOYEE, EmployeePermission.EDIT_EMPLOYEE_PASSWORD, EmployeePermission.REMOVE_EMPLOYEE_GROUP]}
{
permission: EmployeePermission.SET_BROWSER_DEFAULT_GROUP,
description: 'Définir le groupe par défaut',
impliedPermissions: [EmployeePermission.VIEW_EMPLOYEE_GROUP]
},
{
permission: EmployeePermission.ADMIN,
description: 'Administrateur',
impliedPermissions: [EmployeePermission.REMOVE, EmployeePermission.SET_BROWSER_DEFAULT_GROUP, EmployeePermission.REMOVE_EMPLOYEE, EmployeePermission.EDIT_EMPLOYEE_PASSWORD, EmployeePermission.REMOVE_EMPLOYEE_GROUP]
}
]
}

View File

@ -0,0 +1,11 @@
import {MaterialType} from "./materialtype.model";
export class Material {
constructor(
public id: number,
public name: string,
public inventoryQuantity: number,
public materialType: MaterialType
) {
}
}

View File

@ -3,7 +3,8 @@ export class MaterialType {
public id: number,
public name: string,
public prefix: string,
public usePercentages: boolean
public usePercentages: boolean,
public systemType: boolean
) {
}
}

View File

@ -1,10 +1,10 @@
import {Injectable, OnDestroy} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {HttpClient, HttpHeaders, HttpParams} from "@angular/common/http";
import {Observable, Subject} from "rxjs";
import {environment} from "../../../../environments/environment";
import {AppState} from "../app-state";
import {Router} from "@angular/router";
import {takeUntil} from "rxjs/operators";
import {share, takeUntil} from "rxjs/operators";
@Injectable({
providedIn: 'root'
@ -25,42 +25,77 @@ export class ApiService implements OnDestroy {
}
get<T>(url: string, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.get<string>(environment.apiUrl + url, options).pipe(takeUntil(this._destroy$))
}
return this.executeHttpRequest(
httpOptions => this.http.get<T>(environment.apiUrl + url, httpOptions),
needAuthentication,
options
)
}
post<T>(url: string, body: any = {}, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.post<T>(environment.apiUrl + url, body, options).pipe(takeUntil(this._destroy$))
}
return this.executeHttpRequest(
httpOptions => this.http.post<T>(environment.apiUrl + url, body, httpOptions),
needAuthentication,
options
)
}
put<T>(url: string, body: any = {}, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.put<T>(environment.apiUrl + url, body, options).pipe(takeUntil(this._destroy$))
}
return this.executeHttpRequest(
httpOptions => this.http.put<T>(environment.apiUrl + url, body, httpOptions),
needAuthentication,
options
)
}
delete<T>(url: string, needAuthentication = true, options: any = {}): Observable<T> {
if (this.checkAuthenticated(needAuthentication, options)) {
// @ts-ignore
return this.http.delete<T>(environment.apiUrl + url, options).pipe(takeUntil(this._destroy$))
}
return this.executeHttpRequest(
httpOptions => this.http.delete<T>(environment.apiUrl + url, httpOptions),
needAuthentication,
options
)
}
private checkAuthenticated(needAuthentication: boolean, httpOptions: any): boolean {
private executeHttpRequest<T>(requestFn: (httpOptions?: {
headers?: HttpHeaders | {
[header: string]: string | string[];
};
observe?: 'body';
params?: HttpParams | {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}) => Observable<T>, needAuthentication = true, httpOptions: any = {}): Observable<T> {
if (needAuthentication) {
if (!this.appState.isAuthenticated || Date.now() > this.appState.authenticationExpiration) {
if (this.checkAuthenticated()) {
if (httpOptions) {
httpOptions.withCredentials = true
} else {
console.error("httpOptions need to be specified to use credentials in HTTP methods.")
}
} else {
this.navigateToLogin()
return false
}
httpOptions.withCredentials = true
}
return true
const result$ = requestFn(httpOptions)
.pipe(takeUntil(this._destroy$), share())
const errorCheckSubscription = result$.subscribe({
next: () => this.appState.isServerOnline = true,
error: err => {
errorCheckSubscription.unsubscribe()
this.appState.isServerOnline = !(err.status === 0 && err.statusText === "Unknown Error");
}
})
return result$
}
private checkAuthenticated(): boolean {
return this.appState.isAuthenticated && Date.now() <= this.appState.authenticationExpiration
}
private navigateToLogin() {

View File

@ -18,30 +18,43 @@ import {ConfirmBoxComponent} from './components/confirm-box/confirm-box.componen
import {PermissionsListComponent} from './components/permissions-list/permissions-list.component';
import {MatChipsModule} from "@angular/material/chips";
import {PermissionsFieldComponent} from "./components/permissions-field/permissions-field.component";
import { NavComponent } from './components/nav/nav.component';
import {NavComponent} from './components/nav/nav.component';
import {EntityListComponent} from './components/entity-list/entity-list.component';
import {RouterModule} from "@angular/router";
import {EntityAddComponent} from './components/entity-add/entity-add.component';
import {EntityEditComponent} from './components/entity-edit/entity-edit.component';
import {MatSelectModule} from "@angular/material/select";
import {MatOptionModule} from "@angular/material/core";
import {MaterialFileInputModule} from "ngx-material-file-input";
@NgModule({
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent],
exports: [
CommonModule,
HttpClientModule,
HeaderComponent,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatTableModule,
MatCheckboxModule,
MatListModule,
ReactiveFormsModule,
LabeledIconComponent,
ConfirmBoxComponent,
PermissionsListComponent,
PermissionsFieldComponent,
NavComponent
],
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent],
exports: [
CommonModule,
HttpClientModule,
HeaderComponent,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatTableModule,
MatCheckboxModule,
MatListModule,
MatSelectModule,
MatOptionModule,
MaterialFileInputModule,
ReactiveFormsModule,
LabeledIconComponent,
ConfirmBoxComponent,
PermissionsListComponent,
PermissionsFieldComponent,
NavComponent,
EntityListComponent,
EntityAddComponent,
EntityEditComponent
],
imports: [
MatTabsModule,
MatIconModule,
@ -50,7 +63,13 @@ import { NavComponent } from './components/nav/nav.component';
MatChipsModule,
MatCheckboxModule,
MatFormFieldModule,
MaterialFileInputModule,
MatTableModule,
MatInputModule,
MatSelectModule,
MatOptionModule,
ReactiveFormsModule,
RouterModule,
CommonModule
]
})

View File

@ -0,0 +1,2 @@
<cre-nav [links]="links"></cre-nav>
<router-outlet></router-outlet>

View File

@ -0,0 +1,15 @@
import {Component} from '@angular/core';
import {NavLink} from "../../modules/shared/components/nav/nav.component";
import {EmployeePermission} from "../../modules/shared/model/employee";
@Component({
selector: 'cre-inventory-page',
templateUrl: './inventory-page.component.html',
styleUrls: ['./inventory-page.component.sass']
})
export class InventoryPageComponent {
links: NavLink[] = [
{route: '/inventory/materialtype', title: 'Types de produit', permission: EmployeePermission.VIEW_MATERIAL_TYPE},
{route: '/inventory/material', title: 'Produits', permission: EmployeePermission.VIEW_MATERIAL}
]
}

View File

@ -69,7 +69,7 @@ table
overflow: hidden
display: flex
.disabled button
.disabled *
display: none
button

View File

@ -113,7 +113,7 @@ public class FilesService {
return file;
} catch (IOException ex) {
throw new RuntimeException("Impossible de créer un fichier: " + ex.getMessage());
throw new RuntimeException("Impossible de créer un fichier: ", ex);
}
}

View File

@ -257,6 +257,18 @@ private enum class ControllerAuthorizations(
val antMatcher: String,
val permissions: Map<HttpMethod, EmployeePermission>
) {
MATERIALS("/api/material/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_MATERIAL,
HttpMethod.POST to EmployeePermission.EDIT_MATERIAL,
HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL,
HttpMethod.DELETE to EmployeePermission.REMOVE_MATERIAL
)),
MATERIAL_TYPES("/api/materialtype/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_MATERIAL_TYPE,
HttpMethod.POST to EmployeePermission.EDIT_MATERIAL_TYPE,
HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL_TYPE,
HttpMethod.DELETE to EmployeePermission.REMOVE_MATERIAL_TYPE
)),
SET_BROWSER_DEFAULT_GROUP("/api/employee/group/default/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
HttpMethod.POST to EmployeePermission.SET_BROWSER_DEFAULT_GROUP

View File

@ -163,26 +163,37 @@ data class EmployeeLoginRequest(val id: Long, val password: String)
enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> = listOf()) {
// View
VIEW_MATERIAL,
VIEW_MATERIAL_TYPE,
VIEW(listOf(
VIEW_MATERIAL,
VIEW_MATERIAL_TYPE
)),
VIEW_EMPLOYEE,
VIEW_EMPLOYEE_GROUP,
VIEW(listOf(
)),
// Edit
EDIT_MATERIAL(listOf(VIEW_MATERIAL)),
EDIT_MATERIAL_TYPE(listOf(VIEW_MATERIAL_TYPE)),
EDIT(listOf(
EDIT_MATERIAL,
EDIT_MATERIAL_TYPE,
VIEW
)),
EDIT_EMPLOYEE(listOf(VIEW_EMPLOYEE)),
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_EMPLOYEE)),
EDIT_EMPLOYEE_GROUP(listOf(VIEW_EMPLOYEE_GROUP)),
EDIT(listOf(
VIEW
)),
// Remove
REMOVE_EMPLOYEE(listOf(EDIT_EMPLOYEE)),
REMOVE_EMPLOYEE_GROUP(listOf(EDIT_EMPLOYEE_GROUP)),
REMOVE_MATERIAL(listOf(EDIT_MATERIAL)),
REMOVE_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPE)),
REMOVE(listOf(
REMOVE_MATERIAL,
REMOVE_MATERIAL_TYPE,
EDIT
)),
REMOVE_EMPLOYEE(listOf(EDIT_EMPLOYEE)),
REMOVE_EMPLOYEE_GROUP(listOf(EDIT_EMPLOYEE_GROUP)),
// Others
SET_BROWSER_DEFAULT_GROUP(listOf(

View File

@ -6,6 +6,7 @@ import org.springframework.util.Assert
import org.springframework.web.multipart.MultipartFile
import java.util.*
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
@ -44,7 +45,7 @@ open class MaterialSaveDto(
val name: String,
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
@field:Size(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
val inventoryQuantity: Float,
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)

View File

@ -2,6 +2,7 @@ package dev.fyloz.trial.colorrecipesexplorer.rest
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.service.MaterialService
import org.jetbrains.annotations.Nullable
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
@ -9,6 +10,7 @@ import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import javax.validation.Valid
private const val MATERIAL_CONTROLLER_PATH = "api/material"
@ -34,7 +36,7 @@ class MaterialController(materialService: MaterialService) : AbstractRestModelAp
}
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
fun save(entity: MaterialSaveDto, simdutFile: MultipartFile): ResponseEntity<Material> =
fun saveTest(@Valid entity: MaterialSaveDto, simdutFile: MultipartFile?): ResponseEntity<Material> =
super.save(materialSaveDto(name = entity.name, inventoryQuantity = entity.inventoryQuantity, materialType = entity.materialType, simdutFile = simdutFile))
@PostMapping("oldsave")
@ -42,8 +44,8 @@ class MaterialController(materialService: MaterialService) : AbstractRestModelAp
ResponseEntity.notFound().build()
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
fun update(entity: MaterialUpdateDto, simdutFile: MultipartFile): ResponseEntity<Void> =
super.update(materialUpdateDto(name = entity.name, inventoryQuantity = entity.inventoryQuantity, materialType = entity.materialType, simdutFile = simdutFile))
fun update(@Valid entity: MaterialUpdateDto, simdutFile: MultipartFile?): ResponseEntity<Void> =
super.update(materialUpdateDto(id = entity.id, name = entity.name, inventoryQuantity = entity.inventoryQuantity, materialType = entity.materialType, simdutFile = simdutFile))
@PutMapping("oldupdate")
override fun update(entity: MaterialUpdateDto): ResponseEntity<Void> =

View File

@ -40,14 +40,10 @@ class MaterialServiceImpl(materialRepository: MaterialRepository, val mixQuantit
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.write(this, entity.simdutFile)
}
override fun save(entity: Material): Material {
if (existsByName(entity.name))
throw EntityAlreadyExistsRestException(entity.name)
return super.save(entity)
}
override fun update(entity: MaterialUpdateDto): Material =
update(entity.toMaterial()).apply { simdutService.update(entity.simdutFile, this) }
update(entity.toMaterial()).apply {
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.update(entity.simdutFile, this)
}
override fun update(entity: Material): Material {
Assert.notNull(entity.id, "MaterialService.update() was called with a null identifier")

View File

@ -10,6 +10,7 @@ import dev.fyloz.trial.colorrecipesexplorer.repository.NamedJpaRepository
import io.jsonwebtoken.lang.Assert
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.findByIdOrNull
import java.lang.RuntimeException
/**
* A service implementing the basics CRUD operations.
@ -92,7 +93,7 @@ abstract class AbstractModelService<E : Model, S : EntityDto<E>, U : EntityDto<E
return super.update(entity)
}
override fun deleteById(id: Long) = repository.deleteById(id)
override fun deleteById(id: Long) = delete(getById(id)) // Use delete(entity) to prevent code duplication
}
abstract class AbstractNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : NamedJpaRepository<E>>(repository: R) :

View File

@ -0,0 +1,9 @@
spring.datasource.url=jdbc:h2:file:./workdir/recipes
spring.datasource.username=sa
spring.datasource.password=LWK4Y7TvEbNyhu1yCoG3
spring.h2.console.path=/dbconsole
spring.h2.console.enabled=true
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

View File

@ -0,0 +1,5 @@
spring.datasource.url=jdbc:mysql://172.20.0.2/cre
spring.datasource.username=root
spring.datasource.password=pass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

View File

@ -1,9 +1,3 @@
# BDD
spring.datasource.url=jdbc:h2:file:./workdir/recipes
spring.datasource.username=sa
spring.datasource.password=LWK4Y7TvEbNyhu1yCoG3
# CONSOLE DE LA BDD
spring.h2.console.path=/dbconsole
# PORT
server.port=9090
# CRE
@ -17,8 +11,8 @@ cre.security.jwt-duration=18000000
cre.security.root.id=9999
cre.security.root.password=password
# Common user
cre.security.common.id=9998
cre.security.common.password=common
#cre.security.common.id=9998
#cre.security.common.password=common
# TYPES DE PRODUIT PAR DÉFAUT
entities.material-types.systemTypes[0].name=Aucun
entities.material-types.systemTypes[0].prefix=
@ -29,20 +23,12 @@ entities.material-types.systemTypes[1].usepercentages=false
entities.material-types.baseName=Base
# DEBUG
spring.jpa.show-sql=true
spring.h2.console.enabled=true
# Permet d'accéder à la console de la BDD à distance
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
# NE PAS MODIFIER
spring.datasource.driver-class-name=org.h2.Driver
spring.messages.fallback-to-system-locale=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=15MB
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.open-in-view=true
server.http2.enabled=true
server.error.whitelabel.enabled=false
#spring.redis.host=localhost
#spring.redis.port=6379
spring.profiles.active=@spring.profiles.active@

15
todo.txt Normal file
View File

@ -0,0 +1,15 @@
== Icônes pour recettes non-approuvés / quantité faible ==
== Texte SIMDUT inexistant (fiche signalitique) pour les matériaux ==
== Comptes ==
No employé - Permissions - Employés
== Kits de retouche ==
No Job - No Dossier - Qté - Description - Case à cocher - Note
Bouton compléter si tout est coché/imprimé ?
Enregistrer localdatetime/personne pendant une certaine durée