Merge branch 'bug-solving' into 'master'

Bug solving

See merge request color-recipes-explorer/frontend!8
This commit is contained in:
William Nolin 2021-03-15 22:22:16 +00:00
commit e693625ab7
53 changed files with 767 additions and 682 deletions

View File

@ -1,5 +1,6 @@
<cre-header></cre-header>
<cre-global-alert-handler></cre-global-alert-handler>
<cre-loading-wheel></cre-loading-wheel>
<div class="pb-5">
<div class="dark-background"></div>
<router-outlet></router-outlet>

View File

@ -7,6 +7,7 @@ import {environment} from '../../../../environments/environment'
import {ApiService} from '../../shared/service/api.service'
import {Employee, EmployeePermission} from '../../shared/model/employee'
import {ErrorService} from '../../shared/service/error.service'
import {globalLoadingWheel} from '../../shared/components/loading-wheel/loading-wheel.component'
@Injectable({
providedIn: 'root'
@ -47,6 +48,7 @@ export class AccountService implements OnDestroy {
login(id: number, password: string, success: () => void) {
const loginForm = {id, password}
globalLoadingWheel.show()
this.http.post<any>(`${environment.apiUrl}/login`, loginForm, {
withCredentials: true,
observe: 'response' as 'body'
@ -62,7 +64,10 @@ export class AccountService implements OnDestroy {
this.setLoggedInEmployeeFromApi()
success()
},
error: err => this.errorService.handleError(err)
error: err => {
this.errorService.handleError(err)
globalLoadingWheel.hide()
}
})
}
@ -92,7 +97,11 @@ export class AccountService implements OnDestroy {
takeUntil(this.destroy$)
)
.subscribe({
next: employee => this.appState.authenticatedEmployee = employee,
next: employee => {
this.appState.authenticatedEmployee = employee
// At this point the loading wheel should be visible
globalLoadingWheel.hide()
},
error: err => this.errorService.handleError(err)
})
}

View File

@ -6,6 +6,7 @@ import {Observable} from 'rxjs'
import {RecipeImageService} from '../../services/recipe-image.service'
import {environment} from '../../../../../environments/environment'
import {ErrorService} from '../../../shared/service/error.service'
import {globalLoadingWheel} from '../../../shared/components/loading-wheel/loading-wheel.component'
@Component({
selector: 'cre-images-editor',
@ -36,6 +37,7 @@ export class ImagesEditorComponent extends SubscribingComponent {
this.subscribe(
this.imageIds$,
ids => this.hasImages = ids.length > 0,
false,
1
)
}

View File

@ -1,6 +1,6 @@
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core'
import {Mix, MixMaterial, Recipe} from '../../../shared/model/recipe.model'
import {SubscribingComponent} from '../../../shared/components/subscribing.component'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {MixService} from '../../services/mix.service'
import {Observable} from 'rxjs'
import {RecipeService} from '../../services/recipe.service'
@ -23,7 +23,7 @@ import {MatSelect} from '@angular/material/select'
templateUrl: './mix-editor.component.html',
styleUrls: ['./mix-editor.component.sass']
})
export class MixEditorComponent extends SubscribingComponent {
export class MixEditorComponent extends ErrorHandlingComponent {
@ViewChild('mixTable') mixTable: MatTable<MixMaterial>
@ViewChild('deleteConfirmBox') deleteConfirmBox: ConfirmBoxComponent
@ -47,6 +47,16 @@ export class MixEditorComponent extends SubscribingComponent {
hoveredMixMaterial: MixMaterial | null
columns = ['position', 'material', 'quantity', 'units', 'buttonRemove']
deleting = false
handledErrorModels = [{
filter: error => error.status === 409 && !this.deleting,
messageProducer: error => `Un mélange avec le nom '${error.id}' existe déjà dans cette recette`
}, {
filter: error => error.error.status === 409 && this.deleting,
consumer: () => this.deleting = false,
messageProducer: () => 'Ce mélange est utilisé par un ou plusieurs autres mélanges'
}]
constructor(
private mixService: MixService,
private recipeService: RecipeService,
@ -116,6 +126,7 @@ export class MixEditorComponent extends SubscribingComponent {
}
delete() {
this.deleting = true
this.subscribeAndNavigate(this.mixService.delete(this.mixId), `/color/edit/${this.recipeId}`)
}

View File

@ -6,7 +6,7 @@
<div class="mix-actions d-flex flex-row justify-content-between">
<ng-container *ngIf="!editionMode">
<div class="flex-grow-1">
<mat-form-field class="dark">
<mat-form-field class="dark location-input">
<mat-label>Casier</mat-label>
<input matInput type="text" [(ngModel)]="mix.location" (change)="changeLocation($event)"/>
</mat-form-field>
@ -15,12 +15,17 @@
<button mat-raised-button color="accent" (click)="printingConfirmBox.show()">Imprimer</button>
</div>
<div>
<button mat-raised-button color="accent" (click)="deduct.emit()">Déduire</button>
<button mat-raised-button color="accent" (click)="deduct.emit()" disabled title="WIP">Déduire</button>
</div>
</ng-container>
<ng-container *ngIf="editionMode">
<div class="flex-grow-1"></div>
<button mat-raised-button color="accent" routerLink="/color/edit/mix/{{recipe.id}}/{{mix.id}}">Modifier</button>
<button
mat-raised-button
color="accent"
routerLink="/color/edit/mix/{{recipe.id}}/{{mix.id}}">
Modifier
</button>
</ng-container>
</div>
@ -90,9 +95,29 @@
<td mat-footer-cell *matFooterCellDef>{{units}}</td>
</ng-container>
<ng-container matColumnDef="simdut">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mixMaterial; let i = index">
<ng-container
*ngIf="(!hoveredMixMaterial && i === 0) || hoveredMixMaterial === mixMaterial">
<button
mat-raised-button
color="accent"
[disabled]="!hasSimdutMap[mixMaterial.material.id]"
(click)="openSimdutFile(mixMaterial)">
Fiche signalitique
</button>
</ng-container>
</td>
<td mat-footer-cell *matFooterCellDef></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="mixColumns"></tr>
<tr mat-row *matRowDef="let mixMaterial; columns: mixColumns"
[class.low-quantity]="!editionMode && isInLowQuantity(mixMaterial.id)"></tr>
<tr mat-row
*matRowDef="let mixMaterial; columns: mixColumns"
[class.low-quantity]="!editionMode && isInLowQuantity(mixMaterial.id)"
(mouseover)="hoveredMixMaterial = mixMaterial">
</tr>
<ng-container *ngIf="!editionMode">
<tr mat-footer-row *matFooterRowDef="mixColumns"></tr>
</ng-container>

View File

@ -1,7 +1,7 @@
@import '../../../../../custom-theme'
mat-expansion-panel
width: 40rem
width: 48rem
margin: 2rem 0
.mix-actions
@ -14,6 +14,9 @@ mat-expansion-panel
.low-quantity
background-color: #ffb3b3
.location-input
width: 10rem
::ng-deep span.mix-calculated-quantity
&:first-child
color: green

View File

@ -8,6 +8,8 @@ import {PtouchPrinter} from '../../ptouchPrint'
import {ConfirmBoxComponent} from '../../../shared/components/confirm-box/confirm-box.component'
import {ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
import {environment} from '../../../../../environments/environment'
import {MaterialService} from '../../../material/service/material.service'
@Component({
selector: 'cre-mix-table',
@ -15,7 +17,7 @@ import {AlertService} from '../../../shared/service/alert.service'
styleUrls: ['./mix-table.component.sass']
})
export class MixTableComponent extends SubscribingComponent {
private readonly COLUMNS = ['material', 'materialType', 'quantity', 'quantityCalculated', 'quantityUnits']
private readonly COLUMNS = ['material', 'materialType', 'quantity', 'quantityCalculated', 'quantityUnits', 'simdut']
private readonly COLUMNS_STATIC = ['material', 'materialType', 'quantityStatic', 'quantityUnits']
@ViewChild('printingConfirmBox') printingConfirmBox: ConfirmBoxComponent
@ -33,11 +35,14 @@ export class MixTableComponent extends SubscribingComponent {
mixColumns = this.COLUMNS
units = UNIT_MILLILITER
computedQuantities: { id: number, percents: boolean, quantity: number }[] = []
hoveredMixMaterial: MixMaterial | null
hasSimdutMap: any = {}
// BPac printer
printer: PtouchPrinter | null
constructor(
private materialService: MaterialService,
private alertService: AlertService,
errorService: ErrorService,
router: Router,
@ -63,6 +68,12 @@ export class MixTableComponent extends SubscribingComponent {
this.units$,
u => this.convertQuantities(u)
)
this.mix.mixMaterials.map(mm => mm.material).forEach(material => this.subscribe(
this.materialService.hasSimdut(material.id),
b => this.hasSimdutMap[material.id] = b
)
)
}
changeLocation(event: any) {
@ -115,6 +126,10 @@ export class MixTableComponent extends SubscribingComponent {
return Math.round(quantity * 1000) / 1000
}
openSimdutFile(mixMaterial: MixMaterial) {
window.open(`${environment.apiUrl}/material/${mixMaterial.material.id}/simdut`, '_blank')
}
async print() {
const base = this.mix.mixMaterials
.map(ma => ma.material)

View File

@ -23,11 +23,13 @@
<button mat-raised-button color="accent" (click)="addStep()">Ajouter</button>
</th>
<td mat-cell *matCellDef="let step; let i = index">
<button mat-raised-button color="warn" (click)="removeStep(i)">Retirer</button>
<ng-container *ngIf="(!hoveredStep && i === 0) || hoveredStep === step">
<button mat-raised-button color="warn" (click)="removeStep(i)">Retirer</button>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let step; columns: columns"></tr>
<tr mat-row *matRowDef="let step; columns: columns" (mouseover)="hoveredStep = step"></tr>
</table>
</mat-expansion-panel>

View File

@ -13,6 +13,8 @@ export class StepTableComponent {
@Input() steps: RecipeStep[]
hoveredStep : RecipeStep | null
addStep() {
this.steps.push({id: null, message: ""})
this.stepTable.renderRows()

View File

@ -20,7 +20,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
@ -30,7 +30,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Description',
icon: 'text',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
@ -41,7 +41,7 @@ export class AddComponent extends ErrorHandlingComponent {
icon: 'palette',
type: 'color',
defaultValue: "#ffffff",
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une couleur est requise'}
]
@ -53,7 +53,7 @@ export class AddComponent extends ErrorHandlingComponent {
min: 0,
max: 100,
defaultValue: 10,
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Le lustre de la couleur est requis'}
]
@ -86,7 +86,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Bannière',
icon: 'domain',
type: 'select',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une bannière est requise'}
],

View File

@ -1,5 +1,5 @@
import {Component, ViewChild} from '@angular/core'
import {ErrorHandlingComponent, SubscribingComponent} from '../../../shared/components/subscribing.component'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {Recipe} from '../../../shared/model/recipe.model'
import {RecipeService} from '../../services/recipe.service'
import {ActivatedRoute, Router} from '@angular/router'
@ -10,7 +10,7 @@ import {AccountService} from '../../../accounts/services/account.service'
import {EmployeePermission} from '../../../shared/model/employee'
import {EntityEditComponent} from '../../../shared/components/entity-edit/entity-edit.component'
import {ImagesEditorComponent} from '../../components/images-editor/images-editor.component'
import {ErrorHandler, ErrorModel, ErrorService} from '../../../shared/service/error.service'
import {ErrorModel, ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
@Component({
@ -30,7 +30,7 @@ export class EditComponent extends ErrorHandlingComponent {
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
@ -40,7 +40,7 @@ export class EditComponent extends ErrorHandlingComponent {
label: 'Description',
icon: 'text',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
@ -50,7 +50,7 @@ export class EditComponent extends ErrorHandlingComponent {
label: 'Couleur',
icon: 'palette',
type: 'color',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une couleur est requise'}
]

View File

@ -5,11 +5,11 @@
<div class="d-flex flex-column">
<div class="mt-1 pb-2">
<button mat-raised-button color="primary" routerLink="/color/list">Retour</button>
<button mat-raised-button color="primary">Version Excel</button>
<button mat-raised-button color="primary" disabled title="WIP">Version Excel</button>
<button mat-raised-button color="accent" (click)="saveModifications()" [disabled]="!hasModifications">
Enregistrer
</button>
<button mat-raised-button color="accent" (click)="deductQuantities()">Déduire</button>
<button mat-raised-button color="accent" (click)="deductQuantities()" disabled title="WIP">Déduire</button>
</div>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
</div>

View File

@ -88,7 +88,8 @@ export class ExploreComponent extends ErrorHandlingComponent {
saveModifications() {
this.subscribe(
this.recipeService.saveExplorerModifications(this.recipe.id, this.note, this.mixesLocationChanges),
() => this.alertService.pushSuccess('Les modifications ont été enregistrées')
() => this.alertService.pushSuccess('Les modifications ont été enregistrées'),
true
)
}
@ -103,7 +104,8 @@ export class ExploreComponent extends ErrorHandlingComponent {
performDeductQuantities(observable: Observable<void>) {
this.subscribe(
observable,
() => this.alertService.pushSuccess('Les quantités des produits utilisés ont été déduites de l\'inventaire')
() => this.alertService.pushSuccess('Les quantités des produits utilisés ont été déduites de l\'inventaire'),
true
)
}
}

View File

@ -1,6 +1,5 @@
import {Component} from '@angular/core'
import {CompanyService} from '../../service/company.service'
import {Validators} from '@angular/forms'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
import {ActivatedRoute, Router} from '@angular/router'
@ -18,7 +17,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]

View File

@ -2,7 +2,6 @@ import {Component} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {Company} from '../../../shared/model/company.model'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
import {Validators} from '@angular/forms'
import {CompanyService} from '../../service/company.service'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorModel, ErrorService} from '../../../shared/service/error.service'
@ -20,16 +19,20 @@ export class EditComponent extends ErrorHandlingComponent {
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
}
]
deleting = false
handledErrorModels: ErrorModel[] = [{
filter: error => error.status === 409,
filter: error => error.status === 409 && !this.deleting,
messageProducer: error => `Une bannière avec le nom '${error.error.id}' existe déjà`
}, {
filter: error => error.status === 409 && this.deleting,
messageProducer: () => 'Cette bannière est utilisée par une ou plusieurs recettes'
}]
constructor(
@ -61,6 +64,7 @@ export class EditComponent extends ErrorHandlingComponent {
}
delete() {
this.deleting = true
this.subscribeAndNavigate(
this.companyService.delete(this.company.id),
'/catalog/company/list'

View File

@ -1,56 +1,10 @@
<mat-card class="x-centered mt-5">
<mat-card-header>
<mat-card-title>Création d'un utilisateur</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form">
<mat-form-field>
<mat-label>Numéro d'employé</mat-label>
<input matInput type="text" [formControl]="idControl"/>
<mat-icon svgIcon="pound" matSuffix></mat-icon>
<mat-error *ngIf="idControl.invalid">
<span *ngIf="idControl.errors.required">Un numéro d'employé est requis</span>
<span *ngIf="idControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Prénom</mat-label>
<input matInput type="text" [formControl]="firstNameControl"/>
<mat-icon svgIcon="account" matSuffix></mat-icon>
<mat-error *ngIf="firstNameControl.invalid">
<span *ngIf="firstNameControl.errors.required">Un prénom est requis</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Nom</mat-label>
<input matInput type="text" [formControl]="lastNameControl"/>
<mat-icon svgIcon="account" matSuffix></mat-icon>
<mat-error *ngIf="lastNameControl.invalid">
<span *ngIf="lastNameControl.errors.required">Un nom est requis</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Mot de passe</mat-label>
<input matInput type="password" [formControl]="passwordControl"/>
<mat-icon svgIcon="lock" matSuffix></mat-icon>
<mat-error *ngIf="passwordControl.invalid">
<span *ngIf="passwordControl.errors.required">Un mot de passe est requis</span>
<span *ngIf="passwordControl.errors.minlength">Le mot de passe doit comprendre au moins 8 caractères</span>
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="group$ | async as groups">
<mat-label>Groupe</mat-label>
<mat-select [formControl]="groupControl">
<mat-option [value]="null">Aucun</mat-option>
<mat-option *ngFor="let group of groups" [value]="group.id">{{group.name}}</mat-option>
</mat-select>
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
</mat-form-field>
<cre-permissions-field #permissionsField></cre-permissions-field>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Créer</button>
</mat-card-actions>
</mat-card>
<cre-entity-add
title="Création d'un utilisateur"
backButtonLink="/employee/list"
[formFields]="formFields"
(submit)="submit($event)">
</cre-entity-add>
<ng-template #permissionsTemplateRef>
<cre-permissions-field></cre-permissions-field>
</ng-template>

View File

@ -1,2 +1,3 @@
mat-card
max-width: 90rem
cre-entity-add
::ng-deep mat-card
max-width: 90rem

View File

@ -1,13 +1,16 @@
import {Component, ViewChild} from '@angular/core'
import {FormControl, FormGroup, Validators} from '@angular/forms'
import {PermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {EmployeeGroup} from '../../../shared/model/employee'
import {Observable} from 'rxjs'
import {Component, ContentChildren, ViewChild, ViewContainerRef} from '@angular/core'
import {Validators} from '@angular/forms'
import {
currentPermissionsFieldComponent,
PermissionsFieldComponent
} from '../../../shared/components/permissions-field/permissions-field.component'
import {GroupService} from '../../../groups/services/group.service'
import {EmployeeService} from '../../services/employee.service'
import {ActivatedRoute, Router} from '@angular/router'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {ErrorService} from '../../../shared/service/error.service'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
import {map} from 'rxjs/operators'
@Component({
selector: 'cre-add',
@ -15,16 +18,62 @@ import {ErrorService} from '../../../shared/service/error.service'
styleUrls: ['./add.component.sass']
})
export class AddComponent extends ErrorHandlingComponent {
@ViewChild('permissionsField', {static: true}) permissionsField: PermissionsFieldComponent
@ViewChild('permissionsTemplateRef', {static: true}) permissionsTemplateRef
form: FormGroup
idControl: FormControl
firstNameControl: FormControl
lastNameControl: FormControl
passwordControl: FormControl
groupControl: FormControl
group$: Observable<EmployeeGroup[]> | null
formFields: FormField[] = [{
name: 'id',
label: 'Numéro d\'employé',
icon: 'pound',
type: 'number',
required: true,
validator: Validators.compose([Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)]),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un numéro d\'employé est requis'},
{conditionFn: errors => errors.pattern, message: 'Le numéro d\'employé doit être un nombre'}
]
}, {
name: 'firstName',
label: 'Prénom',
icon: 'account',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un prénom est requis'}
]
}, {
name: 'lastName',
label: 'Nom',
icon: 'account',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
}, {
name: 'password',
label: 'Mot de passe',
icon: 'lock',
type: 'password',
required: true,
validator: Validators.minLength(8),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un mot de passe est requis'},
{conditionFn: errors => errors.minlength, message: 'Le mot de passe doit comprendre au moins 8 caractères'}
]
}, {
name: 'groupId',
label: 'Groupe',
icon: 'account-multiple',
type: 'select',
defaultValue: -1,
options$: this.groupService.all.pipe(map(groups => groups.map(g => {
return {value: g.id, label: g.name}
})))
}, {
name: 'permissions',
label: 'Permissions',
type: 'permissions'
}]
constructor(
private employeeService: EmployeeService,
@ -36,35 +85,23 @@ export class AddComponent extends ErrorHandlingComponent {
super(errorService, activatedRoute, router)
}
ngOnInit(): void {
ngOnInit() {
super.ngOnInit()
this.group$ = this.groupService.all
this.idControl = new FormControl(null, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)]))
this.firstNameControl = new FormControl(null, Validators.required)
this.lastNameControl = new FormControl(null, Validators.required)
this.passwordControl = new FormControl(null, Validators.compose([Validators.required, Validators.minLength(8)]))
this.groupControl = new FormControl(null, Validators.min(0))
this.form = new FormGroup({
id: this.idControl,
firstName: this.firstNameControl,
lastName: this.lastNameControl,
password: this.passwordControl,
group: this.groupControl
})
this.formFields[this.formFields.length - 1].template = this.permissionsTemplateRef
}
submit() {
if (this.permissionsField.valid() && this.form.valid) {
submit(values) {
const permissionsField = currentPermissionsFieldComponent
if (permissionsField.valid()) {
this.subscribeAndNavigate(
this.employeeService.save(
parseInt(this.idControl.value),
this.firstNameControl.value,
this.lastNameControl.value,
this.passwordControl.value,
this.groupControl.value,
this.permissionsField.allEnabledPermissions
values.id,
values.firstName,
values.lastName,
values.password,
values.groupId,
permissionsField.allEnabledPermissions
),
'/employee/list'
)

View File

@ -1,50 +1,15 @@
<mat-card *ngIf="employee" class="x-centered mt-5">
<mat-card-header>
<mat-card-title>Modification de l'utilisateur #{{employee.id}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form">
<mat-form-field>
<mat-label>Numéro d'employé</mat-label>
<input matInput type="text" [formControl]="idControl"/>
<mat-icon svgIcon="pound" matSuffix></mat-icon>
<mat-error *ngIf="idControl.invalid">
<span *ngIf="idControl.errors.required">Un numéro d'employé est requis</span>
<span *ngIf="idControl.errors.pattern">Le numéro d'employé doit être un nombre</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Prénom</mat-label>
<input matInput type="text" [formControl]="firstNameControl"/>
<mat-icon svgIcon="account" matSuffix></mat-icon>
<mat-error *ngIf="firstNameControl.invalid">
<span *ngIf="firstNameControl.errors.required">Un prénom est requis</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Nom</mat-label>
<input matInput type="text" [formControl]="lastNameControl"/>
<mat-icon svgIcon="account" matSuffix></mat-icon>
<mat-error *ngIf="lastNameControl.invalid">
<span *ngIf="lastNameControl.errors.required">Un nom est requis</span>
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="group$ | async as groups">
<mat-label>Groupe</mat-label>
<mat-select [formControl]="groupControl">
<mat-option [value]="null">Aucun</mat-option>
<mat-option *ngFor="let group of groups" [value]="group.id">{{group.name}}</mat-option>
</mat-select>
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
</mat-form-field>
<cre-permissions-field #permissionsField [enabledPermissions]="employee.permissions"></cre-permissions-field>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
<button mat-raised-button color="warn" *ngIf="canRemoveEmployee" (click)="confirmBoxComponent.show()">Supprimer
</button>
<button mat-raised-button color="accent" (click)="submit(permissionsField)" [disabled]="form.invalid">Enregistrer</button>
</mat-card-actions>
<cre-confirm-box #confirmBoxComponent message="Voulez-vous vraiment supprimer l'employé {{employee.id}}?" (confirm)="delete()"></cre-confirm-box>
</mat-card>
<cre-entity-edit
*ngIf="employee"
title="Modification de l'utilisateur #{{employee.id}}"
backButtonLink="/employee/list"
deletePermission="REMOVE_EMPLOYEE"
deleteConfirmMessage="Voulez-vous vraiment supprimer l'utilisateur #{{employee.id}}?"
[entity]="employee"
[formFields]="formFields"
(submit)="submit($event)"
(delete)="delete()">
</cre-entity-edit>
<ng-template #permissionsTemplateRef>
<cre-permissions-field [enabledPermissions]="employee.explicitPermissions"></cre-permissions-field>
</ng-template>

View File

@ -1,2 +1,3 @@
mat-card
max-width: 90rem
cre-entity-edit
::ng-deep mat-card
max-width: 90rem

View File

@ -1,14 +1,14 @@
import {Component} from '@angular/core'
import {PermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'
import {Component, ViewChild} from '@angular/core'
import {currentPermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {EmployeeService} from '../../services/employee.service'
import {GroupService} from '../../../groups/services/group.service'
import {ActivatedRoute, Router} from '@angular/router'
import {Observable} from 'rxjs'
import {Employee, EmployeeGroup, EmployeePermission} from '../../../shared/model/employee'
import {Employee} from '../../../shared/model/employee'
import {AccountService} from '../../../accounts/services/account.service'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {ErrorService} from '../../../shared/service/error.service'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
import {map} from 'rxjs/operators'
@Component({
selector: 'cre-edit',
@ -16,20 +16,52 @@ import {ErrorService} from '../../../shared/service/error.service'
styleUrls: ['./edit.component.sass']
})
export class EditComponent extends ErrorHandlingComponent {
@ViewChild('permissionsTemplateRef', {static: true}) permissionsTemplateRef
employee: Employee | null
group$: Observable<EmployeeGroup[]> | null
private _idControl: FormControl
private _firstNameControl: FormControl
private _lastNameControl: FormControl
private _groupControl: FormControl
formFields: FormField[] = [{
name: 'id',
label: 'Numéro d\'employé',
icon: 'pound',
type: 'number',
readonly: true
}, {
name: 'firstName',
label: 'Prénom',
icon: 'account',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un prénom est requis'}
]
}, {
name: 'lastName',
label: 'Nom',
icon: 'account',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
}, {
name: 'groupId',
label: 'Groupe',
icon: 'account-multiple',
type: 'select',
valueFn: employee => employee.group ? employee.group.id : -1,
options$: this.groupService.all.pipe(map(groups => groups.map(g => {
return {value: g.id, label: g.name}
})))
}, {
name: 'permissions',
label: 'Permissions',
type: 'permissions'
}]
constructor(
private accountService: AccountService,
private employeeService: EmployeeService,
private groupService: GroupService,
private formBuilder: FormBuilder,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
@ -46,21 +78,22 @@ export class EditComponent extends ErrorHandlingComponent {
'/employee/list'
)
this.group$ = this.groupService.all
this.formFields[this.formFields.length - 1].template = this.permissionsTemplateRef
}
submit(permissionsField: PermissionsFieldComponent) {
if (permissionsField.valid() && this.form.valid) {
submit(values) {
const permissionsField = currentPermissionsFieldComponent
if (permissionsField.valid()) {
this.subscribe(
this.employeeService.update(
parseInt(this.idControl.value),
this.firstNameControl.value,
this.lastNameControl.value,
parseInt(values.id),
values.firstName,
values.lastName,
permissionsField.allEnabledPermissions
),
() => {
const group = parseInt(this._groupControl.value)
if (!isNaN(group)) {
const group = values.groupId
if (group >= 0) {
this.subscribeAndNavigate(
this.groupService.addEmployeeToGroup(group, this.employee),
'/employee/list'
@ -72,7 +105,7 @@ export class EditComponent extends ErrorHandlingComponent {
'/employee/list'
)
} else {
this.router.navigate(['/employee/list'])
this.urlUtils.navigateTo('/employee/list')
}
}
}
@ -86,47 +119,4 @@ export class EditComponent extends ErrorHandlingComponent {
'/employee/list'
)
}
get form(): FormGroup {
return this.formBuilder.group({
id: this._idControl,
firstName: this._firstNameControl,
lastName: this._lastNameControl,
group: this._groupControl
})
}
get idControl(): FormControl {
this._idControl = this.lazyControl(this._idControl, () => new FormControl({value: this.employee.id, disabled: true}, Validators.compose([Validators.required, Validators.pattern(new RegExp('^[0-9]+$')), Validators.min(0)])))
return this._idControl
}
get firstNameControl(): FormControl {
this._firstNameControl = this.lazyControl(this._firstNameControl, () => new FormControl(this.employee.firstName, Validators.required))
return this._firstNameControl
}
get lastNameControl(): FormControl {
this._lastNameControl = this.lazyControl(this._lastNameControl, () => new FormControl(this.employee.lastName, Validators.required))
return this._lastNameControl
}
get groupControl(): FormControl {
this._groupControl = this.lazyControl(this._groupControl, () => new FormControl(this.employee.group?.id))
return this._groupControl
}
private lazyControl(control: FormControl, provider: () => FormControl): FormControl {
if (control) {
return control
}
if (this.employee) {
return provider()
}
return null
}
get canRemoveEmployee(): boolean {
return this.accountService.hasPermission(EmployeePermission.REMOVE_EMPLOYEE)
}
}

View File

@ -1,66 +1,13 @@
<div class="action-bar">
<button *ngIf="canEditEmployee" mat-raised-button color="accent" routerLink="/employee/add">Ajouter</button>
</div>
<cre-entity-list
addLink="/employee/add"
addPermission="EDIT_EMPLOYEE"
[entities$]="employees$"
[columns]="columns"
[buttons]="buttons"
[expandable]="true"
[rowDetailsTemplate]="employeeDetailsTemplate">
</cre-entity-list>
<table class="mx-auto" *ngIf="employees$ | async as employees" mat-table multiTemplateDataRows [dataSource]="employees">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>Numéro d'employé</th>
<td mat-cell *matCellDef="let employee">{{employee.id}}</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let employee">{{employee.firstName}} {{employee.lastName}}</td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef>Groupe</th>
<td mat-cell *matCellDef="let employee">
<ng-container *ngIf="employee.group">{{employee.group.name}}</ng-container>
<ng-container *ngIf="!employee.group">Aucun</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="lastLogin">
<th mat-header-cell *matHeaderCellDef>Dernière connexion</th>
<td mat-cell *matCellDef="let employee">
<ng-container *ngIf="employee.lastLoginTime">{{getDate(employee.lastLoginTime).toLocaleString()}}</ng-container>
<ng-container *ngIf="!employee.lastLoginTime">Jamais</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="permissionCount">
<th mat-header-cell *matHeaderCellDef>Permissions</th>
<td mat-cell *matCellDef="let employee">
<ng-container *ngIf="employee.permissions">{{employee.permissions.length}}</ng-container>
<ng-container *ngIf="!employee.permissions">0</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canEditEmployee" *matCellDef="let employee">
<button mat-raised-button color="accent" routerLink="/employee/edit/{{employee.id}}">Modifier</button>
</td>
</ng-container>
<ng-container matColumnDef="editPasswordButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canEditEmployeePassword" *matCellDef="let employee">
<button mat-raised-button color="accent" routerLink="/employee/password/edit/{{employee.id}}">Modifier mot de passe</button>
</td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let employee" [attr.colspan]="columns.length">
<div class="entity-detail"
[@detailExpand]="employee === expandedElement ? 'expanded' : 'collapsed'">
<cre-permissions-list [employee]="employee" class="w-100"></cre-permissions-list>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr
mat-row
*matRowDef="let employee; columns: columns"
class="entity-row can-expand"
[class.expanded-row]="expandedElement === employee"
(click)="expandedElement = expandedElement === employee ? null : employee">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
</table>
<ng-template #employeeDetailsTemplate let-employee="entity">
<cre-permissions-list [employee]="employee" class="w-100"></cre-permissions-list>
</ng-template>

View File

@ -23,9 +23,22 @@ import {ErrorService} from '../../../shared/service/error.service'
})
export class ListComponent extends ErrorHandlingComponent {
employees$: Observable<Employee[]>
columns = ['id', 'name', 'group', 'permissionCount', 'lastLogin', 'editButton', 'editPasswordButton']
expandedElement: Employee | null
columns = [
{def: 'id', title: 'Numéro d\'employé', valueFn: e => e.id},
{def: 'name', title: 'Nom', valueFn: e => `${e.firstName} ${e.lastName}`},
{def: 'group', title: 'Groupe', valueFn: e => e.group ? e.group.name : 'Aucun'},
{def: 'permissionCount', title: 'Nombre de permissions', valueFn: e => e.permissions.length},
{def: 'lastLogin', title: 'Dernière connexion', valueFn: e => e.lastLoginTime ? this.getDate(e.lastLoginTime).toLocaleString() : 'Jamais'}
]
buttons = [{
text: 'Modifier',
linkFn: employee => `/employee/edit/${employee.id}`,
permission: EmployeePermission.EDIT_EMPLOYEE
}, {
text: 'Modifier mot de passe',
linkFn: employee => `/employee/password/edit/${employee.id}`,
permission: EmployeePermission.EDIT_EMPLOYEE_PASSWORD
}]
constructor(
private employeeService: EmployeeService,
@ -44,12 +57,4 @@ export class ListComponent extends ErrorHandlingComponent {
getDate(dateString: string) {
return new Date(dateString)
}
get canEditEmployee(): boolean {
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE)
}
get canEditEmployeePassword(): boolean {
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE_PASSWORD)
}
}

View File

@ -1,23 +1,10 @@
<mat-card class="x-centered mt-5">
<mat-card-header>
<mat-card-title>Création d'un groupe</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form">
<mat-form-field class="pb-3">
<mat-label>Nom</mat-label>
<input matInput type="text" [formControl]="nameControl"/>
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
<mat-error *ngIf="nameControl.invalid">
<span *ngIf="nameControl.errors.required">Un nom est requis</span>
<span *ngIf="nameControl.errors.minlength">Le nom d'un groupe doit comprendre au moins 3 caractères</span>
</mat-error>
</mat-form-field>
<cre-permissions-field #permissionsField></cre-permissions-field>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" routerLink="/group/list">Retour</button>
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Créer</button>
</mat-card-actions>
</mat-card>
<cre-entity-add
title="Création d'un groupe"
backButtonLink="/group/list"
[formFields]="formFields"
(submit)="submit($event)">
</cre-entity-add>
<ng-template #permissionsTemplateRef>
<cre-permissions-field></cre-permissions-field>
</ng-template>

View File

@ -1,5 +1,3 @@
mat-card
max-width: 90rem
mat-checkbox
font-size: .8em
cre-entity-add
::ng-deep mat-card
max-width: 90rem

View File

@ -1,10 +1,11 @@
import {Component, ViewChild} from '@angular/core'
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'
import {Validators} from '@angular/forms'
import {GroupService} from '../../services/group.service'
import {ActivatedRoute, Router} from '@angular/router'
import {PermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {currentPermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {ErrorService} from '../../../shared/service/error.service'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
@Component({
selector: 'cre-add',
@ -12,13 +13,26 @@ import {ErrorService} from '../../../shared/service/error.service'
styleUrls: ['./add.component.sass']
})
export class AddComponent extends ErrorHandlingComponent {
@ViewChild('permissionsField', {static: true}) permissionsField: PermissionsFieldComponent
@ViewChild('permissionsTemplateRef', {static: true}) permissionsTemplateRef
form: FormGroup
nameControl: FormControl
formFields: FormField[] = [{
name: 'name',
label: 'Nom',
icon: 'account-multiple',
type: 'text',
required: true,
validator: Validators.minLength(3),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'},
{conditionFn: errors => errors.minlength, message: 'Le nom d\'un groupe doit comprendre au moins 3 caractères'}
]
}, {
name: 'permissions',
label: 'Permissions',
type: 'permissions'
}]
constructor(
private formBuilder: FormBuilder,
private groupService: GroupService,
errorService: ErrorService,
router: Router,
@ -27,17 +41,17 @@ export class AddComponent extends ErrorHandlingComponent {
super(errorService, activatedRoute, router)
}
ngOnInit(): void {
this.nameControl = new FormControl(null, Validators.compose([Validators.required, Validators.minLength(3)]))
this.form = this.formBuilder.group({
name: this.nameControl
})
ngOnInit() {
super.ngOnInit();
this.formFields[this.formFields.length - 1].template = this.permissionsTemplateRef
}
submit() {
if (this.form.valid && this.permissionsField.valid()) {
submit(values) {
const permissionsField = currentPermissionsFieldComponent
if (permissionsField.valid()) {
this.subscribeAndNavigate(
this.groupService.save(this.nameControl.value, this.permissionsField.allEnabledPermissions),
this.groupService.save(values.name, permissionsField.allEnabledPermissions),
'/group/list'
)
}

View File

@ -1,29 +1,15 @@
<mat-card *ngIf="group" class="mt-5 x-centered">
<mat-card-header>
<mat-card-title>Modifier le groupe {{group.name}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form">
<mat-form-field>
<mat-label>Nom</mat-label>
<input matInput type="text" [formControl]="nameControl"/>
<mat-icon svgIcon="account-multiple" matSuffix></mat-icon>
<mat-error *ngIf="nameControl.invalid">
<span *ngIf="nameControl.errors.required">Un nom est requis</span>
<span *ngIf="nameControl.errors.minlength">Le nom d'un groupe doit comprendre au moins 3 caractères</span>
</mat-error>
</mat-form-field>
<cre-permissions-field [enabledPermissions]="group.permissions" #permissionsField></cre-permissions-field>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" routerLink="/employee/list">Retour</button>
<button mat-raised-button color="warn" *ngIf="canRemoveGroup" [disabled]="group.employeeCount > 0"
[title]="group.employeeCount > 0 ? 'Il y a des employés dans le groupe' : ''"
(click)="confirmBoxComponent.show()">Supprimer
</button>
<button mat-raised-button color="accent" (click)="submit()" [disabled]="form.invalid">Enregistrer</button>
</mat-card-actions>
<cre-confirm-box #confirmBoxComponent [message]="confirmBoxMessage" (confirm)="delete()"></cre-confirm-box>
</mat-card>
<cre-entity-edit
*ngIf="group"
title="Modifier le groupe {{group.name}}"
backButtonLink="/group/list"
deletePermission="REMOVE_EMPLOYEE_GROUP"
deleteConfirmMessage="Voulez-vous vraiment supprimer le groupe {{group.name}}?"
[entity]="group"
[formFields]="formFields"
(submit)="submit($event)"
(delete)="delete()">
</cre-entity-edit>
<ng-template #permissionsTemplateRef>
<cre-permissions-field [enabledPermissions]="group.permissions"></cre-permissions-field>
</ng-template>

View File

@ -1,2 +1,3 @@
mat-card
max-width: 90rem
cre-entity-edit
::ng-deep mat-card
max-width: 90rem

View File

@ -1,12 +1,13 @@
import {Component, ViewChild} from '@angular/core'
import {ActivatedRoute, Router} from '@angular/router'
import {EmployeeGroup, EmployeePermission} from '../../../shared/model/employee'
import {EmployeeGroup} from '../../../shared/model/employee'
import {GroupService} from '../../services/group.service'
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'
import {PermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {Validators} from '@angular/forms'
import {currentPermissionsFieldComponent} from '../../../shared/components/permissions-field/permissions-field.component'
import {AccountService} from '../../../accounts/services/account.service'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {ErrorService} from '../../../shared/service/error.service'
import {FormField} from '../../../shared/components/entity-add/entity-add.component'
@Component({
selector: 'cre-edit',
@ -14,16 +15,29 @@ import {ErrorService} from '../../../shared/service/error.service'
styleUrls: ['./edit.component.sass']
})
export class EditComponent extends ErrorHandlingComponent {
@ViewChild('permissionsField') permissionsField: PermissionsFieldComponent
@ViewChild('permissionsTemplateRef', {static: true}) permissionsTemplateRef
formFields: FormField[] = [{
name: 'name',
label: 'Nom',
icon: 'account-multiple',
type: 'text',
required: true,
validator: Validators.minLength(3),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'},
{conditionFn: errors => errors.minlength, message: 'Le nom d\'un groupe doit comprendre au moins 3 caractères'}
]
}, {
name: 'permissions',
label: 'Permissions',
type: 'permissions'
}]
group: EmployeeGroup | null
private _nameControl: FormControl
constructor(
private accountService: AccountService,
private groupService: GroupService,
private formBuilder: FormBuilder,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
@ -39,12 +53,15 @@ export class EditComponent extends ErrorHandlingComponent {
group => this.group = group,
'/group/list'
)
this.formFields[this.formFields.length - 1].template = this.permissionsTemplateRef
}
submit(): void {
if (this.form.valid && this.permissionsField.valid()) {
submit(values): void {
const permissionsField = currentPermissionsFieldComponent
if (permissionsField.valid()) {
this.subscribeAndNavigate(
this.groupService.update(this.group.id, this.nameControl.value, this.permissionsField.allEnabledPermissions),
this.groupService.update(this.group.id, values.name, permissionsField.allEnabledPermissions),
'/group/list'
)
}
@ -56,29 +73,4 @@ export class EditComponent extends ErrorHandlingComponent {
'/group/list'
)
}
get form(): FormGroup {
return this.formBuilder.group({
name: this.nameControl
})
}
get confirmBoxMessage(): string {
return `Voulez-vous vraiment supprimer le groupe ${this.group.name}?`
}
get nameControl(): FormControl {
if (this._nameControl) {
return this._nameControl
}
if (this.group) {
this._nameControl = new FormControl(this.group.name, Validators.compose([Validators.required, Validators.minLength(3)]))
return this._nameControl
}
return null
}
get canRemoveGroup(): boolean {
return this.accountService.hasPermission(EmployeePermission.REMOVE_EMPLOYEE_GROUP)
}
}

View File

@ -1,66 +1,13 @@
<div class="action-bar">
<button *ngIf="canEditGroup" mat-raised-button color="accent" routerLink="/group/add">Ajouter</button>
</div>
<cre-entity-list
addLink="/group/add"
addPermission="EDIT_EMPLOYEE_GROUP"
[entities$]="groups$"
[columns]="columns"
[buttons]="buttons"
[expandable]="true"
[rowDetailsTemplate]="groupDetailsTemplate">
</cre-entity-list>
<table *ngIf="groups$ | async as groups" mat-table class="mx-auto" multiTemplateDataRows [dataSource]="groups">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let group">{{group.name}}</td>
</ng-container>
<ng-container matColumnDef="permissionCount">
<th mat-header-cell *matHeaderCellDef>Nombre de permissions</th>
<td mat-cell *matCellDef="let group">{{group.permissions.length}}</td>
</ng-container>
<ng-container matColumnDef="employeeCount">
<th mat-header-cell *matHeaderCellDef>Nombre d'utilisateurs</th>
<td mat-cell *matCellDef="let group">{{group.employeeCount}}</td>
</ng-container>
<ng-container matColumnDef="defaultGroup">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canSetBrowserDefaultGroup" *matCellDef="let group">
<button
mat-raised-button
color="accent"
[disabled]="isDefaultGroup(group) ? true : null"
(click)="setDefaultGroup(group)">
<ng-container *ngIf="!isDefaultGroup(group)">
Définir par défaut
</ng-container>
<ng-container *ngIf="isDefaultGroup(group)">
Par défaut
</ng-container>
</button>
</td>
</ng-container>
<ng-container matColumnDef="editGroup">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!canEditGroup" *matCellDef="let group">
<button
mat-raised-button
color="accent"
routerLink="/group/edit/{{group.id}}">
Modifier
</button>
</td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let group" [attr.colspan]="columns.length">
<div class="entity-detail"
[@detailExpand]="group == expandedElement && canViewEmployee ? 'expanded' : 'collapsed'">
<cre-permissions-list [group]="group"></cre-permissions-list>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr
mat-row
*matRowDef="let group; columns: columns"
class="entity-row"
[class.expanded-row]="expandedElement === group"
[class.can-expand]="canViewEmployee"
(click)="expandedElement = expandedElement === group ? null : group">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
</table>
<ng-template #groupDetailsTemplate let-group="entity">
<cre-permissions-list [group]="group"></cre-permissions-list>
</ng-template>

View File

@ -1,9 +1,7 @@
import {Component} from '@angular/core'
import {Observable} from 'rxjs'
import {GroupService} from '../../services/group.service'
import {EmployeeGroup, EmployeePermission} from '../../../shared/model/employee'
import {takeUntil} from 'rxjs/operators'
import {animate, state, style, transition, trigger} from '@angular/animations'
import {map} from 'rxjs/operators'
import {AccountService} from '../../../accounts/services/account.service'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {ActivatedRoute, Router} from '@angular/router'
@ -13,20 +11,26 @@ import {AlertService} from '../../../shared/service/alert.service'
@Component({
selector: 'cre-groups',
templateUrl: './list.component.html',
styleUrls: ['./list.component.sass'],
animations: [
trigger('detailExpand', [
state('collapsed', style({height: '0px', minHeight: '0'})),
state('expanded', style({height: '*'})),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
]
styleUrls: ['./list.component.sass']
})
export class ListComponent extends ErrorHandlingComponent {
groups$: Observable<EmployeeGroup[]>
groups$ = this.groupService.all.pipe(map(groups => groups.filter(g => g.id >= 0)))
defaultGroup: EmployeeGroup = null
columns = ['name', 'permissionCount', 'employeeCount', 'defaultGroup', 'editGroup']
expandedElement: EmployeeGroup | null
columns = [
{def: 'name', title: 'Nom', valueFn: g => g.name},
{def: 'permissionCount', title: 'Nombre de permissions', valueFn: g => g.permissions.length},
{def: 'employeeCount', title: 'Nombre d\'utilisateurs', valueFn: g => g.employeeCount}
]
buttons = [{
text: 'Définir par défaut',
clickFn: group => this.setDefaultGroup(group),
permission: EmployeePermission.SET_BROWSER_DEFAULT_GROUP,
disabledFn: group => this.isDefaultGroup(group)
}, {
text: 'Modifier',
linkFn: group => `/group/edit/${group.id}`,
permission: EmployeePermission.EDIT_EMPLOYEE_GROUP
}]
handledErrorModels: ErrorModel[] = [{
filter: error => error.status === 404,
@ -45,33 +49,22 @@ export class ListComponent extends ErrorHandlingComponent {
}
ngOnInit(): void {
this.groups$ = this.groupService.all.pipe(takeUntil(this.destroy$))
this.subscribe(
this.groupService.defaultGroup,
group => this.defaultGroup = group,
true
)
}
setDefaultGroup(group: EmployeeGroup) {
this.subscribe(
this.groupService.setDefaultGroup(group),
() => this.defaultGroup = group
() => this.defaultGroup = group,
true
)
}
isDefaultGroup(group: EmployeeGroup): boolean {
return this.defaultGroup && this.defaultGroup.id == group.id
}
get canViewEmployee(): boolean {
return this.accountService.hasPermission(EmployeePermission.VIEW_EMPLOYEE)
}
get canEditGroup(): boolean {
return this.accountService.hasPermission(EmployeePermission.EDIT_EMPLOYEE_GROUP)
}
get canSetBrowserDefaultGroup(): boolean {
return this.accountService.hasPermission(EmployeePermission.SET_BROWSER_DEFAULT_GROUP)
}
}

View File

@ -1,7 +1,8 @@
import {Injectable} from '@angular/core';
import {ApiService} from "../../shared/service/api.service";
import {Observable} from "rxjs";
import {Employee, EmployeeGroup, EmployeePermission} from "../../shared/model/employee";
import {Injectable} from '@angular/core'
import {ApiService} from '../../shared/service/api.service'
import {Observable} from 'rxjs'
import {Employee, EmployeeGroup, EmployeePermission} from '../../shared/model/employee'
import {tap} from 'rxjs/operators'
@Injectable({
providedIn: 'root'
@ -14,10 +15,16 @@ export class GroupService {
get all(): Observable<EmployeeGroup[]> {
return this.api.get<EmployeeGroup[]>('/employee/group')
.pipe(tap(groups => groups.unshift({
id: -1,
name: 'Aucun',
permissions: [],
employeeCount: 0
})))
}
getById(id: number): Observable<EmployeeGroup> {
return this.api.get<EmployeeGroup>(`/employee/group/${id}`);
return this.api.get<EmployeeGroup>(`/employee/group/${id}`)
}
get defaultGroup(): Observable<EmployeeGroup> {

View File

@ -18,7 +18,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: (errors) => errors.required, message: 'Un nom est requis'},
]
@ -28,7 +28,8 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Préfixe',
icon: 'label-variant',
type: 'text',
validator: Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(3)]),
required: true,
validator: Validators.compose([Validators.minLength(3), Validators.maxLength(3)]),
errorMessages: [
{conditionFn: (errors) => errors.required, message: 'Un préfixe est requis'},
{

View File

@ -20,7 +20,7 @@ export class EditComponent extends ErrorHandlingComponent {
label: 'Nom',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: (errors) => errors.required, message: 'Un nom est requis'},
]
@ -30,7 +30,8 @@ export class EditComponent extends ErrorHandlingComponent {
label: 'Préfixe',
icon: 'label-variant',
type: 'text',
validator: Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(3)]),
required: true,
validator: Validators.compose([Validators.minLength(3), Validators.maxLength(3)]),
errorMessages: [
{conditionFn: (errors) => errors.required, message: 'Un préfixe est requis'},
{
@ -40,14 +41,19 @@ export class EditComponent extends ErrorHandlingComponent {
]
}
]
submittedValues: any | null
deleting = false
submittedValues: any | null
handledErrorModels: ErrorModel[] = [{
filter: error => error.status === 409 && error.error.id === this.submittedValues.name,
filter: error => error.status === 409 && !this.deleting && error.error.id === this.submittedValues.name,
messageProducer: error => `Un type de produit avec le nom '${error.error.id}' existe déjà`
}, {
filter: error => error.status === 409 && error.error.id === this.submittedValues.prefix,
filter: error => error.status === 409 && !this.deleting && error.error.id === this.submittedValues.prefix,
messageProducer: error => `Un type de produit avec le préfixe '${error.error.id}' existe déjà`
}, {
filter: error => error.status === 409 && this.deleting,
consumer: () => this.deleting = true,
messageProducer: () => 'Ce type de produit est utilisé dans une ou plusieurs recettes ou produits'
}]
constructor(
@ -80,6 +86,7 @@ export class EditComponent extends ErrorHandlingComponent {
}
delete() {
this.deleting = true
this.subscribeAndNavigate(
this.materialTypeService.delete(this.materialType.id),
'/catalog/materialtype/list'

View File

@ -20,7 +20,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Code',
icon: 'form-textbox',
type: 'text',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: (errors) => errors.required, message: 'Un code est requis'}
]
@ -30,7 +30,8 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Quantité en inventaire',
icon: 'beaker-outline',
type: 'number',
validator: Validators.compose([Validators.required, Validators.min(0)]),
required: true,
validator: 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'}
@ -42,7 +43,7 @@ export class AddComponent extends ErrorHandlingComponent {
label: 'Type de produit',
icon: 'shape-outline',
type: 'select',
validator: Validators.required,
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un type de produit est requis'}
],

View File

@ -11,112 +11,119 @@ import {environment} from '../../../../../environments/environment'
import {ErrorModel, ErrorService} from '../../../shared/service/error.service'
@Component({
selector: 'cre-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.sass']
selector: 'cre-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.sass']
})
export class EditComponent extends ErrorHandlingComponent {
@ViewChild('simdutTemplate', {static: true}) simdutTemplateRef
@ViewChild('simdutTemplate', {static: true}) simdutTemplateRef
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'
material: Material | null
formFields: FormField[] = [
{
name: 'name',
label: 'Code',
icon: 'form-textbox',
type: 'text',
required: true,
errorMessages: [
{conditionFn: (errors) => errors.required, message: 'Un code est requis'}
]
},
{
name: 'inventoryQuantity',
label: 'Quantité en inventaire',
icon: 'beaker-outline',
type: 'number',
required: true,
validator: 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',
required: true,
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'
}
]
hasSimdut = false
selectedSimdutFile: File | null
deleting = false
handledErrorModels: ErrorModel[] = [{
filter: error => error.status === 409 && !this.deleting,
messageProducer: error => `Un produit avec le nom '${error.error.id}' existe déjà`
}, {
filter: error => error.status === 409 && this.deleting,
consumer: () => this.deleting = false,
messageProducer: () => `Ce produit est utilisé dans une plusieurs recettes`
}]
constructor(
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
]
hasSimdut = false
selectedSimdutFile: File | null
handledErrorModels: ErrorModel[] = [{
filter: error => error.status === 409,
messageProducer: error => `Un produit avec le nom '${error.error.id}' existe déjà`
}]
ngOnInit() {
super.ngOnInit()
constructor(
private materialService: MaterialService,
private materialTypeService: MaterialTypeService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
this.formFields[3].template = this.simdutTemplateRef
ngOnInit() {
super.ngOnInit()
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
this.subscribeEntityById(
this.materialService,
id,
material => this.material = material,
'/catalog/material/list'
)
this.formFields[3].template = this.simdutTemplateRef
this.subscribe(
this.materialService.hasSimdut(id),
b => this.hasSimdut = b
)
}
const id = parseInt(this.activatedRoute.snapshot.paramMap.get('id'))
this.subscribeEntityById(
this.materialService,
id,
material => this.material = material,
'/catalog/material/list'
)
submit(values) {
this.subscribeAndNavigate(
this.materialService.update(this.material.id, values.name, values.inventoryQuantity, values.materialType, this.selectedSimdutFile),
'/catalog/material/list'
)
}
this.subscribe(
this.materialService.hasSimdut(id),
b => this.hasSimdut = b
)
}
delete() {
this.deleting = true
this.subscribeAndNavigate(
this.materialService.delete(this.material.id),
'/catalog/material/list'
)
}
submit(values) {
this.subscribeAndNavigate(
this.materialService.update(this.material.id, values.name, values.inventoryQuantity, values.materialType, this.selectedSimdutFile),
'/catalog/material/list'
)
}
delete() {
this.subscribeAndNavigate(
this.materialService.delete(this.material.id),
'/catalog/material/list'
)
}
openSimdutUrl() {
const simdutUrl = environment.apiUrl + `/material/${this.material.id}/simdut`
window.open(simdutUrl, '_blank')
}
openSimdutUrl() {
const simdutUrl = environment.apiUrl + `/material/${this.material.id}/simdut`
window.open(simdutUrl, '_blank')
}
}

View File

@ -62,6 +62,7 @@ export class ListComponent extends ErrorHandlingComponent {
)
})
},
false,
1
)
}

View File

@ -6,7 +6,7 @@
<form *ngIf="form" [formGroup]="form">
<ng-container *ngFor="let field of formFields">
<ng-container
*ngIf="field.type != 'checkbox' && field.type != 'select' && field.type != 'file' && field.type != 'slider'"
*ngIf="!field.template && field.type != 'checkbox' && field.type != 'select' && field.type != 'file' && field.type != 'slider'"
[ngTemplateOutlet]="fieldTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
@ -30,6 +30,10 @@
[ngTemplateOutlet]="sliderTemplate"
[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>
@ -44,7 +48,12 @@
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"/>
<input
matInput
[type]="field.type"
[formControl]="control"
[step]="field.step ? field.step : null"
[required]="field.required"/>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
<mat-error *ngIf="control.invalid && field.errorMessages">
<ng-container *ngFor="let errorMessage of field.errorMessages">
@ -57,7 +66,7 @@
<ng-template
#checkboxTemplate
let-control="control" let-field="field">
<mat-checkbox [formControl]="control">
<mat-checkbox [formControl]="control" [required]="field.required">
{{field.label}}
</mat-checkbox>
</ng-template>
@ -67,7 +76,7 @@
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-select [formControl]="control" [required]="field.required">
<mat-option *ngFor="let option of options" [value]="option.value">
{{option.label}}
</mat-option>
@ -81,7 +90,11 @@
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>
<ngx-mat-file-input
[accept]="field.fileType"
[formControl]="control"
[required]="field.required">
</ngx-mat-file-input>
</mat-form-field>
</ng-template>

View File

@ -1,5 +1,5 @@
import {Component, EventEmitter, Input, Output} from '@angular/core'
import {FormBuilder, FormControl, FormGroup, ValidatorFn} from '@angular/forms'
import {FormBuilder, FormControl, FormGroup, ValidatorFn, Validators} from '@angular/forms'
import {Observable} from 'rxjs'
@Component({
@ -23,7 +23,8 @@ export class EntityAddComponent {
ngOnInit() {
const formGroup = {}
this.formFields.forEach(f => {
formGroup[f.name] = new FormControl(f.defaultValue, f.validator)
const validator = f.required ? Validators.compose([Validators.required, f.validator]) : f.validator
formGroup[f.name] = new FormControl(f.defaultValue, validator)
})
this.form = this.formBuilder.group(formGroup)
}
@ -47,6 +48,7 @@ export class FormField {
public label?: string,
public icon?: string,
public type?: string,
public required?: boolean,
public validator?: ValidatorFn,
public errorMessages?: FormErrorMessage[],
public valueFn?: (any) => any,

View File

@ -21,12 +21,12 @@
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
[ngTemplateOutlet]="field.template"
*ngIf="field.type == 'slider'"
[ngTemplateOutlet]="sliderTemplate"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
<ng-container
*ngIf="field.type == 'slider'"
[ngTemplateOutlet]="sliderTemplate"
[ngTemplateOutlet]="field.template"
[ngTemplateOutletContext]="{control: getControl(field.name), field: field}">
</ng-container>
</ng-container>
@ -46,7 +46,12 @@
let-control="control" let-field="field">
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<input matInput [type]="field.type" [formControl]="control" [readonly]="field.readonly"/>
<input
matInput
[type]="field.type"
[formControl]="control"
[readonly]="field.readonly"
[required]="field.required"/>
<mat-icon [svgIcon]="field.icon" matSuffix></mat-icon>
<mat-error *ngIf="control.invalid && field.errorMessages">
<ng-container *ngFor="let errorMessage of field.errorMessages">
@ -61,7 +66,7 @@
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-select [formControl]="control" [required]="field.required">
<mat-option *ngFor="let option of options" [value]="option.value">
{{option.label}}
</mat-option>
@ -75,7 +80,11 @@
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>
<ngx-mat-file-input
[accept]="field.fileType"
[formControl]="control"
[required]="field.required">
</ngx-mat-file-input>
</mat-form-field>
</ng-template>

View File

@ -2,7 +2,7 @@
<button *ngIf="hasPermission(addPermission)" mat-raised-button color="accent" [routerLink]="addLink">Ajouter</button>
</div>
<table class="mx-auto" *ngIf="entities$ | async as entities" mat-table [dataSource]="entities">
<table class="mx-auto" *ngIf="entities$ | async as entities" mat-table multiTemplateDataRows [dataSource]="entities">
<!-- Columns -->
<ng-container *ngFor="let column of columns" [matColumnDef]="column.def">
<th mat-header-cell *matHeaderCellDef>{{column.title}}</th>
@ -20,18 +20,40 @@
<!-- 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 mat-cell [class.disabled]="!hasPermissionToUseButton(button)" *matCellDef="let entity; let i = dataIndex">
<ng-container *ngIf="(!hoveredEntity && i === 0) || hoveredEntity === entity">
<button
mat-raised-button
color="accent"
[routerLink]="getRouterLink(button, entity)"
[disabled]="button.disabledFn && button.disabledFn(entity)"
(click)="clickButton(button, entity)">
{{button.text}}
</button>
</ng-container>
</td>
</ng-container>
<ng-container *ngIf="expandable" matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let entity" [attr.colspan]="tableCols.length">
<div class="entity-detail" [@detailExpand]="entity === expandedEntity ? 'expanded' : 'collapsed'">
<ng-container [ngTemplateOutlet]="rowDetailsTemplate"
[ngTemplateOutletContext]="{entity: entity, expandedEntity: expandedEntity}"></ng-container>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="tableCols"></tr>
<tr mat-row *matRowDef="let row; columns: tableCols"></tr>
<tr
mat-row
class="entity-row"
*matRowDef="let row; columns: tableCols"
[class.expanded-row]="expandedEntity === row"
[class.can-expand]="expandable"
(mouseover)="hoveredEntity = row"
(click)="expandedEntity = expandedEntity === row ? null : row">
</tr>
<ng-container *ngIf="expandable">
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
</ng-container>
</table>

View File

@ -2,11 +2,19 @@ import {Component, Input} from '@angular/core'
import {Observable} from 'rxjs'
import {AccountService} from '../../../accounts/services/account.service'
import {EmployeePermission} from '../../model/employee'
import {animate, state, style, transition, trigger} from '@angular/animations'
@Component({
selector: 'cre-entity-list',
templateUrl: './entity-list.component.html',
styleUrls: ['./entity-list.component.sass']
styleUrls: ['./entity-list.component.sass'],
animations: [
trigger('detailExpand', [
state('collapsed', style({height: '0px', minHeight: '0'})),
state('expanded', style({height: '*'})),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
]
})
export class EntityListComponent<T> {
@Input() entities$: Observable<T>
@ -15,6 +23,11 @@ export class EntityListComponent<T> {
@Input() buttons?: TableButton[]
@Input() addLink: string
@Input() addPermission: EmployeePermission
@Input() expandable = false
@Input() rowDetailsTemplate
hoveredEntity: T | null
expandedEntity: T | null
constructor(
private accountService: AccountService
@ -29,23 +42,43 @@ export class EntityListComponent<T> {
return this.accountService.hasPermission(permission)
}
openExternalLink(button: TableButton, entity: T) {
let externalLink = null
getRouterLink(button: TableButton, entity: T): string {
// @ts-ignore
if (button.link && button.link.externalLink) {
if (button.link && !button.link.externalLink) {
// @ts-ignore
externalLink = button.link.externalLink
} else {
const linkFnResult = button.linkFn(entity)
return button.link
} else if (button.linkFn) {
const fnResult = button.linkFn(entity)
// @ts-ignore
if (linkFnResult && linkFnResult.externalLink) {
if (!fnResult.externalLink) {
// @ts-ignore
externalLink = linkFnResult.externalLink
return fnResult
}
}
return undefined
}
if (externalLink) {
window.open(externalLink, '_blank')
clickButton(button: TableButton, entity: T) {
if (button.link || button.linkFn) {
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')
}
} else if (button.clickFn) {
button.clickFn(entity)
}
}
@ -85,6 +118,7 @@ export class TableButton {
public text: string,
public link: string | { externalLink: string } | null,
public linkFn: (T) => string | { externalLink: string } | null,
public clickFn: (T) => void,
public permission: EmployeePermission | null,
public disabledFn: (T) => boolean | null
) {

View File

@ -46,6 +46,7 @@ export class HeaderComponent extends SubscribingComponent {
this._activeLink = data.url
}
},
false,
1
)

View File

@ -0,0 +1,6 @@
<div class="spinner-wrapper" [class.visible]="visible">
<div class="darker-background"></div>
<div class="spinner p-2">
<mat-spinner color="accent"></mat-spinner>
</div>
</div>

View File

@ -0,0 +1,18 @@
.spinner-wrapper
transition: all 100ms
&.visible
opacity: 1 !important
visibility: unset !important
&:not(.visible)
opacity: 0
visibility: hidden
.spinner
max-width: max-content
position: fixed
left: 50%
top: 50%
transform: translate(-50%, -50%)
z-index: 11

View File

@ -0,0 +1,24 @@
import {Component} from '@angular/core'
export let globalLoadingWheel: LoadingWheelComponent | null
@Component({
selector: 'cre-loading-wheel',
templateUrl: './loading-wheel.component.html',
styleUrls: ['./loading-wheel.component.sass']
})
export class LoadingWheelComponent {
visible = false
constructor() {
globalLoadingWheel = this
}
show() {
this.visible = true
}
hide() {
this.visible = false
}
}

View File

@ -1,5 +1,5 @@
<div #permissions class="permissions-field">
<p>{{title}}</p>
<p>{{title}} *</p>
<div class="d-flex flex-row justify-content-between permissions-list">
<ng-container *ngTemplateOutlet="permissionTemplate;context:{type: 'view'}"></ng-container>
<ng-container *ngTemplateOutlet="permissionTemplate;context:{type: 'edit'}"></ng-container>

View File

@ -3,6 +3,10 @@ import {EmployeePermission, mapped_permissions} from "../../model/employee";
import {FormControl} from "@angular/forms";
import {AccountService} from "../../../accounts/services/account.service";
// The current permissions field component. Workaround to be able to access a permission field in template. (See employee/AddComponent)
// This workaround prevent the use of several permissions field component at the same time.
export let currentPermissionsFieldComponent: PermissionsFieldComponent | null
@Component({
selector: 'cre-permissions-field',
templateUrl: './permissions-field.component.html',
@ -35,6 +39,8 @@ export class PermissionsFieldComponent implements OnInit {
this.togglePermission(control, true)
})
}
currentPermissionsFieldComponent = this
}
togglePermission(permission: any, bypassValue?: boolean) {

View File

@ -4,6 +4,7 @@ import {Observable, Subject} from 'rxjs'
import {ActivatedRoute, Router} from '@angular/router'
import {UrlUtils} from '../utils/url.utils'
import {ErrorHandler, ErrorModel, ErrorService} from '../service/error.service'
import {globalLoadingWheel} from './loading-wheel/loading-wheel.component'
export abstract class SubscribingComponent implements OnInit, OnDestroy {
protected subscribers$ = []
@ -17,33 +18,48 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy {
) {
}
subscribe<T>(observable: Observable<T>, resultConsumer: (T) => void, take_count = -1) {
subscribe<T>(observable: Observable<T>, resultConsumer: (T) => void, showWheel = false, take_count = -1) {
if (take_count >= 0) {
observable.pipe(take(take_count), takeUntil(this.destroy$))
} else {
observable.pipe(takeUntil(this.destroy$))
}
this.showLoadingWheel(showWheel)
this.subscribers$.push(observable.subscribe({
next: resultConsumer,
error: err => this.errorService.handleError(err)
next: t => {
resultConsumer(t)
this.hideLoadingWheel(showWheel)
},
error: err => {
this.errorService.handleError(err)
this.hideLoadingWheel(showWheel)
}
}))
}
subscribeAndNavigate(observable: Observable<any>, route: string) {
subscribeAndNavigate(observable: Observable<any>, route: string, showWheel = true) {
this.subscribe(
observable,
() => this.urlUtils.navigateTo(route),
showWheel,
1
)
}
subscribeEntityById<T>(service: any, id: number, resultConsumer: (T) => void, notFoundRoute: string) {
subscribeEntityById<T>(service: any, id: number, resultConsumer: (T) => void, notFoundRoute: string, showWheel = true) {
this.showLoadingWheel(showWheel)
this.subscribers$.push(service.getById(id)
.pipe(take(1), takeUntil(this.destroy$))
.subscribe({
next: e => resultConsumer(e),
error: err => this.handleNotFoundError(err, notFoundRoute)
next: e => {
resultConsumer(e)
this.hideLoadingWheel(showWheel)
},
error: err => {
this.handleNotFoundError(err, notFoundRoute)
this.hideLoadingWheel(showWheel)
}
}))
}
@ -62,6 +78,18 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy {
this.errorService.handleError(error)
}
}
protected showLoadingWheel(shouldShowWheel) {
if (shouldShowWheel) {
globalLoadingWheel.show()
}
}
protected hideLoadingWheel(shouldShowWheel) {
if (shouldShowWheel) {
globalLoadingWheel.hide()
}
}
}
export abstract class ErrorHandlingComponent extends SubscribingComponent implements ErrorHandler {

View File

@ -3,6 +3,7 @@ export class Employee {
public id: number,
public firstName: string,
public lastName: string,
public explicitPermissions: EmployeePermission[],
public permissions: EmployeePermission[],
public group?: EmployeeGroup,
public lastLoginTime?: Date

View File

@ -4,10 +4,10 @@ import {Observable, Subject} from 'rxjs'
import {environment} from '../../../../environments/environment'
import {AppState} from '../app-state'
import {Router} from '@angular/router'
import {map, share, takeUntil} from 'rxjs/operators'
import {map, share, takeUntil, tap} from 'rxjs/operators'
import {valueOr} from '../utils/utils'
import {ErrorService} from './error.service'
import {AccountService} from '../../accounts/services/account.service'
import {globalLoadingWheel} from '../components/loading-wheel/loading-wheel.component'
@Injectable({
providedIn: 'root'

View File

@ -28,12 +28,14 @@ import {MatOptionModule} from '@angular/material/core'
import {MaterialFileInputModule} from 'ngx-material-file-input'
import {FileButtonComponent} from './file-button/file-button.component'
import {GlobalAlertHandlerComponent} from './components/global-alert-handler/global-alert-handler.component'
import {MatSliderModule} from '@angular/material/slider';
import { SliderFieldComponent } from './components/slider-field/slider-field.component'
import {MatSliderModule} from '@angular/material/slider'
import {SliderFieldComponent} from './components/slider-field/slider-field.component'
import {LoadingWheelComponent} from './components/loading-wheel/loading-wheel.component'
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'
@NgModule({
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent],
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent, LoadingWheelComponent],
exports: [
CommonModule,
HttpClientModule,
@ -60,7 +62,8 @@ import { SliderFieldComponent } from './components/slider-field/slider-field.com
EntityAddComponent,
EntityEditComponent,
FileButtonComponent,
GlobalAlertHandlerComponent
GlobalAlertHandlerComponent,
LoadingWheelComponent
],
imports: [
MatTabsModule,
@ -76,6 +79,7 @@ import { SliderFieldComponent } from './components/slider-field/slider-field.com
MatSelectModule,
MatOptionModule,
MatSliderModule,
MatProgressSpinnerModule,
ReactiveFormsModule,
RouterModule,
CommonModule,

View File

@ -200,7 +200,7 @@ div.empty
left: 0
background-color: black
opacity: 0.4
z-index: -1
z-index: 10
.color-warning
color: #fdd835